diff --git a/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh b/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh index 93f2ca8660e60..3a075b451b0dc 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh @@ -6,7 +6,7 @@ xs=("$@") # TODO: Safe to remove this after 2024-03-01 (https://github.com/elastic/kibana/issues/175904) - also clean up usages uploadPrefix_old="gs://elastic-bekitzur-kibana-coverage-live/" -uploadPrefixWithTimeStamp_old="${uploadPrefix}${TIME_STAMP}/" +uploadPrefixWithTimeStamp_old="${uploadPrefix_old}${TIME_STAMP}/" uploadPrefix="gs://elastic-kibana-coverage-live/" uploadPrefixWithTimeStamp="${uploadPrefix}${TIME_STAMP}/" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b1a14c058fa95..b07f76b9683f6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -383,6 +383,7 @@ packages/kbn-eslint-plugin-imports @elastic/kibana-operations packages/kbn-eslint-plugin-telemetry @elastic/obs-knowledge-team examples/eso_model_version_example @elastic/kibana-security x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security +packages/kbn-esql-utils @elastic/kibana-visualizations packages/kbn-event-annotation-common @elastic/kibana-visualizations packages/kbn-event-annotation-components @elastic/kibana-visualizations src/plugins/event_annotation_listing @elastic/kibana-visualizations @@ -529,6 +530,7 @@ x-pack/packages/maps/vector_tile_utils @elastic/kibana-gis x-pack/plugins/metrics_data_access @elastic/obs-knowledge-team x-pack/packages/ml/agg_utils @elastic/ml-ui x-pack/packages/ml/anomaly_utils @elastic/ml-ui +x-pack/packages/ml/cancellable_search @elastic/ml-ui x-pack/packages/ml/category_validator @elastic/ml-ui x-pack/packages/ml/chi2test @elastic/ml-ui x-pack/packages/ml/creation_wizard_utils @elastic/ml-ui diff --git a/docs/maps/clean-data.asciidoc b/docs/maps/clean-data.asciidoc new file mode 100644 index 0000000000000..538e84be53517 --- /dev/null +++ b/docs/maps/clean-data.asciidoc @@ -0,0 +1,183 @@ +[role="xpack"] +[[maps-clean-your-data]] +=== Clean your data + +// https://github.com/elastic/kibana/issues/135319 + +Geospatial fields in {es} have certain restrictions that need to be addressed before upload. On this section a few recipes will be presented to help troubleshooting common issues on this type of data. + +[float] +==== Convert to GeoJSON or Shapefile + +Use https://gdal.org/programs/ogr2ogr.html[ogr2ogr] (part of the https://gdal.org[GDAL/OGR] suite) to convert datasets into a GeoJSON or Esri Shapefile. For example, use the following commands to convert a GPX file into JSON: + +[source,sh] +---- +# Example GPX file from https://www.topografix.com/gpx_sample_files.asp +# +# Convert the GPX waypoints layer into a GeoJSON file +$ ogr2ogr \ + -f GeoJSON "waypoints.geo.json" \ # Output format and file name + "fells_loop.gpx" \ # Input File Name + "waypoints" # Input Layer (usually same as file name) + +# Extract the routes layer into a GeoJSON file +$ ogr2ogr -f "GeoJSON" "routes.geo.json" "fells_loop.gpx" "routes" +---- + +[float] +==== Convert to WGS84 Coordinate Reference System + +{es} only supports WGS84 Coordinate Reference System. Use `ogr2ogr` to convert from other coordinate systems to WGS84. + +On the following example, `ogr2ogr` transforms a shapefile from https://epsg.org/crs_4269/NAD83.html[NAD83] to https://epsg.org/crs_4326/WGS-84.html[WGS84]. The input CRS is detected automatically thanks to the `.prj` sidecar file in the source dataset. + +[source,sh] +---- +# Example NAD83 file from https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_county_5m.zip +# +# Convert the Census Counties shapefile to WGS84 (EPSG:4326) +$ ogr2ogr -f "Esri Shapefile" \ + "cb_2018_us_county_5m.4326.shp" \ # Output file + -t_srs "EPSG:4326" \ # EPSG:4326 is the code for WGS84 + "cb_2018_us_county_5m.shp" \ # Input file + "cb_2018_us_county_5m" # Input layer +---- + +[float] +==== Improve performance by breaking out complex geometries into one geometry per document + +Sometimes geospatial datasets are composed by a small amount of geometries that contain a very large amount of individual part geometries. A good example of this situation is on detailed world country boundaries datasets where records for countries like Canada or Philippines have hundreds of small island geometries. Depending on the final usage of a dataset, you may want to break out this type of dataset to keep one geometry per document, considerably increasing the performance of your index. + +[source,sh] +---- +# Example NAD83 file from www12.statcan.gc.ca/census-recensement/2011/geo/bound-limit/files-fichiers/2016/ler_000b16a_e.zip +# +# Check the number of input features +$ ogrinfo -summary ler_000b16a_e.shp ler_000b16a_e \ + | grep "Feature Count" +Feature Count: 76 + +# Convert to WGS84 exploding the multiple geometries +$ ogr2ogr \ + -f "Esri Shapefile" \ + "ler_000b16a_e.4326-parts.shp" \ # Output file + -explodecollections \ # Convert multiparts into single records + -t_srs "EPSG:4326" \ # Transform to WGS84 + "ler_000b16a_e.shp" \ # Input file + "ler_000b16a_e" # Input layer + +# Check the number of geometries in the output file +# to confirm the 76 records are exploded into 27 thousand rows +$ ogrinfo -summary ler_000b16a_e.4326-parts.shp ler_000b16a_e.4326 \ + | grep "Feature Count" +Feature Count: 27059 +---- + +[WARNING] +==== +A dataset containing records with a very large amount of parts as the one from the example above may even hang in {kib} Maps file uploader. +==== + +[float] +==== Reduce the precision + +Some machine generated datasets are stored with more decimals than are strictly necessary. For reference, the GeoJSON RFC 7946 https://datatracker.ietf.org/doc/html/rfc7946#section-11.2[coordinate precision section] specifies six digits to be a common default to around 10 centimeters on the ground. The file uploader in the Maps application will automatically reduce the precision to 6 decimals but for big datasets it is better to do this before uploading. + +`ogr2ogr` generates GeoJSON files with 7 decimal degrees when requesting `RFC7946` compliant files but using the `COORDINATE_PRECISION` https://gdal.org/drivers/vector/geojson.html#layer-creation-options[GeoJSON layer creation option] it can be downsized even more if that is OK for the usage of the data. + +[source,sh] +---- +# Example NAD83 file from https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_county_5m.zip +# +# Generate a 2008 GeoJSON file +$ ogr2ogr \ + -f GeoJSON \ + "cb_2018_us_county_5m.4326.geo.json" \ # Output file + -t_srs "EPSG:4326" \ # Convert to WGS84 + -lco "RFC7946=NO" \ # Request a 2008 GeoJSON file + "cb_2018_us_county_5m.shp" \ + "cb_2018_us_county_5m" + +# Generate a RFC7946 GeoJSON file +$ ogr2ogr \ + -f GeoJSON \ + "cb_2018_us_county_5m.4326.RFC7946.geo.json" \ # Output file + -t_srs "EPSG:4326" \ # Convert to WGS84 + -lco "RFC7946=YES" \ # Request a RFC7946 GeoJSON file + "cb_2018_us_county_5m.shp" \ + "cb_2018_us_county_5m" + +# Generate a RFC7946 GeoJSON file with just 5 decimal figures +$ ogr2ogr \ + -f GeoJSON \ + "cb_2018_us_county_5m.4326.RFC7946_mini.geo.json" \ # Output file + -t_srs "EPSG:4326" \ # Convert to WGS84 + -lco "RFC7946=YES" \ # Request a RFC7946 GeoJSON file + -lco "COORDINATE_PRECISION=5" \ # Downsize to just 5 decimal positions + "cb_2018_us_county_5m.shp" \ + "cb_2018_us_county_5m" + +# Compare the disk size of the three output files +$ du -h cb_2018_us_county_5m.4326*.geo.json +7,4M cb_2018_us_county_5m.4326.geo.json +6,7M cb_2018_us_county_5m.4326.RFC7946.geo.json +6,1M cb_2018_us_county_5m.4326.RFC7946_mini.geo.json +---- + + +[float] +==== Simplifying region datasets + +Region datasets are polygon datasets where the boundaries of the documents don't overlap. This is common for administrative boundaries, land usage, and other continuous datasets. This type of datasets has the special feature that any geospatial operation modifying the lines of the polygons needs to be applied in the same way to the common sides of the polygons to avoid the generation of thin gap and overlap artifacts. + +https://github.com/mbloch/mapshaper[`mapshaper`] is an excellent tool to work with this type of datasets as it understands datasets of this nature and works with them accordingly. + +Depending on the usage of a region dataset, different geospatial precisions may be adequate. A world countries dataset that is displayed for the entire planet does not need the same precision as a map of the countries in the South Asian continent. + +`mapshaper` offers a https://github.com/mbloch/mapshaper/wiki/Command-Reference#-simplify[`simplify`] command that accepts percentages, resolutions, and different simplification algorithms. + +[source,sh] +---- +# Example NAD83 file from https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_county_5m.zip +# +# Generate a baseline GeoJSON file from OGR +$ ogr2ogr \ + -f GeoJSON "cb_2018_us_county_5m.ogr.geo.json" \ + -t_srs "EPSG:4326" \ + -lco RFC7946=YES \ + "cb_2018_us_county_5m.shp" \ + "cb_2018_us_county_5m" + +# Simplify at different percentages with mapshaper +$ for pct in 10 50 75 99; do \ + mapshaper \ + -i "cb_2018_us_county_5m.shp" \ # Input file + -proj "EPSG:4326" \ # Output projection + -simplify "${pct}%" \ # Simplification + -o cb_2018_us_county_5m.mapshaper_${pct}.geo.json; \ # Output file + done + +# Compare the size of the output files +$ du -h cb_2018_us_county_5m*.geo.json +2,0M cb_2018_us_county_5m.mapshaper_10.geo.json +4,1M cb_2018_us_county_5m.mapshaper_50.geo.json +5,3M cb_2018_us_county_5m.mapshaper_75.geo.json +6,7M cb_2018_us_county_5m.mapshaper_99.geo.json +6,7M cb_2018_us_county_5m.ogr.geo.json +---- + + +[float] +==== Fixing incorrect geometries + +The Maps application expects valid GeoJSON or Shapefile datasets. Apart from the mentioned CRS requirement, geometries need to be valid. Both `ogr2ogr` and `mapshaper` have options to try to fix invalid geometries: + +* OGR https://gdal.org/programs/ogr2ogr.html#cmdoption-ogr2ogr-makevalid[`-makevalid`] option +* Mapshaper https://github.com/mbloch/mapshaper/wiki/Command-Reference#-clean[`-clean`] command + + +[float] +==== And so much more + +`ogr2ogr` and `mapshaper` are excellent geospatial ETL (Extract Transform and Load) utilities that can do much more than viewed here. Reading the documentation in detail is worth investment to improve the quality of the datasets by removing unwanted fields, refining data types, validating value domains, etc. Finally, being command line utilities, both can be automated and added to QA pipelines. diff --git a/docs/maps/index.asciidoc b/docs/maps/index.asciidoc index f924e60cce9e6..6023cbef8a91d 100644 --- a/docs/maps/index.asciidoc +++ b/docs/maps/index.asciidoc @@ -64,5 +64,6 @@ include::search.asciidoc[] include::map-settings.asciidoc[] include::connect-to-ems.asciidoc[] include::import-geospatial-data.asciidoc[] +include::clean-data.asciidoc[] include::indexing-geojson-data-tutorial.asciidoc[] include::trouble-shooting.asciidoc[] diff --git a/docs/maps/trouble-shooting.asciidoc b/docs/maps/trouble-shooting.asciidoc index 3e4a6dfb42dc1..ebb7ec2a65aa8 100644 --- a/docs/maps/trouble-shooting.asciidoc +++ b/docs/maps/trouble-shooting.asciidoc @@ -53,4 +53,4 @@ Increase <> for large data views. [float] ==== Custom tiles are not displayed * When using a custom tile service, ensure your tile server has configured https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[Cross-Origin Resource Sharing (CORS)] so tile requests from your {kib} domain have permission to access your tile server domain. -* Ensure custom vector and tile services have the required coordinate system. Vector data must use EPSG:4326 and tiles must use EPSG:3857. \ No newline at end of file +* Ensure custom vector and tile services have the required coordinate system. Vector data must use EPSG:4326 and tiles must use EPSG:3857. diff --git a/package.json b/package.json index 560ca41044a2d..b74319521bd02 100644 --- a/package.json +++ b/package.json @@ -422,6 +422,7 @@ "@kbn/es-ui-shared-plugin": "link:src/plugins/es_ui_shared", "@kbn/eso-model-version-example": "link:examples/eso_model_version_example", "@kbn/eso-plugin": "link:x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin", + "@kbn/esql-utils": "link:packages/kbn-esql-utils", "@kbn/event-annotation-common": "link:packages/kbn-event-annotation-common", "@kbn/event-annotation-components": "link:packages/kbn-event-annotation-components", "@kbn/event-annotation-listing-plugin": "link:src/plugins/event_annotation_listing", @@ -549,6 +550,7 @@ "@kbn/metrics-data-access-plugin": "link:x-pack/plugins/metrics_data_access", "@kbn/ml-agg-utils": "link:x-pack/packages/ml/agg_utils", "@kbn/ml-anomaly-utils": "link:x-pack/packages/ml/anomaly_utils", + "@kbn/ml-cancellable-search": "link:x-pack/packages/ml/cancellable_search", "@kbn/ml-category-validator": "link:x-pack/packages/ml/category_validator", "@kbn/ml-chi2test": "link:x-pack/packages/ml/chi2test", "@kbn/ml-creation-wizard-utils": "link:x-pack/packages/ml/creation_wizard_utils", @@ -925,7 +927,7 @@ "css-box-model": "^1.2.1", "css.escape": "^1.5.1", "cuid": "^2.1.8", - "cypress-data-session": "^2.7.0", + "cypress-data-session": "^2.8.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", "d3": "3.5.17", @@ -1157,10 +1159,10 @@ "@babel/types": "^7.21.2", "@bazel/ibazel": "^0.16.2", "@bazel/typescript": "4.6.2", - "@cypress/code-coverage": "^3.10.0", - "@cypress/grep": "^3.1.5", + "@cypress/code-coverage": "^3.12.18", + "@cypress/grep": "^4.0.1", "@cypress/snapshot": "^2.1.7", - "@cypress/webpack-preprocessor": "^5.12.2", + "@cypress/webpack-preprocessor": "^6.0.1", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/makelogs": "^6.1.1", "@elastic/synthetics": "^1.5.0", @@ -1528,11 +1530,11 @@ "cssnano": "^5.1.12", "cssnano-preset-default": "^5.2.12", "csstype": "^3.0.2", - "cypress": "^13.3.0", + "cypress": "^13.6.3", "cypress-axe": "^1.5.0", "cypress-file-upload": "^5.0.8", - "cypress-multi-reporters": "^1.6.3", - "cypress-real-events": "^1.10.3", + "cypress-multi-reporters": "^1.6.4", + "cypress-real-events": "^1.11.0", "cypress-recurse": "^1.35.2", "date-fns": "^2.29.3", "debug": "^2.6.9", @@ -1546,7 +1548,7 @@ "eslint-config-prettier": "^9.0.0", "eslint-module-utils": "^2.8.0", "eslint-plugin-ban": "^1.6.0", - "eslint-plugin-cypress": "^2.14.0", + "eslint-plugin-cypress": "^2.15.1", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.28.0", "eslint-plugin-jest": "^27.2.3", @@ -1565,7 +1567,7 @@ "faker": "^5.1.0", "fetch-mock": "^7.3.9", "file-loader": "^4.2.0", - "find-cypress-specs": "^1.35.1", + "find-cypress-specs": "^1.41.4", "form-data": "^4.0.0", "geckodriver": "^4.3.0", "gulp-brotli": "^3.0.0", diff --git a/packages/deeplinks/ml/deep_links.ts b/packages/deeplinks/ml/deep_links.ts index 1c16543512b09..49b644c1ead64 100644 --- a/packages/deeplinks/ml/deep_links.ts +++ b/packages/deeplinks/ml/deep_links.ts @@ -27,6 +27,7 @@ export type LinkId = | 'nodesOverview' | 'nodes' | 'memoryUsage' + | 'esqlDataVisualizer' | 'dataVisualizer' | 'fileUpload' | 'indexDataVisualizer' diff --git a/packages/default-nav/ml/default_navigation.ts b/packages/default-nav/ml/default_navigation.ts index f82ec721b22ee..cc00154907e73 100644 --- a/packages/default-nav/ml/default_navigation.ts +++ b/packages/default-nav/ml/default_navigation.ts @@ -121,6 +121,15 @@ export const defaultNavigation: MlNodeDefinition = { ); }, }, + { + title: i18n.translate('defaultNavigation.ml.esqlDataVisualizer', { + defaultMessage: 'ES|QL', + }), + link: 'ml:esqlDataVisualizer', + getIsActive: ({ pathNameSerialized, prepend }) => { + return pathNameSerialized.includes(prepend('/app/ml/datavisualizer/esql')); + }, + }, { title: i18n.translate('defaultNavigation.ml.dataComparison', { defaultMessage: 'Data drift', diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts index fb9277abb884d..bc8150356e039 100644 --- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts +++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/security_schema.ts @@ -122,6 +122,7 @@ const SecurityAlertOptional = rt.partial({ 'ecs.version': schemaString, 'event.action': schemaString, 'event.kind': schemaString, + 'host.asset.criticality': schemaString, 'kibana.alert.action_group': schemaString, 'kibana.alert.ancestors.rule': schemaString, 'kibana.alert.building_block_type': schemaString, @@ -204,6 +205,7 @@ const SecurityAlertOptional = rt.partial({ 'kibana.alert.workflow_user': schemaString, 'kibana.version': schemaString, tags: schemaStringArray, + 'user.asset.criticality': schemaString, }); // prettier-ignore diff --git a/packages/kbn-es-query/tsconfig.json b/packages/kbn-es-query/tsconfig.json index 9dda3fcbcc2cf..e7bd8b3c037d5 100644 --- a/packages/kbn-es-query/tsconfig.json +++ b/packages/kbn-es-query/tsconfig.json @@ -15,7 +15,7 @@ "kbn_references": [ "@kbn/utility-types", "@kbn/i18n", - "@kbn/safer-lodash-set", + "@kbn/safer-lodash-set" ], "exclude": [ "target/**/*", diff --git a/packages/kbn-esql-utils/README.md b/packages/kbn-esql-utils/README.md new file mode 100644 index 0000000000000..694c90b3416bd --- /dev/null +++ b/packages/kbn-esql-utils/README.md @@ -0,0 +1,4 @@ +# @kbn/esql-utils + +This package contains utilities for ES|QL. + diff --git a/packages/kbn-esql-utils/index.ts b/packages/kbn-esql-utils/index.ts new file mode 100644 index 0000000000000..e478dbbf32d95 --- /dev/null +++ b/packages/kbn-esql-utils/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getESQLAdHocDataview } from './src'; diff --git a/packages/kbn-esql-utils/jest.config.js b/packages/kbn-esql-utils/jest.config.js new file mode 100644 index 0000000000000..32340a902d76b --- /dev/null +++ b/packages/kbn-esql-utils/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-esql-utils'], +}; diff --git a/packages/kbn-esql-utils/kibana.jsonc b/packages/kbn-esql-utils/kibana.jsonc new file mode 100644 index 0000000000000..4dd00764681a5 --- /dev/null +++ b/packages/kbn-esql-utils/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/esql-utils", + "owner": "@elastic/kibana-visualizations" +} diff --git a/packages/kbn-esql-utils/package.json b/packages/kbn-esql-utils/package.json new file mode 100644 index 0000000000000..57425f11e94e7 --- /dev/null +++ b/packages/kbn-esql-utils/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/esql-utils", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "sideEffects": false +} \ No newline at end of file diff --git a/packages/kbn-esql-utils/src/index.ts b/packages/kbn-esql-utils/src/index.ts new file mode 100644 index 0000000000000..a50ff5c59e798 --- /dev/null +++ b/packages/kbn-esql-utils/src/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './utils'; diff --git a/packages/kbn-esql-utils/src/utils/index.ts b/packages/kbn-esql-utils/src/utils/index.ts new file mode 100644 index 0000000000000..7ac96e272fcf7 --- /dev/null +++ b/packages/kbn-esql-utils/src/utils/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; + +// uses browser sha256 method with fallback if unavailable +async function sha256(str: string) { + if (crypto.subtle) { + const enc = new TextEncoder(); + const hash = await crypto.subtle.digest('SHA-256', enc.encode(str)); + return Array.from(new Uint8Array(hash)) + .map((v) => v.toString(16).padStart(2, '0')) + .join(''); + } else { + const { sha256: sha256fn } = await import('./sha256'); + return sha256fn(str); + } +} + +// Some applications need to have a dataview to work properly with ES|QL queries +// This is a helper to create one. The id is constructed from the indexpattern. +// As there are no runtime fields or field formatters or default time fields +// the same adhoc dataview can be constructed/used. This comes with great advantages such +// as solving the problem descibed here https://github.com/elastic/kibana/issues/168131 +export async function getESQLAdHocDataview( + indexPattern: string, + dataViewsService: DataViewsPublicPluginStart +) { + return await dataViewsService.create({ + title: indexPattern, + id: await sha256(`esql-${indexPattern}`), + }); +} diff --git a/packages/kbn-esql-utils/src/utils/sha256.ts b/packages/kbn-esql-utils/src/utils/sha256.ts new file mode 100644 index 0000000000000..dea53f74eeb56 --- /dev/null +++ b/packages/kbn-esql-utils/src/utils/sha256.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Sha256 } from '@kbn/crypto-browser'; + +export const sha256 = async (str: string) => new Sha256().update(str).digest('hex'); diff --git a/packages/kbn-esql-utils/tsconfig.json b/packages/kbn-esql-utils/tsconfig.json new file mode 100644 index 0000000000000..2fe775fb7d586 --- /dev/null +++ b/packages/kbn-esql-utils/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/data-views-plugin", + "@kbn/crypto-browser", + ] +} diff --git a/packages/kbn-monaco/BUILD.bazel b/packages/kbn-monaco/BUILD.bazel index 232ebdb2ba39b..95833afd585b8 100644 --- a/packages/kbn-monaco/BUILD.bazel +++ b/packages/kbn-monaco/BUILD.bazel @@ -29,6 +29,7 @@ SHARED_DEPS = [ "@npm//antlr4ts", "@npm//monaco-editor", "@npm//monaco-yaml", + "@npm//js-levenshtein", ] webpack_cli( diff --git a/packages/kbn-monaco/src/esql/language.ts b/packages/kbn-monaco/src/esql/language.ts index 33a911b2272d6..c37e19fb81d93 100644 --- a/packages/kbn-monaco/src/esql/language.ts +++ b/packages/kbn-monaco/src/esql/language.ts @@ -111,4 +111,25 @@ export const ESQLLang: CustomLangModuleType = { }, }; }, + + getCodeActionProvider: (callbacks?: ESQLCallbacks): monaco.languages.CodeActionProvider => { + return { + async provideCodeActions( + model /** ITextModel*/, + range /** Range*/, + context /** CodeActionContext*/, + token /** CancellationToken*/ + ) { + const astAdapter = new ESQLAstAdapter( + (...uris) => workerProxyService.getWorker(uris), + callbacks + ); + const actions = await astAdapter.codeAction(model, range, context); + return { + actions, + dispose: () => {}, + }; + }, + }; + }, }; diff --git a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.test.ts b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.test.ts index 12a1db12392d5..c3597ba00c4e2 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.test.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.test.ts @@ -121,8 +121,8 @@ function getFunctionSignaturesByReturnType( } return true; }) - .map(({ builtin: isBuiltinFn, name, signatures, ...defRest }) => - isBuiltinFn ? `${name} $0` : `${name}($0)` + .map(({ type, name, signatures, ...defRest }) => + type === 'builtin' ? `${name} $0` : `${name}($0)` ); } diff --git a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.ts b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.ts index 80fdb22ee207a..4cf468febbfd0 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.ts @@ -37,7 +37,6 @@ import { import { collectVariables, excludeVariablesFromCurrentCommand } from '../shared/variables'; import type { AstProviderFn, - ESQLAst, ESQLAstItem, ESQLCommand, ESQLCommandMode, @@ -72,6 +71,7 @@ import { import { EDITOR_MARKER } from '../shared/constants'; import { getAstContext, removeMarkerArgFromArgsList } from '../shared/context'; import { + buildQueryUntilPreviousCommand, getFieldsByTypeHelper, getPolicyHelper, getSourcesHelper, @@ -187,7 +187,7 @@ export async function suggest( const astContext = getAstContext(innerText, ast, offset); // build the correct query to fetch the list of fields - const queryForFields = buildQueryForFields(ast, finalText); + const queryForFields = buildQueryUntilPreviousCommand(ast, finalText); const { getFieldsByType, getFieldsMap } = getFieldsByTypeRetriever( queryForFields, resourceRetriever @@ -260,11 +260,6 @@ export async function suggest( return []; } -export function buildQueryForFields(ast: ESQLAst, queryString: string) { - const prevCommand = ast[Math.max(ast.length - 2, 0)]; - return prevCommand ? queryString.substring(0, prevCommand.location.max + 1) : queryString; -} - function getFieldsByTypeRetriever(queryString: string, resourceRetriever?: ESQLCallbacks) { const helpers = getFieldsByTypeHelper(queryString, resourceRetriever); return { @@ -812,7 +807,7 @@ async function getBuiltinFunctionNextArgument( // technically another boolean value should be suggested, but it is a better experience // to actually suggest a wider set of fields/functions [ - finalType === 'boolean' && getFunctionDefinition(nodeArg.name)?.builtin + finalType === 'boolean' && getFunctionDefinition(nodeArg.name)?.type === 'builtin' ? 'any' : finalType, ], @@ -1013,7 +1008,7 @@ async function getFunctionArgsSuggestions( ? { ...suggestion, insertText: - hasMoreMandatoryArgs && !fnDefinition.builtin + hasMoreMandatoryArgs && fnDefinition.type !== 'builtin' ? `${suggestion.insertText},` : suggestion.insertText, } @@ -1023,7 +1018,8 @@ async function getFunctionArgsSuggestions( return suggestions.map(({ insertText, ...rest }) => ({ ...rest, - insertText: hasMoreMandatoryArgs && !fnDefinition.builtin ? `${insertText},` : insertText, + insertText: + hasMoreMandatoryArgs && fnDefinition.type !== 'builtin' ? `${insertText},` : insertText, })); } diff --git a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/factories.ts b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/factories.ts index 9fc026852f999..45837ed42a86f 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/factories.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/factories.ts @@ -18,7 +18,7 @@ import { CommandOptionsDefinition, CommandModeDefinition, } from '../definitions/types'; -import { getCommandDefinition } from '../shared/helpers'; +import { getCommandDefinition, shouldBeQuotedText } from '../shared/helpers'; import { buildDocumentation, buildFunctionDocumentation } from './documentation_util'; const allFunctions = statsAggregationFunctionDefinitions.concat(evalFunctionsDefinitions); @@ -28,11 +28,8 @@ export const TRIGGER_SUGGESTION_COMMAND = { id: 'editor.action.triggerSuggest', }; -function getSafeInsertText(text: string, { dashSupported }: { dashSupported?: boolean } = {}) { - if (dashSupported) { - return /[^a-zA-Z\d_\.@-]/.test(text) ? `\`${text}\`` : text; - } - return /[^a-zA-Z\d_\.@]/.test(text) ? `\`${text}\`` : text; +function getSafeInsertText(text: string, options: { dashSupported?: boolean } = {}) { + return shouldBeQuotedText(text, options) ? `\`${text}\`` : text; } export function getAutocompleteFunctionDefinition(fn: FunctionDefinition) { diff --git a/packages/kbn-monaco/src/esql/lib/ast/code_actions/index.test.ts b/packages/kbn-monaco/src/esql/lib/ast/code_actions/index.test.ts new file mode 100644 index 0000000000000..58d825ef68aea --- /dev/null +++ b/packages/kbn-monaco/src/esql/lib/ast/code_actions/index.test.ts @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EditorError } from '../../../../types'; +import { CharStreams } from 'antlr4ts'; +import { getActions } from '.'; +import { getParser, ROOT_STATEMENT } from '../../antlr_facade'; +import { ESQLErrorListener } from '../../monaco/esql_error_listener'; +import { AstListener } from '../ast_factory'; +import { wrapAsMonacoMessage } from '../shared/monaco_utils'; +import { ESQLAst } from '../types'; +import { validateAst } from '../validation/validation'; +import { monaco } from '../../../../monaco_imports'; +import { getAllFunctions } from '../shared/helpers'; + +function getCallbackMocks() { + return { + getFieldsFor: jest.fn(async ({ query }) => + /enrich/.test(query) + ? [ + { name: 'otherField', type: 'string' }, + { name: 'yetAnotherField', type: 'number' }, + ] + : /unsupported_index/.test(query) + ? [{ name: 'unsupported_field', type: 'unsupported' }] + : [ + ...['string', 'number', 'date', 'boolean', 'ip'].map((type) => ({ + name: `${type}Field`, + type, + })), + { name: 'geoPointField', type: 'geo_point' }, + { name: 'any#Char$Field', type: 'number' }, + { name: 'kubernetes.something.something', type: 'number' }, + { + name: `listField`, + type: `list`, + }, + { name: '@timestamp', type: 'date' }, + ] + ), + getSources: jest.fn(async () => + ['index', '.secretIndex', 'my-index'].map((name) => ({ + name, + hidden: name.startsWith('.'), + })) + ), + getPolicies: jest.fn(async () => [ + { + name: 'policy', + sourceIndices: ['enrichIndex1'], + matchField: 'otherStringField', + enrichFields: ['other-field', 'yetAnotherField'], + }, + { + name: 'policy[]', + sourceIndices: ['enrichIndex1'], + matchField: 'otherStringField', + enrichFields: ['other-field', 'yetAnotherField'], + }, + ]), + }; +} + +const getAstAndErrors = async ( + text: string | undefined +): Promise<{ + errors: EditorError[]; + ast: ESQLAst; +}> => { + if (text == null) { + return { ast: [], errors: [] }; + } + const errorListener = new ESQLErrorListener(); + const parseListener = new AstListener(); + const parser = getParser(CharStreams.fromString(text), errorListener, parseListener); + + parser[ROOT_STATEMENT](); + + return { ...parseListener.getAst(), errors: errorListener.getErrors() }; +}; + +function createModelAndRange(text: string) { + return { + model: { getValue: () => text } as monaco.editor.ITextModel, + range: {} as monaco.Range, + }; +} + +function createMonacoContext(errors: EditorError[]): monaco.languages.CodeActionContext { + return { + markers: errors, + trigger: 1, + }; +} + +/** + * There are different wats to test the code here: one is a direct unit test of the feature, another is + * an integration test passing from the query statement validation. The latter is more realistic, but + * a little bit more tricky to setup. This function will encapsulate all the complexity + */ +function testQuickFixesFn( + statement: string, + expectedFixes: string[] = [], + options: { equalityCheck?: 'include' | 'equal' } = {}, + { only, skip }: { only?: boolean; skip?: boolean } = {} +) { + const testFn = only ? it.only : skip ? it.skip : it; + const { model, range } = createModelAndRange(statement); + testFn(`${statement} => ["${expectedFixes.join('","')}"]`, async () => { + const callbackMocks = getCallbackMocks(); + const { errors } = await validateAst(statement, getAstAndErrors, callbackMocks); + + const monacoErrors = wrapAsMonacoMessage('error', statement, errors); + const context = createMonacoContext(monacoErrors); + const actions = await getActions(model, range, context, getAstAndErrors, callbackMocks); + const edits = actions.map( + ({ edit }) => (edit?.edits[0] as monaco.languages.IWorkspaceTextEdit).textEdit.text + ); + expect(edits).toEqual( + !options || !options.equalityCheck || options.equalityCheck === 'equal' + ? expectedFixes + : expect.arrayContaining(expectedFixes) + ); + }); +} + +type TestArgs = [string, string[], { equalityCheck?: 'include' | 'equal' }]; + +// Make only and skip work with our custom wrapper +const testQuickFixes = Object.assign(testQuickFixesFn, { + skip: (...args: TestArgs) => { + const paddingArgs = ['equal'].slice(args.length - 2); + return testQuickFixesFn(...((args.length > 1 ? [...args, ...paddingArgs] : args) as TestArgs), { + skip: true, + }); + }, + only: (...args: TestArgs) => { + const paddingArgs = ['equal'].slice(args.length - 2); + return testQuickFixesFn(...((args.length > 1 ? [...args, ...paddingArgs] : args) as TestArgs), { + only: true, + }); + }, +}); + +describe('quick fixes logic', () => { + describe('fixing index spellchecks', () => { + // No error, no quick action + testQuickFixes('FROM index', []); + testQuickFixes('FROM index2', ['index']); + testQuickFixes('FROM myindex', ['index', 'my-index']); + // wildcards + testQuickFixes('FROM index*', []); + testQuickFixes('FROM ind*', []); + testQuickFixes('FROM end*', ['ind*']); + testQuickFixes('FROM endex*', ['index']); + // Too far for the levenstein distance and should not fix with a hidden index + testQuickFixes('FROM secretIndex', []); + testQuickFixes('FROM secretIndex2', []); + }); + + describe('fixing fields spellchecks', () => { + for (const command of ['KEEP', 'DROP', 'EVAL']) { + testQuickFixes(`FROM index | ${command} stringField`, []); + // strongField => stringField + testQuickFixes(`FROM index | ${command} strongField`, ['stringField']); + testQuickFixes(`FROM index | ${command} numberField, strongField`, ['stringField']); + } + testQuickFixes(`FROM index | EVAL round(strongField)`, ['stringField']); + testQuickFixes(`FROM index | EVAL var0 = round(strongField)`, ['stringField']); + testQuickFixes(`FROM index | WHERE round(strongField) > 0`, ['stringField']); + testQuickFixes(`FROM index | WHERE 0 < round(strongField)`, ['stringField']); + testQuickFixes(`FROM index | RENAME strongField as newField`, ['stringField']); + // This levarage the knowledge of the enrich policy fields to suggest the right field + testQuickFixes(`FROM index | ENRICH policy | KEEP yetAnotherField2`, ['yetAnotherField']); + testQuickFixes(`FROM index | ENRICH policy ON strongField`, ['stringField']); + testQuickFixes(`FROM index | ENRICH policy ON stringField WITH yetAnotherField2`, [ + 'yetAnotherField', + ]); + }); + + describe('fixing policies spellchecks', () => { + testQuickFixes(`FROM index | ENRICH poli`, ['policy']); + testQuickFixes(`FROM index | ENRICH mypolicy`, ['policy']); + testQuickFixes(`FROM index | ENRICH policy[`, ['policy', 'policy[]']); + }); + + describe('fixing function spellchecks', () => { + function toFunctionSignature(name: string) { + return `${name}()`; + } + // it should be strange enough to make the function invalid + const BROKEN_PREFIX = 'Q'; + for (const fn of getAllFunctions({ type: 'eval' })) { + // add an A to the function name to make it invalid + testQuickFixes( + `FROM index | EVAL ${BROKEN_PREFIX}${fn.name}()`, + [fn.name].map(toFunctionSignature), + { equalityCheck: 'include' } + ); + testQuickFixes( + `FROM index | EVAL var0 = ${BROKEN_PREFIX}${fn.name}()`, + [fn.name].map(toFunctionSignature), + { equalityCheck: 'include' } + ); + testQuickFixes( + `FROM index | STATS avg(${BROKEN_PREFIX}${fn.name}())`, + [fn.name].map(toFunctionSignature), + { equalityCheck: 'include' } + ); + testQuickFixes( + `FROM index | STATS avg(numberField) BY ${BROKEN_PREFIX}${fn.name}()`, + [fn.name].map(toFunctionSignature), + { equalityCheck: 'include' } + ); + testQuickFixes( + `FROM index | STATS avg(numberField) BY var0 = ${BROKEN_PREFIX}${fn.name}()`, + [fn.name].map(toFunctionSignature), + { equalityCheck: 'include' } + ); + } + for (const fn of getAllFunctions({ type: 'agg' })) { + // add an A to the function name to make it invalid + testQuickFixes( + `FROM index | STATS ${BROKEN_PREFIX}${fn.name}()`, + [fn.name].map(toFunctionSignature), + { equalityCheck: 'include' } + ); + testQuickFixes( + `FROM index | STATS var0 = ${BROKEN_PREFIX}${fn.name}()`, + [fn.name].map(toFunctionSignature), + { equalityCheck: 'include' } + ); + } + // it should preserve the arguments + testQuickFixes(`FROM index | EVAL rAund(numberField)`, ['round(numberField)'], { + equalityCheck: 'include', + }); + testQuickFixes(`FROM index | STATS AVVG(numberField)`, ['avg(numberField)'], { + equalityCheck: 'include', + }); + }); + + describe('fixing wrong quotes', () => { + testQuickFixes(`FROM index | WHERE stringField like 'asda'`, ['"asda"']); + testQuickFixes(`FROM index | WHERE stringField not like 'asda'`, ['"asda"']); + }); + + describe('fixing unquoted field names', () => { + testQuickFixes('FROM index | DROP any#Char$Field', ['`any#Char$Field`']); + testQuickFixes('FROM index | DROP numberField, any#Char$Field', ['`any#Char$Field`']); + }); +}); diff --git a/packages/kbn-monaco/src/esql/lib/ast/code_actions/index.ts b/packages/kbn-monaco/src/esql/lib/ast/code_actions/index.ts new file mode 100644 index 0000000000000..94079d1693b7f --- /dev/null +++ b/packages/kbn-monaco/src/esql/lib/ast/code_actions/index.ts @@ -0,0 +1,383 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { i18n } from '@kbn/i18n'; +import levenshtein from 'js-levenshtein'; +import type { monaco } from '../../../../monaco_imports'; +import { + getFieldsByTypeHelper, + getPolicyHelper, + getSourcesHelper, +} from '../shared/resources_helpers'; +import { getAllFunctions, isSourceItem, shouldBeQuotedText } from '../shared/helpers'; +import { ESQLCallbacks } from '../shared/types'; +import { AstProviderFn, ESQLAst } from '../types'; +import { buildQueryForFieldsFromSource } from '../validation/helpers'; + +type GetSourceFn = () => Promise; +type GetFieldsByTypeFn = (type: string | string[], ignored?: string[]) => Promise; +type GetPoliciesFn = () => Promise; +type GetPolicyFieldsFn = (name: string) => Promise; + +interface Callbacks { + getSources: GetSourceFn; + getFieldsByType: GetFieldsByTypeFn; + getPolicies: GetPoliciesFn; + getPolicyFields: GetPolicyFieldsFn; +} + +function getFieldsByTypeRetriever(queryString: string, resourceRetriever?: ESQLCallbacks) { + const helpers = getFieldsByTypeHelper(queryString, resourceRetriever); + return { + getFieldsByType: async (expectedType: string | string[] = 'any', ignored: string[] = []) => { + const fields = await helpers.getFieldsByType(expectedType, ignored); + return fields; + }, + getFieldsMap: helpers.getFieldsMap, + }; +} + +function getPolicyRetriever(resourceRetriever?: ESQLCallbacks) { + const helpers = getPolicyHelper(resourceRetriever); + return { + getPolicies: async () => { + const policies = await helpers.getPolicies(); + return policies.map(({ name }) => name); + }, + getPolicyFields: async (policy: string) => { + const metadata = await helpers.getPolicyMetadata(policy); + return metadata?.enrichFields || []; + }, + }; +} + +function getSourcesRetriever(resourceRetriever?: ESQLCallbacks) { + const helper = getSourcesHelper(resourceRetriever); + return async () => { + const list = (await helper()) || []; + // hide indexes that start with . + return list.filter(({ hidden }) => !hidden).map(({ name }) => name); + }; +} + +export const getCompatibleFunctionDefinitions = ( + command: string, + option: string | undefined, + returnTypes?: string[], + ignored: string[] = [] +): string[] => { + const fnSupportedByCommand = getAllFunctions({ type: ['eval', 'agg'] }).filter( + ({ name, supportedCommands, supportedOptions }) => + (option ? supportedOptions?.includes(option) : supportedCommands.includes(command)) && + !ignored.includes(name) + ); + if (!returnTypes) { + return fnSupportedByCommand.map(({ name }) => name); + } + return fnSupportedByCommand + .filter((mathDefinition) => + mathDefinition.signatures.some( + (signature) => returnTypes[0] === 'any' || returnTypes.includes(signature.returnType) + ) + ) + .map(({ name }) => name); +}; + +function createAction( + title: string, + solution: string, + error: monaco.editor.IMarkerData, + uri: monaco.Uri +) { + return { + title, + diagnostics: [error], + kind: 'quickfix', + edit: { + edits: [ + { + resource: uri, + textEdit: { + range: error, + text: solution, + }, + versionId: undefined, + }, + ], + }, + isPreferred: true, + }; +} + +async function getSpellingPossibilities(fn: () => Promise, errorText: string) { + const allPossibilities = await fn(); + const allSolutions = allPossibilities.reduce((solutions, item) => { + const distance = levenshtein(item, errorText); + if (distance < 3) { + solutions.push(item); + } + return solutions; + }, [] as string[]); + // filter duplicates + return Array.from(new Set(allSolutions)); +} + +async function getSpellingActionForColumns( + error: monaco.editor.IMarkerData, + uri: monaco.Uri, + queryString: string, + ast: ESQLAst, + { getFieldsByType, getPolicies, getPolicyFields }: Callbacks +) { + const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1); + // @TODO add variables support + const possibleFields = await getSpellingPossibilities(async () => { + const availableFields = await getFieldsByType('any'); + const enrichPolicies = ast.filter(({ name }) => name === 'enrich'); + if (enrichPolicies.length) { + const enrichPolicyNames = enrichPolicies.flatMap(({ args }) => + args.filter(isSourceItem).map(({ name }) => name) + ); + const enrichFields = await Promise.all(enrichPolicyNames.map(getPolicyFields)); + availableFields.push(...enrichFields.flat()); + } + return availableFields; + }, errorText); + return wrapIntoSpellingChangeAction(error, uri, possibleFields); +} + +async function getQuotableActionForColumns( + error: monaco.editor.IMarkerData, + uri: monaco.Uri, + queryString: string, + ast: ESQLAst, + { getFieldsByType }: Callbacks +) { + const commandEndIndex = ast.find((command) => command.location.max > error.endColumn)?.location + .max; + // the error received is unknwonColumn here, but look around the column to see if there's more + // which broke the grammar and the validation code couldn't identify as unquoted column + const remainingCommandText = queryString.substring( + error.endColumn - 1, + commandEndIndex ? commandEndIndex + 1 : undefined + ); + const stopIndex = Math.max( + /,/.test(remainingCommandText) + ? remainingCommandText.indexOf(',') + : /\s/.test(remainingCommandText) + ? remainingCommandText.indexOf(' ') + : remainingCommandText.length, + 0 + ); + const possibleUnquotedText = queryString.substring( + error.endColumn - 1, + error.endColumn + stopIndex + ); + const errorText = queryString + .substring(error.startColumn - 1, error.endColumn + possibleUnquotedText.length) + .trimEnd(); + const actions = []; + if (shouldBeQuotedText(errorText)) { + const availableFields = new Set(await getFieldsByType('any')); + const solution = `\`${errorText}\``; + if (availableFields.has(errorText) || availableFields.has(solution)) { + actions.push( + createAction( + i18n.translate('monaco.esql.quickfix.replaceWithSolution', { + defaultMessage: 'Did you mean {solution} ?', + values: { + solution, + }, + }), + solution, + { ...error, endColumn: error.startColumn + errorText.length }, // override the location + uri + ) + ); + } + } + return actions; +} + +async function getSpellingActionForIndex( + error: monaco.editor.IMarkerData, + uri: monaco.Uri, + queryString: string, + ast: ESQLAst, + { getSources }: Callbacks +) { + const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1); + const possibleSources = await getSpellingPossibilities(async () => { + // Handle fuzzy names via truncation to test levenstein distance + const sources = await getSources(); + if (errorText.endsWith('*')) { + return sources.map((source) => + source.length > errorText.length ? source.substring(0, errorText.length - 1) + '*' : source + ); + } + return sources; + }, errorText); + return wrapIntoSpellingChangeAction(error, uri, possibleSources); +} + +async function getSpellingActionForPolicies( + error: monaco.editor.IMarkerData, + uri: monaco.Uri, + queryString: string, + ast: ESQLAst, + { getPolicies }: Callbacks +) { + const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1); + const possiblePolicies = await getSpellingPossibilities(getPolicies, errorText); + return wrapIntoSpellingChangeAction(error, uri, possiblePolicies); +} + +async function getSpellingActionForFunctions( + error: monaco.editor.IMarkerData, + uri: monaco.Uri, + queryString: string, + ast: ESQLAst +) { + const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1); + // fallback to the last command if not found + const commandContext = + ast.find((command) => command.location.max > error.endColumn) || ast[ast.length - 1]; + if (!commandContext) { + return []; + } + const possibleSolutions = await getSpellingPossibilities( + async () => + getCompatibleFunctionDefinitions(commandContext.name, undefined).concat( + // support nested expressions in STATS + commandContext.name === 'stats' ? getCompatibleFunctionDefinitions('eval', undefined) : [] + ), + errorText.substring(0, errorText.lastIndexOf('(')).toLowerCase() // reduce a bit the distance check making al lowercase + ); + return wrapIntoSpellingChangeAction( + error, + uri, + possibleSolutions.map((fn) => `${fn}${errorText.substring(errorText.lastIndexOf('('))}`) + ); +} + +function wrapIntoSpellingChangeAction( + error: monaco.editor.IMarkerData, + uri: monaco.Uri, + possibleSolution: string[] +): monaco.languages.CodeAction[] { + return possibleSolution.map((solution) => + createAction( + // @TODO: workout why the tooltip is truncating the title here + i18n.translate('monaco.esql.quickfix.replaceWithSolution', { + defaultMessage: 'Did you mean {solution} ?', + values: { + solution, + }, + }), + solution, + error, + uri + ) + ); +} + +function inferCodeFromError(error: monaco.editor.IMarkerData & { owner?: string }) { + if (error.message.includes('missing STRING')) { + const [, value] = error.message.split('at '); + return value.startsWith("'") && value.endsWith("'") ? 'wrongQuotes' : undefined; + } +} + +export async function getActions( + model: monaco.editor.ITextModel, + range: monaco.Range, + context: monaco.languages.CodeActionContext, + astProvider: AstProviderFn, + resourceRetriever?: ESQLCallbacks +): Promise { + const actions: monaco.languages.CodeAction[] = []; + if (context.markers.length === 0) { + return actions; + } + const innerText = model.getValue(); + const { ast } = await astProvider(innerText); + + const queryForFields = buildQueryForFieldsFromSource(innerText, ast); + const { getFieldsByType } = getFieldsByTypeRetriever(queryForFields, resourceRetriever); + const getSources = getSourcesRetriever(resourceRetriever); + const { getPolicies, getPolicyFields } = getPolicyRetriever(resourceRetriever); + + const callbacks = { + getFieldsByType, + getSources, + getPolicies, + getPolicyFields, + }; + + // Markers are sent only on hover and are limited to the hovered area + // so unless there are multiple error/markers for the same area, there's just one + // in some cases, like syntax + semantic errors (i.e. unquoted fields eval field-1 ), there might be more than one + for (const error of context.markers) { + const code = error.code ?? inferCodeFromError(error); + switch (code) { + case 'unknownColumn': + const [columnsSpellChanges, columnsQuotedChanges] = await Promise.all([ + getSpellingActionForColumns(error, model.uri, innerText, ast, callbacks), + getQuotableActionForColumns(error, model.uri, innerText, ast, callbacks), + ]); + actions.push(...(columnsQuotedChanges.length ? columnsQuotedChanges : columnsSpellChanges)); + break; + case 'unknownIndex': + const indexSpellChanges = await getSpellingActionForIndex( + error, + model.uri, + innerText, + ast, + callbacks + ); + actions.push(...indexSpellChanges); + break; + case 'unknownPolicy': + const policySpellChanges = await getSpellingActionForPolicies( + error, + model.uri, + innerText, + ast, + callbacks + ); + actions.push(...policySpellChanges); + break; + case 'unknownFunction': + const fnsSpellChanges = await getSpellingActionForFunctions( + error, + model.uri, + innerText, + ast + ); + actions.push(...fnsSpellChanges); + break; + case 'wrongQuotes': + // it is a syntax error, so location won't be helpful here + const [, errorText] = error.message.split('at '); + actions.push( + createAction( + i18n.translate('monaco.esql.quickfix.replaceWithQuote', { + defaultMessage: 'Change quote to " (double)', + }), + errorText.replaceAll("'", '"'), + // override the location + { ...error, endColumn: error.startColumn + errorText.length }, + model.uri + ) + ); + break; + default: + break; + } + } + return actions; +} diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/aggs.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/aggs.ts index 89d6e8a666d2d..13e12f4504965 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/aggs.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/aggs.ts @@ -21,6 +21,7 @@ function createNumericAggDefinition({ const extraParamsExample = args.length ? `, ${args.map(({ value }) => value).join(',')}` : ''; return { name, + type: 'agg', description, supportedCommands: ['stats'], signatures: [ @@ -93,6 +94,7 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [ .concat([ { name: 'count', + type: 'agg', description: i18n.translate('monaco.esql.definitions.countDoc', { defaultMessage: 'Returns the count of the values in a field.', }), @@ -115,6 +117,7 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [ }, { name: 'count_distinct', + type: 'agg', description: i18n.translate('monaco.esql.definitions.countDistinctDoc', { defaultMessage: 'Returns the count of distinct values in a field.', }), @@ -132,6 +135,7 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [ }, { name: 'st_centroid', + type: 'agg', description: i18n.translate('monaco.esql.definitions.stCentroidDoc', { defaultMessage: 'Returns the count of distinct values in a field.', }), diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/builtin.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/builtin.ts index 8894e221143dd..870c028835e1f 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/builtin.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/builtin.ts @@ -14,9 +14,9 @@ function createMathDefinition( types: Array, description: string, warning?: FunctionDefinition['warning'] -) { +): FunctionDefinition { return { - builtin: true, + type: 'builtin', name, description, supportedCommands: ['eval', 'where', 'row'], @@ -52,9 +52,9 @@ function createComparisonDefinition( description: string; }, warning?: FunctionDefinition['warning'] -) { +): FunctionDefinition { return { - builtin: true, + type: 'builtin' as const, name, description, supportedCommands: ['eval', 'where', 'row'], @@ -113,18 +113,28 @@ export const builtinFunctions: FunctionDefinition[] = [ i18n.translate('monaco.esql.definition.divideDoc', { defaultMessage: 'Divide (/)', }), - (left, right) => { - if (right.type === 'literal' && right.literalType === 'number') { - return right.value === 0 - ? i18n.translate('monaco.esql.divide.warning.divideByZero', { - defaultMessage: 'Cannot divide by zero: {left}/{right}', - values: { - left: left.text, - right: right.value, - }, - }) - : undefined; + (fnDef) => { + const [left, right] = fnDef.args; + const messages = []; + if (!Array.isArray(left) && !Array.isArray(right)) { + if (right.type === 'literal' && right.literalType === 'number') { + if (right.value === 0) { + messages.push({ + type: 'warning' as const, + code: 'divideByZero', + text: i18n.translate('monaco.esql.divide.warning.divideByZero', { + defaultMessage: 'Cannot divide by zero: {left}/{right}', + values: { + left: left.text, + right: right.value, + }, + }), + location: fnDef.location, + }); + } + } } + return messages; } ), createMathDefinition( @@ -133,18 +143,28 @@ export const builtinFunctions: FunctionDefinition[] = [ i18n.translate('monaco.esql.definition.moduleDoc', { defaultMessage: 'Module (%)', }), - (left, right) => { - if (right.type === 'literal' && right.literalType === 'number') { - return right.value === 0 - ? i18n.translate('monaco.esql.divide.warning.zeroModule', { - defaultMessage: 'Module by zero can return null value: {left}/{right}', - values: { - left: left.text, - right: right.value, - }, - }) - : undefined; + (fnDef) => { + const [left, right] = fnDef.args; + const messages = []; + if (!Array.isArray(left) && !Array.isArray(right)) { + if (right.type === 'literal' && right.literalType === 'number') { + if (right.value === 0) { + messages.push({ + type: 'warning' as const, + code: 'moduleByZero', + text: i18n.translate('monaco.esql.divide.warning.zeroModule', { + defaultMessage: 'Module by zero can return null value: {left}/{right}', + values: { + left: left.text, + right: right.value, + }, + }), + location: fnDef.location, + }); + } + } } + return messages; } ), ...[ @@ -184,7 +204,7 @@ export const builtinFunctions: FunctionDefinition[] = [ defaultMessage: 'Greater than or equal to', }), }, - ].map((op) => createComparisonDefinition(op)), + ].map((op): FunctionDefinition => createComparisonDefinition(op)), ...[ // new special comparison operator for strings only { @@ -207,8 +227,8 @@ export const builtinFunctions: FunctionDefinition[] = [ }), }, { name: 'not_rlike', description: '' }, - ].map(({ name, description }) => ({ - builtin: true, + ].map(({ name, description }) => ({ + type: 'builtin' as const, ignoreAsSuggestion: /not/.test(name), name, description, @@ -233,8 +253,8 @@ export const builtinFunctions: FunctionDefinition[] = [ }), }, { name: 'not_in', description: '' }, - ].map(({ name, description }) => ({ - builtin: true, + ].map(({ name, description }) => ({ + type: 'builtin', ignoreAsSuggestion: /not/.test(name), name, description, @@ -284,7 +304,7 @@ export const builtinFunctions: FunctionDefinition[] = [ }), }, ].map(({ name, description }) => ({ - builtin: true, + type: 'builtin' as const, name, description, supportedCommands: ['eval', 'where', 'row'], @@ -300,7 +320,7 @@ export const builtinFunctions: FunctionDefinition[] = [ ], })), { - builtin: true, + type: 'builtin' as const, name: 'not', description: i18n.translate('monaco.esql.definition.notDoc', { defaultMessage: 'Not', @@ -315,7 +335,7 @@ export const builtinFunctions: FunctionDefinition[] = [ ], }, { - builtin: true, + type: 'builtin' as const, name: '=', description: i18n.translate('monaco.esql.definition.assignDoc', { defaultMessage: 'Assign (=)', @@ -334,6 +354,7 @@ export const builtinFunctions: FunctionDefinition[] = [ }, { name: 'functions', + type: 'builtin', description: i18n.translate('monaco.esql.definition.functionsDoc', { defaultMessage: 'Show ES|QL avaialble functions with signatures', }), @@ -347,6 +368,7 @@ export const builtinFunctions: FunctionDefinition[] = [ }, { name: 'info', + type: 'builtin', description: i18n.translate('monaco.esql.definition.infoDoc', { defaultMessage: 'Show information about the current ES node', }), diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/commands.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/commands.ts index 46b10a5837506..dcc9e6000d009 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/commands.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/commands.ts @@ -146,6 +146,7 @@ export const commandDefinitions: CommandDefinition[] = [ defaultMessage: 'PROJECT command is no longer supported, please use KEEP instead', }), type: 'warning', + code: 'projectCommandDeprecated', }); } return messages; @@ -174,6 +175,7 @@ export const commandDefinitions: CommandDefinition[] = [ defaultMessage: 'Removing all fields is not allowed [*]', }), type: 'error' as const, + code: 'dropAllColumnsError', })) ); } @@ -187,6 +189,7 @@ export const commandDefinitions: CommandDefinition[] = [ defaultMessage: 'Drop [@timestamp] will remove all time filters to the search results', }), type: 'warning', + code: 'dropTimestampWarning', }); } return messages; @@ -317,6 +320,7 @@ export const commandDefinitions: CommandDefinition[] = [ }, }), type: 'warning' as const, + code: 'duplicateSettingWarning', })) ); } diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/functions.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/functions.ts index f7919061f423c..a7da512c2a3dd 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/functions.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/functions.ts @@ -1052,4 +1052,5 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [ ...def, supportedCommands: ['eval', 'where', 'row'], supportedOptions: ['by'], + type: 'eval', })); diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/options.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/options.ts index 1204741212cff..76f3ca69b7112 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/options.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/options.ts @@ -99,6 +99,7 @@ export const appendSeparatorOption: CommandOptionsDefinition = { }, }), type: 'error', + code: 'wrongDissectOptionArgumentType', }); } return messages; diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/types.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/types.ts index 6d8aa34583031..c6ea2229b230a 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/types.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/types.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import type { ESQLCommand, ESQLCommandOption, ESQLMessage, ESQLSingleAstItem } from '../types'; +import type { ESQLCommand, ESQLCommandOption, ESQLFunction, ESQLMessage } from '../types'; export interface FunctionDefinition { - builtin?: boolean; + type: 'builtin' | 'agg' | 'eval'; ignoreAsSuggestion?: boolean; name: string; alias?: string[]; @@ -29,7 +29,7 @@ export interface FunctionDefinition { returnType: string; examples?: string[]; }>; - warning?: (...args: ESQLSingleAstItem[]) => string | undefined; + warning?: (fnDef: ESQLFunction) => ESQLMessage[]; } export interface CommandBaseDefinition { diff --git a/packages/kbn-monaco/src/esql/lib/ast/shared/context.ts b/packages/kbn-monaco/src/esql/lib/ast/shared/context.ts index 5295b50b22525..aecb9afb97db9 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/shared/context.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/shared/context.ts @@ -132,7 +132,7 @@ function isNotEnrichClauseAssigment(node: ESQLFunction, command: ESQLCommand) { return node.name !== '=' && command.name !== 'enrich'; } function isBuiltinFunction(node: ESQLFunction) { - return Boolean(getFunctionDefinition(node.name)?.builtin); + return getFunctionDefinition(node.name)?.type === 'builtin'; } export function getAstContext(innerText: string, ast: ESQLAst, offset: number) { diff --git a/packages/kbn-monaco/src/esql/lib/ast/shared/helpers.ts b/packages/kbn-monaco/src/esql/lib/ast/shared/helpers.ts index 0056689b524e9..b0a72c817ac18 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/shared/helpers.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/shared/helpers.ts @@ -166,6 +166,17 @@ export function isSupportedFunction( }; } +export function getAllFunctions(options?: { + type: Array | FunctionDefinition['type']; +}) { + const fns = buildFunctionLookup(); + if (!options?.type) { + return Array.from(fns.values()); + } + const types = new Set(Array.isArray(options.type) ? options.type : [options.type]); + return Array.from(fns.values()).filter((fn) => types.has(fn.type)); +} + export function getFunctionDefinition(name: string) { return buildFunctionLookup().get(name.toLowerCase()); } @@ -482,3 +493,10 @@ export function getLastCharFromTrimmed(text: string) { export function isRestartingExpression(text: string) { return getLastCharFromTrimmed(text) === ','; } + +export function shouldBeQuotedText( + text: string, + { dashSupported }: { dashSupported?: boolean } = {} +) { + return dashSupported ? /[^a-zA-Z\d_\.@-]/.test(text) : /[^a-zA-Z\d_\.@]/.test(text); +} diff --git a/packages/kbn-monaco/src/esql/lib/ast/shared/monaco_utils.ts b/packages/kbn-monaco/src/esql/lib/ast/shared/monaco_utils.ts new file mode 100644 index 0000000000000..4e7cf2cee7d8a --- /dev/null +++ b/packages/kbn-monaco/src/esql/lib/ast/shared/monaco_utils.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EditorError } from '../../../../types'; +import { monaco } from '../../../../monaco_imports'; +import { ESQLMessage } from '../types'; + +// from linear offset to Monaco position +export function offsetToRowColumn(expression: string, offset: number): monaco.Position { + const lines = expression.split(/\n/); + let remainingChars = offset; + let lineNumber = 1; + for (const line of lines) { + if (line.length >= remainingChars) { + return new monaco.Position(lineNumber, remainingChars + 1); + } + remainingChars -= line.length + 1; + lineNumber++; + } + + throw new Error('Algorithm failure'); +} + +export function wrapAsMonacoMessage( + type: 'error' | 'warning', + code: string, + messages: Array +): EditorError[] { + const fallbackPosition = { column: 0, lineNumber: 0 }; + return messages.map((e) => { + if ('severity' in e) { + return e; + } + const startPosition = e.location ? offsetToRowColumn(code, e.location.min) : fallbackPosition; + const endPosition = e.location + ? offsetToRowColumn(code, e.location.max || 0) + : fallbackPosition; + return { + message: e.text, + startColumn: startPosition.column, + startLineNumber: startPosition.lineNumber, + endColumn: endPosition.column + 1, + endLineNumber: endPosition.lineNumber, + severity: type === 'error' ? monaco.MarkerSeverity.Error : monaco.MarkerSeverity.Warning, + _source: 'client' as const, + code: e.code, + }; + }); +} diff --git a/packages/kbn-monaco/src/esql/lib/ast/shared/resources_helpers.ts b/packages/kbn-monaco/src/esql/lib/ast/shared/resources_helpers.ts index fcd4cbb0737ff..d4d5087e1464c 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/shared/resources_helpers.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/shared/resources_helpers.ts @@ -8,6 +8,12 @@ import type { ESQLCallbacks } from './types'; import type { ESQLRealField } from '../validation/types'; +import { ESQLAst } from '../types'; + +export function buildQueryUntilPreviousCommand(ast: ESQLAst, queryString: string) { + const prevCommand = ast[Math.max(ast.length - 2, 0)]; + return prevCommand ? queryString.substring(0, prevCommand.location.max + 1) : queryString; +} export function getFieldsByTypeHelper(queryText: string, resourceRetriever?: ESQLCallbacks) { const cacheFields = new Map(); diff --git a/packages/kbn-monaco/src/esql/lib/ast/types.ts b/packages/kbn-monaco/src/esql/lib/ast/types.ts index b15c02baee5fe..da5bff7551cad 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/types.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/types.ts @@ -85,6 +85,7 @@ export interface ESQLMessage { type: 'error' | 'warning'; text: string; location: ESQLLocation; + code: string; } export type AstProviderFn = ( diff --git a/packages/kbn-monaco/src/esql/lib/ast/validation/errors.ts b/packages/kbn-monaco/src/esql/lib/ast/validation/errors.ts index b68e9aa987051..f04d3d0eb9b22 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/validation/errors.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/validation/errors.ts @@ -232,13 +232,19 @@ export function getMessageFromId({ locations: ESQLLocation; }): ESQLMessage { const { message, type = 'error' } = getMessageAndTypeFromId(payload); - return createMessage(type, message, locations); + return createMessage(type, message, locations, payload.messageId); } -export function createMessage(type: 'error' | 'warning', message: string, location: ESQLLocation) { +export function createMessage( + type: 'error' | 'warning', + message: string, + location: ESQLLocation, + messageId: string +) { return { type, text: message, location, + code: messageId, }; } diff --git a/packages/kbn-monaco/src/esql/lib/ast/validation/validation.ts b/packages/kbn-monaco/src/esql/lib/ast/validation/validation.ts index 18b83db16f234..f8b1efe6fc53d 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/validation/validation.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/validation/validation.ts @@ -51,7 +51,7 @@ import type { ESQLSingleAstItem, ESQLSource, } from '../types'; -import { getMessageFromId, createMessage } from './errors'; +import { getMessageFromId } from './errors'; import type { ESQLRealField, ESQLVariable, ReferenceMaps, ValidationResult } from './types'; import type { ESQLCallbacks } from '../shared/types'; import { @@ -325,11 +325,9 @@ function validateFunction( } // check if the definition has some warning to show: if (fnDefinition.warning) { - const message = fnDefinition.warning( - ...(astFunction.args.filter((arg) => !Array.isArray(arg)) as ESQLSingleAstItem[]) - ); - if (message) { - messages.push(createMessage('warning', message, astFunction.location)); + const payloads = fnDefinition.warning(astFunction); + if (payloads.length) { + messages.push(...payloads); } } // at this point we're sure that at least one signature is matching diff --git a/packages/kbn-monaco/src/esql/lib/monaco/esql_ast_provider.ts b/packages/kbn-monaco/src/esql/lib/monaco/esql_ast_provider.ts index 39bd193d5611c..485c7d89ee832 100644 --- a/packages/kbn-monaco/src/esql/lib/monaco/esql_ast_provider.ts +++ b/packages/kbn-monaco/src/esql/lib/monaco/esql_ast_provider.ts @@ -6,57 +6,15 @@ * Side Public License, v 1. */ -import type { EditorError } from '../../../types'; import type { ESQLCallbacks } from '../ast/shared/types'; import { monaco } from '../../../monaco_imports'; import type { ESQLWorker } from '../../worker/esql_worker'; import { suggest } from '../ast/autocomplete/autocomplete'; import { getHoverItem } from '../ast/hover'; import { getSignatureHelp } from '../ast/signature'; -import type { ESQLMessage } from '../ast/types'; import { validateAst } from '../ast/validation/validation'; - -// from linear offset to Monaco position -export function offsetToRowColumn(expression: string, offset: number): monaco.Position { - const lines = expression.split(/\n/); - let remainingChars = offset; - let lineNumber = 1; - for (const line of lines) { - if (line.length >= remainingChars) { - return new monaco.Position(lineNumber, remainingChars + 1); - } - remainingChars -= line.length + 1; - lineNumber++; - } - - throw new Error('Algorithm failure'); -} - -function wrapAsMonacoMessage( - type: 'error' | 'warning', - code: string, - messages: Array -): EditorError[] { - const fallbackPosition = { column: 0, lineNumber: 0 }; - return messages.map((e) => { - if ('severity' in e) { - return e; - } - const startPosition = e.location ? offsetToRowColumn(code, e.location.min) : fallbackPosition; - const endPosition = e.location - ? offsetToRowColumn(code, e.location.max || 0) - : fallbackPosition; - return { - message: e.text, - startColumn: startPosition.column, - startLineNumber: startPosition.lineNumber, - endColumn: endPosition.column + 1, - endLineNumber: endPosition.lineNumber, - severity: type === 'error' ? monaco.MarkerSeverity.Error : monaco.MarkerSeverity.Warning, - _source: 'client' as const, - }; - }); -} +import { getActions } from '../ast/code_actions'; +import { wrapAsMonacoMessage } from '../ast/shared/monaco_utils'; export class ESQLAstAdapter { constructor( @@ -118,4 +76,14 @@ export class ESQLAstAdapter { })), }; } + + async codeAction( + model: monaco.editor.ITextModel, + range: monaco.Range, + context: monaco.languages.CodeActionContext + ) { + const getAstFn = await this.getAstWorker(model); + const codeActions = await getActions(model, range, context, getAstFn, this.callbacks); + return codeActions; + } } diff --git a/packages/kbn-monaco/src/monaco_imports.ts b/packages/kbn-monaco/src/monaco_imports.ts index f6f89990995eb..95f2fc44d857c 100644 --- a/packages/kbn-monaco/src/monaco_imports.ts +++ b/packages/kbn-monaco/src/monaco_imports.ts @@ -23,6 +23,13 @@ import 'monaco-editor/esm/vs/editor/contrib/hover/browser/hover.js'; // Needed f import 'monaco-editor/esm/vs/editor/contrib/parameterHints/browser/parameterHints.js'; // Needed for signature import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/browser/bracketMatching.js'; // Needed for brackets matching highlight +import 'monaco-editor/esm/vs/editor/contrib/codeAction/browser/codeAction.js'; +import 'monaco-editor/esm/vs/editor/contrib/codeAction/browser/codeActionCommands.js'; +import 'monaco-editor/esm/vs/editor/contrib/codeAction/browser/codeActionContributions.js'; +// import 'monaco-editor/esm/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.js'; +import 'monaco-editor/esm/vs/editor/contrib/codeAction/browser/codeActionMenu.js'; +import 'monaco-editor/esm/vs/editor/contrib/codeAction/browser/codeActionModel.js'; + import 'monaco-editor/esm/vs/language/json/monaco.contribution.js'; import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js'; // Needed for basic javascript support import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution.js'; // Needed for basic xml support diff --git a/packages/kbn-monaco/src/types.ts b/packages/kbn-monaco/src/types.ts index b2559fd919d16..e2268caab771f 100644 --- a/packages/kbn-monaco/src/types.ts +++ b/packages/kbn-monaco/src/types.ts @@ -32,6 +32,7 @@ export interface LanguageProvidersModule { getSuggestionProvider: (callbacks?: Deps) => monaco.languages.CompletionItemProvider; getSignatureProvider?: (callbacks?: Deps) => monaco.languages.SignatureHelpProvider; getHoverProvider?: (callbacks?: Deps) => monaco.languages.HoverProvider; + getCodeActionProvider?: (callbacks?: Deps) => monaco.languages.CodeActionProvider; } export interface CustomLangModuleType @@ -47,6 +48,7 @@ export interface EditorError { endLineNumber: number; endColumn: number; message: string; + code?: string | undefined; } export interface LangValidation { diff --git a/packages/kbn-resizable-layout/src/panels_resizable.tsx b/packages/kbn-resizable-layout/src/panels_resizable.tsx index 6c9bd12674b20..19855cc1792b9 100644 --- a/packages/kbn-resizable-layout/src/panels_resizable.tsx +++ b/packages/kbn-resizable-layout/src/panels_resizable.tsx @@ -18,7 +18,6 @@ import { css } from '@emotion/react'; import { isEqual, round } from 'lodash'; import type { ReactElement } from 'react'; import React, { useCallback, useEffect, useState } from 'react'; -import useLatest from 'react-use/lib/useLatest'; import { ResizableLayoutDirection } from '../types'; import { getContainerSize, percentToPixels, pixelsToPercent } from './utils'; @@ -70,7 +69,7 @@ export const PanelsResizable = ({ euiTheme.border.width.thin, (x) => x / 2 )}; - `; + `; const defaultButtonCss = css` z-index: 3; `; @@ -158,31 +157,27 @@ export const PanelsResizable = ({ if (trigger !== 'pointer') { return; } + setIsResizing(true); }, []); - // EUI will call an outdated version of this callback when the resize ends, - // so we need to make sure on our end that the latest version is called. - const onResizeEndStable = useLatest(() => { - setIsResizing((_isResizing) => { - // We don't want the resize button to retain focus after the resize is complete, - // but EuiResizableContainer will force focus it onClick. To work around this we - // use setTimeout to wait until after onClick has been called before blurring. - if (_isResizing) { - if (document.activeElement instanceof HTMLElement) { - const button = document.activeElement; - setTimeout(() => { - button.blur(); - }); - } - } - return false; - }); - }); - const onResizeEnd = useCallback(() => { - onResizeEndStable.current(); - }, [onResizeEndStable]); + if (!isResizing) { + return; + } + + // We don't want the resize button to retain focus after the resize is complete, + // but EuiResizableContainer will force focus it onClick. To work around this we + // use setTimeout to wait until after onClick has been called before blurring. + if (document.activeElement instanceof HTMLElement) { + const button = document.activeElement; + setTimeout(() => { + button.blur(); + }); + } + + setIsResizing(false); + }, [isResizing]); // Don't render EuiResizableContainer until we have have valid // panel sizes or it can cause the resize functionality to break. diff --git a/packages/kbn-search-connectors/types/native_connectors.ts b/packages/kbn-search-connectors/types/native_connectors.ts index 63c65d7c471a6..62ac7445e8b06 100644 --- a/packages/kbn-search-connectors/types/native_connectors.ts +++ b/packages/kbn-search-connectors/types/native_connectors.ts @@ -261,6 +261,12 @@ export const NATIVE_CONNECTOR_DEFINITIONS: Record/packages/kbn-test/src/jest/result_processors/logging_result_processor.js', }; diff --git a/packages/kbn-test/src/jest/result_processors/logging_result_processor.js b/packages/kbn-test/src/jest/result_processors/logging_result_processor.js new file mode 100644 index 0000000000000..b9b00dc8ee765 --- /dev/null +++ b/packages/kbn-test/src/jest/result_processors/logging_result_processor.js @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const FAILURE_MESSAGE_TRIGGERS = ['but is not defined anymore']; +const log = (...args) => { + const loggable = args.map((arg) => + typeof arg === 'string' ? arg : JSON.stringify(arg, null, 2) + ); + process.stdout.write(`${loggable.join(' ')}\n`, 'utf8'); +}; +/** + * This processor looks for specific errors, and logs the result context of test suites where they occur. + * @param results + * @returns {*} + */ +module.exports = (results) => { + const resultsThatMatchTriggers = results.testResults.filter( + (e) => + e.failureMessage && + FAILURE_MESSAGE_TRIGGERS.some((trigger) => e.failureMessage.includes(trigger)) + ); + + if (resultsThatMatchTriggers.length !== 0) { + log('The following test suites failed, with notable errors:'); + resultsThatMatchTriggers.forEach((e) => { + log(` -> ${e.testFilePath}`, 'Details: ', e, '\n'); + }); + } + + return results; +}; diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx index 3546fcec41af4..1814a850648fc 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx @@ -417,6 +417,11 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ [language, esqlCallbacks] ); + const codeActionProvider = useMemo( + () => (language === 'esql' ? ESQLLang.getCodeActionProvider?.(esqlCallbacks) : undefined), + [language, esqlCallbacks] + ); + const onErrorClick = useCallback(({ startLineNumber, startColumn }: MonacoMessage) => { if (!editor1.current) { return; @@ -541,6 +546,11 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ vertical: 'auto', }, overviewRulerBorder: false, + // this becomes confusing with multiple markers, so quick fixes + // will be proposed only within the tooltip + lightbulb: { + enabled: false, + }, readOnly: isLoading || isDisabled || @@ -776,6 +786,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ return hoverProvider?.provideHover(model, position, token); }, }} + codeActions={codeActionProvider} onChange={onQueryUpdate} editorDidMount={(editor) => { editor1.current = editor; diff --git a/packages/shared-ux/code_editor/impl/code_editor.tsx b/packages/shared-ux/code_editor/impl/code_editor.tsx index ca77fb3605f2f..b41906d5ed456 100644 --- a/packages/shared-ux/code_editor/impl/code_editor.tsx +++ b/packages/shared-ux/code_editor/impl/code_editor.tsx @@ -91,6 +91,13 @@ export interface CodeEditorProps { */ languageConfiguration?: monaco.languages.LanguageConfiguration; + /** + * CodeAction provider for code actions on markers feedback + * Documentation for the provider can be found here: + * https://microsoft.github.io/monaco-editor/docs.html#interfaces/languages.CodeActionProvider.html + */ + codeActions?: monaco.languages.CodeActionProvider; + /** * Function called before the editor is mounted in the view */ @@ -152,6 +159,7 @@ export const CodeEditor: React.FC = ({ hoverProvider, placeholder, languageConfiguration, + codeActions, 'aria-label': ariaLabel = i18n.translate('sharedUXPackages.codeEditor.ariaLabel', { defaultMessage: 'Code Editor', }), @@ -349,6 +357,10 @@ export const CodeEditor: React.FC = ({ if (languageConfiguration) { monaco.languages.setLanguageConfiguration(languageId, languageConfiguration); } + + if (codeActions) { + monaco.languages.registerCodeActionProvider(languageId, codeActions); + } }); // Register themes @@ -366,6 +378,7 @@ export const CodeEditor: React.FC = ({ suggestionProvider, signatureProvider, hoverProvider, + codeActions, languageConfiguration, ] ); diff --git a/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.ts b/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.ts index 09ac5f1e87686..257fa050ba83c 100644 --- a/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.ts +++ b/src/plugins/discover/public/application/main/utils/get_data_view_by_text_based_query_lang.ts @@ -10,6 +10,7 @@ import { getIndexPatternFromSQLQuery, getIndexPatternFromESQLQuery, } from '@kbn/es-query'; +import { getESQLAdHocDataview } from '@kbn/esql-utils'; import { DataView } from '@kbn/data-views-plugin/common'; import { DiscoverServices } from '../../../build_services'; @@ -32,9 +33,7 @@ export async function getDataViewByTextBasedQueryLang( currentDataView?.isPersisted() || indexPatternFromQuery !== currentDataView?.getIndexPattern() ) { - const dataViewObj = await services.dataViews.create({ - title: indexPatternFromQuery, - }); + const dataViewObj = await getESQLAdHocDataview(indexPatternFromQuery, services.dataViews); if (dataViewObj.fields.getByName('@timestamp')?.type === 'date') { dataViewObj.timeFieldName = '@timestamp'; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index def0cb5c85c02..bbb9d7495b2b6 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -81,6 +81,7 @@ "@kbn/shared-ux-button-toolbar", "@kbn/serverless", "@kbn/deeplinks-observability", + "@kbn/esql-utils", "@kbn/managed-content-badge" ], "exclude": ["target/**/*"] diff --git a/src/plugins/input_control_vis/public/deprecation_badge.ts b/src/plugins/input_control_vis/public/deprecation_badge.ts index 5c65301066800..d4da4709a0dc3 100644 --- a/src/plugins/input_control_vis/public/deprecation_badge.ts +++ b/src/plugins/input_control_vis/public/deprecation_badge.ts @@ -6,19 +6,36 @@ * Side Public License, v 1. */ +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { + apiCanAccessViewMode, + CanAccessViewMode, + EmbeddableApiContext, + getInheritedViewMode, + getViewModeSubject, +} from '@kbn/presentation-publishing'; import { Action } from '@kbn/ui-actions-plugin/public'; +import { apiHasVisualizeConfig, HasVisualizeConfig } from '@kbn/visualizations-plugin/public'; -import { Embeddable, ViewMode } from '@kbn/embeddable-plugin/public'; -import { i18n } from '@kbn/i18n'; -import { VisualizeInput } from '@kbn/visualizations-plugin/public'; +import { INPUT_CONTROL_VIS_TYPE } from './input_control_vis_type'; -export const ACTION_DEPRECATION_BADGE = 'ACTION_INPUT_CONTROL_DEPRECATION_BADGE'; +const ACTION_DEPRECATION_BADGE = 'ACTION_INPUT_CONTROL_DEPRECATION_BADGE'; -export interface DeprecationBadgeActionContext { - embeddable: Embeddable; -} +type InputControlDeprecationActionApi = CanAccessViewMode & HasVisualizeConfig; + +const isApiCompatible = (api: unknown | null): api is InputControlDeprecationActionApi => + Boolean(apiCanAccessViewMode(api) && apiHasVisualizeConfig(api)); + +const compatibilityCheck = (api: EmbeddableApiContext['embeddable']) => { + return ( + isApiCompatible(api) && + getInheritedViewMode(api) === ViewMode.EDIT && + api.getVis().type.name === INPUT_CONTROL_VIS_TYPE + ); +}; -export class InputControlDeprecationBadge implements Action { +export class InputControlDeprecationBadge implements Action { public id = ACTION_DEPRECATION_BADGE; public type = ACTION_DEPRECATION_BADGE; public disabled = true; @@ -40,11 +57,22 @@ export class InputControlDeprecationBadge implements Action) => void + ) { + if (!isApiCompatible(embeddable)) return; + return getViewModeSubject(embeddable)?.subscribe(() => { + onChange(compatibilityCheck(embeddable), this); + }); } public async execute() { diff --git a/src/plugins/input_control_vis/public/input_control_vis_type.ts b/src/plugins/input_control_vis/public/input_control_vis_type.ts index 875f87041e1e9..1a4f2614b117e 100644 --- a/src/plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/plugins/input_control_vis/public/input_control_vis_type.ts @@ -13,6 +13,8 @@ import { InputControlVisDependencies } from './plugin'; import { toExpressionAst } from './to_ast'; import { InputControlVisParams } from './types'; +export const INPUT_CONTROL_VIS_TYPE = 'input_control_vis'; + export function createInputControlVisTypeDefinition( deps: InputControlVisDependencies, readOnly: boolean @@ -20,7 +22,7 @@ export function createInputControlVisTypeDefinition( const ControlsTab = getControlsTab(deps); return { - name: 'input_control_vis', + name: INPUT_CONTROL_VIS_TYPE, title: i18n.translate('inputControl.register.controlsTitle', { defaultMessage: 'Input controls', }), diff --git a/src/plugins/input_control_vis/tsconfig.json b/src/plugins/input_control_vis/tsconfig.json index 876a37b925391..5e6020990edd3 100644 --- a/src/plugins/input_control_vis/tsconfig.json +++ b/src/plugins/input_control_vis/tsconfig.json @@ -25,6 +25,7 @@ "@kbn/config-schema", "@kbn/ui-actions-plugin", "@kbn/embeddable-plugin", + "@kbn/presentation-publishing", ], "exclude": [ "target/**/*", diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index b8324ce58ce6c..2d712e4a4c766 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -157,7 +157,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('with folded/unfolded lines in request body', () => { + // FLAKY: https://github.com/elastic/kibana/issues/152825 + describe.skip('with folded/unfolded lines in request body', () => { const enterRequest = async () => { await PageObjects.console.enterRequest('\nGET test/doc/1 \n{\n\t\t"_source": []'); await PageObjects.console.clickPlay(); diff --git a/tsconfig.base.json b/tsconfig.base.json index 7a354cc514608..9e7531586d5da 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -760,6 +760,8 @@ "@kbn/eso-model-version-example/*": ["examples/eso_model_version_example/*"], "@kbn/eso-plugin": ["x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin"], "@kbn/eso-plugin/*": ["x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin/*"], + "@kbn/esql-utils": ["packages/kbn-esql-utils"], + "@kbn/esql-utils/*": ["packages/kbn-esql-utils/*"], "@kbn/event-annotation-common": ["packages/kbn-event-annotation-common"], "@kbn/event-annotation-common/*": ["packages/kbn-event-annotation-common/*"], "@kbn/event-annotation-components": ["packages/kbn-event-annotation-components"], @@ -1052,6 +1054,8 @@ "@kbn/ml-agg-utils/*": ["x-pack/packages/ml/agg_utils/*"], "@kbn/ml-anomaly-utils": ["x-pack/packages/ml/anomaly_utils"], "@kbn/ml-anomaly-utils/*": ["x-pack/packages/ml/anomaly_utils/*"], + "@kbn/ml-cancellable-search": ["x-pack/packages/ml/cancellable_search"], + "@kbn/ml-cancellable-search/*": ["x-pack/packages/ml/cancellable_search/*"], "@kbn/ml-category-validator": ["x-pack/packages/ml/category_validator"], "@kbn/ml-category-validator/*": ["x-pack/packages/ml/category_validator/*"], "@kbn/ml-chi2test": ["x-pack/packages/ml/chi2test"], diff --git a/x-pack/examples/third_party_maps_source_example/public/classes/custom_raster_source.tsx b/x-pack/examples/third_party_maps_source_example/public/classes/custom_raster_source.tsx index a33b72652a26e..881b721e03a3c 100644 --- a/x-pack/examples/third_party_maps_source_example/public/classes/custom_raster_source.tsx +++ b/x-pack/examples/third_party_maps_source_example/public/classes/custom_raster_source.tsx @@ -130,10 +130,6 @@ export class CustomRasterSource implements IRasterSource { return []; } - isESSource(): boolean { - return false; - } - // Returns function used to format value async createFieldFormatter(field: IField): Promise { return null; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/get_evaluate_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/get_evaluate_route.gen.ts deleted file mode 100644 index 0a6281d69d109..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/get_evaluate_route.gen.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from 'zod'; - -/* - * NOTICE: Do not edit this file manually. - * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. - * - * info: - * title: Get Evaluate API endpoint - * version: 1 - */ - -export type GetEvaluateResponse = z.infer; -export const GetEvaluateResponse = z.object({ - agentExecutors: z.array(z.string()), -}); diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/get_evaluate_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/get_evaluate_route.schema.yaml deleted file mode 100644 index b0c0c218eb9ac..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/get_evaluate_route.schema.yaml +++ /dev/null @@ -1,40 +0,0 @@ -openapi: 3.0.0 -info: - title: Get Evaluate API endpoint - version: '1' -paths: - /internal/elastic_assistant/evaluate: - get: - operationId: GetEvaluate - x-codegen-enabled: true - description: Get relevant data for performing an evaluation like available sample data, agents, and evaluators - summary: Get relevant data for performing an evaluation - tags: - - Evaluation API - responses: - '200': - description: Successful response - content: - application/json: - schema: - type: object - properties: - agentExecutors: - type: array - items: - type: string - required: - - agentExecutors - '400': - description: Generic Error - content: - application/json: - schema: - type: object - properties: - statusCode: - type: number - error: - type: string - message: - type: string diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.gen.ts deleted file mode 100644 index d5d1177a9c16e..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.gen.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from 'zod'; - -/* - * NOTICE: Do not edit this file manually. - * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. - * - * info: - * title: Post Evaluate API endpoint - * version: 1 - */ - -export type OutputIndex = z.infer; -export const OutputIndex = z.string().regex(/^.kibana-elastic-ai-assistant-/); - -export type DatasetItem = z.infer; -export const DatasetItem = z.object({ - id: z.string().optional(), - input: z.string(), - prediction: z.string().optional(), - reference: z.string(), - tags: z.array(z.string()).optional(), -}); - -export type Dataset = z.infer; -export const Dataset = z.array(DatasetItem).default([]); - -export type PostEvaluateBody = z.infer; -export const PostEvaluateBody = z.object({ - dataset: Dataset.optional(), - evalPrompt: z.string().optional(), -}); - -export type PostEvaluateRequestQuery = z.infer; -export const PostEvaluateRequestQuery = z.object({ - /** - * Agents parameter description - */ - agents: z.string(), - /** - * Dataset Name parameter description - */ - datasetName: z.string().optional(), - /** - * Evaluation Type parameter description - */ - evaluationType: z.string().optional(), - /** - * Eval Model parameter description - */ - evalModel: z.string().optional(), - /** - * Models parameter description - */ - models: z.string(), - /** - * Output Index parameter description - */ - outputIndex: OutputIndex, - /** - * Project Name parameter description - */ - projectName: z.string().optional(), - /** - * Run Name parameter description - */ - runName: z.string().optional(), -}); -export type PostEvaluateRequestQueryInput = z.input; - -export type PostEvaluateRequestBody = z.infer; -export const PostEvaluateRequestBody = PostEvaluateBody; -export type PostEvaluateRequestBodyInput = z.input; - -export type PostEvaluateResponse = z.infer; -export const PostEvaluateResponse = z.object({ - evaluationId: z.string(), - success: z.boolean(), -}); diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.schema.yaml deleted file mode 100644 index 41a7230e85ac5..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.schema.yaml +++ /dev/null @@ -1,126 +0,0 @@ -openapi: 3.0.0 -info: - title: Post Evaluate API endpoint - version: '1' -paths: - /internal/elastic_assistant/evaluate: - post: - operationId: PostEvaluate - x-codegen-enabled: true - description: Perform an evaluation using sample data against a combination of Agents and Connectors - summary: Performs an evaluation of the Elastic Assistant - tags: - - Evaluation API - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PostEvaluateBody' - parameters: - - name: agents - in: query - description: Agents parameter description - required: true - schema: - type: string - - name: datasetName - in: query - description: Dataset Name parameter description - schema: - type: string - - name: evaluationType - in: query - description: Evaluation Type parameter description - schema: - type: string - - name: evalModel - in: query - description: Eval Model parameter description - schema: - type: string - - name: models - in: query - description: Models parameter description - required: true - schema: - type: string - - name: outputIndex - in: query - description: Output Index parameter description - required: true - schema: - $ref: '#/components/schemas/OutputIndex' - - name: projectName - in: query - description: Project Name parameter description - schema: - type: string - - name: runName - in: query - description: Run Name parameter description - schema: - type: string - responses: - '200': - description: Successful response - content: - application/json: - schema: - type: object - properties: - evaluationId: - type: string - success: - type: boolean - required: - - evaluationId - - success - '400': - description: Generic Error - content: - application/json: - schema: - type: object - properties: - statusCode: - type: number - error: - type: string - message: - type: string -components: - schemas: - OutputIndex: - type: string - pattern: '^.kibana-elastic-ai-assistant-' - DatasetItem: - type: object - properties: - id: - type: string - input: - type: string - prediction: - type: string - reference: - type: string - tags: - type: array - items: - type: string - required: - - input - - reference - Dataset: - type: array - items: - $ref: '#/components/schemas/DatasetItem' - default: [] - PostEvaluateBody: - type: object - properties: - dataset: - $ref: '#/components/schemas/Dataset' - evalPrompt: - type: string diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/index.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/index.ts deleted file mode 100644 index 4257cb9bae149..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// API versioning constants -export const API_VERSIONS = { - public: { - v1: '2023-10-31', - }, - internal: { - v1: '1', - }, -}; - -export const PUBLIC_API_ACCESS = 'public'; -export const INTERNAL_API_ACCESS = 'internal'; - -// Evaluation Schemas -export * from './evaluation/post_evaluate_route.gen'; -export * from './evaluation/get_evaluate_route.gen'; - -// Capabilities Schemas -export * from './capabilities/get_capabilities_route.gen'; diff --git a/x-pack/packages/kbn-elastic-assistant-common/index.ts b/x-pack/packages/kbn-elastic-assistant-common/index.ts index e285be395c71c..a2576038c6f51 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/index.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/index.ts @@ -5,8 +5,7 @@ * 2.0. */ -// Schema constants -export * from './impl/schemas'; +export { GetCapabilitiesResponse } from './impl/schemas/capabilities/get_capabilities_route.gen'; export { defaultAssistantFeatures } from './impl/capabilities'; export type { AssistantFeatures } from './impl/capabilities'; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.test.tsx index 26a37e12c4e53..4c71c1e63f8b3 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.test.tsx @@ -13,6 +13,7 @@ import { fetchConnectorExecuteAction, FetchConnectorExecuteAction, getKnowledgeBaseStatus, + postEvaluation, postKnowledgeBase, } from './api'; import type { Conversation, Message } from '../assistant_context/types'; @@ -339,4 +340,52 @@ describe('API tests', () => { await expect(deleteKnowledgeBase(knowledgeBaseArgs)).resolves.toThrowError('simulated error'); }); }); + + describe('postEvaluation', () => { + it('calls the knowledge base API when correct resource path', async () => { + (mockHttp.fetch as jest.Mock).mockResolvedValue({ success: true }); + const testProps = { + http: mockHttp, + evalParams: { + agents: ['not', 'alphabetical'], + dataset: '{}', + datasetName: 'Test Dataset', + projectName: 'Test Project Name', + runName: 'Test Run Name', + evalModel: ['not', 'alphabetical'], + evalPrompt: 'evalPrompt', + evaluationType: ['not', 'alphabetical'], + models: ['not', 'alphabetical'], + outputIndex: 'outputIndex', + }, + }; + + await postEvaluation(testProps); + + expect(mockHttp.fetch).toHaveBeenCalledWith('/internal/elastic_assistant/evaluate', { + method: 'POST', + body: '{"dataset":{},"evalPrompt":"evalPrompt"}', + headers: { 'Content-Type': 'application/json' }, + query: { + models: 'alphabetical,not', + agents: 'alphabetical,not', + datasetName: 'Test Dataset', + evaluationType: 'alphabetical,not', + evalModel: 'alphabetical,not', + outputIndex: 'outputIndex', + projectName: 'Test Project Name', + runName: 'Test Run Name', + }, + signal: undefined, + }); + }); + it('returns error when error is an error', async () => { + const error = 'simulated error'; + (mockHttp.fetch as jest.Mock).mockImplementation(() => { + throw new Error(error); + }); + + await expect(postEvaluation(knowledgeBaseArgs)).resolves.toThrowError('simulated error'); + }); + }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx index c18193c7fa0a6..f04b99c4e46e1 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx @@ -16,6 +16,7 @@ import { getOptionalRequestParams, hasParsableResponse, } from './helpers'; +import { PerformEvaluationParams } from './settings/evaluation_settings/use_perform_evaluation'; export interface FetchConnectorExecuteAction { isEnabledRAGAlerts: boolean; @@ -334,3 +335,61 @@ export const deleteKnowledgeBase = async ({ return error as IHttpFetchError; } }; + +export interface PostEvaluationParams { + http: HttpSetup; + evalParams?: PerformEvaluationParams; + signal?: AbortSignal | undefined; +} + +export interface PostEvaluationResponse { + evaluationId: string; + success: boolean; +} + +/** + * API call for evaluating models. + * + * @param {Object} options - The options object. + * @param {HttpSetup} options.http - HttpSetup + * @param {string} [options.evalParams] - Params necessary for evaluation + * @param {AbortSignal} [options.signal] - AbortSignal + * + * @returns {Promise} + */ +export const postEvaluation = async ({ + http, + evalParams, + signal, +}: PostEvaluationParams): Promise => { + try { + const path = `/internal/elastic_assistant/evaluate`; + const query = { + agents: evalParams?.agents.sort()?.join(','), + datasetName: evalParams?.datasetName, + evaluationType: evalParams?.evaluationType.sort()?.join(','), + evalModel: evalParams?.evalModel.sort()?.join(','), + outputIndex: evalParams?.outputIndex, + models: evalParams?.models.sort()?.join(','), + projectName: evalParams?.projectName, + runName: evalParams?.runName, + }; + + const response = await http.fetch(path, { + method: 'POST', + body: JSON.stringify({ + dataset: JSON.parse(evalParams?.dataset ?? '[]'), + evalPrompt: evalParams?.evalPrompt ?? '', + }), + headers: { + 'Content-Type': 'application/json', + }, + query, + signal, + }); + + return response as PostEvaluationResponse; + } catch (error) { + return error as IHttpFetchError; + } +}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/capabilities.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/capabilities.test.tsx index 30c113eb0e803..b41d7ac144554 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/capabilities.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/capabilities.test.tsx @@ -13,7 +13,7 @@ import { API_ERROR } from '../../translations'; jest.mock('@kbn/core-http-browser'); const mockHttp = { - get: jest.fn(), + fetch: jest.fn(), } as unknown as HttpSetup; describe('Capabilities API tests', () => { @@ -25,14 +25,15 @@ describe('Capabilities API tests', () => { it('calls the internal assistant API for fetching assistant capabilities', async () => { await getCapabilities({ http: mockHttp }); - expect(mockHttp.get).toHaveBeenCalledWith('/internal/elastic_assistant/capabilities', { + expect(mockHttp.fetch).toHaveBeenCalledWith('/internal/elastic_assistant/capabilities', { + method: 'GET', signal: undefined, version: '1', }); }); it('returns API_ERROR when the response status is error', async () => { - (mockHttp.get as jest.Mock).mockResolvedValue({ status: API_ERROR }); + (mockHttp.fetch as jest.Mock).mockResolvedValue({ status: API_ERROR }); const result = await getCapabilities({ http: mockHttp }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/capabilities.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/capabilities.tsx index 96e6660f6bc0e..59927dbf2c472 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/capabilities.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/capabilities.tsx @@ -6,7 +6,7 @@ */ import { HttpSetup, IHttpFetchError } from '@kbn/core-http-browser'; -import { API_VERSIONS, GetCapabilitiesResponse } from '@kbn/elastic-assistant-common'; +import { GetCapabilitiesResponse } from '@kbn/elastic-assistant-common'; export interface GetCapabilitiesParams { http: HttpSetup; @@ -29,10 +29,13 @@ export const getCapabilities = async ({ try { const path = `/internal/elastic_assistant/capabilities`; - return await http.get(path, { + const response = await http.fetch(path, { + method: 'GET', signal, - version: API_VERSIONS.internal.v1, + version: '1', }); + + return response as GetCapabilitiesResponse; } catch (error) { return error as IHttpFetchError; } diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/use_capabilities.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/use_capabilities.test.tsx index b7648983e6f7a..c9e60b806d1bf 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/use_capabilities.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/capabilities/use_capabilities.test.tsx @@ -11,12 +11,11 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { ReactNode } from 'react'; import React from 'react'; import { useCapabilities, UseCapabilitiesParams } from './use_capabilities'; -import { API_VERSIONS } from '@kbn/elastic-assistant-common'; const statusResponse = { assistantModelEvaluation: true, assistantStreamingEnabled: false }; const http = { - get: jest.fn().mockResolvedValue(statusResponse), + fetch: jest.fn().mockResolvedValue(statusResponse), }; const toasts = { addError: jest.fn(), @@ -37,10 +36,14 @@ describe('useFetchRelatedCases', () => { wrapper: createWrapper(), }); - expect(defaultProps.http.get).toHaveBeenCalledWith('/internal/elastic_assistant/capabilities', { - version: API_VERSIONS.internal.v1, - signal: new AbortController().signal, - }); + expect(defaultProps.http.fetch).toHaveBeenCalledWith( + '/internal/elastic_assistant/capabilities', + { + method: 'GET', + version: '1', + signal: new AbortController().signal, + } + ); expect(toasts.addError).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/evaluate.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/evaluate.test.tsx deleted file mode 100644 index d25953370e97a..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/evaluate.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { postEvaluation } from './evaluate'; -import { HttpSetup } from '@kbn/core-http-browser'; -import { API_VERSIONS } from '@kbn/elastic-assistant-common'; - -jest.mock('@kbn/core-http-browser'); - -const mockHttp = { - post: jest.fn(), -} as unknown as HttpSetup; - -describe('postEvaluation', () => { - it('calls the knowledge base API when correct resource path', async () => { - (mockHttp.post as jest.Mock).mockResolvedValue({ success: true }); - const testProps = { - http: mockHttp, - evalParams: { - agents: ['not', 'alphabetical'], - dataset: '{}', - datasetName: 'Test Dataset', - projectName: 'Test Project Name', - runName: 'Test Run Name', - evalModel: ['not', 'alphabetical'], - evalPrompt: 'evalPrompt', - evaluationType: ['not', 'alphabetical'], - models: ['not', 'alphabetical'], - outputIndex: 'outputIndex', - }, - }; - - await postEvaluation(testProps); - - expect(mockHttp.post).toHaveBeenCalledWith('/internal/elastic_assistant/evaluate', { - body: '{"dataset":{},"evalPrompt":"evalPrompt"}', - headers: { 'Content-Type': 'application/json' }, - query: { - models: 'alphabetical,not', - agents: 'alphabetical,not', - datasetName: 'Test Dataset', - evaluationType: 'alphabetical,not', - evalModel: 'alphabetical,not', - outputIndex: 'outputIndex', - projectName: 'Test Project Name', - runName: 'Test Run Name', - }, - signal: undefined, - version: API_VERSIONS.internal.v1, - }); - }); - it('returns error when error is an error', async () => { - const error = 'simulated error'; - (mockHttp.post as jest.Mock).mockImplementation(() => { - throw new Error(error); - }); - - const knowledgeBaseArgs = { - resource: 'a-resource', - http: mockHttp, - }; - - await expect(postEvaluation(knowledgeBaseArgs)).resolves.toThrowError('simulated error'); - }); -}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/evaluate.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/evaluate.tsx deleted file mode 100644 index 6581e22e77921..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/evaluate.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { HttpSetup, IHttpFetchError } from '@kbn/core-http-browser'; -import { - API_VERSIONS, - GetEvaluateResponse, - PostEvaluateResponse, -} from '@kbn/elastic-assistant-common'; -import { PerformEvaluationParams } from './use_perform_evaluation'; - -export interface PostEvaluationParams { - http: HttpSetup; - evalParams?: PerformEvaluationParams; - signal?: AbortSignal | undefined; -} - -/** - * API call for evaluating models. - * - * @param {Object} options - The options object. - * @param {HttpSetup} options.http - HttpSetup - * @param {string} [options.evalParams] - Params necessary for evaluation - * @param {AbortSignal} [options.signal] - AbortSignal - * - * @returns {Promise} - */ -export const postEvaluation = async ({ - http, - evalParams, - signal, -}: PostEvaluationParams): Promise => { - try { - const path = `/internal/elastic_assistant/evaluate`; - const query = { - agents: evalParams?.agents.sort()?.join(','), - datasetName: evalParams?.datasetName, - evaluationType: evalParams?.evaluationType.sort()?.join(','), - evalModel: evalParams?.evalModel.sort()?.join(','), - outputIndex: evalParams?.outputIndex, - models: evalParams?.models.sort()?.join(','), - projectName: evalParams?.projectName, - runName: evalParams?.runName, - }; - - return await http.post(path, { - body: JSON.stringify({ - dataset: JSON.parse(evalParams?.dataset ?? '[]'), - evalPrompt: evalParams?.evalPrompt ?? '', - }), - headers: { - 'Content-Type': 'application/json', - }, - query, - signal, - version: API_VERSIONS.internal.v1, - }); - } catch (error) { - return error as IHttpFetchError; - } -}; - -export interface GetEvaluationParams { - http: HttpSetup; - signal?: AbortSignal | undefined; -} - -/** - * API call for fetching evaluation data. - * - * @param {Object} options - The options object. - * @param {HttpSetup} options.http - HttpSetup - * @param {AbortSignal} [options.signal] - AbortSignal - * - * @returns {Promise} - */ -export const getEvaluation = async ({ - http, - signal, -}: GetEvaluationParams): Promise => { - try { - const path = `/internal/elastic_assistant/evaluate`; - - return await http.get(path, { - signal, - version: API_VERSIONS.internal.v1, - }); - } catch (error) { - return error as IHttpFetchError; - } -}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/use_evaluation_data.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/use_evaluation_data.tsx deleted file mode 100644 index a37cf18a235ec..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/use_evaluation_data.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useQuery } from '@tanstack/react-query'; -import type { HttpSetup, IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser'; -import type { IToasts } from '@kbn/core-notifications-browser'; -import { i18n } from '@kbn/i18n'; -import { getEvaluation } from './evaluate'; - -const EVALUATION_DATA_QUERY_KEY = ['elastic-assistant', 'evaluation-data']; - -export interface UseEvaluationDataParams { - http: HttpSetup; - toasts?: IToasts; -} - -/** - * Hook for fetching evaluation data, like available agents, test data, etc - * - * @param {Object} options - The options object. - * @param {HttpSetup} options.http - HttpSetup - * @param {IToasts} [options.toasts] - IToasts - * - * @returns {useMutation} mutation hook for setting up the Knowledge Base - */ -export const useEvaluationData = ({ http, toasts }: UseEvaluationDataParams) => { - return useQuery({ - queryKey: EVALUATION_DATA_QUERY_KEY, - queryFn: ({ signal }) => { - // Optional params workaround: see: https://github.com/TanStack/query/issues/1077#issuecomment-1431247266 - return getEvaluation({ http, signal }); - }, - retry: false, - keepPreviousData: true, - // Deprecated, hoist to `queryCache` w/in `QueryClient. See: https://stackoverflow.com/a/76961109 - onError: (error: IHttpFetchError) => { - if (error.name !== 'AbortError') { - toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, { - title: i18n.translate('xpack.elasticAssistant.evaluation.fetchEvaluationDataError', { - defaultMessage: 'Error fetching evaluation data...', - }), - }); - } - }, - }); -}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx index 09cdf6717ca6c..f4fe4d7f8a407 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx @@ -27,16 +27,20 @@ import { import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { GetEvaluateResponse, PostEvaluateResponse } from '@kbn/elastic-assistant-common'; import * as i18n from './translations'; import { useAssistantContext } from '../../../assistant_context'; import { useLoadConnectors } from '../../../connectorland/use_load_connectors'; import { getActionTypeTitle, getGenAiConfig } from '../../../connectorland/helpers'; import { PRECONFIGURED_CONNECTOR } from '../../../connectorland/translations'; -import { usePerformEvaluation } from '../../api/evaluate/use_perform_evaluation'; +import { usePerformEvaluation } from './use_perform_evaluation'; import { getApmLink, getDiscoverLink } from './utils'; -import { useEvaluationData } from '../../api/evaluate/use_evaluation_data'; +import { PostEvaluationResponse } from '../../api'; +/** + * See AGENT_EXECUTOR_MAP in `x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts` + * for the agent name -> executor mapping + */ +const DEFAULT_AGENTS = ['DefaultAgentExecutor', 'OpenAIFunctionsExecutor']; const DEFAULT_EVAL_TYPES_OPTIONS = [ { label: 'correctness' }, { label: 'esql-validator', disabled: true }, @@ -61,11 +65,6 @@ export const EvaluationSettings: React.FC = React.memo(({ onEvaluationSet } = usePerformEvaluation({ http, }); - const { data: evalData } = useEvaluationData({ http }); - const defaultAgents = useMemo( - () => (evalData as GetEvaluateResponse)?.agentExecutors ?? [], - [evalData] - ); // Run Details // Project Name @@ -196,8 +195,8 @@ export const EvaluationSettings: React.FC = React.memo(({ onEvaluationSet [selectedAgentOptions] ); const agentOptions = useMemo(() => { - return defaultAgents.map((label) => ({ label })); - }, [defaultAgents]); + return DEFAULT_AGENTS.map((label) => ({ label })); + }, []); // Evaluation // Evaluation Type @@ -284,12 +283,12 @@ export const EvaluationSettings: React.FC = React.memo(({ onEvaluationSet ]); const discoverLink = useMemo( - () => getDiscoverLink(basePath, (evalResponse as PostEvaluateResponse)?.evaluationId ?? ''), + () => getDiscoverLink(basePath, (evalResponse as PostEvaluationResponse)?.evaluationId ?? ''), [basePath, evalResponse] ); const apmLink = useMemo( - () => getApmLink(basePath, (evalResponse as PostEvaluateResponse)?.evaluationId ?? ''), + () => getApmLink(basePath, (evalResponse as PostEvaluationResponse)?.evaluationId ?? ''), [basePath, evalResponse] ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/use_perform_evaluation.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/use_perform_evaluation.test.tsx similarity index 81% rename from x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/use_perform_evaluation.test.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/use_perform_evaluation.test.tsx index f9fdb2e80b7b2..b065338480549 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/use_perform_evaluation.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/use_perform_evaluation.test.tsx @@ -7,15 +7,14 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { usePerformEvaluation, UsePerformEvaluationParams } from './use_perform_evaluation'; -import { postEvaluation as _postEvaluation } from './evaluate'; +import { postEvaluation as _postEvaluation } from '../../api'; import { useMutation as _useMutation } from '@tanstack/react-query'; -import { API_VERSIONS } from '@kbn/elastic-assistant-common'; const useMutationMock = _useMutation as jest.Mock; const postEvaluationMock = _postEvaluation as jest.Mock; -jest.mock('./evaluate', () => { - const actual = jest.requireActual('./evaluate'); +jest.mock('../../api', () => { + const actual = jest.requireActual('../../api'); return { ...actual, postEvaluation: jest.fn((...args) => actual.postEvaluation(...args)), @@ -38,7 +37,7 @@ const statusResponse = { }; const http = { - post: jest.fn().mockResolvedValue(statusResponse), + fetch: jest.fn().mockResolvedValue(statusResponse), }; const toasts = { addError: jest.fn(), @@ -54,23 +53,20 @@ describe('usePerformEvaluation', () => { const { waitForNextUpdate } = renderHook(() => usePerformEvaluation(defaultProps)); await waitForNextUpdate(); - expect(defaultProps.http.post).toHaveBeenCalledWith('/internal/elastic_assistant/evaluate', { + expect(defaultProps.http.fetch).toHaveBeenCalledWith('/internal/elastic_assistant/evaluate', { + method: 'POST', body: '{"dataset":[],"evalPrompt":""}', headers: { 'Content-Type': 'application/json', }, query: { agents: undefined, - datasetName: undefined, evalModel: undefined, evaluationType: undefined, models: undefined, outputIndex: undefined, - projectName: undefined, - runName: undefined, }, signal: undefined, - version: API_VERSIONS.internal.v1, }); expect(toasts.addError).not.toHaveBeenCalled(); }); @@ -86,8 +82,6 @@ describe('usePerformEvaluation', () => { evaluationType: ['f', 'e'], models: ['h', 'g'], outputIndex: 'outputIndex', - projectName: 'test project', - runName: 'test run', }); return Promise.resolve(res); } catch (e) { @@ -98,23 +92,20 @@ describe('usePerformEvaluation', () => { const { waitForNextUpdate } = renderHook(() => usePerformEvaluation(defaultProps)); await waitForNextUpdate(); - expect(defaultProps.http.post).toHaveBeenCalledWith('/internal/elastic_assistant/evaluate', { + expect(defaultProps.http.fetch).toHaveBeenCalledWith('/internal/elastic_assistant/evaluate', { + method: 'POST', body: '{"dataset":["kewl"],"evalPrompt":"evalPrompt"}', headers: { 'Content-Type': 'application/json', }, query: { agents: 'c,d', - datasetName: undefined, evalModel: 'a,b', evaluationType: 'e,f', models: 'g,h', outputIndex: 'outputIndex', - projectName: 'test project', - runName: 'test run', }, signal: undefined, - version: API_VERSIONS.internal.v1, }); }); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/use_perform_evaluation.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/use_perform_evaluation.tsx similarity index 97% rename from x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/use_perform_evaluation.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/use_perform_evaluation.tsx index 30e95d9d80407..158f7159310ad 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/evaluate/use_perform_evaluation.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/use_perform_evaluation.tsx @@ -9,7 +9,7 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpSetup, IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser'; import type { IToasts } from '@kbn/core-notifications-browser'; import { i18n } from '@kbn/i18n'; -import { postEvaluation } from './evaluate'; +import { postEvaluation } from '../../api'; const PERFORM_EVALUATION_MUTATION_KEY = ['elastic-assistant', 'perform-evaluation']; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.test.tsx index 77ffe90ec8c97..fb52041632718 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.test.tsx @@ -12,9 +12,12 @@ import React from 'react'; import { ContextEditor } from '.'; describe('ContextEditor', () => { - const allow = ['field1', 'field2']; + const allow = Array.from({ length: 20 }, (_, i) => `field${i + 1}`); const allowReplacement = ['field1']; - const rawData = { field1: ['value1'], field2: ['value2'] }; + const rawData = allow.reduce( + (acc, field, index) => ({ ...acc, [field]: [`value${index + 1}`] }), + {} + ); const onListUpdated = jest.fn(); @@ -36,13 +39,17 @@ describe('ContextEditor', () => { }); it('renders the select all fields button with the expected count', () => { - expect(screen.getByTestId('selectAllFields')).toHaveTextContent('Select all 2 fields'); + expect(screen.getByTestId('selectAllFields')).toHaveTextContent('Select all 20 fields'); }); it('updates the table selection when "Select all n fields" is clicked', () => { - userEvent.click(screen.getByTestId('selectAllFields')); + // The table select all checkbox should only select the number of rows visible on the page + userEvent.click(screen.getByTestId('checkboxSelectAll')); + expect(screen.getByTestId('selectedFields')).toHaveTextContent('Selected 10 fields'); - expect(screen.getByTestId('selectedFields')).toHaveTextContent('Selected 2 fields'); + // The select all button should select all rows regardless of visibility + userEvent.click(screen.getByTestId('selectAllFields')); + expect(screen.getByTestId('selectedFields')).toHaveTextContent('Selected 20 fields'); }); it('calls onListUpdated with the expected values when the update button is clicked', () => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.tsx index 5e9e0b960f577..e0b19fe26d672 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization_editor/context_editor/index.tsx @@ -7,7 +7,7 @@ import { EuiInMemoryTable } from '@elastic/eui'; import type { EuiSearchBarProps, EuiTableSelectionType } from '@elastic/eui'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useState, useRef } from 'react'; import { getColumns } from './get_columns'; import { getRows } from './get_rows'; @@ -59,16 +59,25 @@ const ContextEditorComponent: React.FC = ({ rawData, pageSize = DEFAULT_PAGE_SIZE, }) => { + const isAllSelected = useRef(false); // Must be a ref and not state in order not to re-render `selectionValue`, which fires `onSelectionChange` twice const [selected, setSelection] = useState([]); const selectionValue: EuiTableSelectionType = useMemo( () => ({ selectable: () => true, - onSelectionChange: (newSelection) => setSelection(newSelection), - initialSelected: [], + onSelectionChange: (newSelection) => { + if (isAllSelected.current === true) { + // If passed every possible row (including non-visible ones), EuiInMemoryTable + // will fire `onSelectionChange` with only the visible rows - we need to + // ignore this call when that happens and continue to pass all rows + isAllSelected.current = false; + } else { + setSelection(newSelection); + } + }, + selected, }), - [] + [selected] ); - const tableRef = useRef | null>(null); const columns = useMemo(() => getColumns({ onListUpdated, rawData }), [onListUpdated, rawData]); @@ -83,9 +92,8 @@ const ContextEditorComponent: React.FC = ({ ); const onSelectAll = useCallback(() => { - tableRef.current?.setSelection(rows); // updates selection in the EuiInMemoryTable - - setTimeout(() => setSelection(rows), 0); // updates selection in the component state + isAllSelected.current = true; + setSelection(rows); }, [rows]); const pagination = useMemo(() => { @@ -106,7 +114,7 @@ const ContextEditorComponent: React.FC = ({ totalFields={rows.length} /> ), - [onListUpdated, onReset, onSelectAll, rawData, rows.length, selected] + [onListUpdated, onReset, onSelectAll, rawData, rows, selected] ); return ( @@ -120,7 +128,6 @@ const ContextEditorComponent: React.FC = ({ itemId={FIELDS.FIELD} items={rows} pagination={pagination} - ref={tableRef} search={search} selection={selectionValue} sorting={defaultSort} diff --git a/x-pack/packages/ml/cancellable_search/README.md b/x-pack/packages/ml/cancellable_search/README.md new file mode 100644 index 0000000000000..3f7463e69af9e --- /dev/null +++ b/x-pack/packages/ml/cancellable_search/README.md @@ -0,0 +1,3 @@ +# @kbn/ml-cancellable-search + +React hook for cancellable data searching diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.ts b/x-pack/packages/ml/cancellable_search/index.ts similarity index 72% rename from x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.ts rename to x-pack/packages/ml/cancellable_search/index.ts index 7cfdf2a31aca2..7ae3ce1a71ef5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.ts +++ b/x-pack/packages/ml/cancellable_search/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { Pane } from './pane'; +export { useCancellableSearch, type UseCancellableSearch } from './src/use_cancellable_search'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/translations.ts b/x-pack/packages/ml/cancellable_search/jest.config.js similarity index 53% rename from x-pack/plugins/security_solution/public/timelines/components/flyout/pane/translations.ts rename to x-pack/packages/ml/cancellable_search/jest.config.js index 6a0d15af819fa..450cf6662aa45 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/translations.ts +++ b/x-pack/packages/ml/cancellable_search/jest.config.js @@ -5,11 +5,8 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; - -export const TIMELINE_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.timeline.flyout.pane.timelinePropertiesAriaLabel', - { - defaultMessage: 'Timeline Properties', - } -); +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/x-pack/packages/ml/cancellable_search'], +}; diff --git a/x-pack/packages/ml/cancellable_search/kibana.jsonc b/x-pack/packages/ml/cancellable_search/kibana.jsonc new file mode 100644 index 0000000000000..2006bfd746711 --- /dev/null +++ b/x-pack/packages/ml/cancellable_search/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/ml-cancellable-search", + "owner": "@elastic/ml-ui" +} diff --git a/x-pack/packages/ml/cancellable_search/package.json b/x-pack/packages/ml/cancellable_search/package.json new file mode 100644 index 0000000000000..79630662bc5c3 --- /dev/null +++ b/x-pack/packages/ml/cancellable_search/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/ml-cancellable-search", + "description": "React hook for cancellable data searching", + "author": "Machine Learning UI", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} diff --git a/x-pack/packages/ml/cancellable_search/src/use_cancellable_search.ts b/x-pack/packages/ml/cancellable_search/src/use_cancellable_search.ts new file mode 100644 index 0000000000000..78e7e4a17b1fa --- /dev/null +++ b/x-pack/packages/ml/cancellable_search/src/use_cancellable_search.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useRef, useState } from 'react'; +import { type IKibanaSearchResponse, isRunningResponse } from '@kbn/data-plugin/common'; +import { tap } from 'rxjs/operators'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; + +export interface UseCancellableSearch { + runRequest: ( + requestBody: RequestBody, + options?: object + ) => Promise; + cancelRequest: () => void; + isLoading: boolean; +} + +// Similar to aiops/hooks/use_cancellable_search.ts +export function useCancellableSearch(data: DataPublicPluginStart) { + const abortController = useRef(new AbortController()); + const [isLoading, setIsFetching] = useState(false); + + const runRequest = useCallback( + ( + requestBody: RequestBody, + options = {} + ): Promise => { + return new Promise((resolve, reject) => { + data.search + .search(requestBody, { + abortSignal: abortController.current.signal, + ...options, + }) + .pipe( + tap(() => { + setIsFetching(true); + }) + ) + .subscribe({ + next: (result) => { + if (!isRunningResponse(result)) { + setIsFetching(false); + resolve(result); + } else { + // partial results + // Ignore partial results for now. + // An issue with the search function means partial results are not being returned correctly. + } + }, + error: (error) => { + if (error.name === 'AbortError') { + return resolve(null); + } + setIsFetching(false); + reject(error); + }, + }); + }); + }, + [data.search] + ); + + const cancelRequest = useCallback(() => { + abortController.current.abort(); + abortController.current = new AbortController(); + }, []); + + return { runRequest, cancelRequest, isLoading }; +} diff --git a/x-pack/packages/ml/cancellable_search/tsconfig.json b/x-pack/packages/ml/cancellable_search/tsconfig.json new file mode 100644 index 0000000000000..733865038a43f --- /dev/null +++ b/x-pack/packages/ml/cancellable_search/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*", + ], + "kbn_references": [ + "@kbn/data-plugin" + ] +} diff --git a/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx b/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx index 8d500f7d1a815..d16f74561984c 100644 --- a/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx +++ b/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx @@ -90,6 +90,10 @@ interface DatePickerWrapperProps { * Boolean flag to set use of flex group wrapper */ flexGroup?: boolean; + /** + * Boolean flag to disable the date picker + */ + isDisabled?: boolean; } /** @@ -100,7 +104,14 @@ interface DatePickerWrapperProps { * @returns {React.ReactElement} The DatePickerWrapper component. */ export const DatePickerWrapper: FC = (props) => { - const { isAutoRefreshOnly, isLoading = false, showRefresh, width, flexGroup = true } = props; + const { + isAutoRefreshOnly, + isLoading = false, + showRefresh, + width, + flexGroup = true, + isDisabled = false, + } = props; const { data, notifications: { toasts }, @@ -292,6 +303,7 @@ export const DatePickerWrapper: FC = (props) => { commonlyUsedRanges={commonlyUsedRanges} updateButtonProps={{ iconOnly: isWithinLBreakpoint, fill: false }} width={width} + isDisabled={isDisabled} /> {showRefresh === true || !isTimeRangeSelectorEnabled ? ( diff --git a/x-pack/plugins/aiops/common/api/log_categorization/create_categorize_query.ts b/x-pack/plugins/aiops/common/api/log_categorization/create_categorize_query.ts index 1c5f5cbbbc0cd..c3289d1527f2b 100644 --- a/x-pack/plugins/aiops/common/api/log_categorization/create_categorize_query.ts +++ b/x-pack/plugins/aiops/common/api/log_categorization/create_categorize_query.ts @@ -10,11 +10,11 @@ import { cloneDeep } from 'lodash'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; export function createCategorizeQuery( - queryIn: QueryDslQueryContainer, + queryIn: QueryDslQueryContainer | undefined, timeField: string, timeRange: { from: number; to: number } | undefined ) { - const query = cloneDeep(queryIn); + const query = cloneDeep(queryIn ?? { match_all: {} }); if (query.bool === undefined) { query.bool = {}; diff --git a/x-pack/plugins/aiops/public/application/url_state/common.test.ts b/x-pack/plugins/aiops/public/application/url_state/common.test.ts new file mode 100644 index 0000000000000..5a972eec7715b --- /dev/null +++ b/x-pack/plugins/aiops/public/application/url_state/common.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isDefaultSearchQuery } from './common'; + +describe('isDefaultSearchQuery', () => { + it('returns true for default search query', () => { + expect(isDefaultSearchQuery({ match_all: {} })).toBe(true); + }); + + it('returns false for non default search query', () => { + expect( + isDefaultSearchQuery({ + bool: { must_not: [{ term: { 'the-term': 'the-value' } }] }, + }) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/aiops/public/application/url_state/common.ts b/x-pack/plugins/aiops/public/application/url_state/common.ts index 8ca9ec848150c..eb037dabb9648 100644 --- a/x-pack/plugins/aiops/public/application/url_state/common.ts +++ b/x-pack/plugins/aiops/public/application/url_state/common.ts @@ -8,11 +8,16 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Filter, Query } from '@kbn/es-query'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { SEARCH_QUERY_LANGUAGE, type SearchQueryLanguage } from '@kbn/ml-query-utils'; const defaultSearchQuery = { match_all: {}, +} as const; + +export const isDefaultSearchQuery = (arg: unknown): arg is typeof defaultSearchQuery => { + return isPopulatedObject(arg, ['match_all']); }; export interface AiOpsPageUrlState { diff --git a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts index 8d83a88415689..9d80758797324 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts +++ b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts @@ -56,10 +56,7 @@ export function useDiscoverLinks() { }, }); - let path = basePath.get(); - path += '/app/discover#/'; - path += '?_g=' + _g; - path += '&_a=' + encodeURIComponent(_a); + const path = `${basePath.get()}/app/discover#/?_g=${_g}&_a=${encodeURIComponent(_a)}`; window.open(path, '_blank'); }; diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx index 51244a2300634..daac98f67f750 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx @@ -77,7 +77,7 @@ export interface LogRateAnalysisContentProps { /** Optional callback that exposes data of the completed analysis */ onAnalysisCompleted?: (d: LogRateAnalysisResultsData) => void; /** Optional callback that exposes current window parameters */ - onWindowParametersChange?: (wp?: WindowParameters) => void; + onWindowParametersChange?: (wp?: WindowParameters, replace?: boolean) => void; /** Identifier to indicate the plugin utilizing the component */ embeddingOrigin: string; } @@ -126,7 +126,7 @@ export const LogRateAnalysisContent: FC = ({ windowParametersTouched.current = true; if (onWindowParametersChange) { - onWindowParametersChange(windowParameters); + onWindowParametersChange(windowParameters, true); } }, [onWindowParametersChange, windowParameters]); diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_page.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_page.tsx index 4931503b7366e..22b6a68006a5e 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_page.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_page.tsx @@ -113,10 +113,14 @@ export const LogRateAnalysisPage: FC = ({ stickyHistogram }) => { useEffect(() => { if (globalState?.time !== undefined) { - timefilter.setTime({ - from: globalState.time.from, - to: globalState.time.to, - }); + if ( + !isEqual({ from: globalState.time.from, to: globalState.time.to }, timefilter.getTime()) + ) { + timefilter.setTime({ + from: globalState.time.from, + to: globalState.time.to, + }); + } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(globalState?.time), timefilter]); @@ -136,11 +140,14 @@ export const LogRateAnalysisPage: FC = ({ stickyHistogram }) => { }); }, [dataService, searchQueryLanguage, searchString]); - const onWindowParametersHandler = (wp?: WindowParameters) => { - if (!isEqual(wp, stateFromUrl.wp)) { - setUrlState({ - wp: windowParametersToAppState(wp), - }); + const onWindowParametersHandler = (wp?: WindowParameters, replace = false) => { + if (!isEqual(windowParametersToAppState(wp), stateFromUrl.wp)) { + setUrlState( + { + wp: windowParametersToAppState(wp), + }, + replace + ); } }; diff --git a/x-pack/plugins/aiops/public/hooks/use_data.ts b/x-pack/plugins/aiops/public/hooks/use_data.ts index 934a470588960..4f0e2526d7a1d 100644 --- a/x-pack/plugins/aiops/public/hooks/use_data.ts +++ b/x-pack/plugins/aiops/public/hooks/use_data.ts @@ -54,9 +54,14 @@ export const useData = ( timeRangeSelector: selectedDataView?.timeFieldName !== undefined, autoRefreshSelector: true, }); + const timeRangeMemoized = useMemo( + () => timefilter.getActiveBounds(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify(timefilter.getActiveBounds())] + ); const fieldStatsRequest: DocumentStatsSearchStrategyParams | undefined = useMemo(() => { - const timefilterActiveBounds = timeRange ?? timefilter.getActiveBounds(); + const timefilterActiveBounds = timeRange ?? timeRangeMemoized; if (timefilterActiveBounds !== undefined) { _timeBuckets.setInterval('auto'); _timeBuckets.setBounds(timefilterActiveBounds); @@ -72,7 +77,7 @@ export const useData = ( }; } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [lastRefresh, searchQuery, timeRange]); + }, [lastRefresh, searchQuery, timeRange, timeRangeMemoized]); const overallStatsRequest = useMemo(() => { return fieldStatsRequest diff --git a/x-pack/plugins/aiops/public/hooks/use_search.ts b/x-pack/plugins/aiops/public/hooks/use_search.ts index 8c62db36289fd..060e87dab59c5 100644 --- a/x-pack/plugins/aiops/public/hooks/use_search.ts +++ b/x-pack/plugins/aiops/public/hooks/use_search.ts @@ -9,9 +9,15 @@ import { useMemo } from 'react'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import { isQuery } from '@kbn/data-plugin/public'; import { getEsQueryFromSavedSearch } from '../application/utils/search_utils'; -import type { AiOpsIndexBasedAppState } from '../application/url_state/common'; +import { + isDefaultSearchQuery, + type AiOpsIndexBasedAppState, +} from '../application/url_state/common'; +import { createMergedEsQuery } from '../application/utils/search_utils'; + import { useAiopsAppContext } from './use_aiops_app_context'; export const useSearch = ( @@ -37,23 +43,44 @@ export const useSearch = ( [dataView, uiSettings, savedSearch, filterManager] ); - if (searchData === undefined || (aiopsListState && aiopsListState.searchString !== '')) { - if (aiopsListState?.filters && readOnly === false) { - const globalFilters = filterManager?.getGlobalFilters(); + return useMemo(() => { + if (searchData === undefined || (aiopsListState && aiopsListState.searchString !== '')) { + if (aiopsListState?.filters && readOnly === false) { + const globalFilters = filterManager?.getGlobalFilters(); + + if (filterManager) filterManager.setFilters(aiopsListState.filters); + if (globalFilters) filterManager?.addFilters(globalFilters); + } + + // In cases where the url state contains only a KQL query and not yet + // the transformed ES query we regenerate it. This may happen if we restore + // url state on page load coming from another page like ML's Single Metric Viewer. + let searchQuery = aiopsListState?.searchQuery; + const query = { + language: aiopsListState?.searchQueryLanguage, + query: aiopsListState?.searchString, + }; + if ( + (aiopsListState.searchString !== '' || + (Array.isArray(aiopsListState.filters) && aiopsListState.filters.length > 0)) && + (isDefaultSearchQuery(searchQuery) || searchQuery === undefined) && + isQuery(query) + ) { + searchQuery = createMergedEsQuery(query, aiopsListState.filters, dataView, uiSettings); + } - if (filterManager) filterManager.setFilters(aiopsListState.filters); - if (globalFilters) filterManager?.addFilters(globalFilters); + return { + ...(isDefaultSearchQuery(searchQuery) ? {} : { searchQuery }), + searchString: aiopsListState?.searchString, + searchQueryLanguage: aiopsListState?.searchQueryLanguage, + }; + } else { + return { + searchQuery: searchData.searchQuery, + searchString: searchData.searchString, + searchQueryLanguage: searchData.queryLanguage, + }; } - return { - searchQuery: aiopsListState?.searchQuery, - searchString: aiopsListState?.searchString, - searchQueryLanguage: aiopsListState?.searchQueryLanguage, - }; - } else { - return { - searchQuery: searchData.searchQuery, - searchString: searchData.searchString, - searchQueryLanguage: searchData.queryLanguage, - }; - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify([searchData, aiopsListState])]); }; diff --git a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap index d009896a58505..dab0c1c9a5737 100644 --- a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap +++ b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap @@ -697,6 +697,11 @@ Object { "required": false, "type": "keyword", }, + "host.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, "kibana.alert.action_group": Object { "array": false, "required": false, @@ -1706,6 +1711,11 @@ Object { "required": false, "type": "keyword", }, + "user.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, }, } `; @@ -1734,6 +1744,11 @@ Object { "required": false, "type": "keyword", }, + "host.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, "kibana.alert.action_group": Object { "array": false, "required": false, @@ -2743,6 +2758,11 @@ Object { "required": false, "type": "keyword", }, + "user.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, }, } `; @@ -2771,6 +2791,11 @@ Object { "required": false, "type": "keyword", }, + "host.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, "kibana.alert.action_group": Object { "array": false, "required": false, @@ -3780,6 +3805,11 @@ Object { "required": false, "type": "keyword", }, + "user.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, }, } `; @@ -3808,6 +3838,11 @@ Object { "required": false, "type": "keyword", }, + "host.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, "kibana.alert.action_group": Object { "array": false, "required": false, @@ -4817,6 +4852,11 @@ Object { "required": false, "type": "keyword", }, + "user.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, }, } `; @@ -4845,6 +4885,11 @@ Object { "required": false, "type": "keyword", }, + "host.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, "kibana.alert.action_group": Object { "array": false, "required": false, @@ -5854,6 +5899,11 @@ Object { "required": false, "type": "keyword", }, + "user.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, }, } `; @@ -5888,6 +5938,11 @@ Object { "required": false, "type": "keyword", }, + "host.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, "kibana.alert.action_group": Object { "array": false, "required": false, @@ -6897,6 +6952,11 @@ Object { "required": false, "type": "keyword", }, + "user.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, }, } `; @@ -6925,6 +6985,11 @@ Object { "required": false, "type": "keyword", }, + "host.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, "kibana.alert.action_group": Object { "array": false, "required": false, @@ -7934,6 +7999,11 @@ Object { "required": false, "type": "keyword", }, + "user.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, }, } `; @@ -7962,6 +8032,11 @@ Object { "required": false, "type": "keyword", }, + "host.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, "kibana.alert.action_group": Object { "array": false, "required": false, @@ -8971,6 +9046,11 @@ Object { "required": false, "type": "keyword", }, + "user.asset.criticality": Object { + "array": false, + "required": false, + "type": "keyword", + }, }, } `; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index 449f7a190f6f0..735ff95a5edf7 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useCallback, useMemo, useRef, useState } from 'react'; -import type { EuiBasicTable, EuiTableSelectionType } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import type { EuiTableSelectionType } from '@elastic/eui'; import { EuiProgress } from '@elastic/eui'; import { difference, head, isEmpty } from 'lodash/fp'; import styled, { css } from 'styled-components'; @@ -112,11 +112,8 @@ export const AllCasesList = React.memo( [queryParams.sortField, queryParams.sortOrder] ); - const tableRef = useRef(null); - const deselectCases = useCallback(() => { setSelectedCases([]); - tableRef.current?.setSelection([]); }, [setSelectedCases]); const tableOnChangeCallback = useCallback( @@ -175,7 +172,7 @@ export const AllCasesList = React.memo( const euiBasicTableSelectionProps = useMemo>( () => ({ onSelectionChange: setSelectedCases, - initialSelected: selectedCases, + selected: selectedCases, selectable: () => !isReadOnlyPermissions(permissions), }), [permissions, selectedCases] @@ -229,7 +226,6 @@ export const AllCasesList = React.memo( selectedCases={selectedCases} selection={euiBasicTableSelectionProps} sorting={sorting} - tableRef={tableRef} tableRowProps={tableRowProps} deselectCases={deselectCases} selectedColumns={selectedColumns} diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index e50d54b8a9d6f..edd7ab7e9955b 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -35,7 +35,7 @@ interface CasesTableProps { selectedCases: CasesUI; selection: EuiTableSelectionType; sorting: EuiBasicTableProps['sorting']; - tableRef: MutableRefObject; + tableRef?: MutableRefObject; tableRowProps: EuiBasicTableProps['rowProps']; deselectCases: () => void; selectedColumns: CasesColumnSelection[]; diff --git a/x-pack/plugins/cases/public/components/create/severity.test.tsx b/x-pack/plugins/cases/public/components/create/severity.test.tsx index bf81dfc357fc7..5431d17d5f54c 100644 --- a/x-pack/plugins/cases/public/components/create/severity.test.tsx +++ b/x-pack/plugins/cases/public/components/create/severity.test.tsx @@ -31,7 +31,9 @@ const MockHookWrapperComponent: React.FC = ({ children }) => { return
{children}
; }; -describe('Severity form field', () => { +// FLAKY: https://github.com/elastic/kibana/issues/175934 +// FLAKY: https://github.com/elastic/kibana/issues/175935 +describe.skip('Severity form field', () => { let appMockRender: AppMockRenderer; beforeEach(() => { appMockRender = createAppMockRenderer(); diff --git a/x-pack/plugins/cases/public/components/files/file_delete_button.test.tsx b/x-pack/plugins/cases/public/components/files/file_delete_button.test.tsx index 38ed8a20eab40..8c4540aaadbec 100644 --- a/x-pack/plugins/cases/public/components/files/file_delete_button.test.tsx +++ b/x-pack/plugins/cases/public/components/files/file_delete_button.test.tsx @@ -21,7 +21,8 @@ jest.mock('../../containers/use_delete_file_attachment'); const useDeleteFileAttachmentMock = useDeleteFileAttachment as jest.Mock; -describe('FileDeleteButton', () => { +// FLAKY: https://github.com/elastic/kibana/issues/175956 +describe.skip('FileDeleteButton', () => { let appMockRender: AppMockRenderer; const mutate = jest.fn(); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx index bf2c38a54e377..13b4750317810 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx @@ -131,7 +131,7 @@ const BenchmarkSearchField = ({ isLoading={isLoading} placeholder={i18n.translate( 'xpack.csp.benchmarks.benchmarkSearchField.searchPlaceholder', - { defaultMessage: 'Search by benchmark Name' } + { defaultMessage: 'Search by Benchmark Name' } )} incremental /> @@ -151,9 +151,11 @@ export const Benchmarks = () => { }); const queryResult = useCspBenchmarkIntegrationsV2(); + const lowerCaseQueryName = query.name.toLowerCase(); const benchmarkResult = - queryResult.data?.items.filter((obj) => getBenchmarkCisName(obj.id)?.includes(query.name)) || - []; + queryResult.data?.items.filter((obj) => + getBenchmarkCisName(obj.id)?.toLowerCase().includes(lowerCaseQueryName) + ) || []; const totalItemCount = queryResult.data?.items.length || 0; // Check if we have any CSP Integration or not diff --git a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks_table.tsx index 8eaac69ec4e78..25690410a9780 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks_table.tsx @@ -16,7 +16,7 @@ import { EuiFlexItem, EuiLink, } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { generatePath } from 'react-router-dom'; @@ -243,6 +243,12 @@ export const BenchmarksTable = ({ totalItemCount, }; + const benchmarksSorted = useMemo(() => { + return [...benchmarks].sort((benchmarkDataA, benchmarkDataB) => + benchmarkDataA.id.localeCompare(benchmarkDataB.id) + ); + }, [benchmarks]); + const onChange = ({ page }: CriteriaWithPagination) => { setQuery({ page: { ...page, index: page.index + 1 } }); }; @@ -254,7 +260,7 @@ export const BenchmarksTable = ({ return ( [item.id, item.version].join('/')} pagination={pagination} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx index 7fb1fb3a58f55..01623235c42a7 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx @@ -89,7 +89,7 @@ export const RulesContainer = () => { page: 1, perPage: MAX_ITEMS_PER_PAGE, sortField: 'metadata.benchmark.rule_number', - sortOrder: 'asc', + sortOrder: rulesQuery.sortOrder, }, params.benchmarkId, params.benchmarkVersion @@ -206,6 +206,9 @@ export const RulesContainer = () => { /> + setRulesQuery((currentQuery) => ({ ...currentQuery, sortOrder: value })) + } rules_page={rulesPageData.rules_page} total={rulesPageData.total} error={rulesPageData.error} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx index b792ab851fa74..a5423ae912de2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx @@ -16,6 +16,7 @@ import { EuiCheckbox, EuiFlexGroup, EuiFlexItem, + EuiTableSortingType, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { uniqBy } from 'lodash'; @@ -36,6 +37,7 @@ type RulesTableProps = Pick< refetchRulesStates: () => void; selectedRules: CspBenchmarkRulesWithStates[]; setSelectedRules: (rules: CspBenchmarkRulesWithStates[]) => void; + onSortChange: (value: 'asc' | 'desc') => void; }; type GetColumnProps = Pick< @@ -68,6 +70,7 @@ export const RulesTable = ({ refetchRulesStates, selectedRules, setSelectedRules, + onSortChange, }: RulesTableProps) => { const { euiTheme } = useEuiTheme(); const euiPagination: EuiBasicTableProps['pagination'] = { @@ -76,10 +79,24 @@ export const RulesTable = ({ totalItemCount: total, pageSizeOptions: [10, 25, 100], }; + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); + const sorting: EuiTableSortingType = { + sort: { + field: 'metadata.benchmark.rule_number' as keyof CspBenchmarkRulesWithStates, + direction: sortDirection, + }, + }; - const onTableChange = ({ page: pagination }: Criteria) => { + const onTableChange = ({ + page: pagination, + sort: sortOrder, + }: Criteria) => { if (!pagination) return; - setPagination({ page: pagination.index, perPage: pagination.size }); + if (pagination) setPagination({ page: pagination.index, perPage: pagination.size }); + if (sortOrder) { + setSortDirection(sortOrder.direction); + onSortChange(sortOrder.direction); + } }; const rowProps = (row: CspBenchmarkRulesWithStates) => ({ @@ -149,6 +166,7 @@ export const RulesTable = ({ onChange={onTableChange} itemId={(v) => v.metadata.id} rowProps={rowProps} + sorting={sorting} /> ); @@ -221,7 +239,7 @@ const getColumns = ({ name: i18n.translate('xpack.csp.rules.rulesTable.ruleNumberColumnLabel', { defaultMessage: 'Rule Number', }), - width: '100px', + width: '120px', sortable: true, }, { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.test.ts index 2a0130c0f0fd7..a2814e4b48ff0 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.test.ts @@ -20,7 +20,7 @@ describe('getSortedCspBenchmarkRules', () => { { metadata: { benchmark: {} } }, ] as CspBenchmarkRule[]; - const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules); + const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules, 'asc'); expect(sortedCspBenchmarkRules).toEqual([ { metadata: { benchmark: { rule_number: '1.0.0' } } }, @@ -36,7 +36,7 @@ describe('getSortedCspBenchmarkRules', () => { it('edge case - returns empty array if input is empty', () => { const cspBenchmarkRules: CspBenchmarkRule[] = []; - const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules); + const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules, 'asc'); expect(sortedCspBenchmarkRules).toEqual([]); }); @@ -46,7 +46,7 @@ describe('getSortedCspBenchmarkRules', () => { { metadata: { benchmark: { rule_number: '1.0.0' } } }, ] as CspBenchmarkRule[]; - const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules); + const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules, 'asc'); expect(sortedCspBenchmarkRules).toEqual([ { metadata: { benchmark: { rule_number: '1.0.0' } } }, @@ -61,7 +61,7 @@ describe('getSortedCspBenchmarkRules', () => { { metadata: { benchmark: { rule_number: null } } }, ] as CspBenchmarkRule[]; - const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules); + const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules, 'asc'); expect(sortedCspBenchmarkRules).toEqual([ { metadata: { benchmark: { rule_number: '1.0.0' } } }, @@ -78,7 +78,7 @@ describe('getSortedCspBenchmarkRules', () => { { metadata: { benchmark: { rule_number: '3.0.0' } } }, ] as CspBenchmarkRule[]; - const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules); + const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules, 'asc'); expect(sortedCspBenchmarkRules).toEqual([ { metadata: { benchmark: { rule_number: '1.0.0' } } }, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/utils.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/utils.ts index 2f57dc40ca5f9..f118ed0e8c56d 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/utils.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/utils.ts @@ -13,7 +13,10 @@ import { getBenchmarkFromPackagePolicy } from '../../../../common/utils/helpers' import type { CspBenchmarkRule } from '../../../../common/types/latest'; -export const getSortedCspBenchmarkRulesTemplates = (cspBenchmarkRules: CspBenchmarkRule[]) => { +export const getSortedCspBenchmarkRulesTemplates = ( + cspBenchmarkRules: CspBenchmarkRule[], + sortDirection: 'asc' | 'desc' +) => { return cspBenchmarkRules.slice().sort((a, b) => { const ruleNumberA = a?.metadata?.benchmark?.rule_number; const ruleNumberB = b?.metadata?.benchmark?.rule_number; @@ -22,12 +25,19 @@ export const getSortedCspBenchmarkRulesTemplates = (cspBenchmarkRules: CspBenchm const versionB = semverValid(ruleNumberB); if (versionA !== null && versionB !== null) { - return semverCompare(versionA, versionB); + return sortDirection === 'asc' + ? semverCompare(versionA, versionB) + : semverCompare(versionB, versionA); } else { - return String(ruleNumberA).localeCompare(String(ruleNumberB), undefined, { - numeric: true, - sensitivity: 'base', - }); + return sortDirection === 'asc' + ? String(ruleNumberA).localeCompare(String(ruleNumberB), undefined, { + numeric: true, + sensitivity: 'base', + }) + : String(ruleNumberB).localeCompare(String(ruleNumberA), undefined, { + numeric: true, + sensitivity: 'base', + }); } }); }; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v1.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v1.ts index 4971098cb8067..a1d5668ba8c22 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v1.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v1.ts @@ -45,7 +45,7 @@ export const findBenchmarkRuleHandler = async ( ); // Semantic version sorting using semver for valid versions and custom comparison for invalid versions - const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules); + const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules, 'asc'); return { items: sortedCspBenchmarkRules, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v2.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v2.ts index 5054fc211a529..e7db9544b7dd3 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v2.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v2.ts @@ -45,7 +45,7 @@ export const findBenchmarkRuleHandler = async ( ); // Semantic version sorting using semver for valid versions and custom comparison for invalid versions - const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules); + const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules, 'asc'); return { items: sortedCspBenchmarkRules, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v3.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v3.ts index dff6cd3add966..c3b77a1a38ac3 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v3.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v3.ts @@ -35,6 +35,7 @@ export const findBenchmarkRuleHandler = async ( page: options.page, perPage: options.perPage, sortField: options.sortField, + sortOrder: options.sortOrder, fields: options?.fields, filter: getBenchmarkFilterQueryV2(benchmarkId, options.benchmarkVersion || '', { section: sectionFilter, @@ -47,7 +48,10 @@ export const findBenchmarkRuleHandler = async ( ); // Semantic version sorting using semver for valid versions and custom comparison for invalid versions - const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules); + const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates( + cspBenchmarkRules, + options.sortOrder + ); return { items: diff --git a/x-pack/plugins/data_visualizer/common/types/field_stats.ts b/x-pack/plugins/data_visualizer/common/types/field_stats.ts index 5fef7b4ae8f77..ea6956479bfec 100644 --- a/x-pack/plugins/data_visualizer/common/types/field_stats.ts +++ b/x-pack/plugins/data_visualizer/common/types/field_stats.ts @@ -72,18 +72,21 @@ export const isIKibanaSearchResponse = (arg: unknown): arg is IKibanaSearchRespo return isPopulatedObject(arg, ['rawResponse']); }; -export interface NumericFieldStats { +export interface NonSampledNumericFieldStats { fieldName: string; count?: number; min?: number; max?: number; avg?: number; + median?: number; + distribution?: Distribution; +} + +export interface NumericFieldStats extends NonSampledNumericFieldStats { isTopValuesSampled: boolean; topValues: Bucket[]; topValuesSampleSize: number; topValuesSamplerShardSize: number; - median?: number; - distribution?: Distribution; } export interface StringFieldStats { @@ -178,6 +181,7 @@ export type ChartRequestAgg = AggHistogram | AggCardinality | AggTerms; export type ChartData = NumericChartData | OrdinalChartData | UnsupportedChartData; export type BatchStats = + | NonSampledNumericFieldStats | NumericFieldStats | StringFieldStats | BooleanFieldStats @@ -186,6 +190,7 @@ export type BatchStats = | FieldExamples; export type FieldStats = + | NonSampledNumericFieldStats | NumericFieldStats | StringFieldStats | BooleanFieldStats @@ -199,7 +204,6 @@ export function isValidFieldStats(arg: unknown): arg is FieldStats { export interface FieldStatsCommonRequestParams { index: string; - samplerShardSize: number; timeFieldName?: string; earliestMs?: number | undefined; latestMs?: number | undefined; @@ -222,7 +226,6 @@ export interface OverallStatsSearchStrategyParams { aggInterval: TimeBucketsInterval; intervalMs?: number; searchQuery: Query['query']; - samplerShardSize: number; index: string; timeFieldName?: string; runtimeFieldMap?: estypes.MappingRuntimeFields; diff --git a/x-pack/plugins/data_visualizer/kibana.jsonc b/x-pack/plugins/data_visualizer/kibana.jsonc index 79a1e1fedacaf..e3c6e8514dabd 100644 --- a/x-pack/plugins/data_visualizer/kibana.jsonc +++ b/x-pack/plugins/data_visualizer/kibana.jsonc @@ -36,7 +36,8 @@ "esUiShared", "fieldFormats", "uiActions", - "lens" + "lens", + "textBasedLanguages", ] } } diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx index 3d1268140df17..6403e2c1e8130 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx @@ -38,8 +38,9 @@ export interface Props { samplingProbability?: number | null; setSamplingProbability?: (value: number | null) => void; randomSamplerPreference?: RandomSamplerOption; - setRandomSamplerPreference: (value: RandomSamplerOption) => void; + setRandomSamplerPreference?: (value: RandomSamplerOption) => void; loading: boolean; + showSettings?: boolean; } const CalculatingProbabilityMessage = ( @@ -61,6 +62,7 @@ export const DocumentCountContent: FC = ({ loading, randomSamplerPreference, setRandomSamplerPreference, + showSettings = true, }) => { const [showSamplingOptionsPopover, setShowSamplingOptionsPopover] = useState(false); @@ -120,75 +122,79 @@ export const DocumentCountContent: FC = ({ <> - - - + - - } - isOpen={showSamplingOptionsPopover} - closePopover={closeSamplingOptions} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - - - - - - - setRandomSamplerPreference(e.target.value as RandomSamplerOption) - } - /> - - - {randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_MANUAL ? ( - - ) : null} - - {randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_AUTOMATIC ? ( - loading ? ( - CalculatingProbabilityMessage - ) : ( - - ) - ) : null} - - - - + > + + + } + isOpen={showSamplingOptionsPopover} + closePopover={closeSamplingOptions} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + + + + {setRandomSamplerPreference ? ( + + + setRandomSamplerPreference(e.target.value as RandomSamplerOption) + } + /> + + ) : null} + + {randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_MANUAL ? ( + + ) : null} + + {randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_AUTOMATIC ? ( + loading ? ( + CalculatingProbabilityMessage + ) : ( + + ) + ) : null} + + + + + ) : null} ; } -const EMPTY_EXAMPLE = i18n.translate( +export const EMPTY_EXAMPLE = i18n.translate( 'xpack.dataVisualizer.dataGrid.field.examplesList.emptyExampleMessage', { defaultMessage: '(empty)' } ); diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx index e164a37307daf..f921d66e2cd1e 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx @@ -31,18 +31,21 @@ export const IndexBasedDataVisualizerExpandedRow = ({ combinedQuery, onAddFilter, totalDocuments, + typeAccessor = 'type', }: { item: FieldVisConfig; dataView: DataView | undefined; combinedQuery: CombinedQuery; totalDocuments?: number; + typeAccessor?: 'type' | 'secondaryType'; /** * Callback to add a filter to filter bar */ onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void; }) => { const config = { ...item, stats: { ...item.stats, totalDocuments } }; - const { loading, type, existsInDocs, fieldName } = config; + const { loading, existsInDocs, fieldName } = config; + const type = config[typeAccessor]; const dvExpandedRow = useExpandedRowCss(); function getCardContent() { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/document_stats.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/document_stats.tsx index 79a878fb11734..2a7ca2fedc0bd 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/document_stats.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/document_stats.tsx @@ -31,6 +31,7 @@ export const DocumentStat = ({ config, showIcon, totalCount }: Props) => { if (stats === undefined) return null; const { count, sampleCount } = stats; + const total = sampleCount ?? totalCount; // If field exists is docs but we don't have count stats then don't show @@ -39,7 +40,7 @@ export const DocumentStat = ({ config, showIcon, totalCount }: Props) => { count ?? (isIndexBasedFieldVisConfig(config) && config.existsInDocs === true ? undefined : 0); const docsPercent = valueCount !== undefined && total !== undefined - ? `(${roundToDecimalPlace((valueCount / total) * 100)}%)` + ? `(${total === 0 ? 0 : roundToDecimalPlace((valueCount / total) * 100)}%)` : null; const content = ( diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/top_values_preview.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/top_values_preview.tsx index 9ff7b12364aaa..f39a45f56c2ba 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/top_values_preview.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/top_values_preview.tsx @@ -24,7 +24,7 @@ export const TopValuesPreview: FC = ({ config, isNumeric const data: OrdinalDataItem[] = topValues.map((d) => ({ ...d, - key: d.key.toString(), + key: d.key?.toString(), })); const chartData: ChartData = { cardinality, diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx index 71da03d5938bb..b0a5ae1d8a47d 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx @@ -341,7 +341,10 @@ export const DataVisualizerTable = ({ return ; } - if (item.type === SUPPORTED_FIELD_TYPES.NUMBER) { + if ( + item.type === SUPPORTED_FIELD_TYPES.NUMBER || + item.secondaryType === SUPPORTED_FIELD_TYPES.NUMBER + ) { if (isIndexBasedFieldVisConfig(item) && item.stats?.distribution !== undefined) { // If the cardinality is only low, show the top values instead of a distribution chart return item.stats?.distribution?.percentiles.length <= 2 ? ( diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx index 0b2475789091f..111b0a2113403 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx @@ -28,6 +28,7 @@ import { kibanaFieldFormat } from '../utils'; import { ExpandedRowFieldHeader } from '../stats_table/components/expanded_row_field_header'; import { FieldVisStats } from '../../../../../common/types'; import { ExpandedRowPanel } from '../stats_table/components/field_data_expanded_row/expanded_row_panel'; +import { EMPTY_EXAMPLE } from '../examples_list/examples_list'; interface Props { stats: FieldVisStats | undefined; @@ -115,7 +116,8 @@ export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed, > {Array.isArray(topValues) ? topValues.map((value) => { - const fieldValue = value.key_as_string ?? value.key.toString(); + const fieldValue = + value.key_as_string ?? (value.key ? value.key.toString() : EMPTY_EXAMPLE); return ( diff --git a/x-pack/plugins/data_visualizer/public/application/common/constants.ts b/x-pack/plugins/data_visualizer/public/application/common/constants.ts index 3c2ab184c54c1..9d5f5673e9435 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/constants.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/constants.ts @@ -7,6 +7,8 @@ import { i18n } from '@kbn/i18n'; +export const DEFAULT_BAR_TARGET = 75; + export const INDEX_DATA_VISUALIZER_NAME = i18n.translate( 'xpack.dataVisualizer.chrome.help.appName', { diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/promise_all_settled_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/util/promise_all_settled_utils.ts new file mode 100644 index 0000000000000..ade4b5ac04a04 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/common/util/promise_all_settled_utils.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const isFulfilled = ( + input: PromiseSettledResult> +): input is PromiseFulfilledResult> => input.status === 'fulfilled'; +export const isRejected = ( + input: PromiseSettledResult> +): input is PromiseRejectedResult => input.status === 'rejected'; diff --git a/x-pack/plugins/data_visualizer/public/application/data_drift/use_data_drift_result.ts b/x-pack/plugins/data_visualizer/public/application/data_drift/use_data_drift_result.ts index 88b597de5c128..07b74677e8ea9 100644 --- a/x-pack/plugins/data_visualizer/public/application/data_drift/use_data_drift_result.ts +++ b/x-pack/plugins/data_visualizer/public/application/data_drift/use_data_drift_result.ts @@ -54,6 +54,7 @@ import { TimeRange, ComparisonHistogram, } from './types'; +import { isFulfilled, isRejected } from '../common/util/promise_all_settled_utils'; export const getDataComparisonType = (kibanaType: string): DataDriftField['type'] => { switch (kibanaType) { @@ -588,12 +589,6 @@ const fetchHistogramData = async ({ } }; -const isFulfilled = ( - input: PromiseSettledResult> -): input is PromiseFulfilledResult> => input.status === 'fulfilled'; -const isRejected = (input: PromiseSettledResult>): input is PromiseRejectedResult => - input.status === 'rejected'; - type EsRequestParams = NonNullable< IKibanaSearchRequest>['params'] >; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx new file mode 100644 index 0000000000000..2edd03e7f27af --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx @@ -0,0 +1,815 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable react-hooks/exhaustive-deps */ + +import { css } from '@emotion/react'; +import React, { FC, useEffect, useMemo, useState, useCallback, useRef } from 'react'; +import type { Required } from 'utility-types'; +import { + FullTimeRangeSelector, + mlTimefilterRefresh$, + useTimefilter, + DatePickerWrapper, +} from '@kbn/ml-date-picker'; +import { TextBasedLangEditor } from '@kbn/text-based-languages/public'; +import type { AggregateQuery } from '@kbn/es-query'; +import { merge } from 'rxjs'; +import { Comparators } from '@elastic/eui'; + +import { + useEuiBreakpoint, + useIsWithinMaxBreakpoint, + EuiFlexGroup, + EuiFlexItem, + EuiPageTemplate, + EuiPanel, + EuiProgress, + EuiSpacer, +} from '@elastic/eui'; +import { usePageUrlState, useUrlState } from '@kbn/ml-url-state'; +import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils'; +import { getIndexPatternFromSQLQuery, getIndexPatternFromESQLQuery } from '@kbn/es-query'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { getFieldType } from '@kbn/field-utils'; +import { UI_SETTINGS } from '@kbn/data-service'; +import type { SupportedFieldType } from '../../../../../common/types'; +import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme'; +import type { FieldVisConfig } from '../../../common/components/stats_table/types'; +import { DATA_VISUALIZER_INDEX_VIEWER } from '../../constants/index_data_visualizer_viewer'; +import type { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state'; +import { useDataVisualizerKibana } from '../../../kibana_context'; +import { GetAdditionalLinks } from '../../../common/components/results_links'; +import { DocumentCountContent } from '../../../common/components/document_count_content'; +import { useTimeBuckets } from '../../../common/hooks/use_time_buckets'; +import { + DataVisualizerTable, + ItemIdToExpandedRowMap, +} from '../../../common/components/stats_table'; +import type { + MetricFieldsStats, + TotalFieldsStats, +} from '../../../common/components/stats_table/components/field_count_stats'; +import { filterFields } from '../../../common/components/fields_stats_grid/filter_fields'; +import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row'; +import { getOrCreateDataViewByIndexPattern } from '../../search_strategy/requests/get_data_view_by_index_pattern'; +import { FieldCountPanel } from '../../../common/components/field_count_panel'; +import { useESQLFieldStatsData } from '../../hooks/esql/use_esql_field_stats_data'; +import type { NonAggregatableField, OverallStats } from '../../types/overall_stats'; +import { isESQLQuery } from '../../search_strategy/requests/esql_utils'; +import { DEFAULT_BAR_TARGET } from '../../../common/constants'; +import { + type ESQLDefaultLimitSizeOption, + ESQLDefaultLimitSizeSelect, +} from '../search_panel/esql/limit_size'; +import { type Column, useESQLOverallStatsData } from '../../hooks/esql/use_esql_overall_stats_data'; +import { type AggregatableField } from '../../types/esql_data_visualizer'; + +const defaults = getDefaultPageState(); + +interface DataVisualizerPageState { + overallStats: OverallStats; + metricConfigs: FieldVisConfig[]; + totalMetricFieldCount: number; + populatedMetricFieldCount: number; + metricsLoaded: boolean; + nonMetricConfigs: FieldVisConfig[]; + nonMetricsLoaded: boolean; + documentCountStats?: FieldVisConfig; +} + +const defaultSearchQuery = { + match_all: {}, +}; + +export function getDefaultPageState(): DataVisualizerPageState { + return { + overallStats: { + totalCount: 0, + aggregatableExistsFields: [], + aggregatableNotExistsFields: [], + nonAggregatableExistsFields: [], + nonAggregatableNotExistsFields: [], + }, + metricConfigs: [], + totalMetricFieldCount: 0, + populatedMetricFieldCount: 0, + metricsLoaded: false, + nonMetricConfigs: [], + nonMetricsLoaded: false, + documentCountStats: undefined, + }; +} + +interface ESQLDataVisualizerIndexBasedAppState extends DataVisualizerIndexBasedAppState { + limitSize: ESQLDefaultLimitSizeOption; +} + +export interface ESQLDataVisualizerIndexBasedPageUrlState { + pageKey: typeof DATA_VISUALIZER_INDEX_VIEWER; + pageUrlState: Required; +} + +export const getDefaultDataVisualizerListState = ( + overrides?: Partial +): Required => ({ + pageIndex: 0, + pageSize: 25, + sortField: 'fieldName', + sortDirection: 'asc', + visibleFieldTypes: [], + visibleFieldNames: [], + limitSize: '10000', + searchString: '', + searchQuery: defaultSearchQuery, + searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, + filters: [], + showDistributions: true, + showAllFields: false, + showEmptyFields: false, + probability: null, + rndSamplerPref: 'off', + ...overrides, +}); + +export interface IndexDataVisualizerESQLProps { + getAdditionalLinks?: GetAdditionalLinks; +} + +export const IndexDataVisualizerESQL: FC = (dataVisualizerProps) => { + const { services } = useDataVisualizerKibana(); + const { data, fieldFormats, uiSettings } = services; + const euiTheme = useCurrentEuiTheme(); + + const [query, setQuery] = useState({ esql: '' }); + const [currentDataView, setCurrentDataView] = useState(); + + const updateDataView = (dv: DataView) => { + if (dv.id !== currentDataView?.id) { + setCurrentDataView(dv); + } + }; + const [lastRefresh, setLastRefresh] = useState(0); + + const _timeBuckets = useTimeBuckets(); + const timefilter = useTimefilter({ + timeRangeSelector: true, + autoRefreshSelector: true, + }); + + const indexPattern = useMemo(() => { + let indexPatternFromQuery = ''; + if ('sql' in query) { + indexPatternFromQuery = getIndexPatternFromSQLQuery(query.sql); + } + if ('esql' in query) { + indexPatternFromQuery = getIndexPatternFromESQLQuery(query.esql); + } + // we should find a better way to work with ESQL queries which dont need a dataview + if (indexPatternFromQuery === '') { + return undefined; + } + return indexPatternFromQuery; + }, [query]); + + const restorableDefaults = useMemo( + () => getDefaultDataVisualizerListState({}), + // We just need to load the saved preference when the page is first loaded + + [] + ); + + const [dataVisualizerListState, setDataVisualizerListState] = + usePageUrlState( + DATA_VISUALIZER_INDEX_VIEWER, + restorableDefaults + ); + const [globalState, setGlobalState] = useUrlState('_g'); + + const showEmptyFields = + dataVisualizerListState.showEmptyFields ?? restorableDefaults.showEmptyFields; + const toggleShowEmptyFields = () => { + setDataVisualizerListState({ + ...dataVisualizerListState, + showEmptyFields: !dataVisualizerListState.showEmptyFields, + }); + }; + + const limitSize = dataVisualizerListState.limitSize ?? restorableDefaults.limitSize; + + const updateLimitSize = (newLimitSize: ESQLDefaultLimitSizeOption) => { + setDataVisualizerListState({ + ...dataVisualizerListState, + limitSize: newLimitSize, + }); + }; + + useEffect( + function updateAdhocDataViewFromQuery() { + let unmounted = false; + + const update = async () => { + if (!indexPattern) return; + const dv = await getOrCreateDataViewByIndexPattern( + data.dataViews, + indexPattern, + currentDataView + ); + + if (dv) { + updateDataView(dv); + } + }; + + if (!unmounted) { + update(); + } + + return () => { + unmounted = true; + }; + }, + + [indexPattern, data.dataViews, currentDataView] + ); + + /** Search strategy **/ + const fieldStatsRequest = useMemo(() => { + // Obtain the interval to use for date histogram aggregations + // (such as the document count chart). Aim for 75 bars. + const buckets = _timeBuckets; + + const tf = timefilter; + + if (!buckets || !tf || (isESQLQuery(query) && query.esql === '')) return; + const activeBounds = tf.getActiveBounds(); + + let earliest: number | undefined; + let latest: number | undefined; + if (activeBounds !== undefined && currentDataView?.timeFieldName !== undefined) { + earliest = activeBounds.min?.valueOf(); + latest = activeBounds.max?.valueOf(); + } + + const bounds = tf.getActiveBounds(); + const barTarget = uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET) ?? DEFAULT_BAR_TARGET; + buckets.setInterval('auto'); + + if (bounds) { + buckets.setBounds(bounds); + buckets.setBarTarget(barTarget); + } + + const aggInterval = buckets.getInterval(); + + const filter = currentDataView?.timeFieldName + ? ({ + bool: { + must: [], + filter: [ + { + range: { + [currentDataView.timeFieldName]: { + format: 'strict_date_optional_time', + gte: timefilter.getTime().from, + lte: timefilter.getTime().to, + }, + }, + }, + ], + should: [], + must_not: [], + }, + } as QueryDslQueryContainer) + : undefined; + return { + earliest, + latest, + aggInterval, + intervalMs: aggInterval?.asMilliseconds(), + searchQuery: query, + limitSize, + sessionId: undefined, + indexPattern, + timeFieldName: currentDataView?.timeFieldName, + runtimeFieldMap: currentDataView?.getRuntimeMappings(), + lastRefresh, + filter, + }; + }, [ + _timeBuckets, + timefilter, + currentDataView?.id, + JSON.stringify(query), + indexPattern, + lastRefresh, + limitSize, + ]); + + useEffect(() => { + // Force refresh on index pattern change + setLastRefresh(Date.now()); + }, [setLastRefresh]); + + useEffect(() => { + if (globalState?.time !== undefined) { + timefilter.setTime({ + from: globalState.time.from, + to: globalState.time.to, + }); + } + }, [JSON.stringify(globalState?.time), timefilter]); + + useEffect(() => { + const timeUpdateSubscription = merge( + timefilter.getTimeUpdate$(), + timefilter.getAutoRefreshFetch$(), + mlTimefilterRefresh$ + ).subscribe(() => { + setGlobalState({ + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + }); + setLastRefresh(Date.now()); + }); + return () => { + timeUpdateSubscription.unsubscribe(); + }; + }, []); + + useEffect(() => { + if (globalState?.refreshInterval !== undefined) { + timefilter.setRefreshInterval(globalState.refreshInterval); + } + }, [JSON.stringify(globalState?.refreshInterval), timefilter]); + + const { + documentCountStats, + totalCount, + overallStats, + overallStatsProgress, + columns, + cancelOverallStatsRequest, + } = useESQLOverallStatsData(fieldStatsRequest); + + const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs); + const [metricsLoaded] = useState(defaults.metricsLoaded); + const [metricsStats, setMetricsStats] = useState(); + + const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); + const [nonMetricsLoaded] = useState(defaults.nonMetricsLoaded); + + const [fieldStatFieldsToFetch, setFieldStatFieldsToFetch] = useState(); + + const visibleFieldTypes = + dataVisualizerListState.visibleFieldTypes ?? restorableDefaults.visibleFieldTypes; + + const visibleFieldNames = + dataVisualizerListState.visibleFieldNames ?? restorableDefaults.visibleFieldNames; + + useEffect( + function updateFieldStatFieldsToFetch() { + const { sortField, sortDirection } = dataVisualizerListState; + + // Otherwise, sort the list of fields by the initial sort field and sort direction + // Then divide into chunks by the initial page size + + const itemsSorter = Comparators.property( + sortField as string, + Comparators.default(sortDirection as 'asc' | 'desc' | undefined) + ); + + const preslicedSortedConfigs = [...nonMetricConfigs, ...metricConfigs] + .map((c) => ({ + ...c, + name: c.fieldName, + docCount: c.stats?.count, + cardinality: c.stats?.cardinality, + })) + .sort(itemsSorter); + + const filteredItems = filterFields( + preslicedSortedConfigs, + dataVisualizerListState.visibleFieldNames, + dataVisualizerListState.visibleFieldTypes + ); + + const { pageIndex, pageSize } = dataVisualizerListState; + + const pageOfConfigs = filteredItems.filteredFields + ?.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize) + .filter((d) => d.existsInDocs === true); + + setFieldStatFieldsToFetch(pageOfConfigs); + }, + [ + dataVisualizerListState.pageIndex, + dataVisualizerListState.pageSize, + dataVisualizerListState.sortField, + dataVisualizerListState.sortDirection, + nonMetricConfigs, + metricConfigs, + ] + ); + + const { fieldStats, fieldStatsProgress, cancelFieldStatsRequest } = useESQLFieldStatsData({ + searchQuery: fieldStatsRequest?.searchQuery, + columns: fieldStatFieldsToFetch, + filter: fieldStatsRequest?.filter, + limitSize: fieldStatsRequest?.limitSize, + }); + + const createMetricCards = useCallback(() => { + if (!columns || !overallStats) return; + const configs: FieldVisConfig[] = []; + const aggregatableExistsFields: AggregatableField[] = + overallStats.aggregatableExistsFields || []; + + const allMetricFields = columns.filter((f) => { + return f.secondaryType === KBN_FIELD_TYPES.NUMBER; + }); + + const metricExistsFields = allMetricFields.filter((f) => { + return aggregatableExistsFields.find((existsF) => { + return existsF.fieldName === f.name; + }); + }); + + let _aggregatableFields: AggregatableField[] = overallStats.aggregatableExistsFields; + if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) { + _aggregatableFields = _aggregatableFields.concat(overallStats.aggregatableNotExistsFields); + } + + const metricFieldsToShow = + metricsLoaded === true && showEmptyFields === true ? allMetricFields : metricExistsFields; + + metricFieldsToShow.forEach((field) => { + const fieldData = _aggregatableFields.find((f) => { + return f.fieldName === field.name; + }); + if (!fieldData) return; + + const metricConfig: FieldVisConfig = { + ...field, + ...fieldData, + loading: fieldData?.existsInDocs ?? true, + fieldFormat: + currentDataView?.getFormatterForFieldNoDefault(field.name) ?? + fieldFormats.deserialize({ id: field.secondaryType }), + aggregatable: true, + deletable: false, + type: getFieldType(field) as SupportedFieldType, + }; + + configs.push(metricConfig); + }); + + setMetricsStats({ + totalMetricFieldsCount: allMetricFields.length, + visibleMetricsCount: metricFieldsToShow.length, + }); + setMetricConfigs(configs); + }, [metricsLoaded, overallStats, showEmptyFields, columns, currentDataView?.id]); + + const createNonMetricCards = useCallback(() => { + if (!columns || !overallStats) return; + + const allNonMetricFields = columns.filter((f) => { + return f.secondaryType !== KBN_FIELD_TYPES.NUMBER; + }); + // Obtain the list of all non-metric fields which appear in documents + // (aggregatable or not aggregatable). + const populatedNonMetricFields: Column[] = []; // Kibana index pattern non metric fields. + let nonMetricFieldData: Array = []; // Basic non metric field data loaded from requesting overall stats. + const aggregatableExistsFields: AggregatableField[] = + overallStats.aggregatableExistsFields || []; + const nonAggregatableExistsFields: NonAggregatableField[] = + overallStats.nonAggregatableExistsFields || []; + + allNonMetricFields.forEach((f) => { + const checkAggregatableField = aggregatableExistsFields.find( + (existsField) => existsField.fieldName === f.name + ); + + if (checkAggregatableField !== undefined) { + populatedNonMetricFields.push(f); + nonMetricFieldData.push(checkAggregatableField); + } else { + const checkNonAggregatableField = nonAggregatableExistsFields.find( + (existsField) => existsField.fieldName === f.name + ); + + if (checkNonAggregatableField !== undefined) { + populatedNonMetricFields.push(f); + nonMetricFieldData.push(checkNonAggregatableField); + } + } + }); + + if (allNonMetricFields.length !== nonMetricFieldData.length && showEmptyFields === true) { + // Combine the field data obtained from Elasticsearch into a single array. + nonMetricFieldData = nonMetricFieldData.concat( + overallStats.aggregatableNotExistsFields, + overallStats.nonAggregatableNotExistsFields + ); + } + + const nonMetricFieldsToShow = showEmptyFields ? allNonMetricFields : populatedNonMetricFields; + + const configs: FieldVisConfig[] = []; + + nonMetricFieldsToShow.forEach((field) => { + const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.name); + const nonMetricConfig: Partial = { + ...(fieldData ? fieldData : {}), + secondaryType: getFieldType(field) as SupportedFieldType, + loading: fieldData?.existsInDocs ?? true, + deletable: false, + fieldFormat: + currentDataView?.getFormatterForFieldNoDefault(field.name) ?? + fieldFormats.deserialize({ id: field.secondaryType }), + }; + + // Map the field type from the Kibana index pattern to the field type + // used in the data visualizer. + const dataVisualizerType = getFieldType(field) as SupportedFieldType; + if (dataVisualizerType !== undefined) { + nonMetricConfig.type = dataVisualizerType; + } else { + // Add a flag to indicate that this is one of the 'other' Kibana + // field types that do not yet have a specific card type. + nonMetricConfig.type = field.type as SupportedFieldType; + nonMetricConfig.isUnsupportedType = true; + } + + if (field.name !== nonMetricConfig.fieldName) { + nonMetricConfig.displayName = field.name; + } + + configs.push(nonMetricConfig as FieldVisConfig); + }); + + setNonMetricConfigs(configs); + }, [columns, nonMetricsLoaded, overallStats, showEmptyFields, currentDataView?.id]); + + const fieldsCountStats: TotalFieldsStats | undefined = useMemo(() => { + if (!overallStats) return; + + let _visibleFieldsCount = 0; + let _totalFieldsCount = 0; + Object.keys(overallStats).forEach((key) => { + const fieldsGroup = overallStats[key as keyof typeof overallStats]; + if (Array.isArray(fieldsGroup) && fieldsGroup.length > 0) { + _totalFieldsCount += fieldsGroup.length; + } + }); + + if (showEmptyFields === true) { + _visibleFieldsCount = _totalFieldsCount; + } else { + _visibleFieldsCount = + overallStats.aggregatableExistsFields.length + + overallStats.nonAggregatableExistsFields.length; + } + return { visibleFieldsCount: _visibleFieldsCount, totalFieldsCount: _totalFieldsCount }; + }, [overallStats, showEmptyFields]); + + useEffect(() => { + createMetricCards(); + createNonMetricCards(); + }, [overallStats, showEmptyFields]); + + const configs = useMemo(() => { + let combinedConfigs = [...nonMetricConfigs, ...metricConfigs]; + + combinedConfigs = filterFields( + combinedConfigs, + visibleFieldNames, + visibleFieldTypes + ).filteredFields; + + if (fieldStatsProgress.loaded === 100 && fieldStats) { + combinedConfigs = combinedConfigs.map((c) => { + const loadedFullStats = fieldStats.get(c.fieldName) ?? {}; + return loadedFullStats + ? { + ...c, + loading: false, + stats: { ...c.stats, ...loadedFullStats }, + } + : c; + }); + } + return combinedConfigs; + }, [ + nonMetricConfigs, + metricConfigs, + visibleFieldTypes, + visibleFieldNames, + fieldStatsProgress.loaded, + dataVisualizerListState.pageIndex, + dataVisualizerListState.pageSize, + ]); + + // Some actions open up fly-out or popup + // This variable is used to keep track of them and clean up when unmounting + const actionFlyoutRef = useRef<() => void | undefined>(); + useEffect(() => { + const ref = actionFlyoutRef; + return () => { + // Clean up any of the flyout/editor opened from the actions + if (ref.current) { + ref.current(); + } + }; + }, []); + + const getItemIdToExpandedRowMap = useCallback( + function (itemIds: string[], items: FieldVisConfig[]): ItemIdToExpandedRowMap { + return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => { + const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName); + if (item !== undefined) { + m[fieldName] = ( + + ); + } + return m; + }, {} as ItemIdToExpandedRowMap); + }, + [currentDataView, totalCount] + ); + + const hasValidTimeField = useMemo( + () => + currentDataView && + currentDataView.timeFieldName !== undefined && + currentDataView.timeFieldName !== '', + [currentDataView] + ); + + const isWithinLargeBreakpoint = useIsWithinMaxBreakpoint('l'); + const dvPageHeader = css({ + [useEuiBreakpoint(['xs', 's', 'm', 'l'])]: { + flexDirection: 'column', + alignItems: 'flex-start', + }, + }); + + const combinedProgress = useMemo( + () => overallStatsProgress.loaded * 0.3 + fieldStatsProgress.loaded * 0.7, + [overallStatsProgress.loaded, fieldStatsProgress.loaded] + ); + + // Query that has been typed, but has not submitted with cmd + enter + const [localQuery, setLocalQuery] = useState({ esql: '' }); + + const onQueryUpdate = (q?: AggregateQuery) => { + // When user submits a new query + // resets all current requests and other data + if (cancelOverallStatsRequest) { + cancelOverallStatsRequest(); + } + if (cancelFieldStatsRequest) { + cancelFieldStatsRequest(); + } + // Reset field stats to fetch state + setFieldStatFieldsToFetch(undefined); + setMetricConfigs(defaults.metricConfigs); + setNonMetricConfigs(defaults.nonMetricConfigs); + if (q) { + setQuery(q); + } + }; + + useEffect( + function resetFieldStatsFieldToFetch() { + // If query returns 0 document, no need to do more work here + if (totalCount === undefined || totalCount === 0) { + setFieldStatFieldsToFetch(undefined); + return; + } + }, + [totalCount] + ); + + return ( + + + + + + {isWithinLargeBreakpoint ? : null} + + {hasValidTimeField && currentDataView ? ( + + {}} + dataView={currentDataView} + query={undefined} + disabled={false} + timefilter={timefilter} + /> + + ) : null} + + + + + + + false} + isCodeEditorExpanded={true} + detectTimestamp={true} + hideMinimizeButton={true} + hideRunQueryText={false} + /> + + + + + {totalCount !== undefined && ( + <> + + + + + )} + + + + + + + + + + items={configs} + pageState={dataVisualizerListState} + updatePageState={setDataVisualizerListState} + getItemIdToExpandedRowMap={getItemIdToExpandedRowMap} + loading={overallStatsProgress.isRunning} + overallStatsRunning={overallStatsProgress.isRunning} + showPreviewByDefault={dataVisualizerListState.showDistributions ?? true} + onChange={setDataVisualizerListState} + totalCount={totalCount} + /> + + + + + + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx index d752cb4b166f5..cc17387886071 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx @@ -64,9 +64,9 @@ import { SearchPanel } from '../search_panel'; import { ActionsPanel } from '../actions_panel'; import { createMergedEsQuery } from '../../utils/saved_search_utils'; import { DataVisualizerDataViewManagement } from '../data_view_management'; -import { GetAdditionalLinks } from '../../../common/components/results_links'; +import type { GetAdditionalLinks } from '../../../common/components/results_links'; import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data'; -import { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/grid_embeddable'; +import type { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/grid_embeddable'; import { MIN_SAMPLER_PROBABILITY, RANDOM_SAMPLER_OPTION, @@ -115,7 +115,6 @@ export const getDefaultDataVisualizerListState = ( sortDirection: 'asc', visibleFieldTypes: [], visibleFieldNames: [], - samplerShardSize: 5000, searchString: '', searchQuery: defaultSearchQuery, searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/esql/limit_size.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/esql/limit_size.tsx new file mode 100644 index 0000000000000..bcdf3241f5ee3 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/esql/limit_size.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { type ChangeEvent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSelect, EuiText, useGeneratedHtmlId } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +const options = [ + { + value: '5000', + text: i18n.translate('xpack.dataVisualizer.searchPanel.esql.limitSizeOptionLabel', { + defaultMessage: '{limit} rows', + values: { limit: '5,000' }, + }), + }, + { + value: '10000', + text: i18n.translate('xpack.dataVisualizer.searchPanel.esql.limitSizeOptionLabel', { + defaultMessage: '{limit} rows', + values: { limit: '10,000' }, + }), + }, + { + value: '100000', + text: i18n.translate('xpack.dataVisualizer.searchPanel.esql.limitSizeOptionLabel', { + defaultMessage: '{limit} rows', + values: { limit: '100,000' }, + }), + }, + { + value: '1000000', + text: i18n.translate('xpack.dataVisualizer.searchPanel.esql.limitSizeOptionLabel', { + defaultMessage: '{limit} rows', + values: { limit: '1,000,000' }, + }), + }, + { + value: 'none', + text: i18n.translate('xpack.dataVisualizer.searchPanel.esql.analyzeAll', { + defaultMessage: 'Analyze all', + }), + }, +]; + +export type ESQLDefaultLimitSizeOption = '5000' | '10000' | '100000' | '1000000' | 'none'; + +export const ESQLDefaultLimitSizeSelect = ({ + limitSize, + onChangeLimitSize, +}: { + limitSize: string; + onChangeLimitSize: (newLimit: ESQLDefaultLimitSizeOption) => void; +}) => { + const basicSelectId = useGeneratedHtmlId({ prefix: 'dvESQLLimit' }); + + const onChange = (e: ChangeEvent) => { + onChangeLimitSize(e.target.value as ESQLDefaultLimitSizeOption); + }; + + return ( + + + + } + /> + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_field_stats_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_field_stats_data.ts new file mode 100644 index 0000000000000..5c034bf82ebf7 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_field_stats_data.ts @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; +import type { AggregateQuery } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { useEffect, useReducer, useState } from 'react'; +import { chunk } from 'lodash'; +import { useCancellableSearch } from '@kbn/ml-cancellable-search'; +import type { DataStatsFetchProgress, FieldStats } from '../../../../../common/types/field_stats'; +import { useDataVisualizerKibana } from '../../../kibana_context'; +import { getInitialProgress, getReducer } from '../../progress_utils'; +import { isESQLQuery, getSafeESQLLimitSize } from '../../search_strategy/requests/esql_utils'; +import type { Column } from './use_esql_overall_stats_data'; +import { getESQLNumericFieldStats } from '../../search_strategy/esql_requests/get_numeric_field_stats'; +import { getESQLKeywordFieldStats } from '../../search_strategy/esql_requests/get_keyword_fields'; +import { getESQLDateFieldStats } from '../../search_strategy/esql_requests/get_date_field_stats'; +import { getESQLBooleanFieldStats } from '../../search_strategy/esql_requests/get_boolean_field_stats'; +import { getESQLTextFieldStats } from '../../search_strategy/esql_requests/get_text_field_stats'; + +export const useESQLFieldStatsData = ({ + searchQuery, + columns: allColumns, + filter, + limitSize, +}: { + searchQuery?: AggregateQuery; + columns?: T[]; + filter?: QueryDslQueryContainer; + limitSize?: string; +}) => { + const [fieldStats, setFieldStats] = useState>(); + + const [fetchState, setFetchState] = useReducer( + getReducer(), + getInitialProgress() + ); + + const { + services: { + data, + notifications: { toasts }, + }, + } = useDataVisualizerKibana(); + + const { runRequest, cancelRequest } = useCancellableSearch(data); + + useEffect( + function updateFieldStats() { + let unmounted = false; + + const fetchFieldStats = async () => { + cancelRequest(); + + if (!isESQLQuery(searchQuery) || !allColumns) return; + + setFetchState({ + ...getInitialProgress(), + isRunning: true, + error: undefined, + }); + try { + // By default, limit the source data to 100,000 rows + const esqlBaseQuery = searchQuery.esql + getSafeESQLLimitSize(limitSize); + + const totalFieldsCnt = allColumns.length; + const processedFieldStats = new Map(); + + function addToProcessedFieldStats(stats: Array) { + if (!unmounted) { + stats.forEach((field) => { + if (field) { + processedFieldStats.set(field.fieldName!, field); + } + }); + setFetchState({ + loaded: (processedFieldStats.size / totalFieldsCnt) * 100, + }); + } + } + setFieldStats(processedFieldStats); + + const aggregatableFieldsChunks = chunk(allColumns, 25); + + for (const columns of aggregatableFieldsChunks) { + // GETTING STATS FOR NUMERIC FIELDS + await getESQLNumericFieldStats({ + columns: columns.filter((f) => f.secondaryType === 'number'), + filter, + runRequest, + esqlBaseQuery, + }).then(addToProcessedFieldStats); + + // GETTING STATS FOR KEYWORD FIELDS + await getESQLKeywordFieldStats({ + columns: columns.filter( + (f) => f.secondaryType === 'keyword' || f.secondaryType === 'ip' + ), + filter, + runRequest, + esqlBaseQuery, + }).then(addToProcessedFieldStats); + + // GETTING STATS FOR BOOLEAN FIELDS + await getESQLBooleanFieldStats({ + columns: columns.filter((f) => f.secondaryType === 'boolean'), + filter, + runRequest, + esqlBaseQuery, + }).then(addToProcessedFieldStats); + + // GETTING STATS FOR TEXT FIELDS + await getESQLTextFieldStats({ + columns: columns.filter((f) => f.secondaryType === 'text'), + filter, + runRequest, + esqlBaseQuery, + }).then(addToProcessedFieldStats); + + // GETTING STATS FOR DATE FIELDS + await getESQLDateFieldStats({ + columns: columns.filter((f) => f.secondaryType === 'date'), + filter, + runRequest, + esqlBaseQuery, + }).then(addToProcessedFieldStats); + } + setFetchState({ + loaded: 100, + isRunning: false, + }); + } catch (e) { + if (e.name !== 'AbortError') { + const title = i18n.translate( + 'xpack.dataVisualizer.index.errorFetchingESQLFieldStatisticsMessage', + { + defaultMessage: 'Error fetching field statistics for ES|QL query', + } + ); + toasts.addError(e, { + title, + }); + + // Log error to console for better debugging + // eslint-disable-next-line no-console + console.error(`${title}: fetchFieldStats`, e); + setFetchState({ + loaded: 100, + isRunning: false, + error: e, + }); + } + } + }; + fetchFieldStats(); + + return () => { + unmounted = true; + }; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [allColumns, JSON.stringify({ filter }), limitSize] + ); + + return { fieldStats, fieldStatsProgress: fetchState, cancelFieldStatsRequest: cancelRequest }; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts new file mode 100644 index 0000000000000..c23a75e2e3bac --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts @@ -0,0 +1,395 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ESQL_SEARCH_STRATEGY, KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; +import type { AggregateQuery } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { useCallback, useEffect, useMemo, useReducer } from 'react'; +import { type UseCancellableSearch, useCancellableSearch } from '@kbn/ml-cancellable-search'; +import type { estypes } from '@elastic/elasticsearch'; +import type { ISearchOptions } from '@kbn/data-plugin/common'; +import { OMIT_FIELDS } from '../../../../../common/constants'; +import type { TimeBucketsInterval } from '../../../../../common/services/time_buckets'; +import type { + DataStatsFetchProgress, + DocumentCountStats, +} from '../../../../../common/types/field_stats'; +import { getSupportedFieldType } from '../../../common/components/fields_stats_grid/get_field_names'; +import { useDataVisualizerKibana } from '../../../kibana_context'; +import { getInitialProgress, getReducer } from '../../progress_utils'; +import { + getSafeESQLLimitSize, + getSafeESQLName, + isESQLQuery, +} from '../../search_strategy/requests/esql_utils'; +import type { NonAggregatableField } from '../../types/overall_stats'; +import { getESQLSupportedAggs } from '../../utils/get_supported_aggs'; +import type { ESQLDefaultLimitSizeOption } from '../../components/search_panel/esql/limit_size'; +import { getESQLOverallStats } from '../../search_strategy/esql_requests/get_count_and_cardinality'; +import type { AggregatableField } from '../../types/esql_data_visualizer'; +import { + handleError, + type HandleErrorCallback, +} from '../../search_strategy/esql_requests/handle_error'; + +export interface Column { + type: string; + name: string; + secondaryType: string; +} + +interface Data { + timeFieldName?: string; + columns?: Column[]; + totalCount?: number; + nonAggregatableFields?: Array<{ name: string; type: string }>; + aggregatableFields?: Array<{ name: string; type: string; supportedAggs: Set }>; + documentCountStats?: DocumentCountStats; + overallStats?: { + aggregatableExistsFields: AggregatableField[]; + aggregatableNotExistsFields: AggregatableField[]; + nonAggregatableExistsFields: NonAggregatableField[]; + nonAggregatableNotExistsFields: NonAggregatableField[]; + }; +} + +const getESQLDocumentCountStats = async ( + runRequest: UseCancellableSearch['runRequest'], + query: AggregateQuery, + filter?: estypes.QueryDslQueryContainer, + timeFieldName?: string, + intervalMs?: number, + searchOptions?: ISearchOptions, + onError?: HandleErrorCallback +): Promise<{ documentCountStats?: DocumentCountStats; totalCount: number }> => { + if (!isESQLQuery(query)) { + throw Error( + i18n.translate('xpack.dataVisualizer.esql.noQueryProvided', { + defaultMessage: 'No ES|QL query provided', + }) + ); + } + const esqlBaseQuery = query.esql; + let earliestMs = Infinity; + let latestMs = -Infinity; + + if (timeFieldName) { + const aggQuery = ` | EVAL _timestamp_= TO_DOUBLE(DATE_TRUNC(${intervalMs} millisecond, ${getSafeESQLName( + timeFieldName + )})) + | stats rows = count(*) by _timestamp_ + | LIMIT 10000`; + + const request = { + params: { + query: esqlBaseQuery + aggQuery, + ...(filter ? { filter } : {}), + }, + }; + try { + const esqlResults = await runRequest(request, { ...(searchOptions ?? {}), strategy: 'esql' }); + let totalCount = 0; + const _buckets: Record = {}; + // @ts-expect-error ES types needs to be updated with columns and values as part of esql response + esqlResults?.rawResponse.values.forEach((val) => { + const [count, bucket] = val; + _buckets[bucket] = count; + totalCount += count; + if (bucket < earliestMs) { + earliestMs = bucket; + } + if (bucket >= latestMs) { + latestMs = bucket; + } + }); + const result: DocumentCountStats = { + interval: intervalMs, + probability: 1, + randomlySampled: false, + timeRangeEarliest: earliestMs, + timeRangeLatest: latestMs, + buckets: _buckets, + totalCount, + }; + return { documentCountStats: result, totalCount }; + } catch (error) { + handleError({ + request, + error, + onError, + title: i18n.translate('xpack.dataVisualizer.esql.docCountError', { + defaultMessage: `Error getting total count & doc count chart for ES|QL time-series data for request:`, + }), + }); + return Promise.reject(error); + } + } else { + // If not time field, get the total count + const request = { + params: { + query: esqlBaseQuery + ' | STATS _count_ = COUNT(*) | LIMIT 1', + ...(filter ? { filter } : {}), + }, + }; + try { + const esqlResults = await runRequest(request, { ...(searchOptions ?? {}), strategy: 'esql' }); + return { + documentCountStats: undefined, + totalCount: esqlResults?.rawResponse.values[0][0], + }; + } catch (error) { + handleError({ + request, + error, + onError, + title: i18n.translate('xpack.dataVisualizer.esql.docCountNoneTimeseriesError', { + defaultMessage: `Error getting total count for ES|QL data:`, + }), + }); + return Promise.reject(error); + } + } +}; + +export const getInitialData = (): Data => ({ + timeFieldName: undefined, + columns: undefined, + totalCount: undefined, +}); + +const NON_AGGREGATABLE_FIELD_TYPES = new Set([ + KBN_FIELD_TYPES.GEO_SHAPE, + KBN_FIELD_TYPES.GEO_POINT, + KBN_FIELD_TYPES.HISTOGRAM, +]); + +const fieldStatsErrorTitle = i18n.translate( + 'xpack.dataVisualizer.index.errorFetchingESQLFieldStatisticsMessage', + { + defaultMessage: 'Error fetching field statistics for ES|QL query', + } +); + +export const useESQLOverallStatsData = ( + fieldStatsRequest: + | { + earliest: number | undefined; + latest: number | undefined; + aggInterval: TimeBucketsInterval; + intervalMs: number; + searchQuery: AggregateQuery; + indexPattern: string | undefined; + timeFieldName: string | undefined; + lastRefresh: number; + filter?: QueryDslQueryContainer; + limitSize?: ESQLDefaultLimitSizeOption; + } + | undefined +) => { + const { + services: { + data, + notifications: { toasts }, + }, + } = useDataVisualizerKibana(); + + const { runRequest, cancelRequest } = useCancellableSearch(data); + + const [tableData, setTableData] = useReducer(getReducer(), getInitialData()); + const [overallStatsProgress, setOverallStatsProgress] = useReducer( + getReducer(), + getInitialProgress() + ); + const onError = useCallback( + (error, title?: string) => + toasts.addError(error, { + title: title ?? fieldStatsErrorTitle, + }), + [toasts] + ); + + const startFetch = useCallback( + async function fetchOverallStats() { + try { + cancelRequest(); + + if (!fieldStatsRequest) { + return; + } + setOverallStatsProgress({ + ...getInitialProgress(), + isRunning: true, + error: undefined, + }); + setTableData({ totalCount: undefined, documentCountStats: undefined }); + + const { searchQuery, intervalMs, filter, limitSize } = fieldStatsRequest; + + if (!isESQLQuery(searchQuery)) { + return; + } + + const intervalInMs = intervalMs === 0 ? 60 * 60 * 60 * 10 : intervalMs; + + // For doc count chart, we want the full base query without any limit + const esqlBaseQuery = searchQuery.esql; + + const columnsResp = await runRequest( + { + params: { + query: esqlBaseQuery + '| LIMIT 0', + ...(filter ? { filter } : {}), + }, + }, + { strategy: ESQL_SEARCH_STRATEGY } + ); + const columns = columnsResp?.rawResponse + ? // @ts-expect-error ES types need to be updated with columns for ESQL queries + (columnsResp.rawResponse.columns.map((c) => ({ + ...c, + secondaryType: getSupportedFieldType(c.type), + })) as Column[]) + : []; + + const timeFields = columns.filter((d) => d.type === 'date'); + + const dataViewTimeField = timeFields.find( + (f) => f.name === fieldStatsRequest?.timeFieldName + ) + ? fieldStatsRequest?.timeFieldName + : undefined; + + // If a date field named '@timestamp' exists, set that as default time field + // Else, use the default time view defined by data view + // Else, use first available date field as default + const timeFieldName = + timeFields.length > 0 + ? timeFields.find((f) => f.name === '@timestamp') + ? '@timestamp' + : dataViewTimeField ?? timeFields[0].name + : undefined; + + setTableData({ columns, timeFieldName }); + + const { totalCount, documentCountStats } = await getESQLDocumentCountStats( + runRequest, + searchQuery, + filter, + timeFieldName, + intervalInMs, + undefined, + onError + ); + + setTableData({ totalCount, documentCountStats }); + setOverallStatsProgress({ + loaded: 50, + }); + const aggregatableFields: Array<{ + fieldName: string; + name: string; + type: string; + supportedAggs: Set; + secondaryType: string; + aggregatable: boolean; + }> = []; + const nonAggregatableFields: Array<{ + fieldName: string; + name: string; + type: string; + secondaryType: string; + }> = []; + const fields = columns + // Some field types are not supported by ESQL yet + // Also, temporarily removing null columns because it causes problems with some aggs + // See https://github.com/elastic/elasticsearch/issues/104430 + .filter((c) => c.type !== 'unsupported' && c.type !== 'null') + .map((field) => { + return { ...field, aggregatable: !NON_AGGREGATABLE_FIELD_TYPES.has(field.type) }; + }); + + fields?.forEach((field) => { + const fieldName = field.name; + if (!OMIT_FIELDS.includes(fieldName)) { + if (!field.aggregatable) { + nonAggregatableFields.push({ + ...field, + fieldName: field.name, + secondaryType: getSupportedFieldType(field.type), + }); + } else { + aggregatableFields.push({ + ...field, + fieldName: field.name, + secondaryType: getSupportedFieldType(field.type), + supportedAggs: getESQLSupportedAggs(field, true), + aggregatable: true, + }); + } + } + }); + + setTableData({ aggregatableFields, nonAggregatableFields }); + + // COUNT + CARDINALITY + // For % count & cardinality, we want the full base query WITH specified limit + // to safeguard against huge datasets + const esqlBaseQueryWithLimit = searchQuery.esql + getSafeESQLLimitSize(limitSize); + + if (totalCount === 0) { + setOverallStatsProgress({ + loaded: 100, + isRunning: false, + error: undefined, + }); + return; + } + if (totalCount > 0 && fields.length > 0) { + const stats = await getESQLOverallStats({ + runRequest, + fields, + esqlBaseQueryWithLimit, + filter, + limitSize, + totalCount, + onError, + }); + + setTableData({ overallStats: stats }); + setOverallStatsProgress({ + loaded: 100, + isRunning: false, + error: undefined, + }); + } + } catch (error) { + // If error already handled in sub functions, no need to propogate + if (error.name !== 'AbortError' && error.handled !== true) { + toasts.addError(error, { + title: fieldStatsErrorTitle, + }); + // Log error to console for better debugging + // eslint-disable-next-line no-console + console.error(`${fieldStatsErrorTitle}: fetchOverallStats`, error); + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [runRequest, toasts, JSON.stringify({ fieldStatsRequest }), onError] + ); + + // auto-update + useEffect(() => { + startFetch(); + }, [startFetch]); + + return useMemo( + () => ({ ...tableData, overallStatsProgress, cancelOverallStatsRequest: cancelRequest }), + [tableData, overallStatsProgress, cancelRequest] + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts index e4ba7c1ee9050..b012d049ae04f 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts @@ -10,7 +10,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { merge } from 'rxjs'; import type { EuiTableActionsColumnType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { type DataViewField } from '@kbn/data-plugin/common'; +import { UI_SETTINGS, type DataViewField } from '@kbn/data-plugin/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import seedrandom from 'seedrandom'; import type { SamplingOption } from '@kbn/discover-plugin/public/application/main/components/field_stats_table/field_stats_table'; @@ -44,6 +44,7 @@ import { useOverallStats } from './use_overall_stats'; import type { OverallStatsSearchStrategyParams } from '../../../../common/types/field_stats'; import type { AggregatableField, NonAggregatableField } from '../types/overall_stats'; import { getSupportedAggs } from '../utils/get_supported_aggs'; +import { DEFAULT_BAR_TARGET } from '../../common/constants'; const defaults = getDefaultPageState(); @@ -83,7 +84,7 @@ export const useDataVisualizerGridData = ( useExecutionContext(executionContext, embeddableExecutionContext); - const { samplerShardSize, visibleFieldTypes, showEmptyFields } = dataVisualizerListState; + const { visibleFieldTypes, showEmptyFields } = dataVisualizerListState; const [lastRefresh, setLastRefresh] = useState(0); const searchSessionId = input.sessionId; @@ -205,12 +206,12 @@ export const useDataVisualizerGridData = ( } const bounds = tf.getActiveBounds(); - const BAR_TARGET = 75; + const barTarget = uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET) ?? DEFAULT_BAR_TARGET; buckets.setInterval('auto'); if (bounds) { buckets.setBounds(bounds); - buckets.setBarTarget(BAR_TARGET); + buckets.setBarTarget(barTarget); } const aggInterval = buckets.getInterval(); @@ -243,7 +244,6 @@ export const useDataVisualizerGridData = ( aggInterval, intervalMs: aggInterval?.asMilliseconds(), searchQuery, - samplerShardSize, sessionId: searchSessionId, index: currentDataView.title, timeFieldName: currentDataView.timeFieldName, @@ -265,7 +265,6 @@ export const useDataVisualizerGridData = ( JSON.stringify(searchQuery), // eslint-disable-next-line react-hooks/exhaustive-deps JSON.stringify(samplingOption), - samplerShardSize, searchSessionId, lastRefresh, fieldsToFetch, @@ -275,6 +274,7 @@ export const useDataVisualizerGridData = ( ); const { overallStats, progress: overallStatsProgress } = useOverallStats( + false, fieldStatsRequest, lastRefresh, dataVisualizerListState.probability diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts index f47bd9aebb33d..16efc0ef6f1ea 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts @@ -73,6 +73,7 @@ export function useFieldStatsSearchStrategy( } = useDataVisualizerKibana(); const [fieldStats, setFieldStats] = useState>(); + const [fetchState, setFetchState] = useReducer( getReducer(), getInitialProgress() @@ -154,7 +155,6 @@ export function useFieldStatsSearchStrategy( const params: FieldStatsCommonRequestParams = { index: searchStrategyParams.index, - samplerShardSize: searchStrategyParams.samplerShardSize, timeFieldName: searchStrategyParams.timeFieldName, earliestMs: searchStrategyParams.earliest, latestMs: searchStrategyParams.latest, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts index 53fe7d8b1cafd..ff6030c45f96e 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts @@ -66,6 +66,7 @@ export function rateLimitingForkJoin( } export function useOverallStats( + esql = false, searchStrategyParams: TParams | undefined, lastRefresh: number, probability?: number | null diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx index 6b4a401b10629..0ab275e03fc1f 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx @@ -26,6 +26,7 @@ import { type Accessor, type Dictionary, type SetUrlState, + UrlStateProvider, } from '@kbn/ml-url-state'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { getCoreStart, getPluginsStart } from '../../kibana_services'; @@ -33,6 +34,8 @@ import { type IndexDataVisualizerViewProps, IndexDataVisualizerView, } from './components/index_data_visualizer_view'; +import { IndexDataVisualizerESQL } from './components/index_data_visualizer_view/index_data_visualizer_esql'; + import { useDataVisualizerKibana } from '../kibana_context'; import type { GetAdditionalLinks } from '../common/components/results_links'; import { DATA_VISUALIZER_APP_LOCATOR, type IndexDataVisualizerLocatorParams } from './locator'; @@ -80,7 +83,15 @@ export const getLocatorParams = (params: { return locatorParams; }; -export const DataVisualizerStateContextProvider: FC = ({ +const DataVisualizerESQLStateContextProvider = () => { + return ( + + + + ); +}; + +const DataVisualizerStateContextProvider: FC = ({ IndexDataVisualizerComponent, getAdditionalLinks, }) => { @@ -256,9 +267,7 @@ export const DataVisualizerStateContextProvider: FC - ) : ( -
- )} + ) : null} ); }; @@ -266,11 +275,13 @@ export const DataVisualizerStateContextProvider: FC = ({ getAdditionalLinks, showFrozenDataTierChoice = true, + esql, }) => { const coreStart = getCoreStart(); const { @@ -320,10 +331,14 @@ export const IndexDataVisualizer: FC = ({ - + {!esql ? ( + + ) : ( + + )} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_boolean_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_boolean_field_stats.ts new file mode 100644 index 0000000000000..b4adcd4ee4f05 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_boolean_field_stats.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UseCancellableSearch } from '@kbn/ml-cancellable-search'; +import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; +import { ESQL_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; +import pLimit from 'p-limit'; +import type { Column } from '../../hooks/esql/use_esql_overall_stats_data'; +import { getSafeESQLName } from '../requests/esql_utils'; +import { isFulfilled, isRejected } from '../../../common/util/promise_all_settled_utils'; +import { MAX_CONCURRENT_REQUESTS } from '../../constants/index_data_visualizer_viewer'; +import type { BucketCount } from '../../types/esql_data_visualizer'; +import type { BooleanFieldStats, FieldStatsError } from '../../../../../common/types/field_stats'; + +interface Params { + runRequest: UseCancellableSearch['runRequest']; + columns: Column[]; + esqlBaseQuery: string; + filter?: QueryDslQueryContainer; +} + +export const getESQLBooleanFieldStats = async ({ + runRequest, + columns, + esqlBaseQuery, + filter, +}: Params): Promise> => { + const limiter = pLimit(MAX_CONCURRENT_REQUESTS); + + const booleanFields = columns + .filter((f) => f.secondaryType === 'boolean') + .map((field) => { + const query = `| STATS ${getSafeESQLName(`${field.name}_terms`)} = count(${getSafeESQLName( + field.name + )}) BY ${getSafeESQLName(field.name)} + | LIMIT 3`; + + return { + field, + request: { + params: { + query: esqlBaseQuery + query, + ...(filter ? { filter } : {}), + }, + }, + }; + }); + + if (booleanFields.length > 0) { + const booleanTopTermsResp = await Promise.allSettled( + booleanFields.map(({ request }) => + limiter(() => runRequest(request, { strategy: ESQL_SEARCH_STRATEGY })) + ) + ); + if (booleanTopTermsResp) { + return booleanFields.map(({ field, request }, idx) => { + const resp = booleanTopTermsResp[idx]; + + if (!resp) return; + + if (isFulfilled(resp) && resp.value) { + const results = resp.value.rawResponse.values as Array<[BucketCount, boolean]>; + const topValuesSampleSize = results.reduce((acc, row) => acc + row[0], 0); + + let falseCount = 0; + let trueCount = 0; + const terms = results.map((row) => { + if (row[1] === false) { + falseCount = row[0]; + } + if (row[1] === true) { + trueCount = row[0]; + } + return { + key_as_string: row[1]?.toString(), + doc_count: row[0], + percent: row[0] / topValuesSampleSize, + }; + }); + + return { + fieldName: field.name, + topValues: terms, + topValuesSampleSize, + topValuesSamplerShardSize: topValuesSampleSize, + isTopValuesSampled: false, + trueCount, + falseCount, + count: trueCount + falseCount, + } as BooleanFieldStats; + } + + if (isRejected(resp)) { + // Log for debugging purposes + // eslint-disable-next-line no-console + console.error(resp, request); + + return { + fieldName: field.name, + error: resp.reason, + } as FieldStatsError; + } + }); + } + } + return []; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_count_and_cardinality.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_count_and_cardinality.ts new file mode 100644 index 0000000000000..72e67db1fafae --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_count_and_cardinality.ts @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ESQL_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; +import pLimit from 'p-limit'; +import { chunk } from 'lodash'; +import { isDefined } from '@kbn/ml-is-defined'; +import type { ESQLSearchReponse } from '@kbn/es-types'; +import type { UseCancellableSearch } from '@kbn/ml-cancellable-search'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { i18n } from '@kbn/i18n'; +import { getSafeESQLName } from '../requests/esql_utils'; +import { MAX_CONCURRENT_REQUESTS } from '../../constants/index_data_visualizer_viewer'; +import type { NonAggregatableField } from '../../types/overall_stats'; +import { isFulfilled } from '../../../common/util/promise_all_settled_utils'; +import type { ESQLDefaultLimitSizeOption } from '../../components/search_panel/esql/limit_size'; +import type { Column } from '../../hooks/esql/use_esql_overall_stats_data'; +import { AggregatableField } from '../../types/esql_data_visualizer'; +import { handleError, HandleErrorCallback } from './handle_error'; + +interface Field extends Column { + aggregatable?: boolean; +} +const getESQLOverallStatsInChunk = async ({ + runRequest, + fields, + esqlBaseQueryWithLimit, + filter, + limitSize, + totalCount, + onError, +}: { + runRequest: UseCancellableSearch['runRequest']; + fields: Field[]; + esqlBaseQueryWithLimit: string; + filter?: estypes.QueryDslQueryContainer; + limitSize?: ESQLDefaultLimitSizeOption; + totalCount: number; + onError?: HandleErrorCallback; +}) => { + if (fields.length > 0) { + const aggregatableFieldsToQuery = fields.filter((f) => f.aggregatable); + + let countQuery = aggregatableFieldsToQuery.length > 0 ? '| STATS ' : ''; + countQuery += aggregatableFieldsToQuery + .map((field) => { + // count idx = 0, cardinality idx = 1 + return `${getSafeESQLName(`${field.name}_count`)} = COUNT(${getSafeESQLName(field.name)}), + ${getSafeESQLName(`${field.name}_cardinality`)} = COUNT_DISTINCT(${getSafeESQLName( + field.name + )})`; + }) + .join(','); + + const request = { + params: { + query: esqlBaseQueryWithLimit + countQuery, + ...(filter ? { filter } : {}), + }, + }; + try { + const esqlResults = await runRequest(request, { strategy: ESQL_SEARCH_STRATEGY }); + const stats = { + aggregatableExistsFields: [] as AggregatableField[], + aggregatableNotExistsFields: [] as AggregatableField[], + nonAggregatableExistsFields: [] as NonAggregatableField[], + nonAggregatableNotExistsFields: [] as NonAggregatableField[], + }; + + if (!esqlResults) { + return; + } + const esqlResultsResp = esqlResults.rawResponse as unknown as ESQLSearchReponse; + + const sampleCount = + limitSize === 'none' || !isDefined(limitSize) ? totalCount : parseInt(limitSize, 10); + aggregatableFieldsToQuery.forEach((field, idx) => { + const count = esqlResultsResp.values[0][idx * 2] as number; + const cardinality = esqlResultsResp.values[0][idx * 2 + 1] as number; + + if (field.aggregatable === true) { + if (count > 0) { + stats.aggregatableExistsFields.push({ + ...field, + fieldName: field.name, + existsInDocs: true, + stats: { + sampleCount, + count, + cardinality, + }, + }); + } else { + stats.aggregatableNotExistsFields.push({ + ...field, + fieldName: field.name, + existsInDocs: false, + stats: undefined, + }); + } + } else { + const fieldData = { + fieldName: field.name, + existsInDocs: true, + }; + if (count > 0) { + stats.nonAggregatableExistsFields.push(fieldData); + } else { + stats.nonAggregatableNotExistsFields.push(fieldData); + } + } + }); + return stats; + } catch (error) { + handleError({ + error, + request, + onError, + title: i18n.translate('xpack.dataVisualizer.esql.countAndCardinalityError', { + defaultMessage: + 'Unable to fetch count & cardinality for {count} {count, plural, one {field} other {fields}}: {fieldNames}', + values: { + count: aggregatableFieldsToQuery.length, + fieldNames: aggregatableFieldsToQuery.map((r) => r.name).join(), + }, + }), + }); + return Promise.reject(error); + } + } +}; + +/** + * Fetching count and cardinality in chunks of 30 fields per request in parallel + * limiting at 10 requests maximum at a time + * @param runRequest + * @param fields + * @param esqlBaseQueryWithLimit + */ +export const getESQLOverallStats = async ({ + runRequest, + fields, + esqlBaseQueryWithLimit, + filter, + limitSize, + totalCount, + onError, +}: { + runRequest: UseCancellableSearch['runRequest']; + fields: Column[]; + esqlBaseQueryWithLimit: string; + filter?: estypes.QueryDslQueryContainer; + limitSize?: ESQLDefaultLimitSizeOption; + totalCount: number; + onError?: HandleErrorCallback; +}) => { + const limiter = pLimit(MAX_CONCURRENT_REQUESTS); + + const chunkedFields = chunk(fields, 30); + + const resp = await Promise.allSettled( + chunkedFields.map((groupedFields, idx) => + limiter(() => + getESQLOverallStatsInChunk({ + runRequest, + fields: groupedFields, + esqlBaseQueryWithLimit, + limitSize, + filter, + totalCount, + onError, + }) + ) + ) + ); + const results = resp.filter(isFulfilled).map((r) => r.value); + + const stats = results.reduce( + (acc, result) => { + if (acc && result) { + acc.aggregatableExistsFields.push(...result.aggregatableExistsFields); + acc.aggregatableNotExistsFields.push(...result.aggregatableNotExistsFields); + acc.nonAggregatableExistsFields.push(...result.nonAggregatableExistsFields); + acc.nonAggregatableNotExistsFields.push(...result.nonAggregatableNotExistsFields); + } + return acc; + }, + { + aggregatableExistsFields: [] as AggregatableField[], + aggregatableNotExistsFields: [] as AggregatableField[], + nonAggregatableExistsFields: [] as NonAggregatableField[], + nonAggregatableNotExistsFields: [] as NonAggregatableField[], + } + ); + + return stats; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_date_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_date_field_stats.ts new file mode 100644 index 0000000000000..fb06899466576 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_date_field_stats.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UseCancellableSearch } from '@kbn/ml-cancellable-search'; +import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; +import { ESQL_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; +import type { Column } from '../../hooks/esql/use_esql_overall_stats_data'; +import { getSafeESQLName } from '../requests/esql_utils'; +import type { DateFieldStats, FieldStatsError } from '../../../../../common/types/field_stats'; + +interface Params { + runRequest: UseCancellableSearch['runRequest']; + columns: Column[]; + esqlBaseQuery: string; + filter?: QueryDslQueryContainer; +} + +export const getESQLDateFieldStats = async ({ + runRequest, + columns, + esqlBaseQuery, + filter, +}: Params) => { + const dateFields = columns.map((field) => { + return { + field, + query: `${getSafeESQLName(`${field.name}_earliest`)} = MIN(${getSafeESQLName( + field.name + )}), ${getSafeESQLName(`${field.name}_latest`)} = MAX(${getSafeESQLName(field.name)})`, + }; + }); + + if (dateFields.length > 0) { + const dateStatsQuery = ' | STATS ' + dateFields.map(({ query }) => query).join(','); + const request = { + params: { + query: esqlBaseQuery + dateStatsQuery, + ...(filter ? { filter } : {}), + }, + }; + try { + const dateFieldsResp = await runRequest(request, { strategy: ESQL_SEARCH_STRATEGY }); + + if (dateFieldsResp) { + return dateFields.map(({ field: dateField }, idx) => { + const row = dateFieldsResp.rawResponse.values[0] as Array; + + const earliest = row[idx * 2]; + const latest = row[idx * 2 + 1]; + + return { + fieldName: dateField.name, + earliest, + latest, + } as DateFieldStats; + }); + } + } catch (error) { + // Log for debugging purposes + // eslint-disable-next-line no-console + console.error(error, request); + return dateFields.map(({ field }, idx) => { + return { + fieldName: field.name, + error, + } as FieldStatsError; + }); + } + } + return []; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_keyword_fields.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_keyword_fields.ts new file mode 100644 index 0000000000000..0ca4e95ac69a8 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_keyword_fields.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UseCancellableSearch } from '@kbn/ml-cancellable-search'; +import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; +import { ESQL_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; +import pLimit from 'p-limit'; +import type { Column } from '../../hooks/esql/use_esql_overall_stats_data'; +import { getSafeESQLName } from '../requests/esql_utils'; +import { isFulfilled, isRejected } from '../../../common/util/promise_all_settled_utils'; +import { MAX_CONCURRENT_REQUESTS } from '../../constants/index_data_visualizer_viewer'; +import type { BucketCount, BucketTerm } from '../../types/esql_data_visualizer'; +import type { FieldStatsError, StringFieldStats } from '../../../../../common/types/field_stats'; + +interface Params { + runRequest: UseCancellableSearch['runRequest']; + columns: Column[]; + esqlBaseQuery: string; + filter?: QueryDslQueryContainer; +} +export const getESQLKeywordFieldStats = async ({ + runRequest, + columns, + esqlBaseQuery, + filter, +}: Params) => { + const limiter = pLimit(MAX_CONCURRENT_REQUESTS); + + const keywordFields = columns.map((field) => { + const query = + esqlBaseQuery + + `| STATS ${getSafeESQLName(`${field.name}_terms`)} = count(${getSafeESQLName( + field.name + )}) BY ${getSafeESQLName(field.name)} + | LIMIT 10 + | SORT ${getSafeESQLName(`${field.name}_terms`)} DESC`; + return { + field, + request: { + params: { + query, + ...(filter ? { filter } : {}), + }, + }, + }; + }); + + if (keywordFields.length > 0) { + const keywordTopTermsResp = await Promise.allSettled( + keywordFields.map(({ request }) => + limiter(() => runRequest(request, { strategy: ESQL_SEARCH_STRATEGY })) + ) + ); + if (keywordTopTermsResp) { + return keywordFields.map(({ field, request }, idx) => { + const resp = keywordTopTermsResp[idx]; + if (!resp) return; + + if (isFulfilled(resp)) { + const results = resp.value?.rawResponse.values as Array<[BucketCount, BucketTerm]>; + if (results) { + const topValuesSampleSize = results?.reduce((acc: number, row) => acc + row[0], 0); + + const terms = results.map((row) => ({ + key: row[1], + doc_count: row[0], + percent: row[0] / topValuesSampleSize, + })); + + return { + fieldName: field.name, + topValues: terms, + topValuesSampleSize, + topValuesSamplerShardSize: topValuesSampleSize, + isTopValuesSampled: false, + } as StringFieldStats; + } + return; + } + + if (isRejected(resp)) { + // Log for debugging purposes + // eslint-disable-next-line no-console + console.error(resp, request); + + return { + fieldName: field.name, + error: resp.reason, + } as FieldStatsError; + } + }); + } + } + return []; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_numeric_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_numeric_field_stats.ts new file mode 100644 index 0000000000000..ca8684499eb3c --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_numeric_field_stats.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UseCancellableSearch } from '@kbn/ml-cancellable-search'; +import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; +import { ESQL_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; +import { chunk } from 'lodash'; +import pLimit from 'p-limit'; +import type { Column } from '../../hooks/esql/use_esql_overall_stats_data'; +import { processDistributionData } from '../../utils/process_distribution_data'; +import { PERCENTILE_SPACING } from '../requests/constants'; +import { getESQLPercentileQueryArray, getSafeESQLName, PERCENTS } from '../requests/esql_utils'; +import { isFulfilled } from '../../../common/util/promise_all_settled_utils'; +import { MAX_CONCURRENT_REQUESTS } from '../../constants/index_data_visualizer_viewer'; +import { handleError } from './handle_error'; +import type { + FieldStatsError, + NonSampledNumericFieldStats, +} from '../../../../../common/types/field_stats'; + +interface Params { + runRequest: UseCancellableSearch['runRequest']; + columns: Column[]; + esqlBaseQuery: string; + filter?: QueryDslQueryContainer; +} +const getESQLNumericFieldStatsInChunk = async ({ + runRequest, + columns, + esqlBaseQuery, + filter, +}: Params): Promise> => { + // Hashmap of agg to index/order of which is made in the ES|QL query + // {min: 0, max: 1, p0: 2, p5: 3, ..., p100: 22} + const numericAccessorMap = PERCENTS.reduce<{ [key: string]: number }>( + (acc, curr, idx) => { + // +2 for the min and max aggs + acc[`p${curr}`] = idx + 2; + return acc; + }, + { + // First two are min and max aggs + min: 0, + max: 1, + // and percentiles p0, p5, ..., p100 are the rest + } + ); + const numericFields = columns.map((field, idx) => { + const percentiles = getESQLPercentileQueryArray(field.name, PERCENTS); + return { + field, + query: `${getSafeESQLName(`${field.name}_min`)} = MIN(${getSafeESQLName(field.name)}), + ${getSafeESQLName(`${field.name}_max`)} = MAX(${getSafeESQLName(field.name)}), + ${percentiles.join(',')} + `, + // Start index of field in the response, so we know to slice & access the values + startIndex: idx * Object.keys(numericAccessorMap).length, + }; + }); + + if (numericFields.length > 0) { + const numericStatsQuery = '| STATS ' + numericFields.map(({ query }) => query).join(','); + + const request = { + params: { + query: esqlBaseQuery + numericStatsQuery, + ...(filter ? { filter } : {}), + }, + }; + try { + const fieldStatsResp = await runRequest(request, { strategy: ESQL_SEARCH_STRATEGY }); + + if (fieldStatsResp) { + const values = fieldStatsResp.rawResponse.values[0]; + + return numericFields.map(({ field, startIndex }, idx) => { + /** Order of aggs we are expecting back from query + * 0 = min; 23 = startIndex + 0 for 2nd field + * 1 = max; 24 = startIndex + 1 + * 2 p0; 25; 24 = startIndex + 2 + * 3 p5; 26 + * 4 p10; 27 + * ... + * 22 p100; + */ + const min = values[startIndex + numericAccessorMap.min]; + const max = values[startIndex + numericAccessorMap.max]; + const median = values[startIndex + numericAccessorMap.p50]; + + const percentiles = values + .slice(startIndex + numericAccessorMap.p0, startIndex + numericAccessorMap.p100) + .map((value: number) => ({ value })); + + const distribution = processDistributionData(percentiles, PERCENTILE_SPACING, min); + + return { + fieldName: field.name, + ...field, + min, + max, + median, + distribution, + } as NonSampledNumericFieldStats; + }); + } + } catch (error) { + handleError({ error, request }); + return numericFields.map(({ field }) => { + return { + fieldName: field.name, + error, + } as FieldStatsError; + }); + } + } + return []; +}; + +export const getESQLNumericFieldStats = async ({ + runRequest, + columns, + esqlBaseQuery, + filter, +}: Params): Promise> => { + const limiter = pLimit(MAX_CONCURRENT_REQUESTS); + + // Breakdown so that each requests only contains 10 numeric fields + // to prevent potential circuit breaking exception + // or too big of a payload + const numericColumnChunks = chunk(columns, 10); + const numericStats = await Promise.allSettled( + numericColumnChunks.map((numericColumns) => + limiter(() => + getESQLNumericFieldStatsInChunk({ + columns: numericColumns, + filter, + runRequest, + esqlBaseQuery, + }) + ) + ) + ); + + return numericStats.filter(isFulfilled).flatMap((stat) => stat.value); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_text_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_text_field_stats.ts new file mode 100644 index 0000000000000..f4bc710a05839 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_text_field_stats.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UseCancellableSearch } from '@kbn/ml-cancellable-search'; +import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; +import { ESQL_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; +import type { Column } from '../../hooks/esql/use_esql_overall_stats_data'; +import type { FieldExamples, FieldStatsError } from '../../../../../common/types/field_stats'; + +interface Params { + runRequest: UseCancellableSearch['runRequest']; + columns: Column[]; + esqlBaseQuery: string; + filter?: QueryDslQueryContainer; +} + +/** + * Make one query that gets the top 10 rows for each text field requested + * then process the values to showcase examples for each field + * @param + * @returns + */ +export const getESQLTextFieldStats = async ({ + runRequest, + columns: textFields, + esqlBaseQuery, + filter, +}: Params): Promise> => { + try { + if (textFields.length > 0) { + const request = { + params: { + query: + esqlBaseQuery + + `| KEEP ${textFields.map((f) => f.name).join(',')} + | LIMIT 10`, + ...(filter ? { filter } : {}), + }, + }; + const textFieldsResp = await runRequest(request, { strategy: ESQL_SEARCH_STRATEGY }); + + if (textFieldsResp) { + return textFields.map((textField, idx) => { + const examples = (textFieldsResp.rawResponse.values as unknown[][]).map( + (row) => row[idx] + ); + + return { + fieldName: textField.name, + examples, + } as FieldExamples; + }); + } + } + } catch (error) { + return textFields.map((textField, idx) => ({ + fieldName: textField.name, + error, + })) as FieldStatsError[]; + } + return []; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/handle_error.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/handle_error.ts new file mode 100644 index 0000000000000..0d3f845b13f49 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/handle_error.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +interface DataVizError extends Error { + handled?: boolean; +} +export type HandleErrorCallback = (e: DataVizError, title?: string) => void; + +export const handleError = ({ + onError, + request, + error, + title, +}: { + error: DataVizError; + request: object; + onError?: HandleErrorCallback; + title?: string; +}) => { + // Log error and request for debugging purposes + // eslint-disable-next-line no-console + console.error(error, request); + if (onError) { + error.handled = true; + error.message = JSON.stringify(request); + onError( + error, + title ?? + i18n.translate('xpack.dataVisualizer.esql.errorMessage', { + defaultMessage: 'Error excecuting ES|QL request:', + }) + ); + } +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/esql_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/esql_utils.test.ts new file mode 100644 index 0000000000000..d96346f36909f --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/esql_utils.test.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getESQLPercentileQueryArray } from './esql_utils'; + +describe('getESQLPercentileQueryArray', () => { + test('should return correct ESQL query', () => { + const query = getESQLPercentileQueryArray('@odd_field', [0, 50, 100]); + expect(query).toEqual([ + '`@odd_field_p0` = PERCENTILE(`@odd_field`, 0)', + '`@odd_field_p50` = PERCENTILE(`@odd_field`, 50)', + '`@odd_field_p100` = PERCENTILE(`@odd_field`, 100)', + ]); + }); +}); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/esql_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/esql_utils.ts new file mode 100644 index 0000000000000..334b0b06bb0ab --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/esql_utils.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { MAX_PERCENT, PERCENTILE_SPACING } from './constants'; + +export interface ESQLQuery { + esql: string; +} + +/** + * Helper function to escape special characters for field names used in ES|QL queries. + * https://www.elastic.co/guide/en/elasticsearch/reference/current/esql-syntax.html#esql-identifiers + * @param str + * @returns "`str`" + **/ +export const getSafeESQLName = (str: string) => { + return `\`${str}\``; +}; + +export function isESQLQuery(arg: unknown): arg is ESQLQuery { + return isPopulatedObject(arg, ['esql']); +} +export const PERCENTS = Array.from( + Array(MAX_PERCENT / PERCENTILE_SPACING + 1), + (_, i) => i * PERCENTILE_SPACING +); + +export const getESQLPercentileQueryArray = (fieldName: string, percents = PERCENTS) => + percents.map( + (p) => + `${getSafeESQLName(`${fieldName}_p${p}`)} = PERCENTILE(${getSafeESQLName(fieldName)}, ${p})` + ); + +export const getSafeESQLLimitSize = (str?: string) => { + if (str === 'none' || !str) return ''; + return ` | LIMIT ${str}`; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_data_view_by_index_pattern.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_data_view_by_index_pattern.ts new file mode 100644 index 0000000000000..8d0385e8d9f9e --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_data_view_by_index_pattern.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'; + +/** + * Get a saved data view that matches the index pattern (as close as possible) + * or create a new adhoc data view if no matches found + * @param dataViews + * @param indexPatternFromQuery + * @param currentDataView + * @returns + */ +export async function getOrCreateDataViewByIndexPattern( + dataViews: DataViewsContract, + indexPatternFromQuery: string | undefined, + currentDataView: DataView | undefined +) { + if (indexPatternFromQuery) { + const matched = await dataViews.find(indexPatternFromQuery); + + // Only returns persisted data view if it matches index pattern exactly + // Because * in pattern can result in misleading matches (i.e. "kibana*" will return data view with pattern "kibana_1") + // which is not neccessarily the one we want to use + if (matched.length > 0 && matched[0].getIndexPattern() === indexPatternFromQuery) + return matched[0]; + } + + if ( + indexPatternFromQuery && + (currentDataView?.isPersisted() || indexPatternFromQuery !== currentDataView?.getIndexPattern()) + ) { + const dataViewObj = await dataViews.create({ + title: indexPatternFromQuery, + }); + + if (dataViewObj.fields.getByName('@timestamp')?.type === 'date') { + dataViewObj.timeFieldName = '@timestamp'; + } + return dataViewObj; + } + return currentDataView; +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts index 690b8ec29740e..2fe7f9489e145 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts @@ -25,7 +25,7 @@ export const getDocumentCountStats = async ( search: DataPublicPluginStart['search'], params: OverallStatsSearchStrategyParams, searchOptions: ISearchOptions, - browserSessionSeed: string, + browserSessionSeed?: string, probability?: number | null, minimumRandomSamplerDocCount?: number ): Promise => { @@ -193,6 +193,7 @@ export const getDocumentCountStats = async ( } } } + return result; }; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts index f7d1b39f15d3f..a084c5bfa3687 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts @@ -21,7 +21,7 @@ import { isDefined } from '@kbn/ml-is-defined'; import { extractErrorProperties } from '@kbn/ml-error-utils'; import { processTopValues } from './utils'; import { buildAggregationWithSamplingOption } from './build_random_sampler_agg'; -import { MAX_PERCENT, PERCENTILE_SPACING, SAMPLER_TOP_TERMS_THRESHOLD } from './constants'; +import { MAX_PERCENT, PERCENTILE_SPACING } from './constants'; import type { Aggs, Bucket, @@ -154,7 +154,6 @@ export const fetchNumericFieldsStats = ( fields: Field[], options: ISearchOptions ): Observable => { - const { samplerShardSize } = params; const request: estypes.SearchRequest = getNumericFieldsStatsRequest(params, fields); return dataSearch @@ -183,9 +182,6 @@ export const fetchNumericFieldsStats = ( ); const topAggsPath = [...aggsPath, `${safeFieldName}_top`]; - if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { - topAggsPath.push('top'); - } const fieldAgg = get(aggregations, [...topAggsPath], {}) as { buckets: Bucket[] }; const { topValuesSampleSize, topValues } = processTopValues(fieldAgg); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts index 159be48b338e4..f3b70085de33f 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts @@ -19,7 +19,6 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { extractErrorProperties } from '@kbn/ml-error-utils'; import { processTopValues } from './utils'; import { buildAggregationWithSamplingOption } from './build_random_sampler_agg'; -import { SAMPLER_TOP_TERMS_THRESHOLD } from './constants'; import type { Aggs, Field, @@ -71,7 +70,6 @@ export const fetchStringFieldsStats = ( fields: Field[], options: ISearchOptions ): Observable => { - const { samplerShardSize } = params; const request: estypes.SearchRequest = getStringFieldStatsRequest(params, fields); return dataSearch @@ -94,9 +92,6 @@ export const fetchStringFieldsStats = ( const safeFieldName = field.safeFieldName; const topAggsPath = [...aggsPath, `${safeFieldName}_top`]; - if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { - topAggsPath.push('top'); - } const fieldAgg = get(aggregations, [...topAggsPath], {}); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/esql_data_visualizer.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/esql_data_visualizer.ts new file mode 100644 index 0000000000000..d4c4db162d97a --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/esql_data_visualizer.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type BucketCount = number; +export type BucketTerm = string; +export interface AggregatableField { + fieldName: string; + existsInDocs: boolean; + stats?: { + sampleCount: number; + count: number; + cardinality: number; + }; + aggregatable?: boolean; +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/index_data_visualizer_state.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/index_data_visualizer_state.ts index 508d6e0015446..960e3eb269547 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/index_data_visualizer_state.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/index_data_visualizer_state.ts @@ -31,7 +31,6 @@ export interface DataVisualizerIndexBasedAppState extends Omit | null; export type DVKey = keyof Exclude; @@ -32,6 +35,8 @@ export type DVStorageMapped = T extends typeof DV_FROZEN_TIER_P ? number | null : T extends typeof DV_DATA_DRIFT_DISTRIBUTION_CHART_TYPE ? DATA_DRIFT_COMPARISON_CHART_TYPE + : T extends typeof DV_ESQL_LIMIT_SIZE + ? ESQLDefaultLimitSizeOption : null; export const DV_STORAGE_KEYS = [ diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/get_supported_aggs.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/get_supported_aggs.ts index 26c71a7101c7f..8f4f1c570b94e 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/get_supported_aggs.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/get_supported_aggs.ts @@ -74,3 +74,11 @@ export const getSupportedAggs = (field: DataViewField) => { if (field.aggregatable) return SUPPORTED_AGGS.AGGREGATABLE; return SUPPORTED_AGGS.DEFAULT; }; + +export const getESQLSupportedAggs = ( + field: { name: string; type: string }, + aggregatable = true +) => { + if (aggregatable) return SUPPORTED_AGGS.AGGREGATABLE; + return SUPPORTED_AGGS.DEFAULT; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/process_distribution_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/process_distribution_data.ts index 46719c06e2264..3fea814b32d7e 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/process_distribution_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/process_distribution_data.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isDefined } from '@kbn/ml-is-defined'; import { last } from 'lodash'; import type { Distribution } from '../../../../common/types/field_stats'; @@ -70,6 +71,8 @@ export const processDistributionData = ( for (let i = 0; i < totalBuckets; i++) { const bucket = percentileBuckets[i]; + if (!isDefined(bucket.value)) continue; + // Results from the percentiles aggregation can have precision rounding // artifacts e.g returning 200 and 200.000000000123, so check for equality // around double floating point precision i.e. 15 sig figs. diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 9f22cf2e8bda0..9f30b3db0eb8d 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -40,6 +40,7 @@ "@kbn/lens-plugin", "@kbn/maps-plugin", "@kbn/ml-agg-utils", + "@kbn/ml-cancellable-search", "@kbn/ml-date-picker", "@kbn/ml-is-defined", "@kbn/ml-is-populated-object", @@ -70,7 +71,9 @@ "@kbn/ml-chi2test", "@kbn/field-utils", "@kbn/visualization-utils", + "@kbn/text-based-languages", "@kbn/code-editor", + "@kbn/es-types", "@kbn/ui-theme" ], "exclude": [ diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts index 3551ef6b126c3..930374567533b 100644 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts @@ -7,9 +7,9 @@ import { httpServerMock } from '@kbn/core/server/mocks'; import { CAPABILITIES, EVALUATE, KNOWLEDGE_BASE } from '../../common/constants'; import { - PostEvaluateRequestBodyInput, - PostEvaluateRequestQueryInput, -} from '@kbn/elastic-assistant-common'; + PostEvaluateBodyInputs, + PostEvaluatePathQueryInputs, +} from '../schemas/evaluate/post_evaluate'; export const requestMock = { create: httpServerMock.createKibanaRequest, @@ -46,8 +46,8 @@ export const getPostEvaluateRequest = ({ body, query, }: { - body: PostEvaluateRequestBodyInput; - query: PostEvaluateRequestQueryInput; + body: PostEvaluateBodyInputs; + query: PostEvaluatePathQueryInputs; }) => requestMock.create({ body, diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/executors/index.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/executors/index.ts deleted file mode 100644 index b36081ce4bead..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/executors/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AgentExecutor } from './types'; -import { callAgentExecutor } from '../execute_custom_llm_chain'; -import { callOpenAIFunctionsExecutor } from './openai_functions_executor'; - -/** - * To support additional Agent Executors from the UI, add them to this map - * and reference your specific AgentExecutor function - */ -export const AGENT_EXECUTOR_MAP: Record = { - DefaultAgentExecutor: callAgentExecutor, - OpenAIFunctionsExecutor: callOpenAIFunctionsExecutor, -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/model_evaluator/evaluation.ts b/x-pack/plugins/elastic_assistant/server/lib/model_evaluator/evaluation.ts index 291aa9d8c2519..54040d3d1b58e 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/model_evaluator/evaluation.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/model_evaluator/evaluation.ts @@ -12,8 +12,8 @@ import { chunk as createChunks } from 'lodash/fp'; import { Logger } from '@kbn/core/server'; import { ToolingLog } from '@kbn/tooling-log'; import { LangChainTracer, RunCollectorCallbackHandler } from 'langchain/callbacks'; -import { Dataset } from '@kbn/elastic-assistant-common'; import { AgentExecutorEvaluatorWithMetadata } from '../langchain/executors/types'; +import { Dataset } from '../../schemas/evaluate/post_evaluate'; import { callAgentWithRetry, getMessageFromLangChainResponse } from './utils'; import { ResponseBody } from '../langchain/types'; import { isLangSmithEnabled, writeLangSmithFeedback } from '../../routes/evaluate/utils'; @@ -102,6 +102,7 @@ export const performEvaluation = async ({ const chunk = requestChunks.shift() ?? []; const chunkNumber = totalChunks - requestChunks.length; logger.info(`Prediction request chunk: ${chunkNumber} of ${totalChunks}`); + logger.debug(chunk); // Note, order is kept between chunk and dataset, and is preserved w/ Promise.allSettled const chunkResults = await Promise.allSettled(chunk.map((r) => r.request())); diff --git a/x-pack/plugins/elastic_assistant/server/plugin.ts b/x-pack/plugins/elastic_assistant/server/plugin.ts index eec0a08ccb8cd..bbc2c63381fc9 100755 --- a/x-pack/plugins/elastic_assistant/server/plugin.ts +++ b/x-pack/plugins/elastic_assistant/server/plugin.ts @@ -43,7 +43,6 @@ import { GetRegisteredTools, } from './services/app_context'; import { getCapabilitiesRoute } from './routes/capabilities/get_capabilities_route'; -import { getEvaluateRoute } from './routes/evaluate/get_evaluate'; interface CreateRouteHandlerContextParams { core: CoreSetup; @@ -125,7 +124,6 @@ export class ElasticAssistantPlugin postActionsConnectorExecuteRoute(router, getElserId); // Evaluate postEvaluateRoute(router, getElserId); - getEvaluateRoute(router); // Capabilities getCapabilitiesRoute(router); return { diff --git a/x-pack/plugins/elastic_assistant/server/routes/capabilities/get_capabilities_route.ts b/x-pack/plugins/elastic_assistant/server/routes/capabilities/get_capabilities_route.ts index 7c470cdfc2d94..105e1676fb808 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/capabilities/get_capabilities_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/capabilities/get_capabilities_route.ts @@ -8,17 +8,12 @@ import { IKibanaResponse, IRouter } from '@kbn/core/server'; import { transformError } from '@kbn/securitysolution-es-utils'; -import { - API_VERSIONS, - GetCapabilitiesResponse, - INTERNAL_API_ACCESS, -} from '@kbn/elastic-assistant-common'; +import type { GetCapabilitiesResponse } from '@kbn/elastic-assistant-common'; import { CAPABILITIES } from '../../../common/constants'; import { ElasticAssistantRequestHandlerContext } from '../../types'; import { buildResponse } from '../../lib/build_response'; import { DEFAULT_PLUGIN_NAME, getPluginNameFromRequest } from '../helpers'; -import { buildRouteValidationWithZod } from '../../schemas/common'; /** * Get the assistant capabilities for the requesting plugin @@ -28,7 +23,7 @@ import { buildRouteValidationWithZod } from '../../schemas/common'; export const getCapabilitiesRoute = (router: IRouter) => { router.versioned .get({ - access: INTERNAL_API_ACCESS, + access: 'internal', path: CAPABILITIES, options: { tags: ['access:elasticAssistant'], @@ -36,14 +31,8 @@ export const getCapabilitiesRoute = (router: IRouter> => { const resp = buildResponse(response); diff --git a/x-pack/plugins/elastic_assistant/server/routes/evaluate/get_evaluate.ts b/x-pack/plugins/elastic_assistant/server/routes/evaluate/get_evaluate.ts deleted file mode 100644 index bc9922ef5f35a..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/routes/evaluate/get_evaluate.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { type IKibanaResponse, IRouter } from '@kbn/core/server'; -import { transformError } from '@kbn/securitysolution-es-utils'; - -import { - API_VERSIONS, - INTERNAL_API_ACCESS, - GetEvaluateResponse, -} from '@kbn/elastic-assistant-common'; -import { buildResponse } from '../../lib/build_response'; -import { ElasticAssistantRequestHandlerContext } from '../../types'; -import { EVALUATE } from '../../../common/constants'; -import { DEFAULT_PLUGIN_NAME, getPluginNameFromRequest } from '../helpers'; -import { buildRouteValidationWithZod } from '../../schemas/common'; -import { AGENT_EXECUTOR_MAP } from '../../lib/langchain/executors'; - -export const getEvaluateRoute = (router: IRouter) => { - router.versioned - .get({ - access: INTERNAL_API_ACCESS, - path: EVALUATE, - options: { - tags: ['access:elasticAssistant'], - }, - }) - .addVersion( - { - version: API_VERSIONS.internal.v1, - validate: { - response: { - 200: { - body: buildRouteValidationWithZod(GetEvaluateResponse), - }, - }, - }, - }, - async (context, request, response): Promise> => { - const assistantContext = await context.elasticAssistant; - const logger = assistantContext.logger; - - // Validate evaluation feature is enabled - const pluginName = getPluginNameFromRequest({ - request, - defaultPluginName: DEFAULT_PLUGIN_NAME, - logger, - }); - const registeredFeatures = assistantContext.getRegisteredFeatures(pluginName); - if (!registeredFeatures.assistantModelEvaluation) { - return response.notFound(); - } - - try { - return response.ok({ body: { agentExecutors: Object.keys(AGENT_EXECUTOR_MAP) } }); - } catch (err) { - logger.error(err); - const error = transformError(err); - - const resp = buildResponse(response); - return resp.error({ - body: { error: error.message }, - statusCode: error.statusCode, - }); - } - } - ); -}; diff --git a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.test.ts b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.test.ts index 64ec69fa5e943..3ae64f1d89f3b 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.test.ts @@ -9,17 +9,17 @@ import { postEvaluateRoute } from './post_evaluate'; import { serverMock } from '../../__mocks__/server'; import { requestContextMock } from '../../__mocks__/request_context'; import { getPostEvaluateRequest } from '../../__mocks__/request'; -import type { - PostEvaluateRequestBodyInput, - PostEvaluateRequestQueryInput, -} from '@kbn/elastic-assistant-common'; +import { + PostEvaluateBodyInputs, + PostEvaluatePathQueryInputs, +} from '../../schemas/evaluate/post_evaluate'; -const defaultBody: PostEvaluateRequestBodyInput = { +const defaultBody: PostEvaluateBodyInputs = { dataset: undefined, evalPrompt: undefined, }; -const defaultQueryParams: PostEvaluateRequestQueryInput = { +const defaultQueryParams: PostEvaluatePathQueryInputs = { agents: 'agents', datasetName: undefined, evaluationType: undefined, diff --git a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts index 33d19d6fb61e0..aa041175b75ee 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts @@ -5,23 +5,23 @@ * 2.0. */ -import { type IKibanaResponse, IRouter, KibanaRequest } from '@kbn/core/server'; +import { IRouter, KibanaRequest } from '@kbn/core/server'; import { transformError } from '@kbn/securitysolution-es-utils'; import { v4 as uuidv4 } from 'uuid'; -import { - API_VERSIONS, - INTERNAL_API_ACCESS, - PostEvaluateBody, - PostEvaluateRequestQuery, - PostEvaluateResponse, -} from '@kbn/elastic-assistant-common'; import { ESQL_RESOURCE } from '../knowledge_base/constants'; import { buildResponse } from '../../lib/build_response'; +import { buildRouteValidation } from '../../schemas/common'; import { ElasticAssistantRequestHandlerContext, GetElser } from '../../types'; import { EVALUATE } from '../../../common/constants'; +import { PostEvaluateBody, PostEvaluatePathQuery } from '../../schemas/evaluate/post_evaluate'; import { performEvaluation } from '../../lib/model_evaluator/evaluation'; -import { AgentExecutorEvaluatorWithMetadata } from '../../lib/langchain/executors/types'; +import { callAgentExecutor } from '../../lib/langchain/execute_custom_llm_chain'; +import { callOpenAIFunctionsExecutor } from '../../lib/langchain/executors/openai_functions_executor'; +import { + AgentExecutor, + AgentExecutorEvaluatorWithMetadata, +} from '../../lib/langchain/executors/types'; import { ActionsClientLlm } from '../../lib/langchain/llm/actions_client_llm'; import { indexEvaluations, @@ -30,8 +30,15 @@ import { import { fetchLangSmithDataset, getConnectorName, getLangSmithTracer, getLlmType } from './utils'; import { RequestBody } from '../../lib/langchain/types'; import { DEFAULT_PLUGIN_NAME, getPluginNameFromRequest } from '../helpers'; -import { buildRouteValidationWithZod } from '../../schemas/common'; -import { AGENT_EXECUTOR_MAP } from '../../lib/langchain/executors'; + +/** + * To support additional Agent Executors from the UI, add them to this map + * and reference your specific AgentExecutor function + */ +const AGENT_EXECUTOR_MAP: Record = { + DefaultAgentExecutor: callAgentExecutor, + OpenAIFunctionsExecutor: callOpenAIFunctionsExecutor, +}; const DEFAULT_SIZE = 20; @@ -39,215 +46,200 @@ export const postEvaluateRoute = ( router: IRouter, getElser: GetElser ) => { - router.versioned - .post({ - access: INTERNAL_API_ACCESS, + router.post( + { path: EVALUATE, - options: { - tags: ['access:elasticAssistant'], + validate: { + body: buildRouteValidation(PostEvaluateBody), + query: buildRouteValidation(PostEvaluatePathQuery), }, - }) - .addVersion( - { - version: API_VERSIONS.internal.v1, - validate: { - request: { - body: buildRouteValidationWithZod(PostEvaluateBody), - query: buildRouteValidationWithZod(PostEvaluateRequestQuery), - }, - response: { - 200: { - body: buildRouteValidationWithZod(PostEvaluateResponse), - }, - }, - }, - }, - async (context, request, response): Promise> => { - const assistantContext = await context.elasticAssistant; - const logger = assistantContext.logger; - const telemetry = assistantContext.telemetry; - - // Validate evaluation feature is enabled - const pluginName = getPluginNameFromRequest({ - request, - defaultPluginName: DEFAULT_PLUGIN_NAME, - logger, - }); - const registeredFeatures = assistantContext.getRegisteredFeatures(pluginName); - if (!registeredFeatures.assistantModelEvaluation) { - return response.notFound(); + }, + async (context, request, response) => { + const assistantContext = await context.elasticAssistant; + const logger = assistantContext.logger; + const telemetry = assistantContext.telemetry; + + // Validate evaluation feature is enabled + const pluginName = getPluginNameFromRequest({ + request, + defaultPluginName: DEFAULT_PLUGIN_NAME, + logger, + }); + const registeredFeatures = assistantContext.getRegisteredFeatures(pluginName); + if (!registeredFeatures.assistantModelEvaluation) { + return response.notFound(); + } + + try { + const evaluationId = uuidv4(); + const { + evalModel, + evaluationType, + outputIndex, + datasetName, + projectName = 'default', + runName = evaluationId, + } = request.query; + const { dataset: customDataset = [], evalPrompt } = request.body; + const connectorIds = request.query.models?.split(',') || []; + const agentNames = request.query.agents?.split(',') || []; + + const dataset = + datasetName != null ? await fetchLangSmithDataset(datasetName, logger) : customDataset; + + logger.info('postEvaluateRoute:'); + logger.info(`request.query:\n${JSON.stringify(request.query, null, 2)}`); + logger.info(`request.body:\n${JSON.stringify(request.body, null, 2)}`); + logger.info(`Evaluation ID: ${evaluationId}`); + + const totalExecutions = connectorIds.length * agentNames.length * dataset.length; + logger.info('Creating agents:'); + logger.info(`\tconnectors/models: ${connectorIds.length}`); + logger.info(`\tagents: ${agentNames.length}`); + logger.info(`\tdataset: ${dataset.length}`); + logger.warn(`\ttotal baseline agent executions: ${totalExecutions} `); + if (totalExecutions > 50) { + logger.warn( + `Total baseline agent executions >= 50! This may take a while, and cost some money...` + ); } - try { - const evaluationId = uuidv4(); - const { - evalModel, - evaluationType, - outputIndex, - datasetName, - projectName = 'default', - runName = evaluationId, - } = request.query; - const { dataset: customDataset = [], evalPrompt } = request.body; - const connectorIds = request.query.models?.split(',') || []; - const agentNames = request.query.agents?.split(',') || []; - - const dataset = - datasetName != null ? await fetchLangSmithDataset(datasetName, logger) : customDataset; - - logger.info('postEvaluateRoute:'); - logger.info(`request.query:\n${JSON.stringify(request.query, null, 2)}`); - logger.info(`request.body:\n${JSON.stringify(request.body, null, 2)}`); - logger.info(`Evaluation ID: ${evaluationId}`); - - const totalExecutions = connectorIds.length * agentNames.length * dataset.length; - logger.info('Creating agents:'); - logger.info(`\tconnectors/models: ${connectorIds.length}`); - logger.info(`\tagents: ${agentNames.length}`); - logger.info(`\tdataset: ${dataset.length}`); - logger.warn(`\ttotal baseline agent executions: ${totalExecutions} `); - if (totalExecutions > 50) { - logger.warn( - `Total baseline agent executions >= 50! This may take a while, and cost some money...` - ); - } - - // Get the actions plugin start contract from the request context for the agents - const actions = (await context.elasticAssistant).actions; - - // Fetch all connectors from the actions plugin, so we can set the appropriate `llmType` on ActionsClientLlm - const actionsClient = await actions.getActionsClientWithRequest(request); - const connectors = await actionsClient.getBulk({ - ids: connectorIds, - throwIfSystemAction: false, - }); + // Get the actions plugin start contract from the request context for the agents + const actions = (await context.elasticAssistant).actions; - // Fetch any tools registered by the request's originating plugin - const assistantTools = (await context.elasticAssistant).getRegisteredTools( - 'securitySolution' - ); + // Fetch all connectors from the actions plugin, so we can set the appropriate `llmType` on ActionsClientLlm + const actionsClient = await actions.getActionsClientWithRequest(request); + const connectors = await actionsClient.getBulk({ + ids: connectorIds, + throwIfSystemAction: false, + }); - // Get a scoped esClient for passing to the agents for retrieval, and - // writing results to the output index - const esClient = (await context.core).elasticsearch.client.asCurrentUser; - - // Default ELSER model - const elserId = await getElser(request, (await context.core).savedObjects.getClient()); - - // Skeleton request from route to pass to the agents - // params will be passed to the actions executor - const skeletonRequest: KibanaRequest = { - ...request, - body: { - alertsIndexPattern: '', - allow: [], - allowReplacement: [], - params: { - subAction: 'invokeAI', - subActionParams: { - messages: [], - }, + // Fetch any tools registered by the request's originating plugin + const assistantTools = (await context.elasticAssistant).getRegisteredTools( + 'securitySolution' + ); + + // Get a scoped esClient for passing to the agents for retrieval, and + // writing results to the output index + const esClient = (await context.core).elasticsearch.client.asCurrentUser; + + // Default ELSER model + const elserId = await getElser(request, (await context.core).savedObjects.getClient()); + + // Skeleton request from route to pass to the agents + // params will be passed to the actions executor + const skeletonRequest: KibanaRequest = { + ...request, + body: { + alertsIndexPattern: '', + allow: [], + allowReplacement: [], + params: { + subAction: 'invokeAI', + subActionParams: { + messages: [], }, - replacements: {}, - size: DEFAULT_SIZE, - isEnabledKnowledgeBase: true, - isEnabledRAGAlerts: true, }, - }; - - // Create an array of executor functions to call in batches - // One for each connector/model + agent combination - // Hoist `langChainMessages` so they can be batched by dataset.input in the evaluator - const agents: AgentExecutorEvaluatorWithMetadata[] = []; - connectorIds.forEach((connectorId) => { - agentNames.forEach((agentName) => { - logger.info(`Creating agent: ${connectorId} + ${agentName}`); - const llmType = getLlmType(connectorId, connectors); - const connectorName = - getConnectorName(connectorId, connectors) ?? '[unknown connector]'; - const detailedRunName = `${runName} - ${connectorName} + ${agentName}`; - agents.push({ - agentEvaluator: (langChainMessages, exampleId) => - AGENT_EXECUTOR_MAP[agentName]({ - actions, - isEnabledKnowledgeBase: true, - assistantTools, - connectorId, - esClient, - elserId, - langChainMessages, - llmType, - logger, - request: skeletonRequest, - kbResource: ESQL_RESOURCE, - telemetry, - traceOptions: { - exampleId, - projectName, - runName: detailedRunName, - evaluationId, - tags: [ - 'security-assistant-prediction', - ...(connectorName != null ? [connectorName] : []), - runName, - ], - tracers: getLangSmithTracer(detailedRunName, exampleId, logger), - }, - }), - metadata: { - connectorName, - runName: detailedRunName, - }, - }); - }); - }); - logger.info(`Agents created: ${agents.length}`); - - // Evaluator Model is optional to support just running predictions - const evaluatorModel = - evalModel == null || evalModel === '' - ? undefined - : new ActionsClientLlm({ + replacements: {}, + size: DEFAULT_SIZE, + isEnabledKnowledgeBase: true, + isEnabledRAGAlerts: true, + }, + }; + + // Create an array of executor functions to call in batches + // One for each connector/model + agent combination + // Hoist `langChainMessages` so they can be batched by dataset.input in the evaluator + const agents: AgentExecutorEvaluatorWithMetadata[] = []; + connectorIds.forEach((connectorId) => { + agentNames.forEach((agentName) => { + logger.info(`Creating agent: ${connectorId} + ${agentName}`); + const llmType = getLlmType(connectorId, connectors); + const connectorName = + getConnectorName(connectorId, connectors) ?? '[unknown connector]'; + const detailedRunName = `${runName} - ${connectorName} + ${agentName}`; + agents.push({ + agentEvaluator: (langChainMessages, exampleId) => + AGENT_EXECUTOR_MAP[agentName]({ actions, - connectorId: evalModel, - request: skeletonRequest, + isEnabledKnowledgeBase: true, + assistantTools, + connectorId, + esClient, + elserId, + langChainMessages, + llmType, logger, - }); - - const { evaluationResults, evaluationSummary } = await performEvaluation({ - agentExecutorEvaluators: agents, - dataset, - evaluationId, - evaluatorModel, - evaluationPrompt: evalPrompt, - evaluationType, - logger, - runName, + request: skeletonRequest, + kbResource: ESQL_RESOURCE, + telemetry, + traceOptions: { + exampleId, + projectName, + runName: detailedRunName, + evaluationId, + tags: [ + 'security-assistant-prediction', + ...(connectorName != null ? [connectorName] : []), + runName, + ], + tracers: getLangSmithTracer(detailedRunName, exampleId, logger), + }, + }), + metadata: { + connectorName, + runName: detailedRunName, + }, + }); }); + }); + logger.info(`Agents created: ${agents.length}`); + + // Evaluator Model is optional to support just running predictions + const evaluatorModel = + evalModel == null || evalModel === '' + ? undefined + : new ActionsClientLlm({ + actions, + connectorId: evalModel, + request: skeletonRequest, + logger, + }); - logger.info(`Writing evaluation results to index: ${outputIndex}`); - await setupEvaluationIndex({ esClient, index: outputIndex, logger }); - await indexEvaluations({ - esClient, - evaluationResults, - evaluationSummary, - index: outputIndex, - logger, - }); + const { evaluationResults, evaluationSummary } = await performEvaluation({ + agentExecutorEvaluators: agents, + dataset, + evaluationId, + evaluatorModel, + evaluationPrompt: evalPrompt, + evaluationType, + logger, + runName, + }); - return response.ok({ - body: { evaluationId, success: true }, - }); - } catch (err) { - logger.error(err); - const error = transformError(err); - - const resp = buildResponse(response); - return resp.error({ - body: { success: false, error: error.message }, - statusCode: error.statusCode, - }); - } + logger.info(`Writing evaluation results to index: ${outputIndex}`); + await setupEvaluationIndex({ esClient, index: outputIndex, logger }); + await indexEvaluations({ + esClient, + evaluationResults, + evaluationSummary, + index: outputIndex, + logger, + }); + + return response.ok({ + body: { evaluationId, success: true }, + }); + } catch (err) { + logger.error(err); + const error = transformError(err); + + const resp = buildResponse(response); + return resp.error({ + body: { success: false, error: error.message }, + statusCode: error.statusCode, + }); } - ); + } + ); }; diff --git a/x-pack/plugins/elastic_assistant/server/routes/evaluate/utils.ts b/x-pack/plugins/elastic_assistant/server/routes/evaluate/utils.ts index 11f8cb9c2f692..550e89667256e 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/evaluate/utils.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/evaluate/utils.ts @@ -12,7 +12,7 @@ import type { Logger } from '@kbn/core/server'; import type { Run } from 'langsmith/schemas'; import { ToolingLog } from '@kbn/tooling-log'; import { LangChainTracer } from 'langchain/callbacks'; -import { Dataset } from '@kbn/elastic-assistant-common'; +import { Dataset } from '../../schemas/evaluate/post_evaluate'; /** * Returns the LangChain `llmType` for the given connectorId/connectors diff --git a/x-pack/plugins/elastic_assistant/server/schemas/common.ts b/x-pack/plugins/elastic_assistant/server/schemas/common.ts index 5e847aef69fc0..00e97a9326c5e 100644 --- a/x-pack/plugins/elastic_assistant/server/schemas/common.ts +++ b/x-pack/plugins/elastic_assistant/server/schemas/common.ts @@ -14,8 +14,6 @@ import type { RouteValidationResultFactory, RouteValidationError, } from '@kbn/core/server'; -import type { TypeOf, ZodType } from 'zod'; -import { stringifyZodError } from '@kbn/zod-helpers'; type RequestValidationResult = | { @@ -38,14 +36,3 @@ export const buildRouteValidation = (validatedInput: A) => validationResult.ok(validatedInput) ) ); - -export const buildRouteValidationWithZod = - >(schema: T): RouteValidationFunction => - (inputValue: unknown, validationResult: RouteValidationResultFactory) => { - const decoded = schema.safeParse(inputValue); - if (decoded.success) { - return validationResult.ok(decoded.data); - } else { - return validationResult.badRequest(stringifyZodError(decoded.error)); - } - }; diff --git a/x-pack/plugins/elastic_assistant/server/schemas/evaluate/post_evaluate.ts b/x-pack/plugins/elastic_assistant/server/schemas/evaluate/post_evaluate.ts new file mode 100644 index 0000000000000..f520bf9bf93b6 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/schemas/evaluate/post_evaluate.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; + +/** Validates Output Index starts with `.kibana-elastic-ai-assistant-` */ +const outputIndex = new t.Type( + 'OutputIndexPrefixed', + (input): input is string => + typeof input === 'string' && input.startsWith('.kibana-elastic-ai-assistant-'), + (input, context) => + typeof input === 'string' && input.startsWith('.kibana-elastic-ai-assistant-') + ? t.success(input) + : t.failure( + input, + context, + `Type error: Output Index does not start with '.kibana-elastic-ai-assistant-'` + ), + t.identity +); + +/** Validates the URL path of a POST request to the `/evaluate` endpoint */ +export const PostEvaluatePathQuery = t.type({ + agents: t.string, + datasetName: t.union([t.string, t.undefined]), + evaluationType: t.union([t.string, t.undefined]), + evalModel: t.union([t.string, t.undefined]), + models: t.string, + outputIndex, + projectName: t.union([t.string, t.undefined]), + runName: t.union([t.string, t.undefined]), +}); + +export type PostEvaluatePathQueryInputs = t.TypeOf; + +export type DatasetItem = t.TypeOf; +export const DatasetItem = t.type({ + id: t.union([t.string, t.undefined]), + input: t.string, + reference: t.string, + tags: t.union([t.array(t.string), t.undefined]), + prediction: t.union([t.string, t.undefined]), +}); + +export type Dataset = t.TypeOf; +export const Dataset = t.array(DatasetItem); + +/** Validates the body of a POST request to the `/evaluate` endpoint */ +export const PostEvaluateBody = t.type({ + dataset: t.union([Dataset, t.undefined]), + evalPrompt: t.union([t.string, t.undefined]), +}); + +export type PostEvaluateBodyInputs = t.TypeOf; diff --git a/x-pack/plugins/elastic_assistant/tsconfig.json b/x-pack/plugins/elastic_assistant/tsconfig.json index 2717da8d33a3a..dfca7893b2036 100644 --- a/x-pack/plugins/elastic_assistant/tsconfig.json +++ b/x-pack/plugins/elastic_assistant/tsconfig.json @@ -35,7 +35,6 @@ "@kbn/core-analytics-server", "@kbn/elastic-assistant-common", "@kbn/core-http-router-server-mocks", - "@kbn/zod-helpers", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/fleet/common/services/output_helpers.ts b/x-pack/plugins/fleet/common/services/output_helpers.ts index 988be3bc277f7..998a357d57976 100644 --- a/x-pack/plugins/fleet/common/services/output_helpers.ts +++ b/x-pack/plugins/fleet/common/services/output_helpers.ts @@ -19,18 +19,21 @@ import { RESERVED_CONFIG_YML_KEYS, } from '../constants'; +const sameClusterRestrictedPackages = [ + FLEET_SERVER_PACKAGE, + FLEET_SYNTHETICS_PACKAGE, + FLEET_APM_PACKAGE, +]; + /** * Return allowed output type for a given agent policy, * Fleet Server and APM cannot use anything else than same cluster ES */ -export function getAllowedOutputTypeForPolicy(agentPolicy: AgentPolicy) { +export function getAllowedOutputTypeForPolicy(agentPolicy: AgentPolicy): string[] { const isRestrictedToSameClusterES = agentPolicy.package_policies && agentPolicy.package_policies.some( - (p) => - p.package?.name === FLEET_SERVER_PACKAGE || - p.package?.name === FLEET_SYNTHETICS_PACKAGE || - p.package?.name === FLEET_APM_PACKAGE + (p) => p.package?.name && sameClusterRestrictedPackages.includes(p.package?.name) ); if (isRestrictedToSameClusterES) { @@ -40,6 +43,16 @@ export function getAllowedOutputTypeForPolicy(agentPolicy: AgentPolicy) { return Object.values(outputType); } +export function getAllowedOutputTypesForIntegration(packageName: string): string[] { + const isRestrictedToSameClusterES = sameClusterRestrictedPackages.includes(packageName); + + if (isRestrictedToSameClusterES) { + return [outputType.Elasticsearch]; + } + + return Object.values(outputType); +} + export function outputYmlIncludesReservedPerformanceKey( configYml: string, // Dependency injection for `safeLoad` prevents bundle size issues 🤷‍♀️ diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 8333d0257a8f0..93e3bab5529d4 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -583,6 +583,9 @@ export class FleetPlugin } ); + // initialize (generate/encrypt/validate) Uninstall Tokens asynchronously + this.initializeUninstallTokens(); + this.fleetStatus$.next({ level: ServiceStatusLevels.available, summary: 'Fleet is available', @@ -698,4 +701,54 @@ export class FleetPlugin return this.logger; } + + private async initializeUninstallTokens() { + try { + await this.generateUninstallTokens(); + } catch (error) { + appContextService + .getLogger() + .error('Error happened during uninstall token generation.', { error: { message: error } }); + } + + try { + await this.validateUninstallTokens(); + } catch (error) { + appContextService + .getLogger() + .error('Error happened during uninstall token validation.', { error: { message: error } }); + } + } + + private async generateUninstallTokens() { + const logger = appContextService.getLogger(); + + logger.debug('Generating Agent uninstall tokens'); + if (!appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) { + logger.warn( + 'xpack.encryptedSavedObjects.encryptionKey is not configured, agent uninstall tokens are being stored in plain text' + ); + } + await appContextService.getUninstallTokenService()?.generateTokensForAllPolicies(); + + if (appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) { + logger.debug('Checking for and encrypting plain text uninstall tokens'); + await appContextService.getUninstallTokenService()?.encryptTokens(); + } + } + + private async validateUninstallTokens() { + const logger = appContextService.getLogger(); + logger.debug('Validating uninstall tokens'); + + const unintallTokenValidationError = await appContextService + .getUninstallTokenService() + ?.checkTokenValidityForAllPolicies(); + + if (unintallTokenValidationError) { + logger.warn(unintallTokenValidationError.error.message); + } else { + logger.debug('Uninstall tokens validation successful.'); + } + } } diff --git a/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts index e43bc275a9c4d..dfdcf26adaa4e 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/output_helpers.test.ts @@ -13,6 +13,7 @@ import { appContextService } from '..'; import { outputService } from '../output'; import { validateOutputForPolicy } from '.'; +import { validateOutputForNewPackagePolicy } from './outputs_helpers'; jest.mock('../app_context'); jest.mock('../output'); @@ -252,3 +253,94 @@ describe('validateOutputForPolicy', () => { }); }); }); + +describe('validateOutputForNewPackagePolicy', () => { + it('should not allow fleet_server integration to be added to a policy using a logstash output', async () => { + mockHasLicence(true); + mockedOutputService.get.mockResolvedValue({ + type: 'logstash', + } as any); + await expect( + validateOutputForNewPackagePolicy( + savedObjectsClientMock.create(), + { + name: 'Agent policy', + data_output_id: 'test1', + monitoring_output_id: 'test1', + } as any, + 'fleet_server' + ) + ).rejects.toThrow( + 'Integration "fleet_server" cannot be added to agent policy "Agent policy" because it uses output type "logstash".' + ); + }); + + it('should not allow apm integration to be added to a policy using a kafka output', async () => { + mockHasLicence(true); + mockedOutputService.get.mockResolvedValue({ + type: 'kafka', + } as any); + await expect( + validateOutputForNewPackagePolicy( + savedObjectsClientMock.create(), + { + name: 'Agent policy', + data_output_id: 'test1', + monitoring_output_id: 'test1', + } as any, + 'apm' + ) + ).rejects.toThrow( + 'Integration "apm" cannot be added to agent policy "Agent policy" because it uses output type "kafka".' + ); + }); + + it('should not allow synthetics integration to be added to a policy using a default logstash output', async () => { + mockHasLicence(true); + mockedOutputService.get.mockResolvedValue({ + type: 'logstash', + } as any); + mockedOutputService.getDefaultDataOutputId.mockResolvedValue('default'); + await expect( + validateOutputForNewPackagePolicy( + savedObjectsClientMock.create(), + { + name: 'Agent policy', + } as any, + 'synthetics' + ) + ).rejects.toThrow( + 'Integration "synthetics" cannot be added to agent policy "Agent policy" because it uses output type "logstash".' + ); + }); + + it('should allow other integration to be added to a policy using logstash output', async () => { + mockHasLicence(true); + mockedOutputService.get.mockResolvedValue({ + type: 'logstash', + } as any); + + await validateOutputForNewPackagePolicy( + savedObjectsClientMock.create(), + { + name: 'Agent policy', + } as any, + 'nginx' + ); + }); + + it('should allow fleet_server integration to be added to a policy using elasticsearch output', async () => { + mockHasLicence(true); + mockedOutputService.get.mockResolvedValue({ + type: 'elasticsearch', + } as any); + + await validateOutputForNewPackagePolicy( + savedObjectsClientMock.create(), + { + name: 'Agent policy', + } as any, + 'fleet_server' + ); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts b/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts index c7c02d8a53fb5..67f5a7772aa52 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/outputs_helpers.ts @@ -7,6 +7,8 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; +import { getAllowedOutputTypesForIntegration } from '../../../common/services/output_helpers'; + import type { AgentPolicySOAttributes, AgentPolicy } from '../../types'; import { LICENCE_FOR_PER_POLICY_OUTPUT, outputType } from '../../../common/constants'; import { policyHasFleetServer, policyHasSyntheticsIntegration } from '../../../common/services'; @@ -46,7 +48,7 @@ export async function validateOutputForPolicy( soClient: SavedObjectsClientContract, newData: Partial, existingData: Partial = {}, - allowedOutputTypeForPolicy = Object.values(outputType) + allowedOutputTypeForPolicy: string[] = Object.values(outputType) ) { if ( newData.data_output_id === existingData.data_output_id && @@ -93,3 +95,23 @@ export async function validateOutputForPolicy( ); } } + +export async function validateOutputForNewPackagePolicy( + soClient: SavedObjectsClientContract, + agentPolicy: AgentPolicy, + packageName: string +) { + const allowedOutputTypeForPolicy = getAllowedOutputTypesForIntegration(packageName); + + const isOutputTypeRestricted = + allowedOutputTypeForPolicy.length !== Object.values(outputType).length; + + if (isOutputTypeRestricted) { + const dataOutput = await getDataOutputForAgentPolicy(soClient, agentPolicy); + if (!allowedOutputTypeForPolicy.includes(dataOutput.type)) { + throw new OutputInvalidError( + `Integration "${packageName}" cannot be added to agent policy "${agentPolicy.name}" because it uses output type "${dataOutput.type}".` + ); + } + } +} diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 98e3f9247bf36..e89ac0160f62c 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -46,8 +46,6 @@ import { } from '../../common/services'; import { SO_SEARCH_LIMIT, - FLEET_APM_PACKAGE, - outputType, PACKAGES_SAVED_OBJECT_TYPE, DATASET_VAR_NAME, } from '../../common/constants'; @@ -103,7 +101,6 @@ import { getAuthzFromRequest, doesNotHaveRequiredFleetAuthz } from './security'; import { storedPackagePolicyToAgentInputs } from './agent_policies'; import { agentPolicyService } from './agent_policy'; -import { getDataOutputForAgentPolicy } from './agent_policies'; import { getPackageInfo, getInstallation, ensureInstalledPackage } from './epm/packages'; import { getAssetsDataFromAssetsMap } from './epm/packages/assets'; import { compileTemplate } from './epm/agent/agent'; @@ -124,6 +121,7 @@ import { isSecretStorageEnabled, } from './secrets'; import { getPackageAssetsMap } from './epm/packages/get'; +import { validateOutputForNewPackagePolicy } from './agent_policies/outputs_helpers'; export type InputsOverride = Partial & { vars?: Array; @@ -225,11 +223,12 @@ class PackagePolicyClientImpl implements PackagePolicyClient { true ); - if (agentPolicy && enrichedPackagePolicy.package?.name === FLEET_APM_PACKAGE) { - const dataOutput = await getDataOutputForAgentPolicy(soClient, agentPolicy); - if (dataOutput.type === outputType.Logstash) { - throw new FleetError('You cannot add APM to a policy using a logstash output'); - } + if (agentPolicy && enrichedPackagePolicy.package?.name) { + await validateOutputForNewPackagePolicy( + soClient, + agentPolicy, + enrichedPackagePolicy.package?.name + ); } await validateIsNotHostedPolicy(soClient, enrichedPackagePolicy.policy_id, options?.force); diff --git a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts index 5dfd1e5c951f0..5cca694d844ab 100644 --- a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts +++ b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts @@ -31,6 +31,16 @@ import { agentPolicyService } from '../../agent_policy'; import { UninstallTokenService, type UninstallTokenServiceInterface } from '.'; +interface TokenSO { + id: string; + attributes: { + policy_id: string; + token?: string; + token_plain?: string; + }; + created_at: string; +} + describe('UninstallTokenService', () => { const now = new Date().toISOString(); const aDayAgo = new Date(Date.now() - 24 * 3600 * 1000).toISOString(); @@ -41,7 +51,7 @@ describe('UninstallTokenService', () => { let mockBuckets: any[] = []; let uninstallTokenService: UninstallTokenServiceInterface; - function getDefaultSO(encrypted: boolean = true) { + function getDefaultSO(encrypted: boolean = true): TokenSO { return encrypted ? { id: 'test-so-id', @@ -61,7 +71,7 @@ describe('UninstallTokenService', () => { }; } - function getDefaultSO2(encrypted: boolean = true) { + function getDefaultSO2(encrypted: boolean = true): TokenSO { return encrypted ? { id: 'test-so-id-two', @@ -81,6 +91,20 @@ describe('UninstallTokenService', () => { }; } + const decorateSOWithError = (so: TokenSO) => ({ + ...so, + error: new Error('error reason'), + }); + + const decorateSOWithMissingToken = (so: TokenSO) => ({ + ...so, + attributes: { + ...so.attributes, + token: undefined, + token_plain: undefined, + }, + }); + function getDefaultBuckets(encrypted: boolean = true) { const defaultSO = getDefaultSO(encrypted); const defaultSO2 = getDefaultSO2(encrypted); @@ -227,6 +251,26 @@ describe('UninstallTokenService', () => { } ); }); + + it('throws error if token is missing', async () => { + const so = decorateSOWithMissingToken(getDefaultSO(canEncrypt)); + mockCreatePointInTimeFinderAsInternalUser([so]); + + await expect(uninstallTokenService.getToken(so.id)).rejects.toThrowError( + new UninstallTokenError( + 'Invalid uninstall token: Saved object is missing the token attribute.' + ) + ); + }); + + it("throws error if there's a depcryption error", async () => { + const so = decorateSOWithError(getDefaultSO2(canEncrypt)); + mockCreatePointInTimeFinderAsInternalUser([so]); + + await expect(uninstallTokenService.getToken(so.id)).rejects.toThrowError( + new UninstallTokenError("Error when reading Uninstall Token with id 'test-so-id-two'.") + ); + }); }); describe('getTokenMetadata', () => { @@ -507,18 +551,9 @@ describe('UninstallTokenService', () => { describe('check validity of tokens', () => { const okaySO = getDefaultSO(canEncrypt); - const errorWithDecryptionSO2 = { - ...getDefaultSO2(canEncrypt), - error: new Error('error reason'), - }; - const missingTokenSO2 = { - ...getDefaultSO2(canEncrypt), - attributes: { - ...getDefaultSO2(canEncrypt).attributes, - token: undefined, - token_plain: undefined, - }, - }; + const errorWithDecryptionSO1 = decorateSOWithError(getDefaultSO(canEncrypt)); + const errorWithDecryptionSO2 = decorateSOWithError(getDefaultSO2(canEncrypt)); + const missingTokenSO2 = decorateSOWithMissingToken(getDefaultSO2(canEncrypt)); describe('checkTokenValidityForAllPolicies', () => { it('returns null if all of the tokens are available', async () => { @@ -578,20 +613,31 @@ describe('UninstallTokenService', () => { uninstallTokenService.checkTokenValidityForAllPolicies() ).resolves.toStrictEqual({ error: new UninstallTokenError( - 'Invalid uninstall token: Saved object is missing the token attribute.' + 'Failed to validate Uninstall Tokens: 1 of 2 tokens are invalid' ), }); }); - it('returns error if token decryption gives error', async () => { + it('returns error if some of the tokens cannot be decrypted', async () => { mockCreatePointInTimeFinderAsInternalUser([okaySO, errorWithDecryptionSO2]); await expect( uninstallTokenService.checkTokenValidityForAllPolicies() ).resolves.toStrictEqual({ - error: new UninstallTokenError( - "Error when reading Uninstall Token with id 'test-so-id-two'." - ), + error: new UninstallTokenError('Failed to decrypt 1 of 2 Uninstall Token(s)'), + }); + }); + + it('returns error if none of the tokens can be decrypted', async () => { + mockCreatePointInTimeFinderAsInternalUser([ + errorWithDecryptionSO1, + errorWithDecryptionSO2, + ]); + + await expect( + uninstallTokenService.checkTokenValidityForAllPolicies() + ).resolves.toStrictEqual({ + error: new UninstallTokenError('Failed to decrypt 2 of 2 Uninstall Token(s)'), }); }); @@ -607,7 +653,7 @@ describe('UninstallTokenService', () => { }); describe('checkTokenValidityForPolicy', () => { - it('returns empty array if token is available', async () => { + it('returns null if token is available', async () => { mockCreatePointInTimeFinderAsInternalUser(); await expect( @@ -616,28 +662,26 @@ describe('UninstallTokenService', () => { }); it('returns error if token is missing', async () => { - mockCreatePointInTimeFinderAsInternalUser([okaySO, missingTokenSO2]); + mockCreatePointInTimeFinderAsInternalUser([missingTokenSO2]); await expect( uninstallTokenService.checkTokenValidityForPolicy(missingTokenSO2.attributes.policy_id) ).resolves.toStrictEqual({ error: new UninstallTokenError( - 'Invalid uninstall token: Saved object is missing the token attribute.' + 'Failed to validate Uninstall Tokens: 1 of 1 tokens are invalid' ), }); }); it('returns error if token decryption gives error', async () => { - mockCreatePointInTimeFinderAsInternalUser([okaySO, errorWithDecryptionSO2]); + mockCreatePointInTimeFinderAsInternalUser([errorWithDecryptionSO2]); await expect( uninstallTokenService.checkTokenValidityForPolicy( errorWithDecryptionSO2.attributes.policy_id ) ).resolves.toStrictEqual({ - error: new UninstallTokenError( - "Error when reading Uninstall Token with id 'test-so-id-two'." - ), + error: new UninstallTokenError('Failed to decrypt 1 of 1 Uninstall Token(s)'), }); }); diff --git a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts index 2215035684c67..3d43f280ba116 100644 --- a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts +++ b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts @@ -169,9 +169,9 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { public async getToken(id: string): Promise { const filter = `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.id: "${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}:${id}"`; - const uninstallTokens = await this.getDecryptedTokens({ filter }); + const tokenObjects = await this.getDecryptedTokenObjects({ filter }); - return uninstallTokens.length === 1 ? uninstallTokens[0] : null; + return tokenObjects.length === 1 ? this.convertTokenObjectToToken(tokenObjects[0]) : null; } public async getTokenMetadata( @@ -201,6 +201,14 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { } private async getDecryptedTokensForPolicyIds(policyIds: string[]): Promise { + const tokenObjects = await this.getDecryptedTokenObjectsForPolicyIds(policyIds); + + return tokenObjects.map(this.convertTokenObjectToToken); + } + + private async getDecryptedTokenObjectsForPolicyIds( + policyIds: string[] + ): Promise>> { const tokenObjectHits = await this.getTokenObjectsByIncludeFilter(policyIds); if (tokenObjectHits.length === 0) { @@ -211,15 +219,33 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { ({ _id }) => `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.id: "${_id}"` ); - const uninstallTokenChunks: UninstallToken[][] = await asyncMap( - chunk(filterEntries, this.getUninstallTokenVerificationBatchSize()), - (entries) => { - const filter = entries.join(' or '); - return this.getDecryptedTokens({ filter }); + let tokenObjectChunks: Array>> = []; + + try { + tokenObjectChunks = await asyncMap( + chunk(filterEntries, this.getUninstallTokenVerificationBatchSize()), + async (entries) => { + const filter = entries.join(' or '); + return this.getDecryptedTokenObjects({ filter }); + } + ); + } catch (error) { + if (isResponseError(error) && error.message.includes('too_many_nested_clauses')) { + // `too_many_nested_clauses` is considered non-fatal + const errorMessage = + 'Failed to validate uninstall tokens: `too_many_nested_clauses` error received. ' + + 'Setting/decreasing the value of `xpack.fleet.setup.uninstallTokenVerificationBatchSize` in your kibana.yml should help. ' + + `Current value is ${this.getUninstallTokenVerificationBatchSize()}.`; + + appContextService.getLogger().warn(`${errorMessage}: '${error}'`); + + throw new UninstallTokenError(errorMessage); + } else { + throw error; } - ); + } - return uninstallTokenChunks.flat(); + return tokenObjectChunks.flat(); } private getUninstallTokenVerificationBatchSize = () => { @@ -232,9 +258,9 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { return config?.setup?.uninstallTokenVerificationBatchSize ?? 500; }; - private getDecryptedTokens = async ( + private async getDecryptedTokenObjects( options: Partial - ): Promise => { + ): Promise>> { const tokensFinder = await this.esoClient.createPointInTimeFinderDecryptedAsInternalUser( { @@ -243,34 +269,37 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { ...options, } ); - let tokenObject: Array> = []; + let tokenObjects: Array> = []; for await (const result of tokensFinder.find()) { - tokenObject = result.saved_objects; + tokenObjects = result.saved_objects; break; } tokensFinder.close(); - const uninstallTokens: UninstallToken[] = tokenObject.map( - ({ id: _id, attributes, created_at: createdAt, error }) => { - if (error) { - throw new UninstallTokenError(`Error when reading Uninstall Token with id '${_id}'.`); - } + return tokenObjects; + } - this.assertPolicyId(attributes); - this.assertToken(attributes); - this.assertCreatedAt(createdAt); + private convertTokenObjectToToken = ({ + id: _id, + attributes, + created_at: createdAt, + error, + }: SavedObjectsFindResult): UninstallToken => { + if (error) { + throw new UninstallTokenError(`Error when reading Uninstall Token with id '${_id}'.`); + } - return { - id: _id, - policy_id: attributes.policy_id, - token: attributes.token || attributes.token_plain, - created_at: createdAt, - }; - } - ); + this.assertPolicyId(attributes); + this.assertToken(attributes); + this.assertCreatedAt(createdAt); - return uninstallTokens; + return { + id: _id, + policy_id: attributes.policy_id, + token: attributes.token || attributes.token_plain, + created_at: createdAt, + }; }; private async getTokenObjectsByIncludeFilter( @@ -521,33 +550,44 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { public async checkTokenValidityForPolicy( policyId: string ): Promise { - return await this.checkTokenValidity([policyId]); + return await this.checkTokenValidityForPolicies([policyId]); } public async checkTokenValidityForAllPolicies(): Promise { const policyIds = await this.getAllPolicyIds(); - return await this.checkTokenValidity(policyIds); + return await this.checkTokenValidityForPolicies(policyIds); } - private async checkTokenValidity( + private async checkTokenValidityForPolicies( policyIds: string[] ): Promise { try { - await this.getDecryptedTokensForPolicyIds(policyIds); + const tokenObjects = await this.getDecryptedTokenObjectsForPolicyIds(policyIds); + + const numberOfDecryptionErrors = tokenObjects.filter(({ error }) => error).length; + if (numberOfDecryptionErrors > 0) { + return { + error: new UninstallTokenError( + `Failed to decrypt ${numberOfDecryptionErrors} of ${tokenObjects.length} Uninstall Token(s)` + ), + }; + } + + const numberOfTokensWithMissingData = tokenObjects.filter( + ({ attributes, created_at: createdAt }) => + !createdAt || !attributes.policy_id || (!attributes.token && !attributes.token_plain) + ).length; + if (numberOfTokensWithMissingData > 0) { + return { + error: new UninstallTokenError( + `Failed to validate Uninstall Tokens: ${numberOfTokensWithMissingData} of ${tokenObjects.length} tokens are invalid` + ), + }; + } } catch (error) { if (error instanceof UninstallTokenError) { // known errors are considered non-fatal return { error }; - } else if (isResponseError(error) && error.message.includes('too_many_nested_clauses')) { - // `too_many_nested_clauses` is considered non-fatal - const errorMessage = - 'Failed to validate uninstall tokens: `too_many_nested_clauses` error received. ' + - 'Setting/decreasing the value of `xpack.fleet.setup.uninstallTokenVerificationBatchSize` in your kibana.yml should help. ' + - `Current value is ${this.getUninstallTokenVerificationBatchSize()}.`; - - appContextService.getLogger().warn(`${errorMessage}: '${error}'`); - - return { error: new UninstallTokenError(errorMessage) }; } else { const errorMessage = 'Unknown error happened while checking Uninstall Tokens validity'; appContextService.getLogger().error(`${errorMessage}: '${error}'`); diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 8d343e56b1035..af2c19c42c70b 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -197,24 +197,6 @@ async function createSetupSideEffects( throw error; } } - - logger.debug('Generating Agent uninstall tokens'); - if (!appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) { - logger.warn( - 'xpack.encryptedSavedObjects.encryptionKey is not configured, agent uninstall tokens are being stored in plain text' - ); - } - await appContextService.getUninstallTokenService()?.generateTokensForAllPolicies(); - - if (appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) { - logger.debug('Checking for and encrypting plain text uninstall tokens'); - await appContextService.getUninstallTokenService()?.encryptTokens(); - } - - logger.debug('Checking validity of Uninstall Tokens'); - const uninstallTokenError = await appContextService - .getUninstallTokenService() - ?.checkTokenValidityForAllPolicies(); stepSpan?.end(); stepSpan = apm.startSpan('Upgrade agent policy schema', 'preconfiguration'); @@ -234,7 +216,6 @@ async function createSetupSideEffects( ...preconfiguredPackagesNonFatalErrors, ...packagePolicyUpgradeErrors, ...(messageSigningServiceNonFatalError ? [messageSigningServiceNonFatalError] : []), - ...(uninstallTokenError ? [uninstallTokenError] : []), ]; if (nonFatalErrors.length > 0) { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx index bbb8c44a85f7e..7449be91aa029 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx @@ -33,7 +33,6 @@ export const HostsTable = () => { selection, selectedItemsCount, filterSelectedHosts, - refs, } = useHostsTableContext(); return ( @@ -43,7 +42,6 @@ export const HostsTable = () => { filterSelectedHosts={filterSelectedHosts} /> { } = useKibanaContextForPlugin(); const { dataView } = useMetricsDataViewContext(); - const tableRef = useRef(null); - const closeFlyout = useCallback(() => setProperties({ detailsItemId: null }), [setProperties]); const onSelectionChange = (newSelectedItems: HostNodeRow[]) => { @@ -163,7 +156,6 @@ export const useHostsTable = () => { filterManagerService.addFilters(newFilter); setSelectedItems([]); - tableRef.current?.setSelection([]); }, [dataView, filterManagerService, selectedItems]); const reportHostEntryClick = useCallback( @@ -358,6 +350,7 @@ export const useHostsTable = () => { const selection: EuiTableSelectionType = { onSelectionChange, selectable: (item: HostNodeRow) => !!item.name, + selected: selectedItems, }; return { @@ -373,9 +366,6 @@ export const useHostsTable = () => { selection, selectedItemsCount: selectedItems.length, filterSelectedHosts, - refs: { - tableRef, - }, }; }; diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts index 4555f3f8a576d..ada2afe14810e 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { getIndexPatternFromSQLQuery, getIndexPatternFromESQLQuery } from '@kbn/es-query'; import type { AggregateQuery, Query, Filter } from '@kbn/es-query'; +import { getESQLAdHocDataview } from '@kbn/esql-utils'; import { fetchFieldsFromESQL } from '@kbn/text-based-editor'; import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/public'; import type { Suggestion } from '../../../types'; @@ -48,11 +49,10 @@ export const getSuggestions = async ( return adHoc.name === indexPattern; }); - const dataView = await deps.dataViews.create( - dataViewSpec ?? { - title: indexPattern, - } - ); + const dataView = dataViewSpec + ? await deps.dataViews.create(dataViewSpec) + : await getESQLAdHocDataview(indexPattern, deps.dataViews); + if (dataView.fields.getByName('@timestamp')?.type === 'date' && !dataViewSpec) { dataView.timeFieldName = '@timestamp'; } diff --git a/x-pack/plugins/lens/public/datasources/text_based/utils.ts b/x-pack/plugins/lens/public/datasources/text_based/utils.ts index 856e608d347e1..708c499d59908 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/utils.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/utils.ts @@ -7,7 +7,7 @@ import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; - +import { getESQLAdHocDataview } from '@kbn/esql-utils'; import { type AggregateQuery, getIndexPatternFromSQLQuery, @@ -16,7 +16,6 @@ import { import type { DatatableColumn } from '@kbn/expressions-plugin/public'; import { generateId } from '../../id_generator'; import { fetchDataFromAggregateQuery } from './fetch_data_from_aggregate_query'; - import type { IndexPatternRef, TextBasedPrivateState, TextBasedLayerColumn } from './types'; import type { DataViewsState } from '../../state_management'; import { addColumnsToCache } from './fieldlist_cache'; @@ -89,9 +88,8 @@ export async function getStateFromAggregateQuery( let columnsFromQuery: DatatableColumn[] = []; let timeFieldName; try { - const dataView = await dataViews.create({ - title: indexPattern, - }); + const dataView = await getESQLAdHocDataview(indexPattern, dataViews); + if (dataView && dataView.id) { if (dataView?.fields?.getByName('@timestamp')?.type === 'date') { dataView.timeFieldName = '@timestamp'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index 04d69c1afc571..617b15b43b43d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -27,7 +27,6 @@ import { generateId } from '../../../id_generator'; import { mountWithProvider } from '../../../mocks'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { ReactWrapper } from 'enzyme'; -import { addLayer } from '../../../state_management'; import { createIndexPatternServiceMock } from '../../../mocks/data_views_service_mock'; import { AddLayerButton } from '../../../visualizations/xy/add_layer'; import { LayerType } from '@kbn/visualizations-plugin/common'; @@ -190,96 +189,6 @@ describe('ConfigPanel', () => { ); }); - describe('focus behavior when adding or removing layers', () => { - it('should focus the only layer when resetting the layer', async () => { - const { instance } = await prepareAndMountComponent(getDefaultProps()); - const firstLayerFocusable = instance - .find(LayerPanel) - .first() - .find('section') - .first() - .instance(); - act(() => { - instance.find('[data-test-subj="lnsLayerRemove--0"]').first().simulate('click'); - }); - instance.update(); - - const focusedEl = document.activeElement; - expect(focusedEl).toEqual(firstLayerFocusable); - }); - - it('should focus the second layer when removing the first layer', async () => { - const datasourceMap = mockDatasourceMap(); - const defaultProps = getDefaultProps({ datasourceMap }); - // overwriting datasourceLayers to test two layers - frame.datasourceLayers = { - first: datasourceMap.testDatasource.publicAPIMock, - second: datasourceMap.testDatasource.publicAPIMock, - }; - - const { instance } = await prepareAndMountComponent(defaultProps); - const secondLayerFocusable = instance - .find(LayerPanel) - .at(1) - .find('section') - .first() - .instance(); - act(() => { - instance.find('[data-test-subj="lnsLayerRemove--0"]').first().simulate('click'); - }); - instance.update(); - - const focusedEl = document.activeElement; - expect(focusedEl).toEqual(secondLayerFocusable); - }); - - it('should focus the first layer when removing the second layer', async () => { - const datasourceMap = mockDatasourceMap(); - const defaultProps = getDefaultProps({ datasourceMap }); - // overwriting datasourceLayers to test two layers - frame.datasourceLayers = { - first: datasourceMap.testDatasource.publicAPIMock, - second: datasourceMap.testDatasource.publicAPIMock, - }; - const { instance } = await prepareAndMountComponent(defaultProps); - const firstLayerFocusable = instance - .find(LayerPanel) - .first() - .find('section') - .first() - .instance(); - act(() => { - instance.find('[data-test-subj="lnsLayerRemove--1"]').first().simulate('click'); - }); - instance.update(); - - const focusedEl = document.activeElement; - expect(focusedEl).toEqual(firstLayerFocusable); - }); - - it('should focus the added layer', async () => { - const datasourceMap = mockDatasourceMap(); - frame.datasourceLayers = { - first: datasourceMap.testDatasource.publicAPIMock, - newId: datasourceMap.testDatasource.publicAPIMock, - }; - - const defaultProps = getDefaultProps({ datasourceMap }); - - const { instance } = await prepareAndMountComponent(defaultProps, { - dispatch: jest.fn((x) => { - if (x.type === addLayer.type) { - frame.datasourceLayers.newId = datasourceMap.testDatasource.publicAPIMock; - } - }), - }); - - addNewLayer(instance); - const focusedEl = document.activeElement; - expect(focusedEl?.children[0].getAttribute('data-test-subj')).toEqual('lns-layerPanel-1'); - }); - }); - describe('initial default value', () => { function clickToAddDimension(instance: ReactWrapper) { act(() => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index cef598de31af0..c53198957bb0e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -950,6 +950,7 @@ describe('LayerPanel', () => { }); it('should reorder when dropping in the same group', async () => { + jest.useFakeTimers(); mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -997,6 +998,7 @@ describe('LayerPanel', () => { .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') .at(1) .instance(); + jest.runAllTimers(); const focusedEl = document.activeElement; expect(focusedEl).toEqual(secondButton); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 024fb04998d37..6680dc984beed 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -363,8 +363,13 @@ export function LayerPanel( return ( <> -
- +
+
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/use_focus_update.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/use_focus_update.tsx index e7ee06a020ece..7065c37c1daf2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/use_focus_update.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/use_focus_update.tsx @@ -30,7 +30,7 @@ export function useFocusUpdate(ids: string[]) { const element = nextFocusedId && refsById.get(nextFocusedId); if (element) { const focusable = getFirstFocusable(element); - focusable?.focus(); + setTimeout(() => focusable?.focus()); setNextFocusedId(null); } }, [ids, refsById, nextFocusedId]); diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 01470e01785f6..779aa6886b05e 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -105,6 +105,7 @@ "@kbn/visualization-utils", "@kbn/test-eui-helpers", "@kbn/shared-ux-utility", + "@kbn/esql-utils", "@kbn/text-based-editor", "@kbn/managed-content-badge", "@kbn/sort-predicates", diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index adcd1242adce0..9141a37f3eee8 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -63,6 +63,7 @@ import { import { ILayer } from '../classes/layers/layer'; import { hasVectorLayerMethod } from '../classes/layers/vector_layer'; import { OnSourceChangeArgs } from '../classes/sources/source'; +import { isESVectorTileSource } from '../classes/sources/es_source'; import { DRAW_MODE, LAYER_STYLE_TYPE, @@ -827,7 +828,7 @@ export function setTileState( newValue: tileErrors, }); - if (!isLayerGroup(layer) && layer.getSource().isESSource()) { + if (!isLayerGroup(layer) && isESVectorTileSource(layer.getSource())) { getInspectorAdapters(getState()).vectorTiles.setTileResults( layerId, tileMetaFeatures, diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 1031faaf06963..66fee3e3455be 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -45,7 +45,7 @@ import { ISource, SourceEditorArgs } from '../sources/source'; import { DataRequestContext } from '../../actions'; import { IStyle } from '../styles/style'; import { LICENSED_FEATURES } from '../../licensed_features'; -import { IESSource } from '../sources/es_source'; +import { hasESSourceMethod, isESVectorTileSource } from '../sources/es_source'; import { TileErrorsList } from './tile_errors_list'; import { isLayerGroup } from './layer_group'; @@ -72,7 +72,6 @@ export interface ILayer { getSource(): ISource; getSourceForEditing(): ISource; syncData(syncContext: DataRequestContext): void; - supportsElasticsearchFilters(): boolean; supportsFitToBounds(): Promise; getAttributions(): Promise; getLabel(): string; @@ -215,10 +214,6 @@ export class AbstractLayer implements ILayer { return !!this._descriptor.__isPreviewLayer; } - supportsElasticsearchFilters(): boolean { - return this.getSource().isESSource(); - } - async supportsFitToBounds(): Promise { return await this.getSource().supportsFitToBounds(); } @@ -442,7 +437,7 @@ export class AbstractLayer implements ILayer { body: ( @@ -575,7 +570,7 @@ export class AbstractLayer implements ILayer { getGeoFieldNames(): string[] { const source = this.getSource(); - return source.isESSource() ? [(source as IESSource).getGeoFieldName()] : []; + return hasESSourceMethod(source, 'getGeoFieldName') ? [source.getGeoFieldName()] : []; } async getStyleMetaDescriptorFromLocalFeatures(): Promise { diff --git a/x-pack/plugins/maps/public/classes/layers/layer_group/layer_group.tsx b/x-pack/plugins/maps/public/classes/layers/layer_group/layer_group.tsx index 1d8d61f414784..f3bc500a6d7d6 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer_group/layer_group.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer_group/layer_group.tsx @@ -112,12 +112,6 @@ export class LayerGroup implements ILayer { return !!this._descriptor.__isPreviewLayer; } - supportsElasticsearchFilters(): boolean { - return this.getChildren().some((child) => { - return child.supportsElasticsearchFilters(); - }); - } - async supportsFitToBounds(): Promise { return this._asyncSomeChildren('supportsFitToBounds'); } diff --git a/x-pack/plugins/maps/public/classes/layers/tile_errors_list.tsx b/x-pack/plugins/maps/public/classes/layers/tile_errors_list.tsx index 9520184d48753..2eb37042148c4 100644 --- a/x-pack/plugins/maps/public/classes/layers/tile_errors_list.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tile_errors_list.tsx @@ -16,7 +16,7 @@ import { RESPONSE_VIEW_ID } from '../../inspector/vector_tile_adapter/components interface Props { inspectorAdapters: Adapters; - isESSource: boolean; + isESVectorTileSource: boolean; layerId: string; tileErrors: TileError[]; } @@ -59,7 +59,7 @@ export function TileErrorsList(props: Props) { ]; function renderError(tileError: TileError) { - if (!props.isESSource || !tileError.error) { + if (!props.isESVectorTileSource || !tileError.error) { return tileError.message; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts index d67d9ade54625..b59b6bd8b2717 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts @@ -12,6 +12,7 @@ jest.mock('uuid', () => ({ import sinon from 'sinon'; import { MockSyncContext } from '../../__fixtures__/mock_sync_context'; import { IMvtVectorSource } from '../../../sources/vector_source'; +import { IESSource } from '../../../sources/es_source'; import { DataRequest } from '../../../util/data_request'; import { syncMvtSourceData } from './mvt_source_data'; @@ -40,8 +41,8 @@ const mockSource = { isGeoGridPrecisionAware: () => { return false; }, - isESSource: () => { - return false; + isMvt: () => { + return true; }, } as unknown as IMvtVectorSource; @@ -471,10 +472,10 @@ describe('syncMvtSourceData', () => { }, source: { ...mockSource, - isESSource: () => { - return true; + getIndexPatternId: () => { + return '1234'; }, - }, + } as unknown as IMvtVectorSource & IESSource, syncContext, }); sinon.assert.calledOnce(syncContext.inspectorAdapters.vectorTiles.addLayer); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts index 7b3f800fabc3f..88ebfc615881b 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts @@ -12,6 +12,7 @@ import { DataRequest } from '../../../util/data_request'; import { DataRequestContext } from '../../../../actions'; import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; import { IMvtVectorSource } from '../../../sources/vector_source'; +import { isESVectorTileSource } from '../../../sources/es_source'; // shape of sourceDataRequest.getData() export interface MvtSourceData { @@ -83,7 +84,7 @@ export async function syncMvtSourceData({ : prevData.refreshToken; const tileUrl = await source.getTileUrl(requestMeta, refreshToken, hasLabels, buffer); - if (source.isESSource()) { + if (isESVectorTileSource(source)) { syncContext.inspectorAdapters.vectorTiles.addLayer(layerId, layerName, tileUrl); } const sourceData = { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx index 6b597dae01651..29b0f90802833 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx @@ -34,7 +34,7 @@ import { VectorLayerDescriptor, } from '../../../../../common/descriptor_types'; import { ESSearchSource } from '../../../sources/es_search_source'; -import { IESSource } from '../../../sources/es_source'; +import { hasESSourceMethod, isESVectorTileSource } from '../../../sources/es_source'; import { InnerJoin } from '../../../joins/inner_join'; import { LayerIcon } from '../../layer'; import { MvtSourceData, syncMvtSourceData } from './mvt_source_data'; @@ -91,10 +91,11 @@ export class MvtVectorLayer extends AbstractVectorLayer { async getBounds(getDataRequestContext: (layerId: string) => DataRequestContext) { // Add filter to narrow bounds to features with matching join keys let joinKeyFilter; - if (this.getSource().isESSource()) { + const source = this.getSource(); + if (hasESSourceMethod(source, 'getIndexPattern')) { const { join, joinPropertiesMap } = this._getJoinResults(); if (join && joinPropertiesMap) { - const indexPattern = await (this.getSource() as IESSource).getIndexPattern(); + const indexPattern = await source.getIndexPattern(); const joinField = getField(indexPattern, join.getLeftField().getName()); joinKeyFilter = buildPhrasesFilter( joinField, @@ -120,7 +121,7 @@ export class MvtVectorLayer extends AbstractVectorLayer { } getFeatureId(feature: Feature): string | number | undefined { - if (!this.getSource().isESSource()) { + if (!isESVectorTileSource(this.getSource())) { return feature.id; } @@ -130,7 +131,7 @@ export class MvtVectorLayer extends AbstractVectorLayer { } getLayerIcon(isTocIcon: boolean): LayerIcon { - if (!this.getSource().isESSource()) { + if (!isESVectorTileSource(this.getSource())) { // Only ES-sources can have a special meta-tile, not 3rd party vector tile sources return { icon: this.getCurrentStyle().getIcon(false), diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 4cd0eb7398989..15a18bb9a0bca 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -51,6 +51,7 @@ import { VectorStyleRequestMeta, } from '../../../../common/descriptor_types'; import { IVectorSource } from '../../sources/vector_source'; +import { isESVectorTileSource } from '../../sources/es_source'; import { LayerIcon, ILayer, LayerMessage } from '../layer'; import { InnerJoin } from '../../joins/inner_join'; import { isSpatialJoin } from '../../joins/is_spatial_join'; @@ -58,7 +59,7 @@ import { IField } from '../../fields/field'; import { DataRequestContext } from '../../../actions'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; -import { IESSource } from '../../sources/es_source'; +import { hasESSourceMethod } from '../../sources/es_source'; import type { IJoinSource, ITermJoinSource } from '../../sources/join_sources'; import { isTermJoinSource } from '../../sources/join_sources'; import type { IESAggSource } from '../../sources/es_agg_source'; @@ -462,7 +463,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { sourceQuery?: Query; style: IVectorStyle; } & DataRequestContext) { - if (!source.isESSource() || dynamicStyleProps.length === 0) { + if (!hasESSourceMethod(source, 'loadStylePropsMeta') || dynamicStyleProps.length === 0) { return; } @@ -488,7 +489,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { startLoading(dataRequestId, requestToken, nextMeta); const layerName = await this.getDisplayName(source); - const { styleMeta, warnings } = await (source as IESSource).loadStylePropsMeta({ + const { styleMeta, warnings } = await source.loadStylePropsMeta({ layerName, style, dynamicStyleProps, @@ -873,7 +874,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { const isSourceGeoJson = !this.getSource().isMvt(); const filterExpr = getPointFilterExpression( isSourceGeoJson, - this.getSource().isESSource(), + isESVectorTileSource(this.getSource()), this._getJoinFilterExpression(), timesliceMaskConfig ); @@ -980,7 +981,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { const isSourceGeoJson = !this.getSource().isMvt(); const filterExpr = getLabelFilterExpression( isSourceGeoJson, - this.getSource().isESSource(), + isESVectorTileSource(this.getSource()), this._getJoinFilterExpression(), timesliceMaskConfig ); diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index aa41f33efa00b..4dfdbe92a194a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -30,13 +30,11 @@ import { createExtentFilter } from '../../../../common/elasticsearch_util'; import { copyPersistentState } from '../../../reducers/copy_persistent_state'; import { DataRequestAbortError } from '../../util/data_request'; import { expandToTileBoundaries } from '../../util/geo_tile_utils'; -import { IVectorSource } from '../vector_source'; import { AbstractESSourceDescriptor, AbstractSourceDescriptor, DynamicStylePropertyOptions, MapExtent, - StyleMetaData, VectorSourceRequestMeta, } from '../../../../common/descriptor_types'; import { IVectorStyle } from '../../styles/vector/vector_style'; @@ -45,45 +43,12 @@ import { IField } from '../../fields/field'; import { FieldFormatter } from '../../../../common/constants'; import { isValidStringConfig } from '../../util/valid_string_config'; import { mergeExecutionContext } from '../execution_context_utils'; +import type { IESSource } from './types'; export function isSearchSourceAbortError(error: Error) { return error.name === 'AbortError'; } -export interface IESSource extends IVectorSource { - isESSource(): true; - - getId(): string; - - getIndexPattern(): Promise; - - getIndexPatternId(): string; - - getGeoFieldName(): string; - - loadStylePropsMeta({ - layerName, - style, - dynamicStyleProps, - registerCancelCallback, - sourceQuery, - timeFilters, - searchSessionId, - inspectorAdapters, - executionContext, - }: { - layerName: string; - style: IVectorStyle; - dynamicStyleProps: Array>; - registerCancelCallback: (callback: () => void) => void; - sourceQuery?: Query; - timeFilters: TimeRange; - searchSessionId?: string; - inspectorAdapters: Adapters; - executionContext: KibanaExecutionContext; - }): Promise<{ styleMeta: StyleMetaData; warnings: SearchResponseWarning[] }>; -} - export class AbstractESSource extends AbstractVectorSource implements IESSource { indexPattern?: DataView; @@ -151,10 +116,6 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource return []; } - isESSource(): true { - return true; - } - cloneDescriptor(): AbstractSourceDescriptor { const clonedDescriptor = copyPersistentState(this._descriptor); // id used as uuid to track requests in inspector diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/index.ts b/x-pack/plugins/maps/public/classes/sources/es_source/index.ts index 760ee23109eff..8264b353d7a50 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/index.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/index.ts @@ -5,4 +5,6 @@ * 2.0. */ -export * from './es_source'; +export { AbstractESSource } from './es_source'; +export type { IESSource } from './types'; +export { isESSource, isESVectorTileSource, hasESSourceMethod } from './types'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/types.ts b/x-pack/plugins/maps/public/classes/sources/es_source/types.ts new file mode 100644 index 0000000000000..385a00b900488 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_source/types.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Adapters } from '@kbn/inspector-plugin/common/adapters'; +import type { Query } from '@kbn/data-plugin/common'; +import type { KibanaExecutionContext } from '@kbn/core/public'; +import type { TimeRange } from '@kbn/es-query'; +import type { DataView } from '@kbn/data-plugin/common'; +import type { SearchResponseWarning } from '@kbn/search-response-warnings'; +import type { ISource } from '../source'; +import { type IVectorSource, hasVectorSourceMethod } from '../vector_source'; +import { DynamicStylePropertyOptions, StyleMetaData } from '../../../../common/descriptor_types'; +import { IVectorStyle } from '../../styles/vector/vector_style'; +import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; + +export function isESVectorTileSource(source: ISource): boolean { + return ( + hasVectorSourceMethod(source, 'isMvt') && + source.isMvt() && + hasESSourceMethod(source, 'getIndexPatternId') + ); +} + +export function isESSource(source: ISource): source is IESSource { + return ( + typeof (source as IESSource).getId === 'function' && + typeof (source as IESSource).getIndexPattern === 'function' && + typeof (source as IESSource).getIndexPatternId === 'function' && + typeof (source as IESSource).getGeoFieldName === 'function' && + typeof (source as IESSource).loadStylePropsMeta === 'function' + ); +} + +export function hasESSourceMethod( + source: ISource, + methodName: keyof IESSource +): source is Pick { + return typeof (source as IESSource)[methodName] === 'function'; +} + +export interface IESSource extends IVectorSource { + getId(): string; + + getIndexPattern(): Promise; + + getIndexPatternId(): string; + + getGeoFieldName(): string; + + loadStylePropsMeta({ + layerName, + style, + dynamicStyleProps, + registerCancelCallback, + sourceQuery, + timeFilters, + searchSessionId, + inspectorAdapters, + executionContext, + }: { + layerName: string; + style: IVectorStyle; + dynamicStyleProps: Array>; + registerCancelCallback: (callback: () => void) => void; + sourceQuery?: Query; + timeFilters: TimeRange; + searchSessionId?: string; + inspectorAdapters: Adapters; + executionContext: KibanaExecutionContext; + }): Promise<{ styleMeta: StyleMetaData; warnings: SearchResponseWarning[] }>; +} diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index a2a18b79a0928..c40388349f229 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -59,10 +59,6 @@ export interface ISource { isTimeAware(): Promise; getImmutableProperties(dataFilters: DataFilters): Promise; getAttributionProvider(): (() => Promise) | null; - /* - * Returns true when source implements IESSource interface - */ - isESSource(): boolean; renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement | null; supportsFitToBounds(): Promise; cloneDescriptor(): AbstractSourceDescriptor; @@ -146,10 +142,6 @@ export class AbstractSource implements ISource { return []; } - isESSource(): boolean { - return false; - } - // Returns function used to format value async createFieldFormatter(field: IField): Promise { return null; diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts index 7ffc2955a79a6..cec5b1439dd5c 100644 --- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts +++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts @@ -95,14 +95,14 @@ const IS_POINT_FEATURE = [ export function getPointFilterExpression( isSourceGeoJson: boolean, - isESSource: boolean, + isESVectorTileSource: boolean, joinFilter?: FilterSpecification, timesliceMaskConfig?: TimesliceMaskConfig ): FilterSpecification { const filters: FilterSpecification[] = []; if (isSourceGeoJson) { filters.push(EXCLUDE_CENTROID_FEATURES); - } else if (!isSourceGeoJson && isESSource) { + } else if (isESVectorTileSource) { filters.push(['!=', ['get', '_mvt_label_position'], true]); } filters.push(IS_POINT_FEATURE); @@ -112,7 +112,7 @@ export function getPointFilterExpression( export function getLabelFilterExpression( isSourceGeoJson: boolean, - isESSource: boolean, + isESVectorTileSource: boolean, joinFilter?: FilterSpecification, timesliceMaskConfig?: TimesliceMaskConfig ): FilterSpecification { @@ -123,7 +123,7 @@ export function getLabelFilterExpression( // For GeoJSON sources, show label for centroid features or point/multi-point features only. // no explicit isCentroidFeature filter is needed, centroids are points and are included in the geometry filter. filters.push(IS_POINT_FEATURE); - } else if (!isSourceGeoJson && isESSource) { + } else if (isESVectorTileSource) { filters.push(['==', ['get', '_mvt_label_position'], true]); } diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap index 6acd12f1b45a6..f4ab341e00be5 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap @@ -84,7 +84,6 @@ exports[`EditLayerPanel is rendered 1`] = ` "hasErrors": [Function], "hasJoins": [Function], "renderSourceSettingsEditor": [Function], - "supportsElasticsearchFilters": [Function], "supportsFeatureEditing": [Function], "supportsFitToBounds": [Function], } @@ -116,7 +115,6 @@ exports[`EditLayerPanel is rendered 1`] = ` "hasErrors": [Function], "hasJoins": [Function], "renderSourceSettingsEditor": [Function], - "supportsElasticsearchFilters": [Function], "supportsFeatureEditing": [Function], "supportsFitToBounds": [Function], } diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx index c6c78cf9786a6..7ffece731afab 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx @@ -74,9 +74,6 @@ const mockLayer = { canShowTooltip: () => { return true; }, - supportsElasticsearchFilters: () => { - return false; - }, getLayerTypeIconName: () => { return 'vector'; }, diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx index 650e2df8dc114..5562b08ed45b9 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx @@ -29,6 +29,7 @@ import { getData, getCore } from '../../kibana_services'; import { ILayer } from '../../classes/layers/layer'; import { isVectorLayer } from '../../classes/layers/vector_layer'; import { OnSourceChangeArgs } from '../../classes/sources/source'; +import { isESSource } from '../../classes/sources/es_source'; import { IField } from '../../classes/fields/field'; import { isLayerGroup } from '../../classes/layers/layer_group'; import { isSpatialJoin } from '../../classes/joins/is_spatial_join'; @@ -127,7 +128,7 @@ export class EditLayerPanel extends Component { if ( !this.props.selectedLayer || isLayerGroup(this.props.selectedLayer) || - !this.props.selectedLayer.supportsElasticsearchFilters() + !isESSource(this.props.selectedLayer.getSource()) ) { return null; } diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx index 8bc8ea05d481e..6dc06bed2de62 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx @@ -13,6 +13,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { Join } from './resources/join'; import { JoinDocumentationPopover } from './resources/join_documentation_popover'; import { IVectorLayer } from '../../../classes/layers/vector_layer'; +import { isESSource } from '../../../classes/sources/es_source'; import { JoinDescriptor } from '../../../../common/descriptor_types'; import { SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { AddJoinButton } from './add_join_button'; @@ -38,7 +39,7 @@ export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDispla useEffect(() => { let ignore = false; const source = layer.getSource(); - if (!source.isESSource()) { + if (!isESSource(source)) { setSpatialJoinDisableReason( i18n.translate('xpack.maps.layerPanel.joinEditor.spatialJoin.disabled.esSourceOnly', { defaultMessage: 'Spatial joins are not supported for {sourceType}.', diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.test.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.test.tsx index c8d6395c743ba..7fe26cfe910db 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.test.tsx @@ -58,6 +58,10 @@ class MockMbMap { }, }; } + + querySourceFeatures() { + return []; + } } class MockLayer { @@ -75,15 +79,22 @@ class MockLayer { return this._mbSourceId === mbSourceId; } + getMbSourceId() { + return this._mbSourceId; + } + isVisible() { return true; } getSource() { return { - isESSource() { + isMvt: () => { return true; }, + getIndexPatternId: () => { + return '1234'; + }, }; } } @@ -281,9 +292,6 @@ describe('TileStatusTracker', () => { const geojsonLayer1 = createMockLayer('layer1', 'layer1Source'); geojsonLayer1.getSource = () => { return { - isESSource() { - return true; - }, isMvt() { return false; }, diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.tsx index 972e691f6695e..ba57b19147b72 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.tsx @@ -13,7 +13,7 @@ import type { TileError, TileMetaFeature } from '../../../../common/descriptor_t import { SPATIAL_FILTERS_LAYER_ID } from '../../../../common/constants'; import { ILayer } from '../../../classes/layers/layer'; import { isLayerGroup } from '../../../classes/layers/layer_group'; -import { IVectorSource } from '../../../classes/sources/vector_source'; +import { isESVectorTileSource } from '../../../classes/sources/es_source'; import { getTileKey as getCenterTileKey } from '../../../classes/util/geo_tile_utils'; import { boundsToExtent } from '../../../classes/util/maplibre_utils'; import { ES_MVT_META_LAYER_NAME } from '../../../classes/util/tile_meta_feature_utils'; @@ -69,12 +69,7 @@ export class TileStatusTracker extends Component { return; } - const source = layer.getSource(); - if ( - source.isESSource() && - typeof (source as IVectorSource).isMvt === 'function' && - !(source as IVectorSource).isMvt() - ) { + if (!isESVectorTileSource(layer.getSource())) { // clear tile cache when layer is not tiled this._tileErrorCache.clearLayer(layer.getId(), this._updateTileStatusForAllLayers); } @@ -156,7 +151,7 @@ export class TileStatusTracker extends Component { const ajaxError = 'body' in e.error && 'statusText' in e.error ? (e.error as AJAXError) : undefined; - if (!ajaxError || !targetLayer.getSource().isESSource()) { + if (!ajaxError || !isESVectorTileSource(targetLayer.getSource())) { this._updateTileStatusForAllLayers(); return; } @@ -254,11 +249,7 @@ export class TileStatusTracker extends Component { }, 100); _getTileMetaFeatures = (layer: ILayer) => { - const source = layer.getSource(); - return layer.isVisible() && - source.isESSource() && - typeof (source as IVectorSource).isMvt === 'function' && - (source as IVectorSource).isMvt() + return layer.isVisible() && isESVectorTileSource(layer.getSource()) ? // querySourceFeatures can return duplicated features when features cross tile boundaries. // Tile meta will never have duplicated features since by their nature, tile meta is a feature contained within a single tile (this.props.mbMap diff --git a/x-pack/plugins/ml/common/constants/locator.ts b/x-pack/plugins/ml/common/constants/locator.ts index 614c037c13026..cc07b7b4f27a1 100644 --- a/x-pack/plugins/ml/common/constants/locator.ts +++ b/x-pack/plugins/ml/common/constants/locator.ts @@ -40,6 +40,7 @@ export const ML_PAGES = { * Page: Data Visualizer * Open index data visualizer viewer page */ + DATA_VISUALIZER_ESQL: 'datavisualizer/esql', DATA_VISUALIZER_INDEX_VIEWER: 'jobs/new_job/datavisualizer', ANOMALY_DETECTION_CREATE_JOB: 'jobs/new_job', ANOMALY_DETECTION_CREATE_JOB_RECOGNIZER: 'jobs/new_job/recognize', diff --git a/x-pack/plugins/ml/common/types/locator.ts b/x-pack/plugins/ml/common/types/locator.ts index 85b2550eb8e30..10b6122910b71 100644 --- a/x-pack/plugins/ml/common/types/locator.ts +++ b/x-pack/plugins/ml/common/types/locator.ts @@ -44,6 +44,7 @@ export interface MlGenericUrlPageState extends MlIndexBasedSearchState { export type MlGenericUrlState = MLPageState< | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER + | typeof ML_PAGES.DATA_VISUALIZER_ESQL | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_RECOGNIZER | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index b81f632c9a62c..c67092978d38a 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -9,7 +9,7 @@ import { cloneDeep } from 'lodash'; import moment from 'moment'; import rison from '@kbn/rison'; import React, { FC, useEffect, useMemo, useState } from 'react'; -import { APP_ID as MAPS_APP_ID } from '@kbn/maps-plugin/common'; + import { EuiButtonIcon, EuiContextMenuItem, @@ -18,6 +18,9 @@ import { EuiProgress, EuiToolTip, } from '@elastic/eui'; + +import type { SerializableRecord } from '@kbn/utility-types'; +import { APP_ID as MAPS_APP_ID } from '@kbn/maps-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { ES_FIELD_TYPES } from '@kbn/field-types'; @@ -36,15 +39,19 @@ import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { CATEGORIZE_FIELD_TRIGGER } from '@kbn/ml-ui-actions'; import { isDefined } from '@kbn/ml-is-defined'; import { escapeQuotes } from '@kbn/es-query'; +import { isQuery } from '@kbn/data-plugin/public'; + import { PLUGIN_ID } from '../../../../common/constants/app'; -import { mlJobService } from '../../services/job_service'; import { findMessageField, getDataViewIdFromName } from '../../util/index_utils'; import { getInitialAnomaliesLayers, getInitialSourceIndexFieldLayers } from '../../../maps/util'; import { parseInterval } from '../../../../common/util/parse_interval'; +import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator'; +import { getFiltersForDSLQuery } from '../../../../common/util/job_utils'; + +import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; import { escapeKueryForFieldValuePair, replaceStringTokens } from '../../util/string_utils'; import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_utils'; -import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator'; import { escapeDoubleQuotes, getDateFormatTz, @@ -54,8 +61,12 @@ import { usePermissionCheck } from '../../capabilities/check_capabilities'; import type { TimeRangeBounds } from '../../util/time_buckets'; import { useMlKibana } from '../../contexts/kibana'; import { getFieldTypeFromMapping } from '../../services/mapping_service'; + import { getQueryStringForInfluencers } from './get_query_string_for_influencers'; -import { getFiltersForDSLQuery } from '../../../../common/util/job_utils'; + +const LOG_RATE_ANALYSIS_MARGIN_FACTOR = 20; +const LOG_RATE_ANALYSIS_BASELINE_FACTOR = 15; + interface LinksMenuProps { anomaly: MlAnomaliesTableRecord; bounds: TimeRangeBounds; @@ -71,6 +82,7 @@ interface LinksMenuProps { export const LinksMenuUI = (props: LinksMenuProps) => { const [openInDiscoverUrl, setOpenInDiscoverUrl] = useState(); const [discoverUrlError, setDiscoverUrlError] = useState(); + const [openInLogRateAnalysisUrl, setOpenInLogRateAnalysisUrl] = useState(); const [messageField, setMessageField] = useState<{ dataView: DataView; @@ -239,20 +251,39 @@ export const LinksMenuUI = (props: LinksMenuProps) => { } })(); - const generateDiscoverUrl = async () => { + // withWindowParameters is used to generate the url state + // for Log Rate Analysis to create a baseline and deviation + // selection based on the anomaly record timestamp and bucket span. + const generateRedirectUrlPageState = async ( + withWindowParameters = false, + timeAttribute = 'timeRange' + ): Promise => { const interval = props.interval; const dataViewId = await getDataViewId(); const record = props.anomaly.source; - const earliestMoment = moment(record.timestamp).startOf(interval); - if (interval === 'hour') { + // Use the exact timestamp for Log Rate Analysis, + // in all other cases snap it to the provided interval. + const earliestMoment = withWindowParameters + ? moment(record.timestamp) + : moment(record.timestamp).startOf(interval); + + // For Log Rate Analysis, look back further to + // provide enough room for the baseline time range. + // In all other cases look back 1 hour. + if (withWindowParameters) { + earliestMoment.subtract(record.bucket_span * LOG_RATE_ANALYSIS_MARGIN_FACTOR, 's'); + } else if (interval === 'hour') { // Start from the previous hour. earliestMoment.subtract(1, 'h'); } const latestMoment = moment(record.timestamp).add(record.bucket_span, 's'); - if (props.isAggregatedData === true) { + + if (withWindowParameters) { + latestMoment.add(record.bucket_span * LOG_RATE_ANALYSIS_MARGIN_FACTOR, 's'); + } else if (props.isAggregatedData === true) { if (interval === 'hour') { // Show to the end of the next hour. latestMoment.add(1, 'h'); @@ -263,9 +294,16 @@ export const LinksMenuUI = (props: LinksMenuProps) => { const from = timeFormatter(earliestMoment.unix() * 1000); // e.g. 2016-02-08T16:00:00.000Z const to = timeFormatter(latestMoment.unix() * 1000); // e.g. 2016-02-08T18:59:59.000Z + // The window parameters for Log Rate Analysis. + // The deviation time range will span the current anomaly's bucket. + const dMin = record.timestamp; + const dMax = record.timestamp + record.bucket_span * 1000; + const bMax = dMin - record.bucket_span * 1000; + const bMin = bMax - record.bucket_span * 1000 * LOG_RATE_ANALYSIS_BASELINE_FACTOR; + let kqlQuery = ''; - if (record.influencers) { + if (record.influencers && !withWindowParameters) { kqlQuery = record.influencers .filter((influencer) => isDefined(influencer)) .map((influencer) => { @@ -283,9 +321,21 @@ export const LinksMenuUI = (props: LinksMenuProps) => { .join(' AND '); } - const url = await discoverLocator.getRedirectUrl({ + // For multi-metric or population jobs, we add the selected entity for links to + // Log Rate Analysis, so they can be restored as part of the search filter. + if (withWindowParameters && props.anomaly.entityName && props.anomaly.entityValue) { + if (kqlQuery !== '') { + kqlQuery += ' AND '; + } + + kqlQuery = `"${escapeQuotes(props.anomaly.entityName)}":"${escapeQuotes( + props.anomaly.entityValue + '' + )}"`; + } + + return { indexPatternId: dataViewId, - timeRange: { + [timeAttribute]: { from, to, mode: 'absolute', @@ -298,15 +348,76 @@ export const LinksMenuUI = (props: LinksMenuProps) => { dataViewId === null ? [] : getFiltersForDSLQuery(job.datafeed_config.query, dataViewId, job.job_id), - }); + ...(withWindowParameters + ? { + wp: { bMin, bMax, dMin, dMax }, + } + : {}), + }; + }; + + const generateDiscoverUrl = async () => { + const pageState = await generateRedirectUrlPageState(); + const url = await discoverLocator.getRedirectUrl(pageState); if (!unmounted) { setOpenInDiscoverUrl(url); } }; + const generateLogRateAnalysisUrl = async () => { + if ( + props.anomaly.source.function_description !== 'count' || + // Disable link for datafeeds that use aggregations + // and define a non-standard summary count field name + (job.analysis_config.summary_count_field_name !== undefined && + job.analysis_config.summary_count_field_name !== 'doc_count') + ) { + if (!unmounted) { + setOpenInLogRateAnalysisUrl(undefined); + } + return; + } + + const mlLocator = share.url.locators.get(ML_APP_LOCATOR); + + if (!mlLocator) { + // eslint-disable-next-line no-console + console.error('Unable to detect locator for ML or bounds'); + return; + } + const pageState = await generateRedirectUrlPageState(true, 'time'); + + const { indexPatternId, wp, query, filters, ...globalState } = pageState; + + const url = await mlLocator.getRedirectUrl({ + page: ML_PAGES.AIOPS_LOG_RATE_ANALYSIS, + pageState: { + index: indexPatternId, + globalState, + appState: { + logRateAnalysis: { + wp, + ...(isQuery(query) + ? { + filters, + searchString: query.query, + searchQueryLanguage: query.language, + } + : {}), + }, + }, + }, + }); + + if (!unmounted) { + setOpenInLogRateAnalysisUrl(url); + } + }; + if (!isCategorizationAnomalyRecord) { generateDiscoverUrl(); + generateLogRateAnalysisUrl(); } else { getDataViewId(); } @@ -606,10 +717,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => { // Need to encode the _a parameter as it will contain characters such as '+' if using the regex. const { basePath } = kibana.services.http; - let path = basePath.get(); - path += '/app/discover#/'; - path += '?_g=' + _g; - path += '&_a=' + encodeURIComponent(_a); + const path = `${basePath.get()}/app/discover#/?_g=${_g}&_a=${encodeURIComponent(_a)}`; window.open(path, '_blank'); }) .catch((resp) => { @@ -813,10 +921,26 @@ export const LinksMenuUI = (props: LinksMenuProps) => { ); } + if (openInLogRateAnalysisUrl) { + items.push( + + + + ); + } + if (messageField !== null) { items.push( { closePopover(); diff --git a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx index dda32fbaf8af7..e2e5c115cd06a 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx @@ -234,6 +234,16 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) { disabled: false, testSubj: 'mlMainTab indexDataVisualizer', }, + { + id: 'esql_datavisualizer', + pathId: ML_PAGES.DATA_VISUALIZER_ESQL, + name: i18n.translate('xpack.ml.navMenu.esqlDataVisualizerLinkText', { + defaultMessage: 'ES|QL', + }), + disabled: false, + testSubj: 'mlMainTab esqlDataVisualizer', + }, + { id: 'data_drift', pathId: ML_PAGES.DATA_DRIFT_INDEX_SELECT, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index 0a3f49cc882ff..153978c56d7e5 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -17,6 +17,8 @@ import { EuiLink, EuiSpacer, EuiText, + EuiBetaBadge, + EuiTextAlign, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -25,6 +27,7 @@ import { isFullLicense } from '../license'; import { useMlKibana, useNavigateToPath } from '../contexts/kibana'; import { HelpMenu } from '../components/help_menu'; import { MlPageHeader } from '../components/page_header'; +import { ML_PAGES } from '../../locator'; function startTrialDescription() { return ( @@ -49,6 +52,7 @@ function startTrialDescription() { export const DatavisualizerSelector: FC = () => { useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); + const { services: { licenseManagement, @@ -154,6 +158,54 @@ export const DatavisualizerSelector: FC = () => { data-test-subj="mlDataVisualizerCardIndexData" /> + + } + title={ + + <> + {' '} + + } + tooltipPosition={'right'} + /> + + + } + description={ + + } + footer={ + navigateToPath(ML_PAGES.DATA_VISUALIZER_ESQL)} + data-test-subj="mlDataVisualizerSelectESQLButton" + > + + + } + data-test-subj="mlDataVisualizerCardESQLData" + /> + {startTrialVisible === true && ( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx index 69c034738cf5f..092e65f69c35d 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx @@ -15,6 +15,7 @@ import type { GetAdditionalLinksParams, } from '@kbn/data-visualizer-plugin/public'; import { useTimefilter } from '@kbn/ml-date-picker'; +import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import { useMlKibana, useMlLocator } from '../../contexts/kibana'; import { HelpMenu } from '../../components/help_menu'; import { ML_PAGES } from '../../../../common/constants/locator'; @@ -23,8 +24,9 @@ import { mlNodesAvailable, getMlNodeCount } from '../../ml_nodes_check/check_ml_ import { checkPermission } from '../../capabilities/check_capabilities'; import { MlPageHeader } from '../../components/page_header'; import { useEnabledFeatures } from '../../contexts/ml'; +import { TechnicalPreviewBadge } from '../../components/technical_preview_badge/technical_preview_badge'; -export const IndexDataVisualizerPage: FC = () => { +export const IndexDataVisualizerPage: FC<{ esql: boolean }> = ({ esql = false }) => { useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); const { services: { @@ -180,19 +182,34 @@ export const IndexDataVisualizerPage: FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps [mlLocator, mlFeaturesDisabled] ); + const { euiTheme } = useEuiTheme(); + return IndexDataVisualizer ? ( {IndexDataVisualizer !== null ? ( <> - + + + {esql ? ( + <> + + + + + + + + ) : null} + ) : null} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx index 0a53ac7b4a81e..d88f0e5cd2674 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx @@ -6,7 +6,7 @@ */ import React, { FC, useCallback } from 'react'; -import { EuiPageBody, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiPageBody, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; @@ -20,7 +20,13 @@ export interface PageProps { const RESULTS_PER_PAGE = 20; -export const Page: FC = ({ nextStepPath }) => { +export const Page: FC = ({ + nextStepPath, + extraButtons, +}: { + nextStepPath: string; + extraButtons?: React.ReactNode; +}) => { const { contentManagement, uiSettings } = useMlKibana().services; const navigateToPath = useNavigateToPath(); @@ -80,7 +86,13 @@ export const Page: FC = ({ nextStepPath }) => { uiSettings, }} > - + + + {extraButtons ? extraButtons : null} + diff --git a/x-pack/plugins/ml/public/application/routing/components/navigate_to_page_button.tsx b/x-pack/plugins/ml/public/application/routing/components/navigate_to_page_button.tsx new file mode 100644 index 0000000000000..fcc7a01e9f508 --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/components/navigate_to_page_button.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { useNavigateToPath } from '../../contexts/kibana'; + +export const NavigateToPageButton = ({ + nextStepPath, + title, +}: { + nextStepPath: string; + title: string | React.ReactNode; +}) => { + const navigateToPath = useNavigateToPath(); + const onClick = useCallback(() => { + navigateToPath(nextStepPath); + }, [navigateToPath, nextStepPath]); + + return {title}; +}; diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index f14eff64675fc..e0c327a23c34e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -24,7 +24,7 @@ export const indexBasedRouteFactory = ( title: i18n.translate('xpack.ml.dataVisualizer.dataView.docTitle', { defaultMessage: 'Index Data Visualizer', }), - render: () => , + render: () => , breadcrumbs: [ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath), @@ -36,13 +36,34 @@ export const indexBasedRouteFactory = ( ], }); -const PageWrapper: FC = () => { +export const indexESQLBasedRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ + id: 'data_view_datavisualizer_esql', + path: createPath(ML_PAGES.DATA_VISUALIZER_ESQL), + title: i18n.translate('xpack.ml.dataVisualizer.esql.docTitle', { + defaultMessage: 'Index Data Visualizer (ES|QL)', + }), + render: () => , + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.esqlLabel', { + defaultMessage: 'Index Data Visualizer (ES|QL)', + }), + }, + ], +}); + +const PageWrapper: FC<{ esql: boolean }> = ({ esql }) => { const { context } = useRouteResolver('basic', []); return ( - + ); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index 24976cffc43d9..b790490cc5e9f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -8,6 +8,7 @@ import React, { FC } from 'react'; import { Redirect } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; import { ML_PAGES } from '../../../../locator'; import { NavigateToPath, useMlKibana } from '../../../contexts/kibana'; import { createPath, MlRoute, PageLoader, PageProps } from '../../router'; @@ -15,6 +16,7 @@ import { useRouteResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/index_or_search'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { NavigateToPageButton } from '../../components/navigate_to_page_button'; enum MODE { NEW_JOB, @@ -24,6 +26,7 @@ enum MODE { interface IndexOrSearchPageProps extends PageProps { nextStepPath: string; mode: MODE; + extraButtons?: React.ReactNode; } const getBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ @@ -104,14 +107,28 @@ export const dataVizIndexOrSearchRouteFactory = ( title: i18n.translate('xpack.ml.selectDataViewLabel', { defaultMessage: 'Select Data View', }), - render: (props, deps) => ( - - ), + render: (props, deps) => { + const button = ( + + } + /> + ); + return ( + + ); + }, breadcrumbs: getDataVisBreadcrumbs(navigateToPath, basePath), }); @@ -185,7 +202,7 @@ export const changePointDetectionIndexOrSearchRouteFactory = ( breadcrumbs: getChangePointDetectionBreadcrumbs(navigateToPath, basePath), }); -const PageWrapper: FC = ({ nextStepPath, mode }) => { +const PageWrapper: FC = ({ nextStepPath, mode, extraButtons }) => { const { services: { http: { basePath }, @@ -207,7 +224,7 @@ const PageWrapper: FC = ({ nextStepPath, mode }) => { ); return ( - + ); }; diff --git a/x-pack/plugins/ml/public/locator/ml_locator.ts b/x-pack/plugins/ml/public/locator/ml_locator.ts index 05fe312fd9a46..1034101d3211e 100644 --- a/x-pack/plugins/ml/public/locator/ml_locator.ts +++ b/x-pack/plugins/ml/public/locator/ml_locator.ts @@ -88,6 +88,7 @@ export class MlLocatorDefinition implements LocatorDefinition { case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_FROM_PATTERN_ANALYSIS: case ML_PAGES.DATA_VISUALIZER: case ML_PAGES.DATA_VISUALIZER_FILE: + case ML_PAGES.DATA_VISUALIZER_ESQL: case ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER: case ML_PAGES.DATA_VISUALIZER_INDEX_SELECT: case ML_PAGES.AIOPS: diff --git a/x-pack/plugins/ml/public/maps/anomaly_source.tsx b/x-pack/plugins/ml/public/maps/anomaly_source.tsx index 27d43eeb95771..b161650623269 100644 --- a/x-pack/plugins/ml/public/maps/anomaly_source.tsx +++ b/x-pack/plugins/ml/public/maps/anomaly_source.tsx @@ -336,11 +336,6 @@ export class AnomalySource implements IVectorSource { return []; } - isESSource(): boolean { - // IGNORE: This is only relevant if your source is backed by an index-pattern - return false; - } - isFilterByMapBounds(): boolean { // Only implement if you can query this data with a bounding-box return false; diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts index 539c42bf763ca..a66f2fca4e2c1 100644 --- a/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts @@ -263,6 +263,17 @@ function createDeepLinks( }; }, + getESQLDataVisualizerDeepLink: (): AppDeepLink => { + return { + id: 'indexDataVisualizer', + title: i18n.translate('xpack.ml.deepLink.esqlDataVisualizer', { + defaultMessage: 'ES|QL Data Visualizer', + }), + path: `/${ML_PAGES.DATA_VISUALIZER_ESQL}`, + navLinkStatus: getNavStatus(true), + }; + }, + getDataDriftDeepLink: (): AppDeepLink => { return { id: 'dataDrift', diff --git a/x-pack/plugins/observability/public/components/alert_search_bar/containers/state_container.tsx b/x-pack/plugins/observability/public/components/alert_search_bar/containers/state_container.tsx index 5513cc62df626..a08971b0c350e 100644 --- a/x-pack/plugins/observability/public/components/alert_search_bar/containers/state_container.tsx +++ b/x-pack/plugins/observability/public/components/alert_search_bar/containers/state_container.tsx @@ -35,7 +35,7 @@ interface AlertSearchBarStateTransitions { } const defaultState: AlertSearchBarContainerState = { - rangeFrom: 'now-2h', + rangeFrom: 'now-24h', rangeTo: 'now', kuery: '', status: ALL_ALERTS.status, diff --git a/x-pack/plugins/observability/public/components/alerts_table/rule_details/get_rule_details_table_configuration.tsx b/x-pack/plugins/observability/public/components/alerts_table/rule_details/get_rule_details_table_configuration.tsx index 369a3d4c5c12d..d03e90452fa28 100644 --- a/x-pack/plugins/observability/public/components/alerts_table/rule_details/get_rule_details_table_configuration.tsx +++ b/x-pack/plugins/observability/public/components/alerts_table/rule_details/get_rule_details_table_configuration.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { TIMESTAMP } from '@kbn/rule-data-utils'; +import { ALERT_START } from '@kbn/rule-data-utils'; import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { AlertsTableConfigurationRegistry, @@ -35,7 +35,7 @@ export const getRuleDetailsTableConfiguration = ( }), sort: [ { - [TIMESTAMP]: { + [ALERT_START]: { order: 'desc' as SortOrder, }, }, diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/rule_details_tabs.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/rule_details_tabs.tsx index 163aee618f9b9..f5e514d57a68a 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/rule_details_tabs.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/components/rule_details_tabs.tsx @@ -68,20 +68,6 @@ export function RuleDetailsTabs({ ]); const tabs: EuiTabbedContentTab[] = [ - { - id: RULE_DETAILS_EXECUTION_TAB, - name: i18n.translate('xpack.observability.ruleDetails.rule.eventLogTabText', { - defaultMessage: 'Execution history', - }), - 'data-test-subj': 'eventLogListTab', - content: ( - - - {rule && ruleType ? : null} - - - ), - }, { id: RULE_DETAILS_ALERTS_TAB, name: i18n.translate('xpack.observability.ruleDetails.rule.alertsTabText', { @@ -117,6 +103,20 @@ export function RuleDetailsTabs({ ), }, + { + id: RULE_DETAILS_EXECUTION_TAB, + name: i18n.translate('xpack.observability.ruleDetails.rule.eventLogTabText', { + defaultMessage: 'Execution history', + }), + 'data-test-subj': 'eventLogListTab', + content: ( + + + {rule && ruleType ? : null} + + + ), + }, ]; const handleTabIdChange = (newTabId: TabId) => { diff --git a/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx b/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx index 6a9c38b1b1e80..90ed7866a1560 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx @@ -103,7 +103,7 @@ export function RuleDetailsPage() { return urlTabId && [RULE_DETAILS_EXECUTION_TAB, RULE_DETAILS_ALERTS_TAB].includes(urlTabId) ? (urlTabId as TabId) - : RULE_DETAILS_EXECUTION_TAB; + : RULE_DETAILS_ALERTS_TAB; }); const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(); diff --git a/x-pack/plugins/observability_ai_assistant/common/conversation_complete.ts b/x-pack/plugins/observability_ai_assistant/common/conversation_complete.ts index 487a469eea16e..f5fe0d37408c2 100644 --- a/x-pack/plugins/observability_ai_assistant/common/conversation_complete.ts +++ b/x-pack/plugins/observability_ai_assistant/common/conversation_complete.ts @@ -66,7 +66,14 @@ export type MessageAddEvent = StreamingChatResponseEventBase< export type ChatCompletionErrorEvent = StreamingChatResponseEventBase< StreamingChatResponseEventType.ChatCompletionError, - { error: { message: string; stack?: string; code?: ChatCompletionErrorCode } } + { + error: { + message: string; + stack?: string; + code?: ChatCompletionErrorCode; + meta?: Record; + }; + } >; export type StreamingChatResponseEvent = @@ -83,33 +90,41 @@ export type StreamingChatResponseEventWithoutError = Exclude< export enum ChatCompletionErrorCode { InternalError = 'internalError', - NotFound = 'notFound', + NotFoundError = 'notFoundError', TokenLimitReachedError = 'tokenLimitReachedError', } -export class ChatCompletionError extends Error { - code: ChatCompletionErrorCode; +interface ErrorMetaAttributes { + [ChatCompletionErrorCode.InternalError]: {}; + [ChatCompletionErrorCode.NotFoundError]: {}; + [ChatCompletionErrorCode.TokenLimitReachedError]: { + tokenLimit?: number; + tokenCount?: number; + }; +} - constructor(code: ChatCompletionErrorCode, message: string) { +export class ChatCompletionError extends Error { + constructor(public code: T, message: string, public meta?: ErrorMetaAttributes[T]) { super(message); - this.code = code; } } -export function createConversationNotFoundError() { +export function createTokenLimitReachedError(tokenLimit?: number, tokenCount?: number) { return new ChatCompletionError( - ChatCompletionErrorCode.NotFound, - i18n.translate('xpack.observabilityAiAssistant.chatCompletionError.conversationNotFoundError', { - defaultMessage: 'Conversation not found', - }) + ChatCompletionErrorCode.TokenLimitReachedError, + i18n.translate('xpack.observabilityAiAssistant.chatCompletionError.tokenLimitReachedError', { + defaultMessage: `Token limit reached. Token limit is {tokenLimit}, but the current conversation has {tokenCount} tokens.`, + values: { tokenLimit, tokenCount }, + }), + { tokenLimit, tokenCount } ); } -export function createTokenLimitReachedError() { +export function createConversationNotFoundError() { return new ChatCompletionError( - ChatCompletionErrorCode.TokenLimitReachedError, - i18n.translate('xpack.observabilityAiAssistant.chatCompletionError.tokenLimitReachedError', { - defaultMessage: 'Token limit reached', + ChatCompletionErrorCode.NotFoundError, + i18n.translate('xpack.observabilityAiAssistant.chatCompletionError.conversationNotFoundError', { + defaultMessage: 'Conversation not found', }) ); } @@ -118,6 +133,15 @@ export function createInternalServerError(originalErrorMessage: string) { return new ChatCompletionError(ChatCompletionErrorCode.InternalError, originalErrorMessage); } -export function isChatCompletionError(error: Error): error is ChatCompletionError { +export function isTokenLimitReachedError( + error: Error +): error is ChatCompletionError { + return ( + error instanceof ChatCompletionError && + error.code === ChatCompletionErrorCode.TokenLimitReachedError + ); +} + +export function isChatCompletionError(error: Error): error is ChatCompletionError { return error instanceof ChatCompletionError; } diff --git a/x-pack/plugins/observability_ai_assistant/common/utils/throw_serialized_chat_completion_errors.ts b/x-pack/plugins/observability_ai_assistant/common/utils/throw_serialized_chat_completion_errors.ts index e79039c7e153d..8e4718158280b 100644 --- a/x-pack/plugins/observability_ai_assistant/common/utils/throw_serialized_chat_completion_errors.ts +++ b/x-pack/plugins/observability_ai_assistant/common/utils/throw_serialized_chat_completion_errors.ts @@ -20,10 +20,12 @@ export function throwSerializedChatCompletionErrors() { ): Observable> => { return source$.pipe( tap((event) => { + // de-serialise error if (event.type === StreamingChatResponseEventType.ChatCompletionError) { const code = event.error.code ?? ChatCompletionErrorCode.InternalError; const message = event.error.message; - throw new ChatCompletionError(code, message); + const meta = event.error.meta; + throw new ChatCompletionError(code, message, meta); } }), filter( diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts index d68bf549fd9b2..8eb5bf48963da 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts @@ -13,6 +13,7 @@ import { MessageRole, type Message } from '../../common'; import { ConversationCreateEvent, ConversationUpdateEvent, + isTokenLimitReachedError, StreamingChatResponseEventType, } from '../../common/conversation_complete'; import { getAssistantSetupMessage } from '../service/get_assistant_setup_message'; @@ -95,16 +96,36 @@ export function useChat({ const handleError = useCallback( (error: Error) => { - notifications.toasts.addError(error, { - title: i18n.translate('xpack.observabilityAiAssistant.failedToLoadResponse', { - defaultMessage: 'Failed to load response from the AI Assistant', - }), - }); if (error instanceof AbortError) { setChatState(ChatState.Aborted); } else { setChatState(ChatState.Error); } + + if (isTokenLimitReachedError(error)) { + setMessages((msgs) => [ + ...msgs, + { + '@timestamp': new Date().toISOString(), + message: { + content: i18n.translate('xpack.observabilityAiAssistant.tokenLimitError', { + defaultMessage: + 'The conversation has exceeded the token limit. The maximum token limit is **{tokenLimit}**, but the current conversation has **{tokenCount}** tokens. Please start a new conversation to continue.', + values: { tokenLimit: error.meta?.tokenLimit, tokenCount: error.meta?.tokenCount }, + }), + role: MessageRole.Assistant, + }, + }, + ]); + + return; + } + + notifications.toasts.addError(error, { + title: i18n.translate('xpack.observabilityAiAssistant.failedToLoadResponse', { + defaultMessage: 'Failed to load response from the AI Assistant', + }), + }); }, [notifications.toasts] ); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts index e82ea13ece099..81399e330ac5c 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts @@ -22,6 +22,7 @@ import { createConversationNotFoundError, MessageAddEvent, StreamingChatResponseEventType, + createTokenLimitReachedError, type StreamingChatResponseEvent, } from '../../../common/conversation_complete'; import { @@ -459,6 +460,17 @@ export class ObservabilityAIAssistantClient { }, }); + if (executeResult.status === 'error' && executeResult?.serviceMessage) { + const tokenLimitRegex = + /This model's maximum context length is (\d+) tokens\. However, your messages resulted in (\d+) tokens/g; + const tokenLimitRegexResult = tokenLimitRegex.exec(executeResult.serviceMessage); + + if (tokenLimitRegexResult) { + const [, tokenLimit, tokenCount] = tokenLimitRegexResult; + throw createTokenLimitReachedError(parseInt(tokenLimit, 10), parseInt(tokenCount, 10)); + } + } + if (executeResult.status === 'error') { throw internal(`${executeResult?.message} - ${executeResult?.serviceMessage}`); } diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/observable_into_stream.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/observable_into_stream.ts index 5e56c5145fcf4..a1ec52918453f 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/util/observable_into_stream.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/observable_into_stream.ts @@ -29,6 +29,7 @@ export function observableIntoStream( message: error.message, stack: error.stack, code: isChatCompletionError(error) ? error.code : undefined, + meta: error.meta, }, type: StreamingChatResponseEventType.ChatCompletionError, }; diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts_response_actions_form.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts_response_actions_form.cy.ts index 041818824f2f2..45d87f0766633 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts_response_actions_form.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts_response_actions_form.cy.ts @@ -24,223 +24,228 @@ import { import { clickRuleName, inputQuery, typeInECSFieldInput } from '../../tasks/live_query'; import { closeDateTabIfVisible, closeToastIfVisible } from '../../tasks/integrations'; -describe('Alert Event Details - Response Actions Form', { tags: ['@ess', '@serverless'] }, () => { - let multiQueryPackId: string; - let multiQueryPackName: string; - let ruleId: string; - let ruleName: string; - let packId: string; - let packName: string; - const packData = packFixture(); - const multiQueryPackData = multiQueryPackFixture(); - before(() => { - initializeDataViews(); - }); - beforeEach(() => { - loadPack(packData).then((data) => { - packId = data.saved_object_id; - packName = data.name; - }); - loadPack(multiQueryPackData).then((data) => { - multiQueryPackId = data.saved_object_id; - multiQueryPackName = data.name; - }); - loadRule().then((data) => { - ruleId = data.id; - ruleName = data.name; - }); - }); - afterEach(() => { - cleanupPack(packId); - cleanupPack(multiQueryPackId); - cleanupRule(ruleId); - }); - - it('adds response actions with osquery with proper validation and form values', () => { - cy.visit('/app/security/rules'); - clickRuleName(ruleName); - cy.getBySel('globalLoadingIndicator').should('not.exist'); - cy.getBySel('editRuleSettingsLink').click(); - cy.getBySel('globalLoadingIndicator').should('not.exist'); - closeDateTabIfVisible(); - cy.getBySel('edit-rule-actions-tab').click(); - cy.getBySel('globalLoadingIndicator').should('not.exist'); - cy.contains('Response actions are run on each rule execution.'); - cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click(); - - cy.getBySel(RESPONSE_ACTIONS_ERRORS).within(() => { - cy.contains('Query is a required field'); - cy.contains('The timeout value must be 60 seconds or higher.').should('not.exist'); - }); - - // check if changing error state of one input doesn't clear other errors - START - cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { - cy.contains('Advanced').click(); - cy.getBySel('timeout-input').clear(); - cy.contains('The timeout value must be 60 seconds or higher.'); - }); - - cy.getBySel(RESPONSE_ACTIONS_ERRORS).within(() => { - cy.contains('Query is a required field'); - cy.contains('The timeout value must be 60 seconds or higher.'); - }); - - cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { - cy.getBySel('timeout-input').type('6'); - cy.contains('The timeout value must be 60 seconds or higher.'); - }); - cy.getBySel(RESPONSE_ACTIONS_ERRORS).within(() => { - cy.contains('Query is a required field'); - cy.contains('The timeout value must be 60 seconds or higher.'); - }); - cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { - cy.getBySel('timeout-input').type('6'); - cy.contains('The timeout value must be 60 seconds or higher.').should('not.exist'); - }); - cy.getBySel(RESPONSE_ACTIONS_ERRORS).within(() => { - cy.contains('Query is a required field'); - }); - cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { - cy.getBySel('timeout-input').type('6'); - }); - cy.getBySel(RESPONSE_ACTIONS_ERRORS).within(() => { - cy.contains('Query is a required field'); - cy.contains('The timeout value must be 60 seconds or higher.').should('not.exist'); - }); - // check if changing error state of one input doesn't clear other errors - END +// FLAKY: https://github.com/elastic/kibana/issues/169785 +describe.skip( + 'Alert Event Details - Response Actions Form', + { tags: ['@ess', '@serverless'] }, + () => { + let multiQueryPackId: string; + let multiQueryPackName: string; + let ruleId: string; + let ruleName: string; + let packId: string; + let packName: string; + const packData = packFixture(); + const multiQueryPackData = multiQueryPackFixture(); + before(() => { + initializeDataViews(); + }); + beforeEach(() => { + loadPack(packData).then((data) => { + packId = data.saved_object_id; + packName = data.name; + }); + loadPack(multiQueryPackData).then((data) => { + multiQueryPackId = data.saved_object_id; + multiQueryPackName = data.name; + }); + loadRule().then((data) => { + ruleId = data.id; + ruleName = data.name; + }); + }); + afterEach(() => { + cleanupPack(packId); + cleanupPack(multiQueryPackId); + cleanupRule(ruleId); + }); + + it('adds response actions with osquery with proper validation and form values', () => { + cy.visit('/app/security/rules'); + clickRuleName(ruleName); + cy.getBySel('globalLoadingIndicator').should('not.exist'); + cy.getBySel('editRuleSettingsLink').click(); + cy.getBySel('globalLoadingIndicator').should('not.exist'); + closeDateTabIfVisible(); + cy.getBySel('edit-rule-actions-tab').click(); + cy.getBySel('globalLoadingIndicator').should('not.exist'); + cy.contains('Response actions are run on each rule execution.'); + cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click(); + + cy.getBySel(RESPONSE_ACTIONS_ERRORS).within(() => { + cy.contains('Query is a required field'); + cy.contains('The timeout value must be 60 seconds or higher.').should('not.exist'); + }); - cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { - cy.contains('Query is a required field'); - inputQuery('select * from uptime1'); - }); - cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click(); - cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => { - cy.contains('Run a set of queries in a pack').click(); - }); - cy.getBySel(RESPONSE_ACTIONS_ERRORS) - .within(() => { - cy.contains('Pack is a required field'); - }) - .should('exist'); - cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => { - cy.contains('Pack is a required field'); - cy.getBySel('comboBoxInput').type(`${packName}{downArrow}{enter}`); - }); + // check if changing error state of one input doesn't clear other errors - START + cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { + cy.contains('Advanced').click(); + cy.getBySel('timeout-input').clear(); + cy.contains('The timeout value must be 60 seconds or higher.'); + }); - cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click(); + cy.getBySel(RESPONSE_ACTIONS_ERRORS).within(() => { + cy.contains('Query is a required field'); + cy.contains('The timeout value must be 60 seconds or higher.'); + }); + + cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { + cy.getBySel('timeout-input').type('6'); + cy.contains('The timeout value must be 60 seconds or higher.'); + }); + cy.getBySel(RESPONSE_ACTIONS_ERRORS).within(() => { + cy.contains('Query is a required field'); + cy.contains('The timeout value must be 60 seconds or higher.'); + }); + cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { + cy.getBySel('timeout-input').type('6'); + cy.contains('The timeout value must be 60 seconds or higher.').should('not.exist'); + }); + cy.getBySel(RESPONSE_ACTIONS_ERRORS).within(() => { + cy.contains('Query is a required field'); + }); + cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { + cy.getBySel('timeout-input').type('6'); + }); + cy.getBySel(RESPONSE_ACTIONS_ERRORS).within(() => { + cy.contains('Query is a required field'); + cy.contains('The timeout value must be 60 seconds or higher.').should('not.exist'); + }); + // check if changing error state of one input doesn't clear other errors - END - cy.getBySel(RESPONSE_ACTIONS_ITEM_2) - .within(() => { + cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { cy.contains('Query is a required field'); - inputQuery('select * from uptime'); - cy.contains('Query is a required field').should('not.exist'); - cy.contains('Advanced').click(); - typeInECSFieldInput('{downArrow}{enter}'); - cy.getBySel('osqueryColumnValueSelect').type('days{downArrow}{enter}'); - }) - .clickOutside(); - - cy.getBySel('ruleEditSubmitButton').click(); - cy.contains(`${ruleName} was saved`).should('exist'); - closeToastIfVisible(); - - cy.getBySel('globalLoadingIndicator').should('not.exist'); - cy.getBySel('editRuleSettingsLink').click(); - cy.getBySel('globalLoadingIndicator').should('not.exist'); - cy.getBySel('edit-rule-actions-tab').click(); - cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { - cy.contains('select * from uptime1'); - }); - cy.getBySel(RESPONSE_ACTIONS_ITEM_2).within(() => { - cy.contains('select * from uptime'); - cy.contains('Custom key/value pairs. e.g. {"application":"foo-bar","env":"production"}'); - cy.contains('Days of uptime'); - }); - cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => { - cy.getBySel('comboBoxSearchInput').should('have.value', packName); - cy.getBySel('comboBoxInput').type('{selectall}{backspace}{enter}'); - }); - cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { - cy.contains('select * from uptime1'); - cy.getBySel('remove-response-action').click(); - }); - cy.getBySel(RESPONSE_ACTIONS_ITEM_0) - .within(() => { - cy.getBySel('comboBoxSearchInput').click(); - cy.contains('Search for a pack to run'); + inputQuery('select * from uptime1'); + }); + cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click(); + cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => { + cy.contains('Run a set of queries in a pack').click(); + }); + cy.getBySel(RESPONSE_ACTIONS_ERRORS) + .within(() => { + cy.contains('Pack is a required field'); + }) + .should('exist'); + cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => { cy.contains('Pack is a required field'); cy.getBySel('comboBoxInput').type(`${packName}{downArrow}{enter}`); - }) - .clickOutside(); - cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => { - cy.contains('select * from uptime'); - cy.contains('Custom key/value pairs. e.g. {"application":"foo-bar","env":"production"}'); - cy.contains('Days of uptime'); - }); - - cy.intercept('PUT', '/api/detection_engine/rules').as('saveRuleSingleQuery'); - - cy.getBySel('ruleEditSubmitButton').click(); - cy.wait('@saveRuleSingleQuery').should(({ request }) => { - const oneQuery = [ - { - interval: 3600, - query: 'select * from uptime;', - id: Object.keys(packData.queries)[0], - }, - ]; - expect(request.body.response_actions[0].params.queries).to.deep.equal(oneQuery); - }); - - cy.contains(`${ruleName} was saved`).should('exist'); - closeToastIfVisible(); - - cy.getBySel('globalLoadingIndicator').should('not.exist'); - cy.getBySel('editRuleSettingsLink').click(); - cy.getBySel('globalLoadingIndicator').should('not.exist'); - - cy.getBySel('edit-rule-actions-tab').click(); - cy.getBySel(RESPONSE_ACTIONS_ITEM_0) - .within(() => { + }); + + cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click(); + + cy.getBySel(RESPONSE_ACTIONS_ITEM_2) + .within(() => { + cy.contains('Query is a required field'); + inputQuery('select * from uptime'); + cy.contains('Query is a required field').should('not.exist'); + cy.contains('Advanced').click(); + typeInECSFieldInput('{downArrow}{enter}'); + cy.getBySel('osqueryColumnValueSelect').type('days{downArrow}{enter}'); + }) + .clickOutside(); + + cy.getBySel('ruleEditSubmitButton').click(); + cy.contains(`${ruleName} was saved`).should('exist'); + closeToastIfVisible(); + + cy.getBySel('globalLoadingIndicator').should('not.exist'); + cy.getBySel('editRuleSettingsLink').click(); + cy.getBySel('globalLoadingIndicator').should('not.exist'); + cy.getBySel('edit-rule-actions-tab').click(); + cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { + cy.contains('select * from uptime1'); + }); + cy.getBySel(RESPONSE_ACTIONS_ITEM_2).within(() => { + cy.contains('select * from uptime'); + cy.contains('Custom key/value pairs. e.g. {"application":"foo-bar","env":"production"}'); + cy.contains('Days of uptime'); + }); + cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => { cy.getBySel('comboBoxSearchInput').should('have.value', packName); - cy.getBySel('comboBoxInput').type( - `{selectall}{backspace}${multiQueryPackName}{downArrow}{enter}` - ); - cy.contains('SELECT * FROM memory_info;'); - cy.contains('SELECT * FROM system_info;'); - }) - .clickOutside(); - - cy.getBySel(RESPONSE_ACTIONS_ITEM_1) - .within(() => { + cy.getBySel('comboBoxInput').type('{selectall}{backspace}{enter}'); + }); + cy.getBySel(RESPONSE_ACTIONS_ITEM_0).within(() => { + cy.contains('select * from uptime1'); + cy.getBySel('remove-response-action').click(); + }); + cy.getBySel(RESPONSE_ACTIONS_ITEM_0) + .within(() => { + cy.getBySel('comboBoxSearchInput').click(); + cy.contains('Search for a pack to run'); + cy.contains('Pack is a required field'); + cy.getBySel('comboBoxInput').type(`${packName}{downArrow}{enter}`); + }) + .clickOutside(); + cy.getBySel(RESPONSE_ACTIONS_ITEM_1).within(() => { cy.contains('select * from uptime'); cy.contains('Custom key/value pairs. e.g. {"application":"foo-bar","env":"production"}'); cy.contains('Days of uptime'); - }) - .clickOutside(); - cy.intercept('PUT', '/api/detection_engine/rules').as('saveRuleMultiQuery'); - - cy.contains('Save changes').click(); - cy.wait('@saveRuleMultiQuery').should(({ request }) => { - const threeQueries = [ - { - interval: 3600, - query: 'SELECT * FROM memory_info;', - platform: 'linux', - id: Object.keys(multiQueryPackData.queries)[0], - }, - { - interval: 3600, - query: 'SELECT * FROM system_info;', - id: Object.keys(multiQueryPackData.queries)[1], - }, - { - interval: 10, - query: 'select opera_extensions.* from users join opera_extensions using (uid);', - id: Object.keys(multiQueryPackData.queries)[2], - }, - ]; - expect(request.body.response_actions[0].params.queries).to.deep.equal(threeQueries); - }); - }); -}); + }); + + cy.intercept('PUT', '/api/detection_engine/rules').as('saveRuleSingleQuery'); + + cy.getBySel('ruleEditSubmitButton').click(); + cy.wait('@saveRuleSingleQuery').should(({ request }) => { + const oneQuery = [ + { + interval: 3600, + query: 'select * from uptime;', + id: Object.keys(packData.queries)[0], + }, + ]; + expect(request.body.response_actions[0].params.queries).to.deep.equal(oneQuery); + }); + + cy.contains(`${ruleName} was saved`).should('exist'); + closeToastIfVisible(); + + cy.getBySel('globalLoadingIndicator').should('not.exist'); + cy.getBySel('editRuleSettingsLink').click(); + cy.getBySel('globalLoadingIndicator').should('not.exist'); + + cy.getBySel('edit-rule-actions-tab').click(); + cy.getBySel(RESPONSE_ACTIONS_ITEM_0) + .within(() => { + cy.getBySel('comboBoxSearchInput').should('have.value', packName); + cy.getBySel('comboBoxInput').type( + `{selectall}{backspace}${multiQueryPackName}{downArrow}{enter}` + ); + cy.contains('SELECT * FROM memory_info;'); + cy.contains('SELECT * FROM system_info;'); + }) + .clickOutside(); + + cy.getBySel(RESPONSE_ACTIONS_ITEM_1) + .within(() => { + cy.contains('select * from uptime'); + cy.contains('Custom key/value pairs. e.g. {"application":"foo-bar","env":"production"}'); + cy.contains('Days of uptime'); + }) + .clickOutside(); + cy.intercept('PUT', '/api/detection_engine/rules').as('saveRuleMultiQuery'); + + cy.contains('Save changes').click(); + cy.wait('@saveRuleMultiQuery').should(({ request }) => { + const threeQueries = [ + { + interval: 3600, + query: 'SELECT * FROM memory_info;', + platform: 'linux', + id: Object.keys(multiQueryPackData.queries)[0], + }, + { + interval: 3600, + query: 'SELECT * FROM system_info;', + id: Object.keys(multiQueryPackData.queries)[1], + }, + { + interval: 10, + query: 'select opera_extensions.* from users join opera_extensions using (uid);', + id: Object.keys(multiQueryPackData.queries)[2], + }, + ]; + expect(request.body.response_actions[0].params.queries).to.deep.equal(threeQueries); + }); + }); + } +); diff --git a/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx b/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx index 359df30516511..c959d0f41bf4a 100644 --- a/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx +++ b/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useRef, useEffect, FC, ReactNode } from 'react'; +import React, { FC, ReactNode } from 'react'; import { EuiInMemoryTable, EuiBasicTableColumn, EuiLink, Query } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -57,14 +57,6 @@ export const TagTable: FC = ({ actionBar, actions, }) => { - const tableRef = useRef>(null); - - useEffect(() => { - if (tableRef.current) { - tableRef.current.setSelection(selectedTags); - } - }, [selectedTags]); - const columns: Array> = [ { field: 'name', @@ -144,7 +136,6 @@ export const TagTable: FC = ({ return ( = ({ selection={ allowSelection ? { - initialSelected: selectedTags, + selected: selectedTags, onSelectionChange, } : undefined diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts/8.13.0/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts/8.13.0/index.ts index 594dc685097db..e7066058ab8db 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts/8.13.0/index.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts/8.13.0/index.ts @@ -7,8 +7,14 @@ import type { AlertWithCommonFields800 } from '@kbn/rule-registry-plugin/common/schemas/8.0.0'; import type { + LEGACY_ALERT_HOST_CRITICALITY, + LEGACY_ALERT_USER_CRITICALITY, ALERT_HOST_CRITICALITY, ALERT_USER_CRITICALITY, + ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL, + ALERT_HOST_RISK_SCORE_CALCULATED_SCORE_NORM, + ALERT_USER_RISK_SCORE_CALCULATED_LEVEL, + ALERT_USER_RISK_SCORE_CALCULATED_SCORE_NORM, } from '../../../../../field_maps/field_names'; import type { Ancestor8120, @@ -29,8 +35,17 @@ new schemas to the union of all alert schemas, and re-export the new schemas as export type { Ancestor8120 as Ancestor8130 }; export interface BaseFields8130 extends BaseFields8120 { + [LEGACY_ALERT_HOST_CRITICALITY]: string | undefined; + [LEGACY_ALERT_USER_CRITICALITY]: string | undefined; [ALERT_HOST_CRITICALITY]: string | undefined; [ALERT_USER_CRITICALITY]: string | undefined; + /** + * Risk scores fields was added aroung 8.5.0, but the fields were not added to the alert schema + */ + [ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL]: string | undefined; + [ALERT_HOST_RISK_SCORE_CALCULATED_SCORE_NORM]: number | undefined; + [ALERT_USER_RISK_SCORE_CALCULATED_LEVEL]: string | undefined; + [ALERT_USER_RISK_SCORE_CALCULATED_SCORE_NORM]: number | undefined; } export interface WrappedFields8130 { diff --git a/x-pack/plugins/security_solution/common/field_maps/8.13.0/alerts.ts b/x-pack/plugins/security_solution/common/field_maps/8.13.0/alerts.ts index 86c84092891b8..09bb86a205500 100644 --- a/x-pack/plugins/security_solution/common/field_maps/8.13.0/alerts.ts +++ b/x-pack/plugins/security_solution/common/field_maps/8.13.0/alerts.ts @@ -6,10 +6,25 @@ */ import { alertsFieldMap840 } from '../8.4.0'; -import { ALERT_HOST_CRITICALITY, ALERT_USER_CRITICALITY } from '../field_names'; +import { + ALERT_HOST_CRITICALITY, + ALERT_USER_CRITICALITY, + LEGACY_ALERT_HOST_CRITICALITY, + LEGACY_ALERT_USER_CRITICALITY, +} from '../field_names'; export const alertsFieldMap8130 = { ...alertsFieldMap840, + [LEGACY_ALERT_HOST_CRITICALITY]: { + type: 'keyword', + array: false, + required: false, + }, + [LEGACY_ALERT_USER_CRITICALITY]: { + type: 'keyword', + array: false, + required: false, + }, /** * Stores the criticality level for the host, as determined by analysts, in relation to the alert. * The Criticality level is copied from the asset criticality index. diff --git a/x-pack/plugins/security_solution/common/field_maps/field_names.ts b/x-pack/plugins/security_solution/common/field_maps/field_names.ts index 6124cc08ebd2b..b8ef2e61fb390 100644 --- a/x-pack/plugins/security_solution/common/field_maps/field_names.ts +++ b/x-pack/plugins/security_solution/common/field_maps/field_names.ts @@ -17,8 +17,23 @@ export const ALERT_THRESHOLD_RESULT = `${ALERT_NAMESPACE}.threshold_result` as c export const ALERT_THRESHOLD_RESULT_COUNT = `${ALERT_THRESHOLD_RESULT}.count` as const; export const ALERT_NEW_TERMS = `${ALERT_NAMESPACE}.new_terms` as const; export const ALERT_NEW_TERMS_FIELDS = `${ALERT_RULE_PARAMETERS}.new_terms_fields` as const; -export const ALERT_HOST_CRITICALITY = `${ALERT_NAMESPACE}.host.criticality_level` as const; -export const ALERT_USER_CRITICALITY = `${ALERT_NAMESPACE}.user.criticality_level` as const; +/** + * @deprecated Use {@link ALERT_HOST_CRITICALITY} + */ +export const LEGACY_ALERT_HOST_CRITICALITY = `${ALERT_NAMESPACE}.host.criticality_level` as const; +/** + * @deprecated Use {@link ALERT_USER_CRITICALITY} + */ +export const LEGACY_ALERT_USER_CRITICALITY = `${ALERT_NAMESPACE}.user.criticality_level` as const; + +export const ALERT_HOST_CRITICALITY = `host.asset.criticality` as const; +export const ALERT_USER_CRITICALITY = `user.asset.criticality` as const; +export const ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL = `host.risk.calculated_level` as const; +export const ALERT_HOST_RISK_SCORE_CALCULATED_SCORE_NORM = + `host.risk.calculated_score_norm` as const; +export const ALERT_USER_RISK_SCORE_CALCULATED_LEVEL = `user.risk.calculated_level` as const; +export const ALERT_USER_RISK_SCORE_CALCULATED_SCORE_NORM = + `user.risk.calculated_score_norm` as const; export const ALERT_ORIGINAL_EVENT = `${ALERT_NAMESPACE}.original_event` as const; export const ALERT_ORIGINAL_EVENT_ACTION = `${ALERT_ORIGINAL_EVENT}.action` as const; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx index bea9752d6e770..38d3aeb37bac9 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx @@ -139,8 +139,7 @@ const mockConfig = { customHeight: 324, }; -// Failing: See https://github.com/elastic/kibana/issues/175984 -describe.skip('BarChartBaseComponent', () => { +describe('BarChartBaseComponent', () => { let shallowWrapper: ShallowWrapper; const mockBarChartData: ChartSeriesData[] = [ { @@ -297,8 +296,7 @@ describe.skip('BarChartBaseComponent', () => { }); }); -// Failing: See https://github.com/elastic/kibana/issues/175984 -describe.skip.each(chartDataSets)('BarChart with valid data [%o]', (data) => { +describe.each(chartDataSets)('BarChart with valid data [%o]', (data) => { let shallowWrapper: ShallowWrapper; beforeAll(() => { @@ -315,8 +313,7 @@ describe.skip.each(chartDataSets)('BarChart with valid data [%o]', (data) => { }); }); -// Failing: See https://github.com/elastic/kibana/issues/175984 -describe.skip.each(chartDataSets)('BarChart with stackByField', () => { +describe.each(chartDataSets)('BarChart with stackByField', () => { let wrapper: ReactWrapper; const data = [ @@ -391,8 +388,7 @@ describe.skip.each(chartDataSets)('BarChart with stackByField', () => { }); }); -// Failing: See https://github.com/elastic/kibana/issues/175984 -describe.skip.each(chartDataSets)('BarChart with custom color', () => { +describe.each(chartDataSets)('BarChart with custom color', () => { let wrapper: ReactWrapper; const data = [ @@ -455,8 +451,7 @@ describe.skip.each(chartDataSets)('BarChart with custom color', () => { }); }); -// Failing: See https://github.com/elastic/kibana/issues/175984 -describe.skip.each(chartHolderDataSets)('BarChart with invalid data [%o]', (data) => { +describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', (data) => { let shallowWrapper: ShallowWrapper; beforeAll(() => { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx index fc5d91bcaf8e5..1e0c7021929c9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx @@ -70,8 +70,8 @@ const platinumBaseColumns = [ { columnHeaderType: 'not-filtered', id: 'host.risk.calculated_level' }, { columnHeaderType: 'not-filtered', id: 'user.name' }, { columnHeaderType: 'not-filtered', id: 'user.risk.calculated_level' }, - { columnHeaderType: 'not-filtered', id: 'kibana.alert.host.criticality_level' }, - { columnHeaderType: 'not-filtered', id: 'kibana.alert.user.criticality_level' }, + { columnHeaderType: 'not-filtered', id: 'host.asset.criticality' }, + { columnHeaderType: 'not-filtered', id: 'user.asset.criticality' }, { columnHeaderType: 'not-filtered', id: 'process.name' }, { columnHeaderType: 'not-filtered', id: 'file.name' }, { columnHeaderType: 'not-filtered', id: 'source.ip' }, diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts index b22bf5e2ed429..fc3f5afa897a2 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts @@ -9,6 +9,8 @@ import type { EuiDataGridColumn } from '@elastic/eui'; import { ALERT_HOST_CRITICALITY, ALERT_USER_CRITICALITY, + ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL, + ALERT_USER_RISK_SCORE_CALCULATED_LEVEL, } from '../../../../common/field_maps/field_names'; import type { LicenseService } from '../../../../common/license'; import type { ColumnHeaderOptions } from '../../../../common/types'; @@ -64,7 +66,7 @@ const getBaseColumns = ( isPlatinumPlus ? { columnHeaderType: defaultColumnHeaderType, - id: 'host.risk.calculated_level', + id: ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL, } : null, { @@ -74,7 +76,7 @@ const getBaseColumns = ( isPlatinumPlus ? { columnHeaderType: defaultColumnHeaderType, - id: 'user.risk.calculated_level', + id: ALERT_USER_RISK_SCORE_CALCULATED_LEVEL, } : null, isPlatinumPlus diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/alerts_response_console.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/alerts_response_console.cy.ts index 0a91f0e2af552..84fd3f1bbea64 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/alerts_response_console.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/alerts_response_console.cy.ts @@ -104,7 +104,7 @@ describe( getAlertsTableRows().should('have.length.greaterThan', 0); openInvestigateInTimelineView(); - cy.getByTestSubj('timeline-flyout').within(() => { + cy.getByTestSubj('timeline-container').within(() => { openAlertDetailsView(); }); openResponderFromEndpointAlertDetails(); diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index ffebf2e1d780a..2bc671c1b7d78 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -439,7 +439,8 @@ describe('Resolver, when analyzing a tree that has 2 related registry and 1 rela }); }); - describe('when it has loaded', () => { + // FLAKY: https://github.com/elastic/kibana/issues/170118 + describe.skip('when it has loaded', () => { let originBounds: AABB; let firstChildBounds: AABB; let secondChildBounds: AABB; diff --git a/x-pack/plugins/security_solution/public/timelines/components/add_to_favorites/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/add_to_favorites/index.test.tsx index f64375e1327ac..39f40dc74ed9e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/add_to_favorites/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/add_to_favorites/index.test.tsx @@ -30,10 +30,10 @@ jest.mock('react-redux', () => { }; }); -const renderAddFavoritesButton = () => +const renderAddFavoritesButton = (isPartOfGuidedTour = false) => render( - + ); @@ -49,6 +49,7 @@ describe('AddToFavoritesButton', () => { const button = getByTestId('timeline-favorite-empty-star'); expect(button).toBeInTheDocument(); + expect(button).toHaveProperty('id', ''); expect(button.firstChild).toHaveAttribute('data-euiicon-type', 'starEmpty'); expect(queryByTestId('timeline-favorite-filled-star')).not.toBeInTheDocument(); }); @@ -86,4 +87,17 @@ describe('AddToFavoritesButton', () => { expect(getByTestId('timeline-favorite-filled-star')).toBeInTheDocument(); expect(queryByTestId('timeline-favorite-empty-star')).not.toBeInTheDocument(); }); + + it('should use id for guided tour if prop is true', () => { + mockGetState.mockReturnValue({ + ...mockTimelineModel, + status: TimelineStatus.active, + }); + + const { getByTestId } = renderAddFavoritesButton(true); + + const button = getByTestId('timeline-favorite-empty-star'); + + expect(button).toHaveProperty('id', 'add-to-favorites'); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/add_to_favorites/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/add_to_favorites/index.tsx index b1f0b03b4cff1..06055802e2fff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/add_to_favorites/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/add_to_favorites/index.tsx @@ -34,40 +34,46 @@ interface AddToFavoritesButtonProps { * Id of the timeline to be displayed in the bottom bar and within the modal */ timelineId: string; + /** + * Whether the button is a step in the timeline guided tour + */ + isPartOfGuidedTour?: boolean; } /** * This component renders the add to favorites button for timeline. * It is used in the bottom bar as well as in the timeline modal's header. */ -export const AddToFavoritesButton = React.memo(({ timelineId }) => { - const dispatch = useDispatch(); - const { isFavorite, status } = useSelector((state: State) => - selectTimelineById(state, timelineId) - ); +export const AddToFavoritesButton = React.memo( + ({ timelineId, isPartOfGuidedTour = false }) => { + const dispatch = useDispatch(); + const { isFavorite, status } = useSelector((state: State) => + selectTimelineById(state, timelineId) + ); - const isTimelineDraftOrImmutable = status !== TimelineStatus.active; - const label = isFavorite ? REMOVE_FROM_FAVORITES : ADD_TO_FAVORITES; + const isTimelineDraftOrImmutable = status !== TimelineStatus.active; + const label = isFavorite ? REMOVE_FROM_FAVORITES : ADD_TO_FAVORITES; - const handleClick = useCallback( - () => dispatch(timelineActions.updateIsFavorite({ id: timelineId, isFavorite: !isFavorite })), - [dispatch, timelineId, isFavorite] - ); + const handleClick = useCallback( + () => dispatch(timelineActions.updateIsFavorite({ id: timelineId, isFavorite: !isFavorite })), + [dispatch, timelineId, isFavorite] + ); - return ( - - {label} - - ); -}); + return ( + + {label} + + ); + } +); AddToFavoritesButton.displayName = 'AddToFavoritesButton'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.test.tsx index 686a631736550..05828ff9aface 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/bottom_bar/index.test.tsx @@ -32,6 +32,7 @@ describe('TimelineBottomBar', () => { expect(getByTestId('timeline-event-count-badge')).toBeInTheDocument(); expect(getByTestId('timeline-save-status')).toBeInTheDocument(); expect(getByTestId('timeline-favorite-empty-star')).toBeInTheDocument(); + expect(getByTestId('timeline-favorite-empty-star')).toHaveProperty('id', ''); }); test('should not render the event count badge if timeline is open', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx deleted file mode 100644 index 54eb8cf2769a9..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { render, screen } from '@testing-library/react'; - -import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock'; -import { useSourcererDataView } from '../../../../common/containers/sourcerer'; -import { allCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; -import { mockBrowserFields } from '../../../../common/containers/source/mock'; -import { TimelineActionMenu } from '.'; -import { TimelineId, TimelineTabs } from '../../../../../common/types'; -import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__'; - -const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock; -const mockedUseKibana = mockUseKibana(); -const mockCanUseCases = jest.fn(); - -jest.mock('../../../../common/containers/sourcerer'); - -jest.mock('../../../../common/lib/kibana/kibana_react', () => { - const original = jest.requireActual('../../../../common/lib/kibana/kibana_react'); - - return { - ...original, - useKibana: () => ({ - ...mockedUseKibana, - services: { - ...mockedUseKibana.services, - cases: { - ...mockedUseKibana.services.cases, - helpers: { canUseCases: mockCanUseCases }, - }, - }, - application: { - capabilities: { - navLinks: {}, - management: {}, - catalogue: {}, - actions: { show: true, crud: true }, - }, - }, - }), - }; -}); - -jest.mock('@kbn/i18n-react', () => { - const originalModule = jest.requireActual('@kbn/i18n-react'); - const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); - - return { - ...originalModule, - FormattedRelative, - }; -}); - -const sourcererDefaultValue = { - sourcererDefaultValue: mockBrowserFields, - indexPattern: mockIndexPattern, - loading: false, - selectedPatterns: mockIndexNames, -}; - -describe('Action menu', () => { - beforeEach(() => { - // Mocking these services is required for the header component to render. - mockUseSourcererDataView.mockImplementation(() => sourcererDefaultValue); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('AddToCaseButton', () => { - it('renders the button when the user has create and read permissions', () => { - mockCanUseCases.mockReturnValue(allCasesPermissions()); - - render( - - - - ); - - expect( - screen.getByTestId('timeline-modal-attach-to-case-dropdown-button') - ).toBeInTheDocument(); - }); - - it('does not render the button when the user does not have create permissions', () => { - mockCanUseCases.mockReturnValue(readCasesPermissions()); - - render( - - - - ); - - expect( - screen.queryByTestId('timeline-modal-attach-to-case-dropdown-button') - ).not.toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx deleted file mode 100644 index 1fc6e41ec43f3..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; -import { AttachToCaseButton } from '../../modal/actions/attach_to_case_button'; -import { useKibana } from '../../../../common/lib/kibana/kibana_react'; -import { APP_ID } from '../../../../../common'; -import type { TimelineTabs } from '../../../../../common/types'; -import { InspectButton } from '../../../../common/components/inspect'; -import { InputsModelId } from '../../../../common/store/inputs/constants'; -import { NewTimelineButton } from '../../modal/actions/new_timeline_button'; -import { SaveTimelineButton } from '../../modal/actions/save_timeline_button'; -import { OpenTimelineButton } from '../../modal/actions/open_timeline_button'; -import { TIMELINE_TOUR_CONFIG_ANCHORS } from '../../timeline/tour/step_config'; - -interface TimelineActionMenuProps { - mode?: 'compact' | 'normal'; - timelineId: string; - isInspectButtonDisabled: boolean; - activeTab: TimelineTabs; -} - -const VerticalDivider = styled.span` - width: 0px; - height: 20px; - border-left: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; -`; - -const TimelineActionMenuComponent = ({ - mode = 'normal', - timelineId, - activeTab, - isInspectButtonDisabled, -}: TimelineActionMenuProps) => { - const { cases } = useKibana().services; - const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); - - return ( - - - - - - - - - - - {userCasesPermissions.create && userCasesPermissions.read ? ( - <> - - - - - - - - ) : null} - - - - - ); -}; - -export const TimelineActionMenu = React.memo(TimelineActionMenuComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx deleted file mode 100644 index 930acce9f8bd3..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiToolTip, - EuiButtonIcon, - EuiText, -} from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import { isEmpty, get, pick } from 'lodash/fp'; -import { useDispatch, useSelector } from 'react-redux'; -import { getEsQueryConfig } from '@kbn/data-plugin/common'; -import { euiStyled } from '@kbn/kibana-react-plugin/common'; -import { selectTitleByTimelineById } from '../../../store/selectors'; -import { createHistoryEntry } from '../../../../common/utils/global_query_string/helpers'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { timelineActions, timelineSelectors } from '../../../store'; -import type { State } from '../../../../common/store'; -import { useKibana } from '../../../../common/lib/kibana'; -import { useSourcererDataView } from '../../../../common/containers/sourcerer'; -import { combineQueries } from '../../../../common/lib/kuery'; -import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; -import * as i18n from './translations'; -import { TimelineActionMenu } from '../action_menu'; -import { AddToFavoritesButton } from '../../add_to_favorites'; -import { TimelineSaveStatus } from '../../save_status'; -import { timelineDefaults } from '../../../store/defaults'; - -interface FlyoutHeaderPanelProps { - timelineId: string; -} - -const whiteSpaceNoWrapCSS = { 'white-space': 'nowrap' }; -const autoOverflowXCSS = { 'overflow-x': 'auto' }; - -const TimelinePanel = euiStyled(EuiPanel)<{ $isOpen?: boolean }>` - backgroundColor: ${(props) => props.theme.eui.euiColorEmptyShade}; - color: ${(props) => props.theme.eui.euiTextColor}; - padding-inline: ${(props) => props.theme.eui.euiSizeM}; - border-radius: ${({ $isOpen, theme }) => ($isOpen ? theme.eui.euiBorderRadius : '0px')}; -`; - -const FlyoutHeaderPanelComponent: React.FC = ({ timelineId }) => { - const dispatch = useDispatch(); - const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.timeline); - const { uiSettings } = useKibana().services; - const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); - - const title = useSelector((state: State) => selectTitleByTimelineById(state, timelineId)); - - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { activeTab, dataProviders, kqlQuery, timelineType, show, filters, kqlMode } = - useDeepEqualSelector((state) => - pick( - ['activeTab', 'dataProviders', 'kqlQuery', 'timelineType', 'show', 'filters', 'kqlMode'], - getTimeline(state, timelineId) ?? timelineDefaults - ) - ); - const isDataInTimeline = useMemo( - () => !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)), - [dataProviders, kqlQuery] - ); - - const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterQuerySelector(), []); - - const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)); - - const kqlQueryExpression = - isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) && timelineType === 'template' - ? ' ' - : kqlQueryTimeline ?? ''; - - const kqlQueryObj = useMemo( - () => ({ query: kqlQueryExpression, language: 'kuery' }), - [kqlQueryExpression] - ); - - const combinedQueries = useMemo( - () => - combineQueries({ - config: esQueryConfig, - dataProviders, - indexPattern, - browserFields, - filters: filters ? filters : [], - kqlQuery: kqlQueryObj, - kqlMode, - }), - [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryObj] - ); - - const handleClose = useCallback(() => { - createHistoryEntry(); - dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); - }, [dispatch, timelineId]); - - return ( - - - - - - - - - -

{title}

-
-
- - - -
-
- {show && ( - - - - - - - - - - - )} -
-
- ); -}; - -export const FlyoutHeaderPanel = React.memo(FlyoutHeaderPanelComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/selectors.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/selectors.ts deleted file mode 100644 index 441ae5fed1a7e..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/selectors.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createSelector } from 'reselect'; - -import { TimelineStatus } from '../../../../../common/api/timeline'; -import { timelineSelectors } from '../../../store'; - -export const getTimelineStatusByIdSelector = () => - createSelector(timelineSelectors.selectTimeline, (timeline) => ({ - status: timeline?.status ?? TimelineStatus.draft, - updated: timeline?.updated ?? undefined, - isSaving: timeline?.isSaving ?? undefined, - isLoading: timeline?.isLoading ?? undefined, - changed: timeline?.changed ?? undefined, - show: timeline?.show ?? undefined, - })); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/pane.test.tsx deleted file mode 100644 index 08d1465b08fba..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/pane.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React from 'react'; - -import { TestProviders } from '../../../../common/mock'; -import { TimelineId } from '../../../../../common/types/timeline'; -import { Pane } from '.'; - -jest.mock('../../timeline', () => ({ - StatefulTimeline: () =>
, -})); - -const mockIsFullScreen = jest.fn(() => false); -jest.mock('../../../../common/store/selectors', () => ({ - inputsSelectors: { timelineFullScreenSelector: () => mockIsFullScreen() }, -})); - -describe('Pane', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should render the timeline', async () => { - const wrapper = render( - - - - ); - - expect(wrapper.getByTestId('StatefulTimelineMock')).toBeInTheDocument(); - }); - - it('should render without fullscreen className', async () => { - mockIsFullScreen.mockReturnValue(false); - const wrapper = render( - - - - ); - - expect(wrapper.getByTestId('timeline-wrapper')).not.toHaveClass( - 'timeline-wrapper--full-screen' - ); - }); - - it('should render with fullscreen className', async () => { - mockIsFullScreen.mockReturnValue(true); - const wrapper = render( - - - - ); - - expect(wrapper.getByTestId('timeline-wrapper')).toHaveClass('timeline-wrapper--full-screen'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/pane.tsx deleted file mode 100644 index 853c88044cd8b..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/pane.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useRef } from 'react'; -import classNames from 'classnames'; -import { StatefulTimeline } from '../../timeline'; -import type { TimelineId } from '../../../../../common/types/timeline'; -import * as i18n from './translations'; -import { defaultRowRenderers } from '../../timeline/body/renderers'; -import { DefaultCellRenderer } from '../../timeline/cell_rendering/default_cell_renderer'; -import { EuiPortal } from './custom_portal'; -import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; -import { inputsSelectors } from '../../../../common/store/selectors'; -import { usePaneStyles, OverflowHiddenGlobalStyles } from './pane.styles'; - -interface FlyoutPaneComponentProps { - timelineId: TimelineId; - visible?: boolean; -} - -const FlyoutPaneComponent: React.FC = ({ - timelineId, - visible = true, -}) => { - const ref = useRef(null); - const isFullScreen = useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false; - - const styles = usePaneStyles(); - const wrapperClassName = classNames('timeline-wrapper', styles, { - 'timeline-wrapper--full-screen': isFullScreen, - 'timeline-wrapper--hidden': !visible, - }); - - return ( -
- -
-
- -
-
-
- {visible && } -
- ); -}; - -export const Pane = React.memo(FlyoutPaneComponent); - -Pane.displayName = 'Pane'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/custom_portal.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/custom_portal.tsx similarity index 54% rename from x-pack/plugins/security_solution/public/timelines/components/flyout/pane/custom_portal.tsx rename to x-pack/plugins/security_solution/public/timelines/components/modal/custom_portal.tsx index 861f90851c32c..87c4343620790 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/custom_portal.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/custom_portal.tsx @@ -15,81 +15,62 @@ import type { ReactNode } from 'react'; import { Component } from 'react'; import { createPortal } from 'react-dom'; -interface InsertPositionsMap { - after: InsertPosition; - before: InsertPosition; -} - -export const insertPositions: InsertPositionsMap = { - after: 'afterend', - before: 'beforebegin', -}; - -export interface EuiPortalProps { +export interface CustomEuiPortalProps { /** * ReactNode to render as this component's content */ children: ReactNode; - insert?: { sibling: HTMLElement | null; position: 'before' | 'after' }; - portalRef?: (ref: HTMLDivElement | null) => void; + /** + * Sibling is the React node or HTMLElement to insert the portal next to + * Position specifies the portal's relative position, either before or after + */ + sibling: HTMLDivElement | null; } -export class EuiPortal extends Component { +export class CustomEuiPortal extends Component { portalNode: HTMLDivElement | null = null; - constructor(props: EuiPortalProps) { + constructor(props: CustomEuiPortalProps) { super(props); if (typeof window === 'undefined') return; // Prevent SSR errors - const { insert } = this.props; + const { sibling } = this.props; this.portalNode = document.createElement('div'); this.portalNode.dataset.euiportal = 'true'; - if (insert == null || insert.sibling == null) { + if (sibling == null) { // no insertion defined, append to body document.body.appendChild(this.portalNode); } else { // inserting before or after an element - const { sibling, position } = insert; - sibling.insertAdjacentElement(insertPositions[position], this.portalNode); + sibling.insertAdjacentElement('afterend', this.portalNode); } } - componentDidMount() { - this.updatePortalRef(this.portalNode); - } - componentWillUnmount() { if (this.portalNode?.parentNode) { this.portalNode.parentNode.removeChild(this.portalNode); } - this.updatePortalRef(null); } - componentDidUpdate(prevProps: Readonly): void { - if (!deepEqual(prevProps.insert, this.props.insert) && this.portalNode?.parentNode) { + componentDidUpdate(prevProps: Readonly): void { + if (!deepEqual(prevProps.sibling, this.props.sibling) && this.portalNode?.parentNode) { this.portalNode.parentNode.removeChild(this.portalNode); } if (this.portalNode) { - if (this.props.insert == null || this.props.insert.sibling == null) { + if (this.props == null || this.props.sibling == null) { // no insertion defined, append to body document.body.appendChild(this.portalNode); } else { // inserting before or after an element - const { sibling, position } = this.props.insert; - sibling.insertAdjacentElement(insertPositions[position], this.portalNode); + const { sibling } = this.props; + sibling.insertAdjacentElement('afterend', this.portalNode); } } } - updatePortalRef(ref: HTMLDivElement | null) { - if (this.props.portalRef) { - this.props.portalRef(ref); - } - } - render() { return this.portalNode ? createPortal(this.props.children, this.portalNode) : null; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx new file mode 100644 index 0000000000000..15246435852fb --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { TestProviders } from '../../../../common/mock'; +import { TimelineModalHeader } from '.'; +import { render } from '@testing-library/react'; +import { useSourcererDataView } from '../../../../common/containers/sourcerer'; +import { useCreateTimeline } from '../../../hooks/use_create_timeline'; +import { useInspect } from '../../../../common/components/inspect/use_inspect'; +import { useKibana } from '../../../../common/lib/kibana'; +import { timelineActions } from '../../../store'; + +jest.mock('../../../../common/containers/sourcerer'); +jest.mock('../../../hooks/use_create_timeline'); +jest.mock('../../../../common/components/inspect/use_inspect'); +jest.mock('../../../../common/lib/kibana'); + +const mockGetState = jest.fn(); +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: jest.fn(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + useSelector: (selector: any) => + selector({ + timeline: { + timelineById: { + 'timeline-1': { + ...mockGetState(), + }, + }, + }, + }), + }; +}); + +const timelineId = 'timeline-1'; +const renderTimelineModalHeader = () => + render( + + + + ); + +describe('TimelineModalHeader', () => { + (useCreateTimeline as jest.Mock).mockReturnValue(jest.fn()); + (useInspect as jest.Mock).mockReturnValue(jest.fn()); + + it('should render all dom elements', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + browserFields: {}, + indexPattern: { fields: [], title: '' }, + }); + + const { getByTestId, getByText } = renderTimelineModalHeader(); + + expect(getByTestId('timeline-favorite-empty-star')).toBeInTheDocument(); + expect(getByText('Untitled timeline')).toBeInTheDocument(); + expect(getByTestId('timeline-save-status')).toBeInTheDocument(); + expect(getByTestId('timeline-modal-header-actions')).toBeInTheDocument(); + expect(getByTestId('timeline-modal-new-timeline-dropdown-button')).toBeInTheDocument(); + expect(getByTestId('timeline-modal-open-timeline-button')).toBeInTheDocument(); + expect(getByTestId('inspect-empty-button')).toBeInTheDocument(); + expect(getByTestId('timeline-modal-save-timeline')).toBeInTheDocument(); + expect(getByTestId('timeline-modal-header-close-button')).toBeInTheDocument(); + }); + + it('should show attach to case if user has the correct permissions', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + browserFields: {}, + indexPattern: { fields: [], title: '' }, + }); + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + navigateToApp: jest.fn(), + }, + cases: { + helpers: { + canUseCases: jest.fn().mockReturnValue({ + create: true, + read: true, + }), + }, + }, + uiSettings: { + get: jest.fn(), + }, + }, + }); + + const { getByTestId } = renderTimelineModalHeader(); + + expect(getByTestId('timeline-modal-attach-to-case-dropdown-button')).toBeInTheDocument(); + }); + + it('should call showTimeline action when closing timeline', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + browserFields: {}, + indexPattern: { fields: [], title: '' }, + }); + + const spy = jest.spyOn(timelineActions, 'showTimeline'); + + const { getByTestId } = renderTimelineModalHeader(); + + getByTestId('timeline-modal-header-close-button').click(); + + expect(spy).toHaveBeenCalledWith({ + id: timelineId, + show: false, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.tsx new file mode 100644 index 0000000000000..aca4fda13f697 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.tsx @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiToolTip, + EuiButtonIcon, + EuiText, +} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import styled from 'styled-components'; +import { TIMELINE_TOUR_CONFIG_ANCHORS } from '../../timeline/tour/step_config'; +import { NewTimelineButton } from '../actions/new_timeline_button'; +import { OpenTimelineButton } from '../actions/open_timeline_button'; +import { APP_ID } from '../../../../../common'; +import { + selectDataInTimeline, + selectKqlQuery, + selectTimelineById, + selectTitleByTimelineById, +} from '../../../store/selectors'; +import { createHistoryEntry } from '../../../../common/utils/global_query_string/helpers'; +import { timelineActions } from '../../../store'; +import type { State } from '../../../../common/store'; +import { useKibana } from '../../../../common/lib/kibana'; +import { useSourcererDataView } from '../../../../common/containers/sourcerer'; +import { combineQueries } from '../../../../common/lib/kuery'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import * as i18n from '../translations'; +import { AddToFavoritesButton } from '../../add_to_favorites'; +import { TimelineSaveStatus } from '../../save_status'; +import { InspectButton } from '../../../../common/components/inspect'; +import { InputsModelId } from '../../../../common/store/inputs/constants'; +import { AttachToCaseButton } from '../actions/attach_to_case_button'; +import { SaveTimelineButton } from '../actions/save_timeline_button'; + +const whiteSpaceNoWrapCSS = { 'white-space': 'nowrap' }; +const autoOverflowXCSS = { 'overflow-x': 'auto' }; +const VerticalDivider = styled.span` + width: 0; + height: 20px; + border-left: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; +`; +const TimelinePanel = euiStyled(EuiPanel)` + backgroundColor: ${(props) => props.theme.eui.euiColorEmptyShade}; + color: ${(props) => props.theme.eui.euiTextColor}; + padding-inline: ${(props) => props.theme.eui.euiSizeM}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; +`; + +interface FlyoutHeaderPanelProps { + /** + * Id of the timeline to be displayed within the modal + */ + timelineId: string; +} + +/** + * Component rendered at the top of the timeline modal. It contains the timeline title, all the action buttons (save, open, favorite...) and the close button + */ +export const TimelineModalHeader = React.memo(({ timelineId }) => { + const dispatch = useDispatch(); + const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.timeline); + const { cases, uiSettings } = useKibana().services; + const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); + + const title = useSelector((state: State) => selectTitleByTimelineById(state, timelineId)); + const isDataInTimeline = useSelector((state: State) => selectDataInTimeline(state, timelineId)); + const kqlQueryObj = useSelector((state: State) => selectKqlQuery(state, timelineId)); + + const { activeTab, dataProviders, timelineType, filters, kqlMode } = useSelector((state: State) => + selectTimelineById(state, timelineId) + ); + + const combinedQueries = useMemo( + () => + combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters: filters ? filters : [], + kqlQuery: kqlQueryObj, + kqlMode, + }), + [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQueryObj] + ); + const isInspectDisabled = !isDataInTimeline || combinedQueries?.filterQuery === undefined; + + const closeTimeline = useCallback(() => { + createHistoryEntry(); + dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); + }, [dispatch, timelineId]); + + return ( + + + + + + + + + +

{title}

+
+
+ + + +
+
+ + + + + + + + + + + + {userCasesPermissions.create && userCasesPermissions.read ? ( + <> + + + + + + + + ) : null} + + + + + + + + + + +
+
+ ); +}); + +TimelineModalHeader.displayName = 'TimelineModalHeader'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/pane.styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/index.styles.tsx similarity index 84% rename from x-pack/plugins/security_solution/public/timelines/components/flyout/pane/pane.styles.tsx rename to x-pack/plugins/security_solution/public/timelines/components/modal/index.styles.tsx index 95bea7d742ca1..08b3c85e2df9a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/pane.styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/index.styles.tsx @@ -19,6 +19,7 @@ import { export const usePaneStyles = () => { const EuiTheme = useEuiTheme(); const { euiTheme } = EuiTheme; + return css` // euiOverlayMask styles position: fixed; @@ -26,9 +27,6 @@ export const usePaneStyles = () => { left: 0; right: 0; bottom: 0; - display: flex; - align-items: center; - justify-content: center; background: ${transparentize(euiTheme.colors.ink, 0.5)}; z-index: ${euiTheme.levels.flyout}; @@ -36,13 +34,12 @@ export const usePaneStyles = () => { animation: ${euiAnimFadeIn} ${euiTheme.animation.fast} ease-in; } - &.timeline-wrapper--hidden { + &.timeline-portal-overlay-mask--hidden { display: none; } - .timeline-flyout { + .timeline-container { min-width: 150px; - height: inherit; position: fixed; top: var(--euiFixedHeadersOffset, 0); right: 0; @@ -53,15 +50,9 @@ export const usePaneStyles = () => { animation: ${euiAnimSlideInUp(euiTheme.size.xxl)} ${euiTheme.animation.normal} cubic-bezier(0.39, 0.575, 0.565, 1); } - - .timeline-body { - height: 100%; - display: flex; - flex-direction: column; - } } - &:not(.timeline-wrapper--full-screen) .timeline-flyout { + &:not(.timeline-portal-overlay-mask--full-screen) .timeline-container { margin: ${euiTheme.size.m}; border-radius: ${euiTheme.border.radius.medium}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/index.test.tsx new file mode 100644 index 0000000000000..fdcdc5a501ae9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/index.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; + +import { TestProviders } from '../../../common/mock'; +import { TimelineId } from '../../../../common/types/timeline'; +import { TimelineModal } from '.'; + +jest.mock('../timeline', () => ({ + StatefulTimeline: () =>
, +})); + +const mockIsFullScreen = jest.fn(() => false); +jest.mock('../../../common/store/selectors', () => ({ + inputsSelectors: { timelineFullScreenSelector: () => mockIsFullScreen() }, +})); + +const renderTimelineModal = () => + render( + + + + ); + +describe('TimelineModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the timeline', async () => { + const { getByTestId } = renderTimelineModal(); + + expect(getByTestId('StatefulTimelineMock')).toBeInTheDocument(); + }); + + it('should render without fullscreen className', async () => { + mockIsFullScreen.mockReturnValue(false); + + const { getByTestId } = renderTimelineModal(); + + expect(getByTestId('timeline-portal-overlay-mask')).not.toHaveClass( + 'timeline-portal-overlay-mask--full-screen' + ); + }); + + it('should render with fullscreen className', async () => { + mockIsFullScreen.mockReturnValue(true); + + const { getByTestId } = renderTimelineModal(); + + expect(getByTestId('timeline-portal-overlay-mask')).toHaveClass( + 'timeline-portal-overlay-mask--full-screen' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/index.tsx new file mode 100644 index 0000000000000..feb5cf74494a3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/index.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import classNames from 'classnames'; +import { StatefulTimeline } from '../timeline'; +import type { TimelineId } from '../../../../common/types/timeline'; +import { defaultRowRenderers } from '../timeline/body/renderers'; +import { DefaultCellRenderer } from '../timeline/cell_rendering/default_cell_renderer'; +import { CustomEuiPortal } from './custom_portal'; +import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { inputsSelectors } from '../../../common/store/selectors'; +import { usePaneStyles, OverflowHiddenGlobalStyles } from './index.styles'; + +const TIMELINE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.timeline.modal.timelinePropertiesAriaLabel', + { + defaultMessage: 'Timeline Properties', + } +); + +interface TimelineModalProps { + /** + * Id of the timeline to be displayed within the modal + */ + timelineId: TimelineId; + /** + * If true the timeline modal will be visible + */ + visible?: boolean; +} + +/** + * Renders the timeline modal. Internally this is using an EuiPortal. + */ +export const TimelineModal = React.memo(({ timelineId, visible = true }) => { + const ref = useRef(null); + const isFullScreen = useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false; + + const styles = usePaneStyles(); + const wrapperClassName = classNames('timeline-portal-overlay-mask', styles, { + 'timeline-portal-overlay-mask--full-screen': isFullScreen, + 'timeline-portal-overlay-mask--hidden': !visible, + }); + + const sibling: HTMLDivElement | null = useMemo(() => (!visible ? ref?.current : null), [visible]); + + return ( +
+ +
+
+ +
+
+
+ {visible && } +
+ ); +}); + +TimelineModal.displayName = 'TimelineModal'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/modal/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts rename to x-pack/plugins/security_solution/public/timelines/components/modal/translations.ts diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 2ae297019e9bb..c40b54d8cd3b9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -18,7 +18,7 @@ import { timelineDefaults } from '../../store/defaults'; import { defaultHeaders } from './body/column_headers/default_headers'; import type { CellValueElementProps } from './cell_rendering'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { FlyoutHeaderPanel } from '../flyout/header'; +import { TimelineModalHeader } from '../modal/header'; import type { TimelineId, RowRenderer, TimelineTabs } from '../../../../common/types/timeline'; import { TimelineType } from '../../../../common/api/timeline'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; @@ -39,6 +39,12 @@ const TimelineTemplateBadge = styled.div` font-size: 0.8em; `; +const TimelineBody = styled.div` + height: 100%; + display: flex; + flex-direction: column; +`; + export const TimelineContext = createContext<{ timelineId: string | null }>({ timelineId: null }); export interface Props { renderCellValue: (props: CellValueElementProps) => React.ReactNode; @@ -216,7 +222,7 @@ const StatefulTimelineComponent: React.FC = ({ ref={containerElement} > -
+ {timelineType === TimelineType.template && ( {i18n.TIMELINE_TEMPLATE} @@ -227,7 +233,7 @@ const StatefulTimelineComponent: React.FC = ({ $isVisible={!timelineFullScreen} data-test-subj="timeline-hide-show-container" > - + = ({ timelineDescription={description} timelineFullScreen={timelineFullScreen} /> -
+ {showTimelineTour ? ( timelineById[timelineId] ); +/** + * Selector that returns the timeline dataProviders. + */ +const selectTimelineDataProviders = createSelector( + selectTimelineById, + (timeline) => timeline?.dataProviders +); + /** * Selector that returns the timeline saved title. */ @@ -101,9 +109,19 @@ const selectTimelineTitle = createSelector(selectTimelineById, (timeline) => tim /** * Selector that returns the timeline type. */ -const selectTimelineTimelineType = createSelector( +const selectTimelineType = createSelector(selectTimelineById, (timeline) => timeline?.timelineType); + +/** + * Selector that returns the timeline kqlQuery. + */ +const selectTimelineKqlQuery = createSelector(selectTimelineById, (timeline) => timeline?.kqlQuery); + +/** + * Selector that returns the kqlQuery.filterQuery.kuery.expression of a timeline. + */ +export const selectKqlFilterQueryExpression = createSelector( selectTimelineById, - (timeline) => timeline?.timelineType + (timeline) => timeline?.kqlQuery?.filterQuery?.kuery?.expression ); /** @@ -114,7 +132,7 @@ const selectTimelineTimelineType = createSelector( */ export const selectTitleByTimelineById = createSelector( selectTimelineTitle, - selectTimelineTimelineType, + selectTimelineType, (savedTitle, timelineType): string => { if (!isEmpty(savedTitle)) { return savedTitle; @@ -125,3 +143,30 @@ export const selectTitleByTimelineById = createSelector( return UNTITLED_TIMELINE; } ); + +/** + * Selector that returns the timeline query in a {@link Query} format. + */ +export const selectKqlQuery = createSelector( + selectTimelineDataProviders, + selectKqlFilterQueryExpression, + selectTimelineType, + (dataProviders, kqlFilterQueryExpression, timelineType): Query => { + const kqlQueryExpression = + isEmpty(dataProviders) && isEmpty(kqlFilterQueryExpression) && timelineType === 'template' + ? ' ' + : kqlFilterQueryExpression ?? ''; + return { query: kqlQueryExpression, language: 'kuery' }; + } +); + +/** + * Selector that returns true if the timeline has data providers or a kqlQuery filterQuery expression. + */ +export const selectDataInTimeline = createSelector( + selectTimelineDataProviders, + selectTimelineKqlQuery, + (dataProviders, kqlQuery): boolean => { + return !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)); + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/wrapper/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/wrapper/index.test.tsx index 7eac49f7e066d..7bf16595decdd 100644 --- a/x-pack/plugins/security_solution/public/timelines/wrapper/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/wrapper/index.test.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '../../common/mock/react_beautiful_dnd'; - import { TestProviders } from '../../common/mock'; import { TimelineId } from '../../../common/types/timeline'; import * as timelineActions from '../store/actions'; @@ -45,7 +44,7 @@ describe('TimelineWrapper', () => { ); - expect(getByTestId('flyout-pane')).toBeInTheDocument(); + expect(getByTestId('timeline-portal-ref')).toBeInTheDocument(); expect(getByTestId('timeline-bottom-bar')).toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/timelines/wrapper/index.tsx b/x-pack/plugins/security_solution/public/timelines/wrapper/index.tsx index 7b1c0743dbce6..0a6e959967405 100644 --- a/x-pack/plugins/security_solution/public/timelines/wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/wrapper/index.tsx @@ -9,10 +9,10 @@ import { EuiFocusTrap, EuiWindowEvent, keys } from '@elastic/eui'; import React, { useMemo, useCallback } from 'react'; import type { AppLeaveHandler } from '@kbn/core/public'; import { useDispatch } from 'react-redux'; +import { TimelineModal } from '../components/modal'; import type { TimelineId } from '../../../common/types'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { TimelineBottomBar } from '../components/bottom_bar'; -import { Pane } from '../components/flyout/pane'; import { getTimelineShowStatusByIdSelector } from '../store/selectors'; import { useTimelineSavePrompt } from '../../common/hooks/timeline/use_timeline_save_prompt'; import { timelineActions } from '../store'; @@ -58,7 +58,7 @@ export const TimelineWrapper: React.FC = React.memo( return ( <> - + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index 024f1b123ff99..d81fe7d020282 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -80,8 +80,14 @@ import { ALERT_RULE_THREAT, ALERT_RULE_EXCEPTIONS_LIST, ALERT_RULE_IMMUTABLE, + LEGACY_ALERT_HOST_CRITICALITY, + LEGACY_ALERT_USER_CRITICALITY, ALERT_HOST_CRITICALITY, ALERT_USER_CRITICALITY, + ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL, + ALERT_HOST_RISK_SCORE_CALCULATED_SCORE_NORM, + ALERT_USER_RISK_SCORE_CALCULATED_LEVEL, + ALERT_USER_RISK_SCORE_CALCULATED_SCORE_NORM, } from '../../../../../../common/field_maps/field_names'; import type { CompleteRule, RuleParams } from '../../../rule_schema'; import { commonParamsCamelToSnake, typeSpecificCamelToSnake } from '../../../rule_management'; @@ -259,8 +265,14 @@ export const buildAlert = ( 'kibana.alert.rule.severity': params.severity, 'kibana.alert.rule.building_block_type': params.buildingBlockType, // asset criticality fields will be enriched before ingestion + [LEGACY_ALERT_HOST_CRITICALITY]: undefined, + [LEGACY_ALERT_USER_CRITICALITY]: undefined, [ALERT_HOST_CRITICALITY]: undefined, [ALERT_USER_CRITICALITY]: undefined, + [ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL]: undefined, + [ALERT_HOST_RISK_SCORE_CALCULATED_SCORE_NORM]: undefined, + [ALERT_USER_RISK_SCORE_CALCULATED_LEVEL]: undefined, + [ALERT_USER_RISK_SCORE_CALCULATED_SCORE_NORM]: undefined, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/__mocks__/alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/__mocks__/alerts.ts index efbf39d815aea..218ba29bfdc45 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/__mocks__/alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/__mocks__/alerts.ts @@ -70,6 +70,12 @@ import { ALERT_RULE_TIMESTAMP_OVERRIDE, ALERT_HOST_CRITICALITY, ALERT_USER_CRITICALITY, + LEGACY_ALERT_HOST_CRITICALITY, + LEGACY_ALERT_USER_CRITICALITY, + ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL, + ALERT_HOST_RISK_SCORE_CALCULATED_SCORE_NORM, + ALERT_USER_RISK_SCORE_CALCULATED_LEVEL, + ALERT_USER_RISK_SCORE_CALCULATED_SCORE_NORM, } from '../../../../../../../common/field_maps/field_names'; export const createAlert = ( @@ -196,8 +202,14 @@ export const createAlert = ( rule_name_override: undefined, timestamp_override: undefined, }, + [LEGACY_ALERT_HOST_CRITICALITY]: undefined, + [LEGACY_ALERT_USER_CRITICALITY]: undefined, [ALERT_HOST_CRITICALITY]: undefined, [ALERT_USER_CRITICALITY]: undefined, + [ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL]: undefined, + [ALERT_HOST_RISK_SCORE_CALCULATED_SCORE_NORM]: undefined, + [ALERT_USER_RISK_SCORE_CALCULATED_LEVEL]: undefined, + [ALERT_USER_RISK_SCORE_CALCULATED_SCORE_NORM]: undefined, ...data, }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/host_risk.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/host_risk.ts index 1b34f6cb87859..7599cf28e9183 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/host_risk.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/host_risk.ts @@ -6,7 +6,10 @@ */ import { set } from '@kbn/safer-lodash-set'; import { cloneDeep } from 'lodash'; - +import { + ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL, + ALERT_HOST_RISK_SCORE_CALCULATED_SCORE_NORM, +} from '../../../../../../../common/field_maps/field_names'; import { getHostRiskIndex } from '../../../../../../../common/search_strategy/security_solution/risk_score/common'; import { RiskScoreFields } from '../../../../../../../common/search_strategy/security_solution/risk_score/all'; import { createSingleFieldMatchEnrichment } from '../create_single_field_match_enrichment'; @@ -43,10 +46,10 @@ export const createHostRiskEnrichments: CreateRiskEnrichment = async ({ } const newEvent = cloneDeep(event); if (riskLevel) { - set(newEvent, '_source.host.risk.calculated_level', riskLevel); + set(newEvent, `_source.${ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL}`, riskLevel); } if (riskScore) { - set(newEvent, '_source.host.risk.calculated_score_norm', riskScore); + set(newEvent, `_source.${ALERT_HOST_RISK_SCORE_CALCULATED_SCORE_NORM}`, riskScore); } return newEvent; }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/user_risk.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/user_risk.ts index 27ae894f28134..ad4ca20b140ac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/user_risk.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/enrichment_by_type/user_risk.ts @@ -6,6 +6,10 @@ */ import { set } from '@kbn/safer-lodash-set'; import { cloneDeep } from 'lodash'; +import { + ALERT_USER_RISK_SCORE_CALCULATED_LEVEL, + ALERT_USER_RISK_SCORE_CALCULATED_SCORE_NORM, +} from '../../../../../../../common/field_maps/field_names'; import { getUserRiskIndex } from '../../../../../../../common/search_strategy/security_solution/risk_score/common'; import { RiskScoreFields } from '../../../../../../../common/search_strategy/security_solution/risk_score/all'; import { createSingleFieldMatchEnrichment } from '../create_single_field_match_enrichment'; @@ -42,10 +46,10 @@ export const createUserRiskEnrichments: CreateRiskEnrichment = async ({ } const newEvent = cloneDeep(event); if (riskLevel) { - set(newEvent, '_source.user.risk.calculated_level', riskLevel); + set(newEvent, `_source.${ALERT_USER_RISK_SCORE_CALCULATED_LEVEL}`, riskLevel); } if (riskScore) { - set(newEvent, '_source.user.risk.calculated_score_norm', riskScore); + set(newEvent, `_source.${ALERT_USER_RISK_SCORE_CALCULATED_SCORE_NORM}`, riskScore); } return newEvent; }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.test.ts index 4c87c6f5a8272..ac3368e9fa7fb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/enrichments/index.test.ts @@ -211,8 +211,8 @@ describe('enrichEvents', () => { ...createEntity('user', 'user name 1'), ...createEntity('host', 'host name 1'), - 'kibana.alert.host.criticality_level': 'low', - 'kibana.alert.user.criticality_level': 'important', + 'host.asset.criticality': 'low', + 'user.asset.criticality': 'important', }), createAlert('2', { ...createEntity('host', 'user name 1'), diff --git a/x-pack/plugins/threat_intelligence/cypress/screens/timeline.ts b/x-pack/plugins/threat_intelligence/cypress/screens/timeline.ts index e9e960db9858b..6861c63496d7d 100644 --- a/x-pack/plugins/threat_intelligence/cypress/screens/timeline.ts +++ b/x-pack/plugins/threat_intelligence/cypress/screens/timeline.ts @@ -22,7 +22,7 @@ export const INDICATORS_TABLE_CELL_TIMELINE_BUTTON = `[data-test-subj="${CELL_TI export const TIMELINE_DATA_PROVIDERS_WRAPPER = `[data-test-subj="dataProviders"]`; export const TIMELINE_DRAGGABLE_ITEM = `[data-test-subj="providerContainer"]`; export const TIMELINE_AND_OR_BADGE = `[data-test-subj="and-or-badge"]`; -export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; +export const CLOSE_TIMELINE_BTN = '[data-test-subj="timeline-modal-header-close-button"]'; export const FLYOUT_OVERVIEW_TAB_TABLE_ROW_TIMELINE_BUTTON = `[data-test-subj="${INDICATORS_FLYOUT_OVERVIEW_TABLE}${VALUE_ACTION_TIMELINE_BUTTON_TEST_ID}"]`; export const FLYOUT_OVERVIEW_TAB_BLOCKS_TIMELINE_BUTTON = `[data-test-subj="${INDICATORS_FLYOUT_OVERVIEW_HIGH_LEVEL_BLOCKS}${VALUE_ACTION_TIMELINE_BUTTON_TEST_ID}"]`; export const FLYOUT_INVESTIGATE_IN_TIMELINE_ITEM = `[data-test-subj="${INDICATOR_FLYOUT_TAKE_ACTION_INVESTIGATE_IN_TIMELINE_TEST_ID}"]`; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 63b76a5cb04ac..3d784e40249b6 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -35991,7 +35991,6 @@ "xpack.securitySolution.timeline.file.fromOriginalPathDescription": "depuis son chemin d'origine", "xpack.securitySolution.timeline.flyout.header.closeTimelineButtonLabel": "Fermer {isTimeline, select, true {la chronologie} false {le modèle}}", "xpack.securitySolution.timeline.flyout.pane.removeColumnButtonLabel": "Supprimer la colonne", - "xpack.securitySolution.timeline.flyout.pane.timelinePropertiesAriaLabel": "Propriétés de la chronologie", "xpack.securitySolution.timeline.flyoutTimelineTemplateLabel": "Modèle de chronologie", "xpack.securitySolution.timeline.fullScreenButton": "Plein écran", "xpack.securitySolution.timeline.graphOverlay.closeAnalyzerButton": "Fermer l'analyseur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5ac0b945b1173..4dcd88dd9f4fe 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -35991,7 +35991,6 @@ "xpack.securitySolution.timeline.file.fromOriginalPathDescription": "元のパスから", "xpack.securitySolution.timeline.flyout.header.closeTimelineButtonLabel": "{isTimeline, select, true {タイムライン} false {テンプレート}}を閉じる", "xpack.securitySolution.timeline.flyout.pane.removeColumnButtonLabel": "列を削除", - "xpack.securitySolution.timeline.flyout.pane.timelinePropertiesAriaLabel": "タイムラインのプロパティ", "xpack.securitySolution.timeline.flyoutTimelineTemplateLabel": "タイムラインテンプレート", "xpack.securitySolution.timeline.fullScreenButton": "全画面", "xpack.securitySolution.timeline.graphOverlay.closeAnalyzerButton": "アナライザーを閉じる", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index eeb98e4c894a1..65228022a6acf 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -35973,7 +35973,6 @@ "xpack.securitySolution.timeline.file.fromOriginalPathDescription": "从其原始路径", "xpack.securitySolution.timeline.flyout.header.closeTimelineButtonLabel": "关闭{isTimeline, select, true {时间线} false {模板}}", "xpack.securitySolution.timeline.flyout.pane.removeColumnButtonLabel": "移除列", - "xpack.securitySolution.timeline.flyout.pane.timelinePropertiesAriaLabel": "时间线属性", "xpack.securitySolution.timeline.flyoutTimelineTemplateLabel": "时间线模板", "xpack.securitySolution.timeline.fullScreenButton": "全屏", "xpack.securitySolution.timeline.graphOverlay.closeAnalyzerButton": "关闭分析器", diff --git a/x-pack/test/accessibility/apps/group2/lens.ts b/x-pack/test/accessibility/apps/group2/lens.ts index 860138fc77701..e4ef53efc3548 100644 --- a/x-pack/test/accessibility/apps/group2/lens.ts +++ b/x-pack/test/accessibility/apps/group2/lens.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -14,6 +15,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const listingTable = getService('listingTable'); const kibanaServer = getService('kibanaServer'); + const find = getService('find'); + + const hasFocus = async (testSubject: string) => { + const targetElement = await testSubjects.find(testSubject); + const activeElement = await find.activeElement(); + return (await targetElement._webElement.getId()) === (await activeElement._webElement.getId()); + }; describe('Lens Accessibility', () => { const lensChartName = 'MyLensChart'; @@ -175,5 +183,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await listingTable.clickDeleteSelected(); await PageObjects.common.clickConfirmOnModal(); }); + + describe('focus behavior when adding or removing layers', () => { + it('should focus the added layer', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.createLayer(); + expect(await hasFocus('lns-layerPanel-1')).to.be(true); + }); + it('should focus the remaining layer when the first is removed', async () => { + await PageObjects.lens.removeLayer(0); + expect(await hasFocus('lns-layerPanel-0')).to.be(true); + await PageObjects.lens.createLayer(); + await PageObjects.lens.removeLayer(1); + expect(await hasFocus('lns-layerPanel-0')).to.be(true); + }); + it('should focus the only layer when resetting the layer', async () => { + await PageObjects.lens.removeLayer(); + expect(await hasFocus('lns-layerPanel-0')).to.be(true); + }); + }); }); } diff --git a/x-pack/test/functional/apps/aiops/index.ts b/x-pack/test/functional/apps/aiops/index.ts index 0326a2f80b082..8706d3d242c6b 100644 --- a/x-pack/test/functional/apps/aiops/index.ts +++ b/x-pack/test/functional/apps/aiops/index.ts @@ -30,6 +30,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); loadTestFile(require.resolve('./log_rate_analysis')); + loadTestFile(require.resolve('./log_rate_analysis_anomaly_table')); loadTestFile(require.resolve('./change_point_detection')); loadTestFile(require.resolve('./log_pattern_analysis')); loadTestFile(require.resolve('./log_pattern_analysis_in_discover')); diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis.ts index 4bb1393b5c910..45fef76fa7170 100644 --- a/x-pack/test/functional/apps/aiops/log_rate_analysis.ts +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis.ts @@ -18,6 +18,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const browser = getService('browser'); const elasticChart = getService('elasticChart'); const aiops = getService('aiops'); + const retry = getService('retry'); // AIOps / Log Rate Analysis lives in the ML UI so we need some related services. const ml = getService('ml'); @@ -165,45 +166,52 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // The group switch should be disabled by default await aiops.logRateAnalysisPage.assertLogRateAnalysisResultsGroupSwitchExists(false); - if (!isTestDataExpectedWithSampleProbability(testData.expected)) { - // Enabled grouping - await aiops.logRateAnalysisPage.clickLogRateAnalysisResultsGroupSwitchOn(); + await retry.tryForTime(30 * 1000, async () => { + if (!isTestDataExpectedWithSampleProbability(testData.expected)) { + // Enabled grouping + await aiops.logRateAnalysisPage.clickLogRateAnalysisResultsGroupSwitchOn(); - await aiops.logRateAnalysisResultsGroupsTable.assertLogRateAnalysisResultsTableExists(); + await aiops.logRateAnalysisResultsGroupsTable.assertLogRateAnalysisResultsTableExists(); + await aiops.logRateAnalysisResultsGroupsTable.scrollAnalysisTableIntoView(); - const analysisGroupsTable = - await aiops.logRateAnalysisResultsGroupsTable.parseAnalysisTable(); + const analysisGroupsTable = + await aiops.logRateAnalysisResultsGroupsTable.parseAnalysisTable(); - const actualAnalysisGroupsTable = orderBy(analysisGroupsTable, 'group'); - const expectedAnalysisGroupsTable = orderBy(testData.expected.analysisGroupsTable, 'group'); + const actualAnalysisGroupsTable = orderBy(analysisGroupsTable, 'group'); + const expectedAnalysisGroupsTable = orderBy( + testData.expected.analysisGroupsTable, + 'group' + ); - expect(actualAnalysisGroupsTable).to.be.eql( - expectedAnalysisGroupsTable, - `Expected analysis groups table to be ${JSON.stringify( - expectedAnalysisGroupsTable - )}, got ${JSON.stringify(actualAnalysisGroupsTable)}` - ); + expect(actualAnalysisGroupsTable).to.be.eql( + expectedAnalysisGroupsTable, + `Expected analysis groups table to be ${JSON.stringify( + expectedAnalysisGroupsTable + )}, got ${JSON.stringify(actualAnalysisGroupsTable)}` + ); + } + }); + if (!isTestDataExpectedWithSampleProbability(testData.expected)) { await ml.testExecution.logTestStep('expand table row'); await aiops.logRateAnalysisResultsGroupsTable.assertExpandRowButtonExists(); await aiops.logRateAnalysisResultsGroupsTable.expandRow(); + await aiops.logRateAnalysisResultsGroupsTable.scrollAnalysisTableIntoView(); - if (!isTestDataExpectedWithSampleProbability(testData.expected)) { - const analysisTable = await aiops.logRateAnalysisResultsTable.parseAnalysisTable(); - - const actualAnalysisTable = orderBy(analysisTable, ['fieldName', 'fieldValue']); - const expectedAnalysisTable = orderBy(testData.expected.analysisTable, [ - 'fieldName', - 'fieldValue', - ]); - - expect(actualAnalysisTable).to.be.eql( - expectedAnalysisTable, - `Expected analysis table results to be ${JSON.stringify( - expectedAnalysisTable - )}, got ${JSON.stringify(actualAnalysisTable)}` - ); - } + const analysisTable = await aiops.logRateAnalysisResultsTable.parseAnalysisTable(); + + const actualAnalysisTable = orderBy(analysisTable, ['fieldName', 'fieldValue']); + const expectedAnalysisTable = orderBy(testData.expected.analysisTable, [ + 'fieldName', + 'fieldValue', + ]); + + expect(actualAnalysisTable).to.be.eql( + expectedAnalysisTable, + `Expected analysis table results to be ${JSON.stringify( + expectedAnalysisTable + )}, got ${JSON.stringify(actualAnalysisTable)}` + ); await ml.testExecution.logTestStep('open the field filter'); await aiops.logRateAnalysisPage.assertFieldFilterPopoverButtonExists(false); @@ -226,23 +234,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await ml.testExecution.logTestStep('regroup results'); await aiops.logRateAnalysisPage.clickFieldFilterApplyButton(); - if (!isTestDataExpectedWithSampleProbability(testData.expected)) { - const filteredAnalysisGroupsTable = - await aiops.logRateAnalysisResultsGroupsTable.parseAnalysisTable(); - - const actualFilteredAnalysisGroupsTable = orderBy(filteredAnalysisGroupsTable, 'group'); - const expectedFilteredAnalysisGroupsTable = orderBy( - testData.expected.filteredAnalysisGroupsTable, - 'group' - ); - - expect(actualFilteredAnalysisGroupsTable).to.be.eql( - expectedFilteredAnalysisGroupsTable, - `Expected filtered analysis groups table to be ${JSON.stringify( - expectedFilteredAnalysisGroupsTable - )}, got ${JSON.stringify(actualFilteredAnalysisGroupsTable)}` - ); - } + const filteredAnalysisGroupsTable = + await aiops.logRateAnalysisResultsGroupsTable.parseAnalysisTable(); + + const actualFilteredAnalysisGroupsTable = orderBy(filteredAnalysisGroupsTable, 'group'); + const expectedFilteredAnalysisGroupsTable = orderBy( + testData.expected.filteredAnalysisGroupsTable, + 'group' + ); + + expect(actualFilteredAnalysisGroupsTable).to.be.eql( + expectedFilteredAnalysisGroupsTable, + `Expected filtered analysis groups table to be ${JSON.stringify( + expectedFilteredAnalysisGroupsTable + )}, got ${JSON.stringify(actualFilteredAnalysisGroupsTable)}` + ); } if (testData.action !== undefined) { @@ -268,41 +274,23 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); } - // Failing: See https://github.com/elastic/kibana/issues/172606 - describe.skip('log rate analysis', async function () { + describe('log rate analysis', async function () { for (const testData of logRateAnalysisTestData) { describe(`with '${testData.sourceIndexOrSavedSearch}'`, function () { before(async () => { await aiops.logRateAnalysisDataGenerator.generateData(testData.dataGenerator); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.securityUI.loginAsMlPowerUser(); await ml.testResources.createDataViewIfNeeded( testData.sourceIndexOrSavedSearch, '@timestamp' ); - - await ml.testResources.setKibanaTimeZoneToUTC(); - - if (testData.dataGenerator === 'kibana_sample_data_logs') { - await PageObjects.security.login('elastic', 'changeme', { - expectSuccess: true, - }); - - await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { - useActualUrl: true, - }); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.home.addSampleDataSet('logs'); - await PageObjects.header.waitUntilLoadingHasFinished(); - } else { - await ml.securityUI.loginAsMlPowerUser(); - } }); after(async () => { await elasticChart.setNewChartUiDebugFlag(false); - if (testData.dataGenerator !== 'kibana_sample_data_logs') { - await ml.testResources.deleteDataViewByTitle(testData.sourceIndexOrSavedSearch); - } await aiops.logRateAnalysisDataGenerator.removeGeneratedData(testData.dataGenerator); }); diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/kibana_logs_data_view_test_data.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/kibana_logs_data_view_test_data.ts index 2d85fe1e64210..a22015a623559 100644 --- a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/kibana_logs_data_view_test_data.ts +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/kibana_logs_data_view_test_data.ts @@ -14,7 +14,7 @@ export const kibanaLogsDataViewTestData: TestData = { analysisType: LOG_RATE_ANALYSIS_TYPE.SPIKE, dataGenerator: 'kibana_sample_data_logs', isSavedSearch: false, - sourceIndexOrSavedSearch: 'kibana_sample_data_logs', + sourceIndexOrSavedSearch: 'kibana_sample_data_logstsdb', brushIntervalFactor: 1, chartClickCoordinates: [235, 0], fieldSelectorSearch: 'referer', @@ -29,7 +29,7 @@ export const kibanaLogsDataViewTestData: TestData = { }, }, expected: { - totalDocCountFormatted: '14,074', + totalDocCountFormatted: '14,068', analysisGroupsTable: [ { group: diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis_anomaly_table.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis_anomaly_table.ts new file mode 100644 index 0000000000000..c92ecd8b044c3 --- /dev/null +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis_anomaly_table.ts @@ -0,0 +1,287 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; + +import type { LogRateAnalysisType } from '@kbn/aiops-utils'; +import type { Datafeed, Job } from '@kbn/ml-plugin/server/shared'; + +import { isDefaultSearchQuery } from '@kbn/aiops-plugin/public/application/url_state/common'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; + +import type { LogRateAnalysisDataGenerator } from '../../services/aiops/log_rate_analysis_data_generator'; + +function getJobWithDataFeed( + detectorFunction: string, + detectorField?: string, + partitionFieldName?: string, + query: QueryDslQueryContainer = { match_all: {} } +) { + const postFix = `${detectorFunction}${detectorField ? `_${detectorField}` : ''}${ + partitionFieldName ? `_${partitionFieldName}` : '' + }${!isDefaultSearchQuery(query) ? '_with_query' : ''}`; + const jobId = `fq_lra_${postFix}`; + + // @ts-expect-error not full interface + const jobConfig: Job = { + job_id: jobId, + description: `${detectorFunction}(${ + detectorField ? detectorField : '' + }) on farequote dataset with 15m bucket span`, + groups: ['farequote', 'automated', partitionFieldName ? 'multi-metric' : 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: detectorFunction, + ...(detectorField ? { field_name: detectorField } : {}), + ...(partitionFieldName ? { partition_field_name: partitionFieldName } : {}), + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + model_plot_config: { enabled: true }, + }; + + // @ts-expect-error not full interface + const datafeedConfig: Datafeed = { + datafeed_id: `datafeed-fq_lra_${postFix}`, + indices: ['ft_farequote'], + job_id: jobId, + query, + }; + + return { jobConfig, datafeedConfig }; +} + +interface TestData { + jobConfig: Job; + datafeedConfig: Datafeed; + analysisType: LogRateAnalysisType; + dataGenerator: LogRateAnalysisDataGenerator; + entitySelectionField?: string; + entitySelectionValue?: string; + expected: { + anomalyTableLogRateAnalysisButtonAvailable: boolean; + totalDocCount?: number; + analysisResults?: Array<{ + fieldName: string; + fieldValue: string; + impact: string; + logRate: string; + pValue: string; + }>; + }; +} + +const testData: TestData[] = [ + // Single metric job, should find AAL with log rate analysis + { + ...getJobWithDataFeed('count'), + analysisType: 'spike', + dataGenerator: 'farequote_with_spike', + expected: { + anomalyTableLogRateAnalysisButtonAvailable: true, + totalDocCount: 7869, + analysisResults: [ + { + fieldName: 'airline', + fieldValue: 'AAL', + impact: 'High', + logRate: 'Chart type:bar chart', + pValue: '8.96e-49', + }, + ], + }, + }, + // Multi metric job, should filter by AAL, no significant results + { + ...getJobWithDataFeed('high_count', undefined, 'airline'), + analysisType: 'spike', + dataGenerator: 'farequote_with_spike', + entitySelectionField: 'airline', + entitySelectionValue: 'AAL', + expected: { + anomalyTableLogRateAnalysisButtonAvailable: true, + totalDocCount: 910, + }, + }, + // Single metric job with datafeed query filter, no significant results + { + ...getJobWithDataFeed('count', undefined, undefined, { + bool: { + must: [ + { + term: { + airline: { + value: 'AAL', + }, + }, + }, + ], + }, + }), + analysisType: 'spike', + dataGenerator: 'farequote_with_spike', + expected: { + anomalyTableLogRateAnalysisButtonAvailable: true, + totalDocCount: 910, + }, + }, + // Single metric job with non-count detector, link should not be available + { + ...getJobWithDataFeed('mean', 'responsetime'), + analysisType: 'spike', + dataGenerator: 'farequote_with_spike', + expected: { + anomalyTableLogRateAnalysisButtonAvailable: false, + }, + }, +]; + +export default function ({ getService }: FtrProviderContext) { + const aiops = getService('aiops'); + const browser = getService('browser'); + const comboBox = getService('comboBox'); + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + const ml = getService('ml'); + + describe('anomaly table with link to log rate analysis', async function () { + this.tags(['ml']); + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.createDataViewIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const page of ['anomaly explorer', 'single metric viewer']) { + for (const td of testData) { + const { + jobConfig, + datafeedConfig, + analysisType, + dataGenerator, + entitySelectionField, + entitySelectionValue, + expected, + } = td; + describe(`via ${page} for job ${jobConfig.job_id}`, async function () { + before(async () => { + await ml.api.createAndRunAnomalyDetectionLookbackJob(jobConfig, datafeedConfig); + + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.testResources.deleteDataViewByTitle('ft_farequote'); + }); + + it('should navigate to ML job management', async () => { + await ml.testExecution.logTestStep('navigate to job list'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + }); + + it(`should load the ${page} for job '${jobConfig.job_id}`, async () => { + await ml.testExecution.logTestStep('open job in single metric viewer'); + await ml.jobTable.filterWithSearchString(jobConfig.job_id, 1); + + if (page === 'single metric viewer') { + await ml.jobTable.clickOpenJobInSingleMetricViewerButton(jobConfig.job_id); + } else { + await ml.jobTable.clickOpenJobInAnomalyExplorerButton(jobConfig.job_id); + } + + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + + if (page === 'single metric viewer' && entitySelectionField && entitySelectionValue) { + await testSubjects.existOrFail( + `mlSingleMetricViewerEntitySelection ${entitySelectionField}` + ); + await comboBox.set( + `mlSingleMetricViewerEntitySelection ${entitySelectionField} > comboBoxInput`, + entitySelectionValue + ); + } + }); + + it(`should show the anomaly table`, async () => { + await ml.testExecution.logTestStep('displays the anomalies table'); + await ml.anomaliesTable.assertTableExists(); + + await ml.testExecution.logTestStep('anomalies table is not empty'); + await ml.anomaliesTable.assertTableNotEmpty(); + }); + + if (expected.anomalyTableLogRateAnalysisButtonAvailable) { + it('should click the log rate analysis action', async () => { + await ml.anomaliesTable.assertAnomalyActionsMenuButtonExists(0); + await ml.anomaliesTable.scrollRowIntoView(0); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonEnabled(0, true); + await ml.anomaliesTable.assertAnomalyActionLogRateAnalysisButtonExists(0); + await ml.anomaliesTable.ensureAnomalyActionLogRateAnalysisButtonClicked(0); + + if (expected.totalDocCount !== undefined) { + await aiops.logPatternAnalysisPage.assertTotalDocumentCount(expected.totalDocCount); + } + }); + + const shouldHaveResults = expected.analysisResults !== undefined; + + it('should complete the analysis', async () => { + await aiops.logRateAnalysisPage.assertAnalysisComplete( + analysisType, + dataGenerator, + !shouldHaveResults + ); + }); + + if (shouldHaveResults) { + it('should show analysis results', async () => { + await aiops.logRateAnalysisResultsTable.assertLogRateAnalysisResultsTableExists(); + const actualAnalysisTable = + await aiops.logRateAnalysisResultsTable.parseAnalysisTable(); + + expect(actualAnalysisTable).to.be.eql( + expected.analysisResults, + `Expected analysis table to be ${JSON.stringify( + expected.analysisResults + )}, got ${JSON.stringify(actualAnalysisTable)}` + ); + }); + } + + it('should navigate back to the anomaly table', async () => { + await browser.goBack(); + + await ml.testExecution.logTestStep('displays the anomalies table'); + await ml.anomaliesTable.assertTableExists(); + + await ml.testExecution.logTestStep('anomalies table is not empty'); + await ml.anomaliesTable.assertTableNotEmpty(); + }); + } else { + it('should not show the log rate analysis action', async () => { + await ml.anomaliesTable.assertAnomalyActionsMenuButtonExists(0); + await ml.anomaliesTable.scrollRowIntoView(0); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonEnabled(0, true); + await ml.anomaliesTable.assertAnomalyActionLogRateAnalysisButtonNotExists(0); + }); + } + }); + } + } + }); +} diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index 8f5fe5dc9bc11..c95249e927084 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -45,7 +45,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.unsetTime(); }); - describe('Customize time range', () => { + // FLAKY: https://github.com/elastic/kibana/issues/104578 + describe.skip('Customize time range', () => { it('should be possible to customize time range for saved searches on dashboards', async () => { await PageObjects.dashboard.navigateToApp(); await PageObjects.dashboard.clickNewDashboard(); diff --git a/x-pack/test/functional/services/aiops/log_rate_analysis_data_generator.ts b/x-pack/test/functional/services/aiops/log_rate_analysis_data_generator.ts index 7e1ead80b4ff8..824e296d448d3 100644 --- a/x-pack/test/functional/services/aiops/log_rate_analysis_data_generator.ts +++ b/x-pack/test/functional/services/aiops/log_rate_analysis_data_generator.ts @@ -46,7 +46,9 @@ const BASELINE_TS = DEVIATION_TS - DAY_MS * 1; function getTextFieldMessage(timestamp: number, user: string, url: string, responseCode: string) { const date = new Date(timestamp); - return `${user} [${date.toLocaleString('en-US')}] "GET /${url} HTTP/1.1" ${responseCode}`; + return `${user} [${date.toLocaleString('en-US', { + timeZone: 'UTC', + })}] "GET /${url} HTTP/1.1" ${responseCode}`; } function getArtificialLogsWithDeviation( @@ -224,7 +226,9 @@ export function LogRateAnalysisDataGeneratorProvider({ getService }: FtrProvider public async generateData(dataGenerator: LogRateAnalysisDataGenerator) { switch (dataGenerator) { case 'kibana_sample_data_logs': - // will be added via UI + await esArchiver.loadIfNeeded( + 'test/functional/fixtures/es_archiver/kibana_sample_data_logs_tsdb' + ); break; case 'farequote_with_spike': @@ -328,7 +332,9 @@ export function LogRateAnalysisDataGeneratorProvider({ getService }: FtrProvider public async removeGeneratedData(dataGenerator: LogRateAnalysisDataGenerator) { switch (dataGenerator) { case 'kibana_sample_data_logs': - // do not remove + await esArchiver.unload( + 'test/functional/fixtures/es_archiver/kibana_sample_data_logs_tsdb' + ); break; case 'farequote_with_spike': diff --git a/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts b/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts index a8733fb2114a7..e46b751ece54d 100644 --- a/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts +++ b/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts @@ -152,9 +152,8 @@ export function LogRateAnalysisPageProvider({ getService, getPageObject }: FtrPr }, async clickLogRateAnalysisResultsGroupSwitchOn() { - await testSubjects.clickWhenNotDisabledWithoutRetry('aiopsLogRateAnalysisGroupSwitchOn'); - await retry.tryForTime(30 * 1000, async () => { + await testSubjects.clickWhenNotDisabledWithoutRetry('aiopsLogRateAnalysisGroupSwitchOn'); await testSubjects.existOrFail('aiopsLogRateAnalysisGroupSwitch checked'); }); }, @@ -245,7 +244,8 @@ export function LogRateAnalysisPageProvider({ getService, getPageObject }: FtrPr async assertAnalysisComplete( analysisType: LogRateAnalysisType, - dataGenerator: LogRateAnalysisDataGenerator + dataGenerator: LogRateAnalysisDataGenerator, + noResults = false ) { const dataGeneratorParts = dataGenerator.split('_'); const zeroDocsFallback = dataGeneratorParts.includes('zerodocsfallback'); @@ -254,6 +254,11 @@ export function LogRateAnalysisPageProvider({ getService, getPageObject }: FtrPr const currentProgressTitle = await testSubjects.getVisibleText('aiopsAnalysisComplete'); expect(currentProgressTitle).to.be('Analysis complete'); + if (noResults) { + await testSubjects.existOrFail('aiopsNoResultsFoundEmptyPrompt'); + return; + } + await testSubjects.existOrFail('aiopsAnalysisTypeCalloutTitle'); const currentAnalysisTypeCalloutTitle = await testSubjects.getVisibleText( 'aiopsAnalysisTypeCalloutTitle' diff --git a/x-pack/test/functional/services/aiops/log_rate_analysis_results_groups_table.ts b/x-pack/test/functional/services/aiops/log_rate_analysis_results_groups_table.ts index 129778a23d69a..abe734ef6526f 100644 --- a/x-pack/test/functional/services/aiops/log_rate_analysis_results_groups_table.ts +++ b/x-pack/test/functional/services/aiops/log_rate_analysis_results_groups_table.ts @@ -59,6 +59,10 @@ export function LogRateAnalysisResultsGroupsTableProvider({ getService }: FtrPro return rows; } + public async scrollAnalysisTableIntoView() { + await testSubjects.scrollIntoView('aiopsLogRateAnalysisResultsGroupsTable'); + } + public rowSelector(rowId: string, subSelector?: string) { const row = `~aiopsLogRateAnalysisResultsGroupsTable > ~row-${rowId}`; return !subSelector ? row : `${row} > ${subSelector}`; diff --git a/x-pack/test/functional/services/ml/anomalies_table.ts b/x-pack/test/functional/services/ml/anomalies_table.ts index 8e6c652b33d97..52eaf5715f673 100644 --- a/x-pack/test/functional/services/ml/anomalies_table.ts +++ b/x-pack/test/functional/services/ml/anomalies_table.ts @@ -131,6 +131,24 @@ export function MachineLearningAnomaliesTableProvider({ getService }: FtrProvide ); }, + async assertAnomalyActionLogRateAnalysisButtonExists(rowIndex: number) { + await this.ensureAnomalyActionsMenuOpen(rowIndex); + await testSubjects.existOrFail('mlAnomaliesListRowAction_runLogRateAnalysisButton'); + }, + + async assertAnomalyActionLogRateAnalysisButtonNotExists(rowIndex: number) { + await this.ensureAnomalyActionsMenuOpen(rowIndex); + await testSubjects.missingOrFail('mlAnomaliesListRowAction_runLogRateAnalysisButton'); + }, + + async ensureAnomalyActionLogRateAnalysisButtonClicked(rowIndex: number) { + await retry.tryForTime(10 * 1000, async () => { + await this.ensureAnomalyActionsMenuOpen(rowIndex); + await testSubjects.click('mlAnomaliesListRowAction_runLogRateAnalysisButton'); + await testSubjects.existOrFail('aiopsLogRateAnalysisPage'); + }); + }, + /** * Asserts selected number of rows per page on the pagination control. * @param rowsNumber diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts index b6686549b1311..f258f72769fe1 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts @@ -170,8 +170,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const response = JSON.parse(data); - expect(response.message).to.contain( - `an error occurred while running the action - Status code: 400. Message: API Error: Bad Request - This model's maximum context length is 8192 tokens. However, your messages resulted in 11036 tokens. Please reduce the length of the messages.` + expect(response.message).to.be( + `Token limit reached. Token limit is 8192, but the current conversation has 11036 tokens.` ); }); diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/state_synchronization.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/state_synchronization.ts index 9711443fcbf84..5ea77239f0f06 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/state_synchronization.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/state_synchronization.ts @@ -61,7 +61,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await assertAlertsPageState({ kuery: '', // workflowStatus: 'Open', - timeRange: 'Last 2 hours', + timeRange: 'Last 24 hours', }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts index 0eddd53de4c90..77d8b5ffb373d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts @@ -646,7 +646,7 @@ export default ({ getService }: FtrProviderContext) => { const { previewId } = await previewRule({ supertest, rule }); const previewAlerts = await getPreviewAlerts({ es, previewId }); const fullAlert = previewAlerts[0]._source; - expect(fullAlert?.['kibana.alert.host.criticality_level']).to.eql('important'); + expect(fullAlert?.['host.asset.criticality']).to.eql('important'); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts index 643728fbd27c1..c582b9d94f3ce 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts @@ -892,9 +892,7 @@ export default ({ getService }: FtrProviderContext) => { expect(previewAlerts.length).toBe(1); - expect(previewAlerts[0]?._source?.['kibana.alert.host.criticality_level']).toBe( - 'very_important' - ); + expect(previewAlerts[0]?._source?.['host.asset.criticality']).toBe('very_important'); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts index a7d12cc048244..e222f1ddd7cb4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts @@ -289,8 +289,8 @@ export default ({ getService }: FtrProviderContext) => { expect(previewAlerts.length).toBe(1); const fullAlert = previewAlerts[0]._source; - expect(fullAlert?.['kibana.alert.host.criticality_level']).toBe('normal'); - expect(fullAlert?.['kibana.alert.user.criticality_level']).toBe('very_important'); + expect(fullAlert?.['host.asset.criticality']).toBe('normal'); + expect(fullAlert?.['user.asset.criticality']).toBe('very_important'); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/new_terms.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/new_terms.ts index 5d1726e0cc1c7..7cacab1066da4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/new_terms.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/new_terms.ts @@ -1062,8 +1062,8 @@ export default ({ getService }: FtrProviderContext) => { const previewAlerts = await getPreviewAlerts({ es, previewId }); const fullAlert = previewAlerts[0]._source; - expect(fullAlert?.['kibana.alert.host.criticality_level']).to.eql('normal'); - expect(fullAlert?.['kibana.alert.user.criticality_level']).to.eql('very_important'); + expect(fullAlert?.['host.asset.criticality']).to.eql('normal'); + expect(fullAlert?.['user.asset.criticality']).to.eql('very_important'); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/query.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/query.ts index 060dfff2b20bc..ac7aa41223c9e 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/query.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/query.ts @@ -296,12 +296,8 @@ export default ({ getService }: FtrProviderContext) => { }; const { previewId } = await previewRule({ supertest, rule }); const previewAlerts = await getPreviewAlerts({ es, previewId }); - expect(previewAlerts[0]?._source?.['kibana.alert.host.criticality_level']).to.eql( - 'important' - ); - expect(previewAlerts[0]?._source?.['kibana.alert.user.criticality_level']).to.eql( - 'very_important' - ); + expect(previewAlerts[0]?._source?.['host.asset.criticality']).to.eql('important'); + expect(previewAlerts[0]?._source?.['user.asset.criticality']).to.eql('very_important'); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match.ts index 60215e58030dc..e8cbeb2c1b4b3 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threat_match.ts @@ -1663,8 +1663,8 @@ export default ({ getService }: FtrProviderContext) => { return expect(fullAlert).to.be.ok(); } - expect(fullAlert?.['kibana.alert.host.criticality_level']).to.eql('low'); - expect(fullAlert?.['kibana.alert.user.criticality_level']).to.eql('very_important'); + expect(fullAlert?.['host.asset.criticality']).to.eql('low'); + expect(fullAlert?.['user.asset.criticality']).to.eql('very_important'); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threshold.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threshold.ts index 71f25f48345b4..9449750b38465 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threshold.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/threshold.ts @@ -452,7 +452,7 @@ export default ({ getService }: FtrProviderContext) => { const previewAlerts = await getPreviewAlerts({ es, previewId, sort: ['host.name'] }); const fullAlert = previewAlerts[0]?._source; - expect(fullAlert?.['kibana.alert.host.criticality_level']).toEqual('important'); + expect(fullAlert?.['host.asset.criticality']).toEqual('important'); }); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts index 0a2f275263307..2115238da2511 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts @@ -46,7 +46,8 @@ import { waitForTimelinesPanelToBeLoaded } from '../../../tasks/timelines'; import { TIMELINES_URL } from '../../../urls/navigation'; -describe('Timeline Templates', { tags: ['@ess', '@serverless'] }, () => { +// FLAKY: https://github.com/elastic/kibana/issues/175955 +describe.skip('Timeline Templates', { tags: ['@ess', '@serverless'] }, () => { beforeEach(() => { login(); deleteTimelines(); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/security_main.ts b/x-pack/test/security_solution_cypress/cypress/screens/security_main.ts index 6eacc61dd76ad..5836234c4058c 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/security_main.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/security_main.ts @@ -5,8 +5,6 @@ * 2.0. */ -export const CLOSE_TIMELINE_BUTTON = '[data-test-subj="close-timeline"]'; - export const MAIN_PAGE = '[data-test-subj="kibanaChrome"]'; export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="timeline-bottom-bar-title-button"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts index e348f71245662..964cf26ec13e2 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts @@ -26,7 +26,7 @@ export const SELECT_CASE = (id: string) => { return `[data-test-subj="cases-table-row-select-${id}"]`; }; -export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; +export const CLOSE_TIMELINE_BTN = '[data-test-subj="timeline-modal-header-close-button"]'; export const COMBO_BOX_INPUT = '[data-test-subj="comboBoxInput"]'; @@ -177,7 +177,7 @@ export const TIMELINE_FILTER_OPERATOR = '[data-test-subj="filterOperatorList"]'; export const TIMELINE_FILTER_VALUE = '[data-test-subj="filterParamsComboBox phraseParamsComboxBox"]'; -export const TIMELINE_FLYOUT = '[data-test-subj="timeline-flyout"]'; +export const TIMELINE_FLYOUT = '[data-test-subj="timeline-container"]'; export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="query-tab-flyout-header"]'; @@ -185,7 +185,7 @@ export const TIMELINE_HEADER = '[data-test-subj="timeline-hide-show-container"]' export const TIMELINE_INSPECT_BUTTON = `${TIMELINE_FLYOUT} [data-test-subj="inspect-empty-button"]`; -export const TIMELINE_PANEL = `[data-test-subj="timeline-flyout-header-panel"]`; +export const TIMELINE_PANEL = `[data-test-subj="timeline-modal-header-panel"]`; export const TIMELINE_QUERY = '[data-test-subj="timelineQueryInput"]'; @@ -205,7 +205,7 @@ export const TIMELINE_LUCENELANGUAGE_BUTTON = '[data-test-subj="luceneLanguageMe export const TIMELINE_KQLLANGUAGE_BUTTON = '[data-test-subj="kqlLanguageMenuItem"]'; -export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; +export const TIMELINE_TITLE = '[data-test-subj="timeline-modal-header-title"]'; export const TIMELINE_TITLE_INPUT = '[data-test-subj="save-timeline-modal-title-input"]'; @@ -225,9 +225,9 @@ export const TIMELINE_SAVE_MODAL_SAVE_AS_NEW_SWITCH = export const TIMELINE_EXIT_FULL_SCREEN_BUTTON = '[data-test-subj="exit-full-screen"]'; -export const TIMELINE_FLYOUT_WRAPPER = '[data-test-subj="flyout-pane"]'; +export const TIMELINE_FLYOUT_WRAPPER = '[data-test-subj="timeline-portal-ref"]'; -export const TIMELINE_WRAPPER = '[data-test-subj="timeline-wrapper"]'; +export const TIMELINE_WRAPPER = '[data-test-subj="timeline-portal-overlay-mask"]'; export const TIMELINE_FULL_SCREEN_BUTTON = '[data-test-subj="full-screen-active"]'; @@ -316,7 +316,7 @@ export const NEW_TIMELINE_ACTION = getDataTestSubjectSelector( 'timeline-modal-new-timeline-dropdown-button' ); -export const SAVE_TIMELINE_ACTION = getDataTestSubjectSelector('save-timeline-action'); +export const SAVE_TIMELINE_ACTION = getDataTestSubjectSelector('timeline-modal-save-timeline'); export const SAVE_TIMELINE_ACTION_BTN = getDataTestSubjectSelector('timeline-modal-save-timeline'); export const SAVE_TIMELINE_TOOLTIP = getDataTestSubjectSelector( diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index 3cd55c8e8d9fe..b052b214206d3 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -542,7 +542,7 @@ export const fillDefineNewTermsRuleAndContinue = (rule: NewTermsRuleCreateProps) cy.get(NEW_TERMS_INPUT_AREA).find(INPUT).type(rule.new_terms_fields[0], { delay: 35 }); cy.get(EUI_FILTER_SELECT_ITEM).click(); - // eslint-disable-next-line cypress/unsafe-to-chain-command + cy.focused().type('{esc}'); // Close combobox dropdown so next inputs can be interacted with const historySize = convertHistoryStartToSize(rule.history_window_start); const historySizeNumber = historySize.slice(0, historySize.length - 1); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/network/flows.ts b/x-pack/test/security_solution_cypress/cypress/tasks/network/flows.ts index 5aa9ae55688cc..5317b442bb26b 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/network/flows.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/network/flows.ts @@ -48,5 +48,5 @@ export const clickOnShowTopN = () => { export const clickOnCopyValue = () => { cy.get(COPY).first().focus(); - cy.focused().click(); // eslint-disable-line cypress/unsafe-to-chain-command + cy.focused().click(); }; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/security_main.ts b/x-pack/test/security_solution_cypress/cypress/tasks/security_main.ts index fbf31d19e5a4c..3ed801310da1c 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/security_main.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/security_main.ts @@ -6,8 +6,9 @@ */ import { recurse } from 'cypress-recurse'; -import { CLOSE_TIMELINE_BUTTON, TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON } from '../screens/security_main'; +import { TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON } from '../screens/security_main'; import { + CLOSE_TIMELINE_BTN, TIMELINE_EXIT_FULL_SCREEN_BUTTON, TIMELINE_FULL_SCREEN_BUTTON, TIMELINE_WRAPPER, @@ -20,12 +21,12 @@ export const openTimelineUsingToggle = () => { return cy.get(TIMELINE_WRAPPER); }, // Retry if somehow the timeline wrapper is still hidden - ($timelineWrapper) => !$timelineWrapper.hasClass('timeline-wrapper--hidden') + ($timelineWrapper) => !$timelineWrapper.hasClass('timeline-portal-overlay-mask--hidden') ); }; export const closeTimelineUsingCloseButton = () => { - cy.get(CLOSE_TIMELINE_BUTTON).filter(':visible').click(); + cy.get(CLOSE_TIMELINE_BTN).filter(':visible').click(); }; export const enterFullScreenMode = () => { diff --git a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts index 802cbf8be16fc..b1ee0897fd0b0 100644 --- a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts @@ -260,7 +260,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await updateArtifact(testData, { policyId: policyInfo.packagePolicy.id }); // Check edited artifact is in the list with new values (wait for list to be updated) - await retry.waitForWithTimeout('entry is updated in list', 10000, async () => { + await retry.waitForWithTimeout('entry is updated in list', 20000, async () => { const currentValue = await testSubjects.getVisibleText( `${testData.pagePrefix}-card-criteriaConditions${ testData.pagePrefix === 'EventFiltersListPage' ? '-condition' : '' diff --git a/x-pack/test/security_solution_ftr/page_objects/timeline/index.ts b/x-pack/test/security_solution_ftr/page_objects/timeline/index.ts index 26345722ef326..2e5cc8492eaa2 100644 --- a/x-pack/test/security_solution_ftr/page_objects/timeline/index.ts +++ b/x-pack/test/security_solution_ftr/page_objects/timeline/index.ts @@ -10,7 +10,7 @@ import { DATE_RANGE_OPTION_TO_TEST_SUBJ_MAP } from '@kbn/security-solution-plugi import { FtrService } from '../../../functional/ftr_provider_context'; const TIMELINE_BOTTOM_BAR_CONTAINER_TEST_SUBJ = 'timeline-bottom-bar'; -const TIMELINE_CLOSE_BUTTON_TEST_SUBJ = 'close-timeline'; +const TIMELINE_CLOSE_BUTTON_TEST_SUBJ = 'timeline-modal-header-close-button'; const TIMELINE_MODAL_PAGE_TEST_SUBJ = 'timeline'; const TIMELINE_TAB_QUERY_TEST_SUBJ = 'timeline-tab-content-query'; diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/existing_fields.ts b/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/existing_fields.ts index 6dd1542bbea39..1b3bf8a63caa2 100644 --- a/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/existing_fields.ts +++ b/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/existing_fields.ts @@ -93,7 +93,8 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await PageObjects.svlCommonPage.forceLogout(); }); - describe('existence', () => { + // FLAKY: https://github.com/elastic/kibana/issues/172781 + describe.skip('existence', () => { it('should find which fields exist in the sample documents', async () => { const sidebarFields = await PageObjects.unifiedFieldList.getAllFieldNames(); expect(sidebarFields.sort()).to.eql([...metaFields, ...fieldsWithData].sort()); diff --git a/yarn.lock b/yarn.lock index c2634b0431587..515c3b6e2c314 100644 --- a/yarn.lock +++ b/yarn.lock @@ -439,10 +439,10 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.10.3", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.2", "@babel/parser@^7.21.8", "@babel/parser@^7.22.15", "@babel/parser@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b" - integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.10.3", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.8", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0", "@babel/parser@^7.23.6": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" + integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3": version "7.23.3" @@ -661,7 +661,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-jsx@^7.17.12", "@babel/plugin-syntax-jsx@^7.18.6", "@babel/plugin-syntax-jsx@^7.22.5", "@babel/plugin-syntax-jsx@^7.23.3", "@babel/plugin-syntax-jsx@^7.7.2": +"@babel/plugin-syntax-jsx@^7.17.12", "@babel/plugin-syntax-jsx@^7.22.5", "@babel/plugin-syntax-jsx@^7.23.3", "@babel/plugin-syntax-jsx@^7.7.2": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz#8f2e4f8a9b5f9aa16067e142c1ac9cd9f810f473" integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg== @@ -1495,25 +1495,25 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz#b6b8d81780b9a9f6459f4bfe9226ac6aefaefe87" integrity sha512-aG20vknL4/YjQF9BSV7ts4EWm/yrjagAN7OWBNmlbEOUiu0llj4OGrFoOKK3g2vey4/p2omKCoHrWtPxSwV3HA== -"@cypress/code-coverage@^3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@cypress/code-coverage/-/code-coverage-3.10.0.tgz#2132dbb7ae068cab91790926d50a9bf85140cab4" - integrity sha512-K5pW2KPpK4vKMXqxd6vuzo6m9BNgpAv1LcrrtmqAtOJ1RGoEILXYZVost0L6Q+V01NyY7n7jXIIfS7LR3nP6YA== +"@cypress/code-coverage@^3.12.18": + version "3.12.19" + resolved "https://registry.yarnpkg.com/@cypress/code-coverage/-/code-coverage-3.12.19.tgz#5bf197cb48826c315c7cce7acf1b3057de34a033" + integrity sha512-RNpgESArIwX2PG7k0KEb941eSYSBEGF1WB5NPeWrVJMX6KeAxj3Ki5aeYlFeV+wxoAJ+7gcF4s5xV18BQLCpjQ== dependencies: - "@cypress/webpack-preprocessor" "^5.11.0" + "@cypress/webpack-preprocessor" "^6.0.0" chalk "4.1.2" - dayjs "1.10.7" + dayjs "1.11.10" debug "4.3.4" execa "4.1.0" - globby "11.0.4" - istanbul-lib-coverage "3.0.0" - js-yaml "3.14.1" + globby "11.1.0" + istanbul-lib-coverage "^3.0.0" + js-yaml "4.1.0" nyc "15.1.0" -"@cypress/grep@^3.1.5": - version "3.1.5" - resolved "https://registry.yarnpkg.com/@cypress/grep/-/grep-3.1.5.tgz#d21a7194e2dd172daf864ac7a5ffc9313cc122f7" - integrity sha512-dbLKP9wGLId+TwTRFDcWVcr9AvJ06W3K7dVeJzLONiPbI5/XJh2mDZvnoyJlAz+VZxdwe0+nejk/CPmuphuzkQ== +"@cypress/grep@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cypress/grep/-/grep-4.0.1.tgz#bce679f85da286c4979bb9ffc79b2782dc5b75c6" + integrity sha512-i3mWy4mG6nxF7m93W0nzsMZkl0PflGa4+SygA9P92tELayYYAaRKlr07I4fo5PnwoPk1H9IEbXoMFJkhfTMxtg== dependencies: debug "^4.3.4" find-test-names "^1.19.0" @@ -1557,13 +1557,13 @@ snap-shot-compare "2.8.3" snap-shot-store "1.2.3" -"@cypress/webpack-preprocessor@^5.11.0", "@cypress/webpack-preprocessor@^5.12.2": - version "5.12.2" - resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-5.12.2.tgz#9cc623a5629980d7f2619569bffc8e3f05a701ae" - integrity sha512-t29wEFvI87IMnCd8taRunwStNsFjFWg138fGF0hPQOYgSj30fbzCEwFD9cAQLYMMcjjuXcnnw8yOfkzIZBBNVQ== +"@cypress/webpack-preprocessor@^6.0.0", "@cypress/webpack-preprocessor@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-6.0.1.tgz#5369527c063b2f4718a125ddbd163c5775086e06" + integrity sha512-WVNeFVSnFKxE3WZNRIriduTgqJRpevaiJIPlfqYTTzfXRD7X1Pv4woDE+G4caPV9bJqVKmVFiwzrXMRNeJxpxA== dependencies: bluebird "3.7.1" - debug "^4.3.2" + debug "^4.3.4" lodash "^4.17.20" "@cypress/xvfb@^1.2.4": @@ -4584,6 +4584,10 @@ version "0.0.0" uid "" +"@kbn/esql-utils@link:packages/kbn-esql-utils": + version "0.0.0" + uid "" + "@kbn/event-annotation-common@link:packages/kbn-event-annotation-common": version "0.0.0" uid "" @@ -5168,6 +5172,10 @@ version "0.0.0" uid "" +"@kbn/ml-cancellable-search@link:x-pack/packages/ml/cancellable_search": + version "0.0.0" + uid "" + "@kbn/ml-category-validator@link:x-pack/packages/ml/category_validator": version "0.0.0" uid "" @@ -9905,7 +9913,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@20.10.5", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@>=18.0.0", "@types/node@^10.1.0", "@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.11.8", "@types/node@^14.14.20 || ^16.0.0", "@types/node@^18.11.18", "@types/node@^18.17.5": +"@types/node@*", "@types/node@20.10.5", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@>=18.0.0", "@types/node@^10.1.0", "@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.11.8", "@types/node@^14.14.20 || ^16.0.0", "@types/node@^18.11.18": version "20.10.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.5.tgz#47ad460b514096b7ed63a1dae26fad0914ed3ab2" integrity sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw== @@ -12875,13 +12883,14 @@ caching-transform@^4.0.0: package-hash "^4.0.0" write-file-atomic "^3.0.0" -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" call-me-maybe@^1.0.1: version "1.0.1" @@ -14299,10 +14308,10 @@ cypress-axe@^1.5.0: resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-1.5.0.tgz#95082734583da77b51ce9b7784e14a442016c7a1" integrity sha512-Hy/owCjfj+25KMsecvDgo4fC/781ccL+e8p+UUYoadGVM2ogZF9XIKbiM6KI8Y3cEaSreymdD6ZzccbI2bY0lQ== -cypress-data-session@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/cypress-data-session/-/cypress-data-session-2.7.0.tgz#1dbae30798a7f7351c67b40cc20fdf28af512eac" - integrity sha512-Rj7WZ/Vn/givI6ja+y/c3QlT3jiwpVDReuBuYm66A31/Vi9PfCoI0SSY1pL/dUUrFEcqCLEFZiTiAWhgTtjXwQ== +cypress-data-session@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/cypress-data-session/-/cypress-data-session-2.8.0.tgz#65795dafea449ecc78b9e5ea69d734d4f7d9cb15" + integrity sha512-WYx2aruBwrpNNLVxHcD7lHHbwNCSKMHRBZEsaaV8sARoyAJsohIAhzOldIUE2n02O2ICaVYYjpF2cdxFumRI0w== dependencies: cypress-plugin-config "^1.2.0" debug "^4.3.2" @@ -14313,10 +14322,10 @@ cypress-file-upload@^5.0.8: resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1" integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g== -cypress-multi-reporters@^1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.6.3.tgz#0f0da8db4caf8d7a21f94e5209148348416d7c71" - integrity sha512-klb9pf6oAF4WCLHotu9gdB8ukYBdeTzbEMuESKB3KT54HhrZj65vQxubAgrULV5H2NWqxHdUhlntPbKZChNvEw== +cypress-multi-reporters@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.6.4.tgz#6f9d25ed8a0d8d7fa5597977adcd2237d1249931" + integrity sha512-3xU2t6pZjZy/ORHaCvci5OT1DAboS4UuMMM8NBAizeb2C9qmHt+cgAjXgurazkwkPRdO7ccK39M5ZaPCju0r6A== dependencies: debug "^4.3.4" lodash "^4.17.21" @@ -14326,10 +14335,10 @@ cypress-plugin-config@^1.2.0: resolved "https://registry.yarnpkg.com/cypress-plugin-config/-/cypress-plugin-config-1.2.1.tgz#aa7eaa55ab5ce5e186ab7d0e37cc7e42bfb609b4" integrity sha512-z+bQ7oyfDKun51HiCVNBOR+g38/nYRJ7zVdCZT2/9UozzE8P4iA1zF/yc85ePZLy5NOj/0atutoUPBBR5SqjSQ== -cypress-real-events@^1.10.3: - version "1.10.3" - resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.10.3.tgz#e2e949ea509cc4306df6c238de1a9982d67360e5" - integrity sha512-YN3fn+CJIAM638sE6uMvv2/n3PsWowdd0rOiN6ZoyezNAMyENfuQHvccLKZpN+apGfQZYetCml6QXLYgDid2fg== +cypress-real-events@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.11.0.tgz#292fe5281c5b6e955524e766ab7fec46930c7763" + integrity sha512-4LXVRsyq+xBh5TmlEyO1ojtBXtN7xw720Pwb9rEE9rkJuXmeH3VyoR1GGayMGr+Itqf11eEjfDewtDmcx6PWPQ== cypress-recurse@^1.35.2: version "1.35.2" @@ -14338,14 +14347,13 @@ cypress-recurse@^1.35.2: dependencies: humanize-duration "^3.27.3" -cypress@^13.3.0: - version "13.3.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.3.0.tgz#d00104661b337d662c5a4280a051ee59f8aa1e31" - integrity sha512-mpI8qcTwLGiA4zEQvTC/U1xGUezVV4V8HQCOYjlEOrVmU1etVvxOjkCXHGwrlYdZU/EPmUiWfsO3yt1o+Q2bgw== +cypress@^13.6.3: + version "13.6.3" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.6.3.tgz#54f03ca07ee56b2bc18211e7bd32abd2533982ba" + integrity sha512-d/pZvgwjAyZsoyJ3FOsJT5lDsqnxQ/clMqnNc++rkHjbkkiF2h9s0JsZSyyH4QXhVFW3zPFg82jD25roFLOdZA== dependencies: "@cypress/request" "^3.0.0" "@cypress/xvfb" "^1.2.4" - "@types/node" "^18.17.5" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" arch "^2.2.0" @@ -14718,10 +14726,10 @@ dateformat@^4.5.1: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== -dayjs@1.10.7, dayjs@^1.10.4: - version "1.10.7" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" - integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== +dayjs@1.11.10, dayjs@^1.10.4: + version "1.11.10" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== debug-fabulous@1.X: version "1.1.0" @@ -14846,25 +14854,29 @@ deep-equal@^1.0.0, deep-equal@^1.0.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" -deep-equal@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0" - integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA== +deep-equal@^2.0.3, deep-equal@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" + integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== dependencies: - es-abstract "^1.17.5" - es-get-iterator "^1.1.0" - is-arguments "^1.0.4" - is-date-object "^1.0.2" - is-regex "^1.0.5" + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.5" + es-get-iterator "^1.1.3" + get-intrinsic "^1.2.2" + is-arguments "^1.1.1" + is-array-buffer "^3.0.2" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" isarray "^2.0.5" - object-is "^1.1.2" + object-is "^1.1.5" object-keys "^1.1.1" - object.assign "^4.1.0" - regexp.prototype.flags "^1.3.0" - side-channel "^1.0.2" - which-boxed-primitive "^1.0.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" which-collection "^1.0.1" - which-typed-array "^1.1.2" + which-typed-array "^1.1.13" deep-extend@^0.6.0: version "0.6.0" @@ -14926,6 +14938,15 @@ defer-to-connect@^2.0.0, defer-to-connect@^2.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== +define-data-property@^1.0.1, define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -15930,7 +15951,7 @@ error-stack-parser@^2.0.4, error-stack-parser@^2.0.6: dependencies: stackframe "^1.1.1" -es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5, es-abstract@^1.19.0, es-abstract@^1.20.4, es-abstract@^1.21.2, es-abstract@^1.4.3, es-abstract@^1.9.0: +es-abstract@^1.17.0-next.1, es-abstract@^1.19.0, es-abstract@^1.20.4, es-abstract@^1.21.2, es-abstract@^1.4.3, es-abstract@^1.9.0: version "1.22.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc" integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw== @@ -15980,18 +16001,20 @@ es-array-method-boxes-properly@^1.0.0: resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== -es-get-iterator@^1.0.2, es-get-iterator@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" - integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== +es-get-iterator@^1.0.2, es-get-iterator@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== dependencies: - es-abstract "^1.17.4" - has-symbols "^1.0.1" - is-arguments "^1.0.4" - is-map "^2.0.1" - is-set "^2.0.1" - is-string "^1.0.5" + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" es-module-lexer@^0.9.0: version "0.9.3" @@ -16241,10 +16264,10 @@ eslint-plugin-ban@^1.6.0: dependencies: requireindex "~1.2.0" -eslint-plugin-cypress@^2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.14.0.tgz#c65e1f592680dd25bbd00c86194ee85fecf59bd7" - integrity sha512-eW6tv7iIg7xujleAJX4Ujm649Bf5jweqa4ObPEIuueYRyLZt7qXGWhCY/n4bfeFW/j6nQZwbIBHKZt6EKcL/cg== +eslint-plugin-cypress@^2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.15.1.tgz#336afa7e8e27451afaf65aa359c9509e0a4f3a7b" + integrity sha512-eLHLWP5Q+I4j2AWepYq0PgFEei9/s5LvjuSqWrxurkg1YZ8ltxdvMNmdSf0drnsNo57CTgYY/NIHHLRSWejR7w== dependencies: globals "^13.20.0" @@ -16891,7 +16914,7 @@ fast-glob@^2.2.6: merge2 "^1.2.3" micromatch "^3.1.10" -fast-glob@^3.0.3, fast-glob@^3.1.1, fast-glob@^3.2.11, fast-glob@^3.2.2, fast-glob@^3.2.9, fast-glob@^3.3.2: +fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.2, fast-glob@^3.2.9, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -17175,22 +17198,22 @@ find-cache-dir@^3.2.0, find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-cypress-specs@^1.35.1: - version "1.35.1" - resolved "https://registry.yarnpkg.com/find-cypress-specs/-/find-cypress-specs-1.35.1.tgz#89f633de14ab46c2afc6fee992a470596526f721" - integrity sha512-ngLPf/U/I8jAS6vn5ljClETa6seG+fmr3oXw6BcYX3xVIk7D8jNljHUIJCTDvnK90XOI1cflYGuNFDezhRBNvQ== +find-cypress-specs@^1.41.4: + version "1.41.4" + resolved "https://registry.yarnpkg.com/find-cypress-specs/-/find-cypress-specs-1.41.4.tgz#583595c502e785f7bb44dbb5a9ba2f09caf59e94" + integrity sha512-voIB7Po/bHOyXkFyU6KE1mOBWX5q+rT+wAVJKqKFKFFDJCcvg9fGyCG/xVDga6ZznQxM4oX6uvaOAW1PKHM95g== dependencies: "@actions/core" "^1.10.0" arg "^5.0.1" console.table "^0.10.0" debug "^4.3.3" - find-test-names "1.28.13" + find-test-names "1.28.18" globby "^11.1.0" minimatch "^3.0.4" pluralize "^8.0.0" require-and-forget "^1.0.1" shelljs "^0.8.5" - spec-change "^1.7.1" + spec-change "^1.10.0" ts-node "^10.9.1" find-root@^1.1.0: @@ -17198,13 +17221,13 @@ find-root@^1.1.0: resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== -find-test-names@1.28.13, find-test-names@^1.19.0: - version "1.28.13" - resolved "https://registry.yarnpkg.com/find-test-names/-/find-test-names-1.28.13.tgz#871d5585d1f618ed772ffe544ea475ab4657ca83" - integrity sha512-hVatCLbiZmvBwqYNGTkVNbeJwK/8pvkXKQGji+23GzW8fVFHcEaRID77eQYItLKGwa1Tmu0AK2LjcUtuid57FQ== +find-test-names@1.28.18, find-test-names@^1.19.0: + version "1.28.18" + resolved "https://registry.yarnpkg.com/find-test-names/-/find-test-names-1.28.18.tgz#a10acba4ebd2e6db8e182e0fcc6f85d78fa29969" + integrity sha512-hhnGdkWK+qEA5Z02Tu0OqGQIUjFZNyOCE4WaJpbhW4hAF1+NZ7OCr0Bss9RCaj7BBtjoIjkU93utobQ8pg2iVg== dependencies: - "@babel/parser" "^7.21.2" - "@babel/plugin-syntax-jsx" "^7.18.6" + "@babel/parser" "^7.23.0" + "@babel/plugin-syntax-jsx" "^7.22.5" acorn-walk "^8.2.0" debug "^4.3.3" globby "^11.0.4" @@ -17634,10 +17657,10 @@ fsu@^1.1.1: resolved "https://registry.yarnpkg.com/fsu/-/fsu-1.1.1.tgz#bd36d3579907c59d85b257a75b836aa9e0c31834" integrity sha512-xQVsnjJ/5pQtcKh+KjUoZGzVWn4uNkchxTF6Lwjr4Gf7nQr8fmUfhKJ62zE77+xQg9xnxi5KUps7XGs+VC986A== -function-bind@^1.0.2, function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.0.2, function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== function.prototype.name@^1.1.0, function.prototype.name@^1.1.2, function.prototype.name@^1.1.5: version "1.1.5" @@ -17719,15 +17742,15 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" - integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + function-bind "^1.1.2" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-nonce@^1.0.0: version "1.0.1" @@ -18025,16 +18048,16 @@ globby@10.0.0: merge2 "^1.2.3" slash "^3.0.0" -globby@11.0.4: - version "11.0.4" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" - integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== +globby@11.1.0, globby@^11.0.1, globby@^11.0.2, globby@^11.0.4, globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" slash "^3.0.0" globby@^10.0.1: @@ -18051,18 +18074,6 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" -globby@^11.0.1, globby@^11.0.2, globby@^11.0.4, globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -18297,12 +18308,12 @@ has-glob@^1.0.0: dependencies: is-glob "^3.0.0" -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== dependencies: - get-intrinsic "^1.1.1" + get-intrinsic "^1.2.2" has-proto@^1.0.1: version "1.0.1" @@ -18395,6 +18406,13 @@ hasha@^5.0.0: is-stream "^2.0.0" type-fest "^0.8.0" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + hast-to-hyperscript@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.0.tgz#768fb557765fe28749169c885056417342d71e83" @@ -18966,7 +18984,7 @@ ignore@^4.0.3: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.0.5, ignore@^5.1.1, ignore@^5.1.4, ignore@^5.2.0, ignore@^5.3.0: +ignore@^5.0.5, ignore@^5.1.1, ignore@^5.2.0, ignore@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== @@ -19135,13 +19153,13 @@ install-artifact-from-github@^1.3.5: resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.5.tgz#88c96fe40e5eb21d45586d564208c648a1dbf38d" integrity sha512-gZHC7f/cJgXz7MXlHFBxPVMsvIbev1OQN1uKQYKVJDydGNm9oYf9JstbU4Atnh/eSvk41WtEovoRm+8IF686xg== -internal-slot@^1.0.3, internal-slot@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" - integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== +internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930" + integrity sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg== dependencies: - get-intrinsic "^1.2.0" - has "^1.0.3" + get-intrinsic "^1.2.2" + hasown "^2.0.0" side-channel "^1.0.4" "internmap@1 - 2": @@ -19287,10 +19305,13 @@ is-any-array@^2.0.0: resolved "https://registry.yarnpkg.com/is-any-array/-/is-any-array-2.0.1.tgz#9233242a9c098220290aa2ec28f82ca7fa79899e" integrity sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ== -is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== +is-arguments@^1.0.4, is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: version "3.0.2" @@ -19380,10 +19401,12 @@ is-data-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-date-object@^1.0.1, is-date-object@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== +is-date-object@^1.0.1, is-date-object@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" is-decimal@^1.0.0, is-decimal@^1.0.2: version "1.0.4" @@ -19521,10 +19544,10 @@ is-lambda@^1.0.1: resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= -is-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" - integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== +is-map@^2.0.1, is-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== is-native@^1.0.1: version "1.0.1" @@ -19708,10 +19731,10 @@ is-relative@^1.0.0: dependencies: is-unc-path "^1.0.0" -is-set@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" - integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== +is-set@^2.0.1, is-set@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== is-shared-array-buffer@^1.0.2: version "1.0.2" @@ -19929,11 +19952,6 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-lib-coverage@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" - integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== - istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.0.0-alpha.1, istanbul-lib-coverage@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" @@ -20684,14 +20702,6 @@ js-tiktoken@^1.0.7: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@3.14.1, js-yaml@^3.13.1, js-yaml@^3.14.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -20699,6 +20709,14 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +js-yaml@^3.13.1, js-yaml@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -23573,13 +23591,13 @@ object-inspect@^1.12.3, object-inspect@^1.6.0, object-inspect@^1.7.0, object-ins resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== -object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" - integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== +object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2, object-is@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.5" object-keys@^1.1.1: version "1.1.1" @@ -26592,14 +26610,14 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0, regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" - integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== dependencies: call-bind "^1.0.2" define-properties "^1.2.0" - functions-have-names "^1.2.3" + set-function-name "^2.0.0" regexpp@^3.0.0: version "3.2.0" @@ -27666,6 +27684,26 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-function-length@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.0.tgz#2f81dc6c16c7059bda5ab7c82c11f03a515ed8e1" + integrity sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w== + dependencies: + define-data-property "^1.1.1" + function-bind "^1.1.2" + get-intrinsic "^1.2.2" + gopd "^1.0.1" + has-property-descriptors "^1.0.1" + +set-function-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + set-getter@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.1.tgz#a3110e1b461d31a9cfc8c5c9ee2e9737ad447102" @@ -27837,7 +27875,7 @@ should@^13.2.1: should-type-adaptors "^1.0.1" should-util "^1.0.0" -side-channel@^1.0.2, side-channel@^1.0.4: +side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== @@ -28332,13 +28370,14 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" -spec-change@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/spec-change/-/spec-change-1.7.1.tgz#3c56185c887a15482f1fbb3362916fc97c8fdb9f" - integrity sha512-bZmtSmS5w6M6Snae+AGp+y89MZ7QG2SZW1v3Au83+YWcZzCu0YtH2hXruJWXg6VdYUpQ3n+m9bRrWmwLaPkFjQ== +spec-change@^1.10.0, spec-change@^1.7.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/spec-change/-/spec-change-1.10.0.tgz#09770b40402a06d0aac8cdb550ecaade38590d05" + integrity sha512-IMhfPFDbpLBBT/bjSVPLmRxPcCd43XH1MuSGgd3BxBeOLYIVvaca65C3T6cR5ouB+xKOERwsk3Y1RF46JmaquA== dependencies: arg "^5.0.2" debug "^4.3.4" + deep-equal "^2.2.3" dependency-tree "^10.0.9" globby "^11.1.0" lazy-ass "^2.0.3" @@ -28529,6 +28568,13 @@ stickyfill@^1.1.1: resolved "https://registry.yarnpkg.com/stickyfill/-/stickyfill-1.1.1.tgz#39413fee9d025c74a7e59ceecb23784cc0f17f02" integrity sha512-GCp7vHAfpao+Qh/3Flh9DXEJ/qSi0KJwJw6zYlZOtRYXWUIpMM6mC2rIep/dK8RQqwW0KxGJIllmjPIBOGN8AA== +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + store2@^2.12.0: version "2.12.0" resolved "https://registry.yarnpkg.com/store2/-/store2-2.12.0.tgz#e1f1b7e1a59b6083b2596a8d067f6ee88fd4d3cf" @@ -31499,7 +31545,7 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" -which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2: +which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== @@ -31525,13 +31571,13 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which-typed-array@^1.1.10, which-typed-array@^1.1.11, which-typed-array@^1.1.2: - version "1.1.11" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" - integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== +which-typed-array@^1.1.10, which-typed-array@^1.1.11, which-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" + integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== dependencies: available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + call-bind "^1.0.4" for-each "^0.3.3" gopd "^1.0.1" has-tostringtag "^1.0.0"