diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 2841dce49bb26..fcf4a82c0801c 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -372,7 +372,7 @@ steps: agents: queue: n2-8-spot key: storybooks - timeout_in_minutes: 60 + timeout_in_minutes: 80 retry: automatic: - exit_status: '-1' diff --git a/.buildkite/pipelines/pull_request/storybooks.yml b/.buildkite/pipelines/pull_request/storybooks.yml index 81d13b628e049..8f76879231de2 100644 --- a/.buildkite/pipelines/pull_request/storybooks.yml +++ b/.buildkite/pipelines/pull_request/storybooks.yml @@ -4,4 +4,4 @@ steps: agents: queue: n2-8-spot key: storybooks - timeout_in_minutes: 60 + timeout_in_minutes: 80 diff --git a/.buildkite/scripts/lifecycle/post_build.sh b/.buildkite/scripts/lifecycle/post_build.sh index b5bdfd751a6e3..446ca4b28c559 100755 --- a/.buildkite/scripts/lifecycle/post_build.sh +++ b/.buildkite/scripts/lifecycle/post_build.sh @@ -12,7 +12,7 @@ fi ts-node "$(dirname "${0}")/ci_stats_complete.ts" if [[ "${GITHUB_PR_NUMBER:-}" ]]; then - DOCS_CHANGES_URL="https://kibana_$GITHUB_PR_NUMBER}.docs-preview.app.elstc.co/diff" + DOCS_CHANGES_URL="https://kibana_bk_$GITHUB_PR_NUMBER}.docs-preview.app.elstc.co/diff" DOCS_CHANGES=$(curl --connect-timeout 10 -m 10 -sf "$DOCS_CHANGES_URL" || echo '') if [[ "$DOCS_CHANGES" && "$DOCS_CHANGES" != "There aren't any differences!" ]]; then diff --git a/.buildkite/scripts/steps/cloud/purge_projects.ts b/.buildkite/scripts/steps/cloud/purge_projects.ts index c3c427c6a3885..dbf0060fe8a45 100644 --- a/.buildkite/scripts/steps/cloud/purge_projects.ts +++ b/.buildkite/scripts/steps/cloud/purge_projects.ts @@ -10,7 +10,7 @@ import { execSync } from 'child_process'; import axios from 'axios'; async function getPrProjects() { - const match = /^kibana-pr-([0-9]+)-(elasticsearch|security|observability)$/; + const match = /^(keep.?)?kibana-pr-([0-9]+)-(elasticsearch|security|observability)$/; try { return ( await Promise.all([ diff --git a/.buildkite/scripts/steps/fips/build.sh b/.buildkite/scripts/steps/fips/build.sh index dcd3cc3b0bb2f..c54e16053b038 100644 --- a/.buildkite/scripts/steps/fips/build.sh +++ b/.buildkite/scripts/steps/fips/build.sh @@ -33,3 +33,14 @@ docker logout docker.elastic.co # Moving to `target/` first will keep `buildkite-agent` from including directories in the artifact name cd "$KIBANA_DIR/target" buildkite-agent artifact upload "./*docker-image*.tar.gz" + +KIBANA_UBI_FIPS_IMAGE="docker.elastic.co/kibana-ci/kibana-ubi-fips:$FULL_VERSION-$BUILDKITE_COMMIT" + +cat < boolean) | undefined" + ], + "path": "src/plugins/console/public/types/plugin_dependencies.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "console", + "id": "def-public.ConsolePluginStart.openEmbeddedConsole", + "type": "Function", + "tags": [], + "label": "openEmbeddedConsole", + "description": [ + "\nopenEmbeddedConsole is available if the embedded console can be rendered. Calling\nthis function will open the embedded console on the page if it is currently rendered." + ], + "signature": [ + "((content?: string | undefined) => void) | undefined" + ], + "path": "src/plugins/console/public/types/plugin_dependencies.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "console", + "id": "def-public.ConsolePluginStart.openEmbeddedConsole.$1", + "type": "string", + "tags": [], + "label": "content", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/console/public/types/plugin_dependencies.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] } ], "initialIsOpen": false diff --git a/api_docs/console.mdx b/api_docs/console.mdx index 527e60940783b..bb66912bcb867 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github description: API docs for the console plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/platform-deployment-management](https://github.com/orgs/elasti | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 29 | 0 | 23 | 0 | +| 32 | 0 | 24 | 0 | ## Client diff --git a/api_docs/content_management.mdx b/api_docs/content_management.mdx index 0588bc2a0bf97..0e80a36b2a6e1 100644 --- a/api_docs/content_management.mdx +++ b/api_docs/content_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/contentManagement title: "contentManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the contentManagement plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'contentManagement'] --- import contentManagementObj from './content_management.devdocs.json'; diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index 926346635e430..9a050ec299b13 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github description: API docs for the controls plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index 8e5c36fbc0747..2ce940f46dd41 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github description: API docs for the customIntegrations plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index 7d5bedf0444df..45df395c57beb 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboard plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] --- import dashboardObj from './dashboard.devdocs.json'; diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index 791ceaa540a1d..e6ef382e215ed 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboardEnhanced plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.devdocs.json b/api_docs/data.devdocs.json index 48315db6ab3eb..d9c469084784c 100644 --- a/api_docs/data.devdocs.json +++ b/api_docs/data.devdocs.json @@ -3939,6 +3939,10 @@ "plugin": "alerting", "path": "x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts" }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts" + }, { "plugin": "stackAlerts", "path": "x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts" diff --git a/api_docs/data.mdx b/api_docs/data.mdx index 0266dc6c63480..028c60ed612f9 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github description: API docs for the data plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] --- import dataObj from './data.devdocs.json'; diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index 6d57ee43cdb19..93971928c7a2c 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github description: API docs for the data.query plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; diff --git a/api_docs/data_search.devdocs.json b/api_docs/data_search.devdocs.json index d05fccecd35ff..4a605c11c427d 100644 --- a/api_docs/data_search.devdocs.json +++ b/api_docs/data_search.devdocs.json @@ -10448,6 +10448,10 @@ "plugin": "alerting", "path": "x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts" }, + { + "plugin": "alerting", + "path": "x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts" + }, { "plugin": "stackAlerts", "path": "x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts" diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 6cf0ec8b332ee..2e712511f4adf 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github description: API docs for the data.search plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] --- import dataSearchObj from './data_search.devdocs.json'; diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index a986ac73a1a4a..44cc140245361 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewEditor plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] --- import dataViewEditorObj from './data_view_editor.devdocs.json'; diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index b51cccc090336..4ba268a88db0c 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewFieldEditor plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] --- import dataViewFieldEditorObj from './data_view_field_editor.devdocs.json'; diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index 2448185780faf..af38065be262c 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewManagement plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index 84e6eae5ffd18..eb3c2bf226b84 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViews plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] --- import dataViewsObj from './data_views.devdocs.json'; diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index b09f9dd8a0a27..0150e9e40e149 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github description: API docs for the dataVisualizer plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] --- import dataVisualizerObj from './data_visualizer.devdocs.json'; diff --git a/api_docs/dataset_quality.mdx b/api_docs/dataset_quality.mdx index 27f53e5af8347..d6bc68c2741c1 100644 --- a/api_docs/dataset_quality.mdx +++ b/api_docs/dataset_quality.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/datasetQuality title: "datasetQuality" image: https://source.unsplash.com/400x175/?github description: API docs for the datasetQuality plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'datasetQuality'] --- import datasetQualityObj from './dataset_quality.devdocs.json'; diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index a433ea47849f4..e31c68f84f380 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 3d5d34b7d900c..155bc9da9e270 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -358,11 +358,11 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| | | [wrap_search_source_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.ts#:~:text=create) | - | -| | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch) | - | +| | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch) | - | | | [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion) | - | | | [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion), [retrieve_migrated_legacy_actions.mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock.ts#:~:text=migrationVersion)+ 10 more | - | | | [wrap_search_source_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.ts#:~:text=create) | - | -| | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch) | - | +| | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts#:~:text=fetch) | - | | | [plugin.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/plugin.test.ts#:~:text=getKibanaFeatures) | 8.8.0 | | | [maintenance_windows.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/public/application/maintenance_windows.tsx#:~:text=KibanaThemeProvider), [maintenance_windows.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/public/application/maintenance_windows.tsx#:~:text=KibanaThemeProvider), [maintenance_windows.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/public/application/maintenance_windows.tsx#:~:text=KibanaThemeProvider) | - | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/plugin.ts#:~:text=license%24), [license_state.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/license_state.test.ts#:~:text=license%24), [license_state.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting/server/lib/license_state.test.ts#:~:text=license%24) | 8.8.0 | @@ -1084,7 +1084,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| | | [register_ml_alerts.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts#:~:text=registerNavigation) | - | -| | [anomaly_charts_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx#:~:text=KibanaThemeProvider), [anomaly_charts_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx#:~:text=KibanaThemeProvider), [anomaly_charts_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx#:~:text=KibanaThemeProvider), [anomaly_swimlane_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx#:~:text=KibanaThemeProvider), [anomaly_swimlane_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx#:~:text=KibanaThemeProvider), [anomaly_swimlane_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx#:~:text=KibanaThemeProvider), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/app.tsx#:~:text=KibanaThemeProvider), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/app.tsx#:~:text=KibanaThemeProvider), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/app.tsx#:~:text=KibanaThemeProvider), [jobs_list_page.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx#:~:text=KibanaThemeProvider)+ 2 more | - | +| | [anomaly_charts_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx#:~:text=KibanaThemeProvider), [anomaly_charts_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx#:~:text=KibanaThemeProvider), [anomaly_charts_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx#:~:text=KibanaThemeProvider), [single_metric_viewer_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx#:~:text=KibanaThemeProvider), [single_metric_viewer_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx#:~:text=KibanaThemeProvider), [single_metric_viewer_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx#:~:text=KibanaThemeProvider), [anomaly_swimlane_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx#:~:text=KibanaThemeProvider), [anomaly_swimlane_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx#:~:text=KibanaThemeProvider), [anomaly_swimlane_embeddable.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx#:~:text=KibanaThemeProvider), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/application/app.tsx#:~:text=KibanaThemeProvider)+ 5 more | - | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/public/plugin.ts#:~:text=license%24) | 8.8.0 | | | [plugin.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/plugin.ts#:~:text=license%24) | 8.8.0 | | | [annotations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/ml/server/routes/annotations.ts#:~:text=authc) | - | @@ -1339,12 +1339,12 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | | [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/app.tsx#:~:text=KibanaThemeProvider), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/app.tsx#:~:text=KibanaThemeProvider), [app.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/app/app.tsx#:~:text=KibanaThemeProvider) | - | | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 7 more | 8.8.0 | | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 7 more | 8.8.0 | -| | [create_threat_signal.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts#:~:text=license%24), [create_event_signal.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts#:~:text=license%24), [query.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts#:~:text=license%24), [threshold.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts#:~:text=license%24) | 8.8.0 | +| | [create_threat_signals.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts#:~:text=license%24), [query.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts#:~:text=license%24), [threshold.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts#:~:text=license%24) | 8.8.0 | | | [request_context_factory.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/request_context_factory.ts#:~:text=authc), [route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts#:~:text=authc), [create_signals_migration_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts#:~:text=authc), [delete_signals_migration_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts#:~:text=authc), [finalize_signals_migration_route.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts#:~:text=authc), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts#:~:text=authc) | - | | | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx#:~:text=DeprecatedCellValueElementProps), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx#:~:text=DeprecatedCellValueElementProps) | - | | | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx#:~:text=DeprecatedRowRenderer), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx#:~:text=DeprecatedRowRenderer) | - | | | [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#:~:text=BeatFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/search_strategy/endpoint_fields/index.ts#:~:text=BeatFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/search_strategy/endpoint_fields/index.ts#:~:text=BeatFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/search_strategy/endpoint_fields/index.ts#:~:text=BeatFields) | - | -| | [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#:~:text=BrowserField), [helpers.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts#:~:text=BrowserField), [helpers.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts#:~:text=BrowserField), [helpers.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts#:~:text=BrowserField), [helpers.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts#:~:text=BrowserField), [columns.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx#:~:text=BrowserField), [columns.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx#:~:text=BrowserField), [enrichment_summary.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx#:~:text=BrowserField), [enrichment_summary.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx#:~:text=BrowserField), [table_tab.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx#:~:text=BrowserField)+ 31 more | - | +| | [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#:~:text=BrowserField), [helpers.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts#:~:text=BrowserField), [helpers.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts#:~:text=BrowserField), [helpers.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts#:~:text=BrowserField), [helpers.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts#:~:text=BrowserField), [columns.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx#:~:text=BrowserField), [columns.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx#:~:text=BrowserField), [enrichment_summary.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx#:~:text=BrowserField), [enrichment_summary.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx#:~:text=BrowserField), [table_tab.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx#:~:text=BrowserField)+ 33 more | - | | | [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#:~:text=BrowserFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts#:~:text=BrowserFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts#:~:text=BrowserFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/types/header_actions/index.ts#:~:text=BrowserFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/types/header_actions/index.ts#:~:text=BrowserFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/types/header_actions/index.ts#:~:text=BrowserFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/lib/kuery/index.ts#:~:text=BrowserFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/lib/kuery/index.ts#:~:text=BrowserFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/lib/kuery/index.ts#:~:text=BrowserFields), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/lib/kuery/index.ts#:~:text=BrowserFields)+ 108 more | - | | | [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#:~:text=IndexFieldsStrategyRequest), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/search_strategy/endpoint_fields/index.ts#:~:text=IndexFieldsStrategyRequest), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/search_strategy/endpoint_fields/index.ts#:~:text=IndexFieldsStrategyRequest), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/search_strategy/endpoint_fields/index.ts#:~:text=IndexFieldsStrategyRequest), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=IndexFieldsStrategyRequest), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=IndexFieldsStrategyRequest) | - | | | [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#:~:text=IndexFieldsStrategyResponse), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/search_strategy/endpoint_fields/index.ts#:~:text=IndexFieldsStrategyResponse), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/search_strategy/endpoint_fields/index.ts#:~:text=IndexFieldsStrategyResponse), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/search_strategy/endpoint_fields/index.ts#:~:text=IndexFieldsStrategyResponse), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=IndexFieldsStrategyResponse), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=IndexFieldsStrategyResponse) | - | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index e41eae851ed2d..1c755e1ba8642 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team description: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -165,4 +165,4 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | --------|-------|-----------|-----------| | securitySolution | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 7 more | 8.8.0 | | securitySolution | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 7 more | 8.8.0 | -| securitySolution | | [create_threat_signal.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts#:~:text=license%24), [create_event_signal.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts#:~:text=license%24), [query.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts#:~:text=license%24), [threshold.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts#:~:text=license%24) | 8.8.0 | \ No newline at end of file +| securitySolution | | [create_threat_signals.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts#:~:text=license%24), [query.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts#:~:text=license%24), [threshold.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/threshold.ts#:~:text=license%24) | 8.8.0 | \ No newline at end of file diff --git a/api_docs/dev_tools.devdocs.json b/api_docs/dev_tools.devdocs.json index 624340d7d5db6..effd1a91e5271 100644 --- a/api_docs/dev_tools.devdocs.json +++ b/api_docs/dev_tools.devdocs.json @@ -197,7 +197,42 @@ "functions": [], "interfaces": [], "enums": [], - "misc": [], + "misc": [ + { + "parentPluginId": "devTools", + "id": "def-public.DEV_TOOLS_FEATURE_ID", + "type": "string", + "tags": [], + "label": "DEV_TOOLS_FEATURE_ID", + "description": [ + "\nThe UI Setting prefix and category for dev tools UI Settings" + ], + "signature": [ + "\"devTools\"" + ], + "path": "src/plugins/dev_tools/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "devTools", + "id": "def-public.ENABLE_DOCKED_CONSOLE_UI_SETTING_ID", + "type": "string", + "tags": [], + "label": "ENABLE_DOCKED_CONSOLE_UI_SETTING_ID", + "description": [ + "\nUI Setting ID for enabling / disabling the docked console in Kibana" + ], + "signature": [ + "\"devTools:enableDockedConsole\"" + ], + "path": "src/plugins/dev_tools/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], "objects": [], "setup": { "parentPluginId": "devTools", @@ -267,7 +302,42 @@ "functions": [], "interfaces": [], "enums": [], - "misc": [], + "misc": [ + { + "parentPluginId": "devTools", + "id": "def-common.DEV_TOOLS_FEATURE_ID", + "type": "string", + "tags": [], + "label": "DEV_TOOLS_FEATURE_ID", + "description": [ + "\nThe UI Setting prefix and category for dev tools UI Settings" + ], + "signature": [ + "\"devTools\"" + ], + "path": "src/plugins/dev_tools/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "devTools", + "id": "def-common.ENABLE_DOCKED_CONSOLE_UI_SETTING_ID", + "type": "string", + "tags": [], + "label": "ENABLE_DOCKED_CONSOLE_UI_SETTING_ID", + "description": [ + "\nUI Setting ID for enabling / disabling the docked console in Kibana" + ], + "signature": [ + "\"devTools:enableDockedConsole\"" + ], + "path": "src/plugins/dev_tools/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], "objects": [] } } \ No newline at end of file diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 359ab0e7288e9..65fd870b52391 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github description: API docs for the devTools plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] --- import devToolsObj from './dev_tools.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/platform-deployment-management](https://github.com/orgs/elasti | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 12 | 0 | 10 | 3 | +| 16 | 0 | 10 | 3 | ## Client @@ -31,3 +31,11 @@ Contact [@elastic/platform-deployment-management](https://github.com/orgs/elasti ### Classes +### Consts, variables and types + + +## Common + +### Consts, variables and types + + diff --git a/api_docs/discover.devdocs.json b/api_docs/discover.devdocs.json index 0671ccf421cf3..60885dad84dc3 100644 --- a/api_docs/discover.devdocs.json +++ b/api_docs/discover.devdocs.json @@ -72,6 +72,96 @@ } ], "interfaces": [ + { + "parentPluginId": "discover", + "id": "def-public.DataDocumentsMsg", + "type": "Interface", + "tags": [], + "label": "DataDocumentsMsg", + "description": [], + "signature": [ + { + "pluginId": "discover", + "scope": "public", + "docId": "kibDiscoverPluginApi", + "section": "def-public.DataDocumentsMsg", + "text": "DataDocumentsMsg" + }, + " extends ", + "DataMsg" + ], + "path": "src/plugins/discover/public/application/main/services/discover_data_state_container.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "discover", + "id": "def-public.DataDocumentsMsg.result", + "type": "Array", + "tags": [], + "label": "result", + "description": [], + "signature": [ + "DataTableRecord", + "[] | undefined" + ], + "path": "src/plugins/discover/public/application/main/services/discover_data_state_container.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "discover", + "id": "def-public.DataDocumentsMsg.textBasedQueryColumns", + "type": "Array", + "tags": [], + "label": "textBasedQueryColumns", + "description": [], + "signature": [ + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.DatatableColumn", + "text": "DatatableColumn" + }, + "[] | undefined" + ], + "path": "src/plugins/discover/public/application/main/services/discover_data_state_container.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "discover", + "id": "def-public.DataDocumentsMsg.textBasedHeaderWarning", + "type": "string", + "tags": [], + "label": "textBasedHeaderWarning", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/discover/public/application/main/services/discover_data_state_container.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "discover", + "id": "def-public.DataDocumentsMsg.interceptedWarnings", + "type": "Array", + "tags": [], + "label": "interceptedWarnings", + "description": [], + "signature": [ + "SearchResponseIncompleteWarning", + "[] | undefined" + ], + "path": "src/plugins/discover/public/application/main/services/discover_data_state_container.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "discover", "id": "def-public.DiscoverAppState", diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index 962b6c9db5e41..b47712cb842ad 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github description: API docs for the discover plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] --- import discoverObj from './discover.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 151 | 0 | 104 | 22 | +| 156 | 0 | 109 | 23 | ## Client diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index db0763d64fdb3..f50ea600dc2c2 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the discoverEnhanced plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/ecs_data_quality_dashboard.mdx b/api_docs/ecs_data_quality_dashboard.mdx index 0a9d3737f22cd..4c0ad20483eda 100644 --- a/api_docs/ecs_data_quality_dashboard.mdx +++ b/api_docs/ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ecsDataQualityDashboard title: "ecsDataQualityDashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the ecsDataQualityDashboard plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ecsDataQualityDashboard'] --- import ecsDataQualityDashboardObj from './ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/elastic_assistant.mdx b/api_docs/elastic_assistant.mdx index 62ab029aa3e2f..13dfeb0b641c9 100644 --- a/api_docs/elastic_assistant.mdx +++ b/api_docs/elastic_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/elasticAssistant title: "elasticAssistant" image: https://source.unsplash.com/400x175/?github description: API docs for the elasticAssistant plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'elasticAssistant'] --- import elasticAssistantObj from './elastic_assistant.devdocs.json'; diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index e4757ac0fa026..e2b24384583be 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddable plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] --- import embeddableObj from './embeddable.devdocs.json'; diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index edacdc9d052fa..de166a20184c0 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddableEnhanced plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] --- import embeddableEnhancedObj from './embeddable_enhanced.devdocs.json'; diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index 53d150a26ef56..5e77b2f36d665 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the encryptedSavedObjects plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] --- import encryptedSavedObjectsObj from './encrypted_saved_objects.devdocs.json'; diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index 31ce75f352a2b..959aa41bee249 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the enterpriseSearch plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] --- import enterpriseSearchObj from './enterprise_search.devdocs.json'; diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index cebbea3aa42b1..5244efee18bf2 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github description: API docs for the esUiShared plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] --- import esUiSharedObj from './es_ui_shared.devdocs.json'; diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index cc9070788f35c..3a1cbe07c2426 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotation plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] --- import eventAnnotationObj from './event_annotation.devdocs.json'; diff --git a/api_docs/event_annotation_listing.mdx b/api_docs/event_annotation_listing.mdx index 059381734ebd4..58bcbab03f923 100644 --- a/api_docs/event_annotation_listing.mdx +++ b/api_docs/event_annotation_listing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotationListing title: "eventAnnotationListing" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotationListing plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotationListing'] --- import eventAnnotationListingObj from './event_annotation_listing.devdocs.json'; diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index bc0599a12a058..6c6eb503d46d8 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github description: API docs for the eventLog plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; diff --git a/api_docs/exploratory_view.mdx b/api_docs/exploratory_view.mdx index 0c5e1b26620d6..c845068d49099 100644 --- a/api_docs/exploratory_view.mdx +++ b/api_docs/exploratory_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/exploratoryView title: "exploratoryView" image: https://source.unsplash.com/400x175/?github description: API docs for the exploratoryView plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'exploratoryView'] --- import exploratoryViewObj from './exploratory_view.devdocs.json'; diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index 910ad5fcb79dc..2c26256a58515 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionError plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] --- import expressionErrorObj from './expression_error.devdocs.json'; diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index 6c9ac5adda786..e0796e969a312 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionGauge plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] --- import expressionGaugeObj from './expression_gauge.devdocs.json'; diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index 192bc924a0157..d2ea8a0e64c6b 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionHeatmap plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] --- import expressionHeatmapObj from './expression_heatmap.devdocs.json'; diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index b24dc2180c5ff..58a5a495a84ae 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionImage plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] --- import expressionImageObj from './expression_image.devdocs.json'; diff --git a/api_docs/expression_legacy_metric_vis.mdx b/api_docs/expression_legacy_metric_vis.mdx index 847888ad3d734..bd2ee88518a4f 100644 --- a/api_docs/expression_legacy_metric_vis.mdx +++ b/api_docs/expression_legacy_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionLegacyMetricVis title: "expressionLegacyMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionLegacyMetricVis plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionLegacyMetricVis'] --- import expressionLegacyMetricVisObj from './expression_legacy_metric_vis.devdocs.json'; diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index 05e169cdacb4f..dd579c1cef1a2 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetric plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] --- import expressionMetricObj from './expression_metric.devdocs.json'; diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index b84b768bd7765..5ff8e85f61f95 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetricVis plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] --- import expressionMetricVisObj from './expression_metric_vis.devdocs.json'; diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index e9741fccf718a..5153d966efe57 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionPartitionVis plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] --- import expressionPartitionVisObj from './expression_partition_vis.devdocs.json'; diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index ef330144a3107..596cc2f7e6b8c 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRepeatImage plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] --- import expressionRepeatImageObj from './expression_repeat_image.devdocs.json'; diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index e29d8c3310626..f3d6ce4379b45 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRevealImage plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] --- import expressionRevealImageObj from './expression_reveal_image.devdocs.json'; diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index 951cb74b24a4b..435c1e00cdbb9 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionShape plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] --- import expressionShapeObj from './expression_shape.devdocs.json'; diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index f8dd39ea344b2..b5356fceb49af 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionTagcloud plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] --- import expressionTagcloudObj from './expression_tagcloud.devdocs.json'; diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index 6a8c5d756baeb..0ab68091b1ade 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionXY plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index 56fd7d9a3e9e2..31aac69f17f1f 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github description: API docs for the expressions plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] --- import expressionsObj from './expressions.devdocs.json'; diff --git a/api_docs/features.mdx b/api_docs/features.mdx index cce1d5fed656e..4355e1ae31535 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github description: API docs for the features plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] --- import featuresObj from './features.devdocs.json'; diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index 671764bec61f0..f2f150f5a4ec2 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github description: API docs for the fieldFormats plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] --- import fieldFormatsObj from './field_formats.devdocs.json'; diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index 836a66f011251..4c29d4ccf87ab 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github description: API docs for the fileUpload plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] --- import fileUploadObj from './file_upload.devdocs.json'; diff --git a/api_docs/files.mdx b/api_docs/files.mdx index 51c67ed1e825d..ad4072afdae89 100644 --- a/api_docs/files.mdx +++ b/api_docs/files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/files title: "files" image: https://source.unsplash.com/400x175/?github description: API docs for the files plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; diff --git a/api_docs/files_management.mdx b/api_docs/files_management.mdx index 9b30702e06533..77114d365fd8f 100644 --- a/api_docs/files_management.mdx +++ b/api_docs/files_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/filesManagement title: "filesManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the filesManagement plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'filesManagement'] --- import filesManagementObj from './files_management.devdocs.json'; diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index c9116c07c7ca6..d64ad71a0c38b 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github description: API docs for the fleet plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index 80c2deb4cd624..169aa8c614bcd 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the globalSearch plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] --- import globalSearchObj from './global_search.devdocs.json'; diff --git a/api_docs/guided_onboarding.mdx b/api_docs/guided_onboarding.mdx index a4d1a189f4d6f..086213d6a81d3 100644 --- a/api_docs/guided_onboarding.mdx +++ b/api_docs/guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/guidedOnboarding title: "guidedOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the guidedOnboarding plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'guidedOnboarding'] --- import guidedOnboardingObj from './guided_onboarding.devdocs.json'; diff --git a/api_docs/home.mdx b/api_docs/home.mdx index 2d95568af2c61..e4e7d60a20fae 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github description: API docs for the home plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/image_embeddable.mdx b/api_docs/image_embeddable.mdx index 58557d1df0f81..f6fcdf2c591bb 100644 --- a/api_docs/image_embeddable.mdx +++ b/api_docs/image_embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/imageEmbeddable title: "imageEmbeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the imageEmbeddable plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'imageEmbeddable'] --- import imageEmbeddableObj from './image_embeddable.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index 605daf26ab6e8..a40b0acf29873 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexLifecycleManagement plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] --- import indexLifecycleManagementObj from './index_lifecycle_management.devdocs.json'; diff --git a/api_docs/index_management.devdocs.json b/api_docs/index_management.devdocs.json index 930f9a12fe45b..f8c8586ef5d98 100644 --- a/api_docs/index_management.devdocs.json +++ b/api_docs/index_management.devdocs.json @@ -2766,6 +2766,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "indexManagement", + "id": "def-common.TemplateDeserialized.ignoreMissingComponentTemplates", + "type": "Array", + "tags": [], + "label": "ignoreMissingComponentTemplates", + "description": [], + "signature": [ + "string[] | undefined" + ], + "path": "x-pack/plugins/index_management/common/types/templates.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "indexManagement", "id": "def-common.TemplateDeserialized.version", @@ -3089,6 +3103,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "indexManagement", + "id": "def-common.TemplateListItem.composedOf", + "type": "Array", + "tags": [], + "label": "composedOf", + "description": [], + "signature": [ + "string[] | undefined" + ], + "path": "x-pack/plugins/index_management/common/types/templates.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "indexManagement", "id": "def-common.TemplateListItem._kbnMeta", @@ -3209,6 +3237,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "indexManagement", + "id": "def-common.TemplateSerialized.ignore_missing_component_templates", + "type": "Array", + "tags": [], + "label": "ignore_missing_component_templates", + "description": [], + "signature": [ + "string[] | undefined" + ], + "path": "x-pack/plugins/index_management/common/types/templates.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "indexManagement", "id": "def-common.TemplateSerialized.version", diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index 84ce30e93ad1e..ef262558f1113 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexManagement plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] --- import indexManagementObj from './index_management.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/platform-deployment-management](https://github.com/orgs/elasti | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 223 | 0 | 218 | 4 | +| 226 | 0 | 221 | 4 | ## Client diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index 4f031fef2daf0..9e9fb18d712a1 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github description: API docs for the infra plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] --- import infraObj from './infra.devdocs.json'; diff --git a/api_docs/ingest_pipelines.mdx b/api_docs/ingest_pipelines.mdx index 1e230e88c34f0..55c2fe1db07c7 100644 --- a/api_docs/ingest_pipelines.mdx +++ b/api_docs/ingest_pipelines.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ingestPipelines title: "ingestPipelines" image: https://source.unsplash.com/400x175/?github description: API docs for the ingestPipelines plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ingestPipelines'] --- import ingestPipelinesObj from './ingest_pipelines.devdocs.json'; diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index 2df6572eec141..e91a6e420a1db 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github description: API docs for the inspector plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] --- import inspectorObj from './inspector.devdocs.json'; diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index e8dd04b37bde9..619a88c16d339 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github description: API docs for the interactiveSetup plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] --- import interactiveSetupObj from './interactive_setup.devdocs.json'; diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx index 1f8f965aae613..80fc01383bc6b 100644 --- a/api_docs/kbn_ace.mdx +++ b/api_docs/kbn_ace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ace title: "@kbn/ace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ace plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] --- import kbnAceObj from './kbn_ace.devdocs.json'; diff --git a/api_docs/kbn_actions_types.mdx b/api_docs/kbn_actions_types.mdx index 0763ecf7f78ca..28296500042c7 100644 --- a/api_docs/kbn_actions_types.mdx +++ b/api_docs/kbn_actions_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-actions-types title: "@kbn/actions-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/actions-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/actions-types'] --- import kbnActionsTypesObj from './kbn_actions_types.devdocs.json'; diff --git a/api_docs/kbn_aiops_components.mdx b/api_docs/kbn_aiops_components.mdx index c6126bde35517..47eb03dd8e885 100644 --- a/api_docs/kbn_aiops_components.mdx +++ b/api_docs/kbn_aiops_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-components title: "@kbn/aiops-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-components plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-components'] --- import kbnAiopsComponentsObj from './kbn_aiops_components.devdocs.json'; diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index 2362583ae493e..4094b48259a8a 100644 --- a/api_docs/kbn_aiops_utils.mdx +++ b/api_docs/kbn_aiops_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-utils title: "@kbn/aiops-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] --- import kbnAiopsUtilsObj from './kbn_aiops_utils.devdocs.json'; diff --git a/api_docs/kbn_alerting_api_integration_helpers.mdx b/api_docs/kbn_alerting_api_integration_helpers.mdx index adae00f67b092..af33915564639 100644 --- a/api_docs/kbn_alerting_api_integration_helpers.mdx +++ b/api_docs/kbn_alerting_api_integration_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-api-integration-helpers title: "@kbn/alerting-api-integration-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-api-integration-helpers plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-api-integration-helpers'] --- import kbnAlertingApiIntegrationHelpersObj from './kbn_alerting_api_integration_helpers.devdocs.json'; diff --git a/api_docs/kbn_alerting_state_types.mdx b/api_docs/kbn_alerting_state_types.mdx index 6cf39a09235b2..a398d622da1c1 100644 --- a/api_docs/kbn_alerting_state_types.mdx +++ b/api_docs/kbn_alerting_state_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-state-types title: "@kbn/alerting-state-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-state-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-state-types'] --- import kbnAlertingStateTypesObj from './kbn_alerting_state_types.devdocs.json'; diff --git a/api_docs/kbn_alerting_types.mdx b/api_docs/kbn_alerting_types.mdx index dd559ffd26fe5..81c9403ac0c35 100644 --- a/api_docs/kbn_alerting_types.mdx +++ b/api_docs/kbn_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-types title: "@kbn/alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-types'] --- import kbnAlertingTypesObj from './kbn_alerting_types.devdocs.json'; diff --git a/api_docs/kbn_alerts_as_data_utils.mdx b/api_docs/kbn_alerts_as_data_utils.mdx index 0808269e690c2..748e0c9d0ff58 100644 --- a/api_docs/kbn_alerts_as_data_utils.mdx +++ b/api_docs/kbn_alerts_as_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-as-data-utils title: "@kbn/alerts-as-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-as-data-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-as-data-utils'] --- import kbnAlertsAsDataUtilsObj from './kbn_alerts_as_data_utils.devdocs.json'; diff --git a/api_docs/kbn_alerts_ui_shared.mdx b/api_docs/kbn_alerts_ui_shared.mdx index 4456a7af94562..59fade1071a09 100644 --- a/api_docs/kbn_alerts_ui_shared.mdx +++ b/api_docs/kbn_alerts_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-ui-shared title: "@kbn/alerts-ui-shared" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-ui-shared plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-ui-shared'] --- import kbnAlertsUiSharedObj from './kbn_alerts_ui_shared.devdocs.json'; diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index 2c154af34afe2..50efd953efd64 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] --- import kbnAnalyticsObj from './kbn_analytics.devdocs.json'; diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index 3a3ddaf341bb6..43792105a00d8 100644 --- a/api_docs/kbn_analytics_client.mdx +++ b/api_docs/kbn_analytics_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-client title: "@kbn/analytics-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-client plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] --- import kbnAnalyticsClientObj from './kbn_analytics_client.devdocs.json'; diff --git a/api_docs/kbn_analytics_collection_utils.mdx b/api_docs/kbn_analytics_collection_utils.mdx index 938d7d35faf27..05d494990f347 100644 --- a/api_docs/kbn_analytics_collection_utils.mdx +++ b/api_docs/kbn_analytics_collection_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-collection-utils title: "@kbn/analytics-collection-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-collection-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-collection-utils'] --- import kbnAnalyticsCollectionUtilsObj from './kbn_analytics_collection_utils.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx index 6429e4fd664f6..4f6b89f965ee4 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-browser title: "@kbn/analytics-shippers-elastic-v3-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-browser'] --- import kbnAnalyticsShippersElasticV3BrowserObj from './kbn_analytics_shippers_elastic_v3_browser.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx index 1dd782b9c7090..5a0cc6c7c59b7 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-common title: "@kbn/analytics-shippers-elastic-v3-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-common'] --- import kbnAnalyticsShippersElasticV3CommonObj from './kbn_analytics_shippers_elastic_v3_common.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx index 7dfc5486d4500..556ff060fdd07 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-server title: "@kbn/analytics-shippers-elastic-v3-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-server'] --- import kbnAnalyticsShippersElasticV3ServerObj from './kbn_analytics_shippers_elastic_v3_server.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_fullstory.mdx b/api_docs/kbn_analytics_shippers_fullstory.mdx index 96aa885356288..fd8a277edad3f 100644 --- a/api_docs/kbn_analytics_shippers_fullstory.mdx +++ b/api_docs/kbn_analytics_shippers_fullstory.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-fullstory title: "@kbn/analytics-shippers-fullstory" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-fullstory plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index ed1531ec2001b..27160f3e49f68 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-config-loader plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] --- import kbnApmConfigLoaderObj from './kbn_apm_config_loader.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace.mdx b/api_docs/kbn_apm_synthtrace.mdx index 3e1e47dfb25ef..bde6dc15d68c5 100644 --- a/api_docs/kbn_apm_synthtrace.mdx +++ b/api_docs/kbn_apm_synthtrace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace title: "@kbn/apm-synthtrace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace_client.devdocs.json b/api_docs/kbn_apm_synthtrace_client.devdocs.json index 7c7c53d407261..a84b8813a4500 100644 --- a/api_docs/kbn_apm_synthtrace_client.devdocs.json +++ b/api_docs/kbn_apm_synthtrace_client.devdocs.json @@ -2622,7 +2622,7 @@ "label": "LogDocument", "description": [], "signature": [ - "{ '@timestamp'?: number | undefined; } & Partial<{ 'input.type': string; 'log.file.path'?: string | undefined; 'service.name'?: string | undefined; 'data_stream.namespace': string; 'data_stream.type': string; 'data_stream.dataset': string; message?: string | undefined; 'error.message'?: string | undefined; 'event.original'?: string | undefined; 'event.dataset': string; 'log.level'?: string | undefined; 'host.name'?: string | undefined; 'trace.id'?: string | undefined; 'agent.id'?: string | undefined; 'agent.name'?: string | undefined; 'orchestrator.cluster.name'?: string | undefined; 'orchestrator.cluster.id'?: string | undefined; 'orchestrator.resource.id'?: string | undefined; 'orchestrator.namespace'?: string | undefined; 'container.name'?: string | undefined; 'cloud.provider'?: string | undefined; 'cloud.region'?: string | undefined; 'cloud.availability_zone'?: string | undefined; 'cloud.project.id'?: string | undefined; 'cloud.instance.id'?: string | undefined; }>" + "{ '@timestamp'?: number | undefined; } & Partial<{ 'input.type': string; 'log.file.path'?: string | undefined; 'service.name'?: string | undefined; 'data_stream.namespace': string; 'data_stream.type': string; 'data_stream.dataset': string; message?: string | undefined; 'error.message'?: string | undefined; 'event.original'?: string | undefined; 'event.dataset': string; 'log.level'?: string | undefined; 'host.name'?: string | undefined; 'trace.id'?: string | undefined; 'agent.id'?: string | undefined; 'agent.name'?: string | undefined; 'orchestrator.cluster.name'?: string | undefined; 'orchestrator.cluster.id'?: string | undefined; 'orchestrator.resource.id'?: string | undefined; 'orchestrator.namespace'?: string | undefined; 'container.name'?: string | undefined; 'cloud.provider'?: string | undefined; 'cloud.region'?: string | undefined; 'cloud.availability_zone'?: string | undefined; 'cloud.project.id'?: string | undefined; 'cloud.instance.id'?: string | undefined; 'error.stack_trace'?: string | undefined; 'error.exception.stacktrace'?: string | undefined; 'error.log.stacktrace'?: string | undefined; }>" ], "path": "packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts", "deprecated": false, diff --git a/api_docs/kbn_apm_synthtrace_client.mdx b/api_docs/kbn_apm_synthtrace_client.mdx index 25d3e9cc79ec3..8399c7485e42c 100644 --- a/api_docs/kbn_apm_synthtrace_client.mdx +++ b/api_docs/kbn_apm_synthtrace_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace-client title: "@kbn/apm-synthtrace-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace-client plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace-client'] --- import kbnApmSynthtraceClientObj from './kbn_apm_synthtrace_client.devdocs.json'; diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index 0ac7a3a59f241..fbfd6bdd0aad8 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] --- import kbnApmUtilsObj from './kbn_apm_utils.devdocs.json'; diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index f75fe45564b3b..6a612449687be 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/axe-config plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_bfetch_error.mdx b/api_docs/kbn_bfetch_error.mdx index d46de8af8627c..f955c3ff8bfa3 100644 --- a/api_docs/kbn_bfetch_error.mdx +++ b/api_docs/kbn_bfetch_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-bfetch-error title: "@kbn/bfetch-error" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/bfetch-error plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/bfetch-error'] --- import kbnBfetchErrorObj from './kbn_bfetch_error.devdocs.json'; diff --git a/api_docs/kbn_calculate_auto.mdx b/api_docs/kbn_calculate_auto.mdx index 602a8996070c3..38b63c0c931cb 100644 --- a/api_docs/kbn_calculate_auto.mdx +++ b/api_docs/kbn_calculate_auto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-calculate-auto title: "@kbn/calculate-auto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/calculate-auto plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/calculate-auto'] --- import kbnCalculateAutoObj from './kbn_calculate_auto.devdocs.json'; diff --git a/api_docs/kbn_calculate_width_from_char_count.mdx b/api_docs/kbn_calculate_width_from_char_count.mdx index f861e1c0050c4..906cac0b05fda 100644 --- a/api_docs/kbn_calculate_width_from_char_count.mdx +++ b/api_docs/kbn_calculate_width_from_char_count.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-calculate-width-from-char-count title: "@kbn/calculate-width-from-char-count" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/calculate-width-from-char-count plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/calculate-width-from-char-count'] --- import kbnCalculateWidthFromCharCountObj from './kbn_calculate_width_from_char_count.devdocs.json'; diff --git a/api_docs/kbn_cases_components.mdx b/api_docs/kbn_cases_components.mdx index 1767a5304bdc0..07ea440bf6979 100644 --- a/api_docs/kbn_cases_components.mdx +++ b/api_docs/kbn_cases_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cases-components title: "@kbn/cases-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cases-components plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cases-components'] --- import kbnCasesComponentsObj from './kbn_cases_components.devdocs.json'; diff --git a/api_docs/kbn_cell_actions.mdx b/api_docs/kbn_cell_actions.mdx index 1ec35fd6fe8f5..76ab0103ac06f 100644 --- a/api_docs/kbn_cell_actions.mdx +++ b/api_docs/kbn_cell_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cell-actions title: "@kbn/cell-actions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cell-actions plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cell-actions'] --- import kbnCellActionsObj from './kbn_cell_actions.devdocs.json'; diff --git a/api_docs/kbn_chart_expressions_common.mdx b/api_docs/kbn_chart_expressions_common.mdx index b771c0d3d6920..ed49298af5f61 100644 --- a/api_docs/kbn_chart_expressions_common.mdx +++ b/api_docs/kbn_chart_expressions_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-expressions-common title: "@kbn/chart-expressions-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-expressions-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-expressions-common'] --- import kbnChartExpressionsCommonObj from './kbn_chart_expressions_common.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index cd2a38645c91e..fe7dbe24dd064 100644 --- a/api_docs/kbn_chart_icons.mdx +++ b/api_docs/kbn_chart_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-icons title: "@kbn/chart-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-icons plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-icons'] --- import kbnChartIconsObj from './kbn_chart_icons.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index 0b55255cef877..55f01999c743b 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-core plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] --- import kbnCiStatsCoreObj from './kbn_ci_stats_core.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_performance_metrics.mdx b/api_docs/kbn_ci_stats_performance_metrics.mdx index 58fb180401961..d145d3d1d4fca 100644 --- a/api_docs/kbn_ci_stats_performance_metrics.mdx +++ b/api_docs/kbn_ci_stats_performance_metrics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-performance-metrics title: "@kbn/ci-stats-performance-metrics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-performance-metrics plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-performance-metrics'] --- import kbnCiStatsPerformanceMetricsObj from './kbn_ci_stats_performance_metrics.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index b9335f88725a8..4a8ac49cea9c1 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-reporter plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] --- import kbnCiStatsReporterObj from './kbn_ci_stats_reporter.devdocs.json'; diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index c9c712c09b190..35725be6673fb 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cli-dev-mode plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_code_editor.mdx b/api_docs/kbn_code_editor.mdx index 320fcdf6a12d8..25484cf59b24b 100644 --- a/api_docs/kbn_code_editor.mdx +++ b/api_docs/kbn_code_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor title: "@kbn/code-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor'] --- import kbnCodeEditorObj from './kbn_code_editor.devdocs.json'; diff --git a/api_docs/kbn_code_editor_mock.mdx b/api_docs/kbn_code_editor_mock.mdx index 6353e16c8bcfa..d02ec2122933f 100644 --- a/api_docs/kbn_code_editor_mock.mdx +++ b/api_docs/kbn_code_editor_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor-mock title: "@kbn/code-editor-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor-mock plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor-mock'] --- import kbnCodeEditorMockObj from './kbn_code_editor_mock.devdocs.json'; diff --git a/api_docs/kbn_code_owners.mdx b/api_docs/kbn_code_owners.mdx index b500748561914..3457cf8385e93 100644 --- a/api_docs/kbn_code_owners.mdx +++ b/api_docs/kbn_code_owners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-owners title: "@kbn/code-owners" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-owners plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-owners'] --- import kbnCodeOwnersObj from './kbn_code_owners.devdocs.json'; diff --git a/api_docs/kbn_coloring.devdocs.json b/api_docs/kbn_coloring.devdocs.json index 64eb4e83e7721..16719bc0c8a0e 100644 --- a/api_docs/kbn_coloring.devdocs.json +++ b/api_docs/kbn_coloring.devdocs.json @@ -486,7 +486,9 @@ "ColorCode", " | ", "GradientColor", - ", getPaletteFn: (paletteId: string) => ", + " | (", + "LoopColor", + " & { paletteId: string; colorIndex: number; }), getPaletteFn: (paletteId: string) => ", "CategoricalPalette", ", isDarkMode: boolean, index: number, total: number) => string" ], @@ -523,7 +525,10 @@ " | ", "ColorCode", " | ", - "GradientColor" + "GradientColor", + " | (", + "LoopColor", + " & { paletteId: string; colorIndex: number; })" ], "path": "packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts", "deprecated": false, @@ -607,7 +612,9 @@ "CategoricalColor", " | ", "ColorCode", - ", getPaletteFn: (paletteId: string) => ", + " | (", + "LoopColor", + " & { paletteId: string; colorIndex: number; }), getPaletteFn: (paletteId: string) => ", "CategoricalPalette", ", isDarkMode: boolean) => string" ], @@ -625,7 +632,10 @@ "signature": [ "CategoricalColor", " | ", - "ColorCode" + "ColorCode", + " | (", + "LoopColor", + " & { paletteId: string; colorIndex: number; })" ], "path": "packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts", "deprecated": false, @@ -708,7 +718,7 @@ "label": "getColorFactory", "description": [], "signature": [ - "(model: ", + "({ assignments, specialAssignments, colorMode, paletteId }: ", "Config", ", getPaletteFn: (paletteId: string) => ", "CategoricalPalette", @@ -731,7 +741,7 @@ "id": "def-common.getColorFactory.$1", "type": "Object", "tags": [], - "label": "model", + "label": "{ assignments, specialAssignments, colorMode, paletteId }", "description": [], "signature": [ "Config" @@ -2892,6 +2902,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/coloring", + "id": "def-common.DEFAULT_OTHER_ASSIGNMENT_INDEX", + "type": "number", + "tags": [], + "label": "DEFAULT_OTHER_ASSIGNMENT_INDEX", + "description": [], + "signature": [ + "0" + ], + "path": "packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/coloring", "id": "def-common.DEFAULT_PALETTE_NAME", @@ -3113,20 +3138,6 @@ "deprecated": false, "trackAdoption": false, "children": [ - { - "parentPluginId": "@kbn/coloring", - "id": "def-common.DEFAULT_COLOR_MAPPING_CONFIG.assignmentMode", - "type": "string", - "tags": [], - "label": "assignmentMode", - "description": [], - "signature": [ - "\"auto\"" - ], - "path": "packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts", - "deprecated": false, - "trackAdoption": false - }, { "parentPluginId": "@kbn/coloring", "id": "def-common.DEFAULT_COLOR_MAPPING_CONFIG.assignments", @@ -3149,7 +3160,7 @@ "label": "specialAssignments", "description": [], "signature": [ - "{ rule: { type: \"other\"; }; color: { type: \"categorical\"; paletteId: string; colorIndex: number; }; touched: false; }[]" + "{ rule: { type: \"other\"; }; color: { type: \"loop\"; }; touched: false; }[]" ], "path": "packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts", "deprecated": false, @@ -3262,7 +3273,7 @@ "label": "getColor", "description": [], "signature": [ - "(valueInRange: number) => string" + "(indexInRange: number, isDarkMode: boolean, loop: boolean) => string" ], "path": "packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts", "deprecated": false, @@ -3273,7 +3284,7 @@ "id": "def-common.ElasticBrandPalette.getColor.$1", "type": "number", "tags": [], - "label": "valueInRange", + "label": "indexInRange", "description": [], "signature": [ "number" @@ -3282,6 +3293,36 @@ "deprecated": false, "trackAdoption": false, "isRequired": true + }, + { + "parentPluginId": "@kbn/coloring", + "id": "def-common.ElasticBrandPalette.getColor.$2", + "type": "boolean", + "tags": [], + "label": "isDarkMode", + "description": [], + "signature": [ + "boolean" + ], + "path": "packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/coloring", + "id": "def-common.ElasticBrandPalette.getColor.$3", + "type": "boolean", + "tags": [], + "label": "loop", + "description": [], + "signature": [ + "boolean" + ], + "path": "packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true } ], "returnComment": [] @@ -3355,7 +3396,7 @@ "label": "getColor", "description": [], "signature": [ - "(valueInRange: number) => string" + "(indexInRange: number, isDarkMode: boolean, loop: boolean) => string" ], "path": "packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts", "deprecated": false, @@ -3366,7 +3407,7 @@ "id": "def-common.EUIAmsterdamColorBlindPalette.getColor.$1", "type": "number", "tags": [], - "label": "valueInRange", + "label": "indexInRange", "description": [], "signature": [ "number" @@ -3375,6 +3416,36 @@ "deprecated": false, "trackAdoption": false, "isRequired": true + }, + { + "parentPluginId": "@kbn/coloring", + "id": "def-common.EUIAmsterdamColorBlindPalette.getColor.$2", + "type": "boolean", + "tags": [], + "label": "isDarkMode", + "description": [], + "signature": [ + "boolean" + ], + "path": "packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/coloring", + "id": "def-common.EUIAmsterdamColorBlindPalette.getColor.$3", + "type": "boolean", + "tags": [], + "label": "loop", + "description": [], + "signature": [ + "boolean" + ], + "path": "packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true } ], "returnComment": [] @@ -3448,7 +3519,7 @@ "label": "getColor", "description": [], "signature": [ - "(valueInRange: number) => string" + "(indexInRange: number, isDarkMode: boolean, loop: boolean) => string" ], "path": "packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts", "deprecated": false, @@ -3459,7 +3530,7 @@ "id": "def-common.KibanaV7LegacyPalette.getColor.$1", "type": "number", "tags": [], - "label": "valueInRange", + "label": "indexInRange", "description": [], "signature": [ "number" @@ -3468,6 +3539,36 @@ "deprecated": false, "trackAdoption": false, "isRequired": true + }, + { + "parentPluginId": "@kbn/coloring", + "id": "def-common.KibanaV7LegacyPalette.getColor.$2", + "type": "boolean", + "tags": [], + "label": "isDarkMode", + "description": [], + "signature": [ + "boolean" + ], + "path": "packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/coloring", + "id": "def-common.KibanaV7LegacyPalette.getColor.$3", + "type": "boolean", + "tags": [], + "label": "loop", + "description": [], + "signature": [ + "boolean" + ], + "path": "packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true } ], "returnComment": [] diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index f675651096bce..c0f08ec8ec330 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/coloring plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] --- import kbnColoringObj from './kbn_coloring.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 211 | 0 | 174 | 8 | +| 217 | 0 | 180 | 9 | ## Common diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index 352c9fbf5774d..b409ac5cb81a9 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] --- import kbnConfigObj from './kbn_config.devdocs.json'; diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index b8fa2ab445158..d0a67372e1e3f 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] --- import kbnConfigMocksObj from './kbn_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 9bf2e8798378d..77761dafbacc3 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-schema plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_content_management_content_editor.devdocs.json b/api_docs/kbn_content_management_content_editor.devdocs.json index 63a1e49929db0..45ec5e84ec7c4 100644 --- a/api_docs/kbn_content_management_content_editor.devdocs.json +++ b/api_docs/kbn_content_management_content_editor.devdocs.json @@ -184,7 +184,7 @@ "signature": [ "{ onSave?: ((args: { id: string; title: string; description?: string | undefined; tags: string[]; }) => Promise) | undefined; item: ", "Item", - "; isReadonly?: boolean | undefined; entityName: string; customValidators?: ", + "; isReadonly?: boolean | undefined; readonlyReason?: string | undefined; entityName: string; customValidators?: ", "CustomValidators", " | undefined; }" ], diff --git a/api_docs/kbn_content_management_content_editor.mdx b/api_docs/kbn_content_management_content_editor.mdx index 288b0cd8429f2..64cfbfaf3d388 100644 --- a/api_docs/kbn_content_management_content_editor.mdx +++ b/api_docs/kbn_content_management_content_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-content-editor title: "@kbn/content-management-content-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-content-editor plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-content-editor'] --- import kbnContentManagementContentEditorObj from './kbn_content_management_content_editor.devdocs.json'; diff --git a/api_docs/kbn_content_management_tabbed_table_list_view.mdx b/api_docs/kbn_content_management_tabbed_table_list_view.mdx index 83f0afa466430..a44c32d310b61 100644 --- a/api_docs/kbn_content_management_tabbed_table_list_view.mdx +++ b/api_docs/kbn_content_management_tabbed_table_list_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-tabbed-table-list-view title: "@kbn/content-management-tabbed-table-list-view" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-tabbed-table-list-view plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-tabbed-table-list-view'] --- import kbnContentManagementTabbedTableListViewObj from './kbn_content_management_tabbed_table_list_view.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list_view.devdocs.json b/api_docs/kbn_content_management_table_list_view.devdocs.json index 6710f11d5bd94..99b7b1265d65d 100644 --- a/api_docs/kbn_content_management_table_list_view.devdocs.json +++ b/api_docs/kbn_content_management_table_list_view.devdocs.json @@ -19,7 +19,7 @@ "section": "def-common.UserContentCommonSchema", "text": "UserContentCommonSchema" }, - ">({ title, description, entityName, entityNamePlural, initialFilter, headingId, initialPageSize, listingLimit, urlStateEnabled, customTableColumn, emptyPrompt, findItems, createItem, editItem, deleteItems, getDetailViewLink, onClickTitle, rowItemActions, id: listingId, contentEditor, children, titleColumnName, additionalRightSideActions, withoutPageTemplateWrapper, itemIsEditable, }: ", + ">({ title, description, entityName, entityNamePlural, initialFilter, headingId, initialPageSize, listingLimit, urlStateEnabled, customTableColumn, emptyPrompt, findItems, createItem, editItem, deleteItems, getDetailViewLink, onClickTitle, rowItemActions, id: listingId, contentEditor, children, titleColumnName, additionalRightSideActions, withoutPageTemplateWrapper, }: ", { "pluginId": "@kbn/content-management-table-list-view", "scope": "public", @@ -38,7 +38,7 @@ "id": "def-public.TableListView.$1", "type": "CompoundType", "tags": [], - "label": "{\n title,\n description,\n entityName,\n entityNamePlural,\n initialFilter,\n headingId,\n initialPageSize,\n listingLimit,\n urlStateEnabled = true,\n customTableColumn,\n emptyPrompt,\n findItems,\n createItem,\n editItem,\n deleteItems,\n getDetailViewLink,\n onClickTitle,\n rowItemActions,\n id: listingId,\n contentEditor,\n children,\n titleColumnName,\n additionalRightSideActions,\n withoutPageTemplateWrapper,\n itemIsEditable,\n}", + "label": "{\n title,\n description,\n entityName,\n entityNamePlural,\n initialFilter,\n headingId,\n initialPageSize,\n listingLimit,\n urlStateEnabled = true,\n customTableColumn,\n emptyPrompt,\n findItems,\n createItem,\n editItem,\n deleteItems,\n getDetailViewLink,\n onClickTitle,\n rowItemActions,\n id: listingId,\n contentEditor,\n children,\n titleColumnName,\n additionalRightSideActions,\n withoutPageTemplateWrapper,\n}", "description": [], "signature": [ { @@ -79,7 +79,7 @@ "section": "def-public.TableListViewTableProps", "text": "TableListViewTableProps" }, - ", \"id\" | \"entityName\" | \"entityNamePlural\" | \"initialFilter\" | \"headingId\" | \"initialPageSize\" | \"listingLimit\" | \"urlStateEnabled\" | \"customTableColumn\" | \"emptyPrompt\" | \"findItems\" | \"createItem\" | \"editItem\" | \"deleteItems\" | \"getDetailViewLink\" | \"onClickTitle\" | \"rowItemActions\" | \"contentEditor\" | \"titleColumnName\" | \"withoutPageTemplateWrapper\" | \"itemIsEditable\"> & { title: string; description?: string | undefined; additionalRightSideActions?: React.ReactNode[] | undefined; children?: React.ReactNode; }" + ", \"id\" | \"entityName\" | \"entityNamePlural\" | \"initialFilter\" | \"headingId\" | \"initialPageSize\" | \"listingLimit\" | \"urlStateEnabled\" | \"customTableColumn\" | \"emptyPrompt\" | \"findItems\" | \"createItem\" | \"editItem\" | \"deleteItems\" | \"getDetailViewLink\" | \"onClickTitle\" | \"rowItemActions\" | \"contentEditor\" | \"titleColumnName\" | \"withoutPageTemplateWrapper\"> & { title: string; description?: string | undefined; additionalRightSideActions?: React.ReactNode[] | undefined; children?: React.ReactNode; }" ], "path": "packages/content-management/table_list_view/src/table_list_view.tsx", "deprecated": false, diff --git a/api_docs/kbn_content_management_table_list_view.mdx b/api_docs/kbn_content_management_table_list_view.mdx index 29e3e3fb6e9de..c1c4fa33ae5da 100644 --- a/api_docs/kbn_content_management_table_list_view.mdx +++ b/api_docs/kbn_content_management_table_list_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list-view title: "@kbn/content-management-table-list-view" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list-view plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list-view'] --- import kbnContentManagementTableListViewObj from './kbn_content_management_table_list_view.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list_view_common.mdx b/api_docs/kbn_content_management_table_list_view_common.mdx index 3caf4dedb360d..b8241df42b20e 100644 --- a/api_docs/kbn_content_management_table_list_view_common.mdx +++ b/api_docs/kbn_content_management_table_list_view_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list-view-common title: "@kbn/content-management-table-list-view-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list-view-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list-view-common'] --- import kbnContentManagementTableListViewCommonObj from './kbn_content_management_table_list_view_common.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list_view_table.devdocs.json b/api_docs/kbn_content_management_table_list_view_table.devdocs.json index a35fd912e140e..218d6016ccdee 100644 --- a/api_docs/kbn_content_management_table_list_view_table.devdocs.json +++ b/api_docs/kbn_content_management_table_list_view_table.devdocs.json @@ -109,7 +109,7 @@ "section": "def-common.UserContentCommonSchema", "text": "UserContentCommonSchema" }, - ">({ tableCaption, entityName, entityNamePlural, initialFilter: initialQuery, headingId, initialPageSize, listingLimit, urlStateEnabled, customTableColumn, emptyPrompt, rowItemActions, findItems, createItem, editItem, itemIsEditable, deleteItems, getDetailViewLink, onClickTitle, id: listingId, contentEditor, titleColumnName, withoutPageTemplateWrapper, onFetchSuccess, refreshListBouncer, setPageDataTestSubject, }: ", + ">({ tableCaption, entityName, entityNamePlural, initialFilter: initialQuery, headingId, initialPageSize, listingLimit, urlStateEnabled, customTableColumn, emptyPrompt, rowItemActions, findItems, createItem, editItem, deleteItems, getDetailViewLink, onClickTitle, id: listingId, contentEditor, titleColumnName, withoutPageTemplateWrapper, onFetchSuccess, refreshListBouncer, setPageDataTestSubject, }: ", { "pluginId": "@kbn/content-management-table-list-view-table", "scope": "public", @@ -775,40 +775,6 @@ ], "returnComment": [] }, - { - "parentPluginId": "@kbn/content-management-table-list-view-table", - "id": "def-public.TableListViewTableProps.itemIsEditable", - "type": "Function", - "tags": [], - "label": "itemIsEditable", - "description": [ - "\nHandler to set edit action visiblity, and content editor readonly state per item. If not provided all non-managed items are considered editable. Note: Items with the managed property set to true will always be non-editable." - ], - "signature": [ - "((item: T) => boolean) | undefined" - ], - "path": "packages/content-management/table_list_view_table/src/table_list_view_table.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/content-management-table-list-view-table", - "id": "def-public.TableListViewTableProps.itemIsEditable.$1", - "type": "Uncategorized", - "tags": [], - "label": "item", - "description": [], - "signature": [ - "T" - ], - "path": "packages/content-management/table_list_view_table/src/table_list_view_table.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] - }, { "parentPluginId": "@kbn/content-management-table-list-view-table", "id": "def-public.TableListViewTableProps.titleColumnName", @@ -961,7 +927,7 @@ "label": "RowActions", "description": [], "signature": [ - "{ delete?: { enabled: boolean; reason?: string | undefined; } | undefined; }" + "{ delete?: { enabled: boolean; reason?: string | undefined; } | undefined; edit?: { enabled: boolean; reason?: string | undefined; } | undefined; }" ], "path": "packages/content-management/table_list_view_table/src/types.ts", "deprecated": false, diff --git a/api_docs/kbn_content_management_table_list_view_table.mdx b/api_docs/kbn_content_management_table_list_view_table.mdx index 43b3aa976cf31..35302060bb561 100644 --- a/api_docs/kbn_content_management_table_list_view_table.mdx +++ b/api_docs/kbn_content_management_table_list_view_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list-view-table title: "@kbn/content-management-table-list-view-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list-view-table plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list-view-table'] --- import kbnContentManagementTableListViewTableObj from './kbn_content_management_table_list_view_table.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sh | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 52 | 0 | 34 | 3 | +| 50 | 0 | 33 | 3 | ## Client diff --git a/api_docs/kbn_content_management_utils.mdx b/api_docs/kbn_content_management_utils.mdx index 0f4c9036bee49..8b308d064e6e4 100644 --- a/api_docs/kbn_content_management_utils.mdx +++ b/api_docs/kbn_content_management_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-utils title: "@kbn/content-management-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-utils'] --- import kbnContentManagementUtilsObj from './kbn_content_management_utils.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index 5c4feb84eb1ba..afb48904fa94c 100644 --- a/api_docs/kbn_core_analytics_browser.mdx +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser title: "@kbn/core-analytics-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] --- import kbnCoreAnalyticsBrowserObj from './kbn_core_analytics_browser.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx index 07136a907a3bf..ecbf9052304ed 100644 --- a/api_docs/kbn_core_analytics_browser_internal.mdx +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal title: "@kbn/core-analytics-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] --- import kbnCoreAnalyticsBrowserInternalObj from './kbn_core_analytics_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx index 63258f3c47d15..778f1f4f71bea 100644 --- a/api_docs/kbn_core_analytics_browser_mocks.mdx +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks title: "@kbn/core-analytics-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] --- import kbnCoreAnalyticsBrowserMocksObj from './kbn_core_analytics_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server.mdx b/api_docs/kbn_core_analytics_server.mdx index 695d28c0134d8..63d59ccb10f87 100644 --- a/api_docs/kbn_core_analytics_server.mdx +++ b/api_docs/kbn_core_analytics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server title: "@kbn/core-analytics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server'] --- import kbnCoreAnalyticsServerObj from './kbn_core_analytics_server.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_internal.mdx b/api_docs/kbn_core_analytics_server_internal.mdx index a2b32db67bddb..94922dc9157c2 100644 --- a/api_docs/kbn_core_analytics_server_internal.mdx +++ b/api_docs/kbn_core_analytics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-internal title: "@kbn/core-analytics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-internal'] --- import kbnCoreAnalyticsServerInternalObj from './kbn_core_analytics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_mocks.mdx b/api_docs/kbn_core_analytics_server_mocks.mdx index 244fcfaf34db5..2f6a910c4688c 100644 --- a/api_docs/kbn_core_analytics_server_mocks.mdx +++ b/api_docs/kbn_core_analytics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-mocks title: "@kbn/core-analytics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-mocks'] --- import kbnCoreAnalyticsServerMocksObj from './kbn_core_analytics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser.mdx b/api_docs/kbn_core_application_browser.mdx index 39ee36437db30..0cf719e5588c5 100644 --- a/api_docs/kbn_core_application_browser.mdx +++ b/api_docs/kbn_core_application_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser title: "@kbn/core-application-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser'] --- import kbnCoreApplicationBrowserObj from './kbn_core_application_browser.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_internal.mdx b/api_docs/kbn_core_application_browser_internal.mdx index 78467a9be966a..b6aa80659d319 100644 --- a/api_docs/kbn_core_application_browser_internal.mdx +++ b/api_docs/kbn_core_application_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-internal title: "@kbn/core-application-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-internal'] --- import kbnCoreApplicationBrowserInternalObj from './kbn_core_application_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_mocks.mdx b/api_docs/kbn_core_application_browser_mocks.mdx index c943b2ba31290..7cf8a8b00a93d 100644 --- a/api_docs/kbn_core_application_browser_mocks.mdx +++ b/api_docs/kbn_core_application_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-mocks title: "@kbn/core-application-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-mocks'] --- import kbnCoreApplicationBrowserMocksObj from './kbn_core_application_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_common.mdx b/api_docs/kbn_core_application_common.mdx index 9c6a61750d08a..7aad0aa4c5569 100644 --- a/api_docs/kbn_core_application_common.mdx +++ b/api_docs/kbn_core_application_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-common title: "@kbn/core-application-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-common'] --- import kbnCoreApplicationCommonObj from './kbn_core_application_common.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_internal.mdx b/api_docs/kbn_core_apps_browser_internal.mdx index 925f06201ac93..2e243e13530e4 100644 --- a/api_docs/kbn_core_apps_browser_internal.mdx +++ b/api_docs/kbn_core_apps_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-internal title: "@kbn/core-apps-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-internal'] --- import kbnCoreAppsBrowserInternalObj from './kbn_core_apps_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_mocks.mdx b/api_docs/kbn_core_apps_browser_mocks.mdx index 007e706834014..acdd32a7bfe42 100644 --- a/api_docs/kbn_core_apps_browser_mocks.mdx +++ b/api_docs/kbn_core_apps_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-mocks title: "@kbn/core-apps-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-mocks'] --- import kbnCoreAppsBrowserMocksObj from './kbn_core_apps_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_apps_server_internal.mdx b/api_docs/kbn_core_apps_server_internal.mdx index 822678a4634ca..e10e733169a21 100644 --- a/api_docs/kbn_core_apps_server_internal.mdx +++ b/api_docs/kbn_core_apps_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-server-internal title: "@kbn/core-apps-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-server-internal'] --- import kbnCoreAppsServerInternalObj from './kbn_core_apps_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index de1c6e41bde19..984dced41f5a2 100644 --- a/api_docs/kbn_core_base_browser_mocks.mdx +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks title: "@kbn/core-base-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] --- import kbnCoreBaseBrowserMocksObj from './kbn_core_base_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index 487ca8516e901..9d918db9e235a 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] --- import kbnCoreBaseCommonObj from './kbn_core_base_common.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx index 8f3d657ed031f..bc8385268b917 100644 --- a/api_docs/kbn_core_base_server_internal.mdx +++ b/api_docs/kbn_core_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-internal title: "@kbn/core-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-internal'] --- import kbnCoreBaseServerInternalObj from './kbn_core_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index 13aaee49fb3d6..285e4af68bc9f 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] --- import kbnCoreBaseServerMocksObj from './kbn_core_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_browser_mocks.mdx b/api_docs/kbn_core_capabilities_browser_mocks.mdx index 87400852bf5ba..878a8a1147833 100644 --- a/api_docs/kbn_core_capabilities_browser_mocks.mdx +++ b/api_docs/kbn_core_capabilities_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-browser-mocks title: "@kbn/core-capabilities-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-browser-mocks'] --- import kbnCoreCapabilitiesBrowserMocksObj from './kbn_core_capabilities_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_common.mdx b/api_docs/kbn_core_capabilities_common.mdx index 155e3d1ab32c1..f8b6a2b6c32d4 100644 --- a/api_docs/kbn_core_capabilities_common.mdx +++ b/api_docs/kbn_core_capabilities_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-common title: "@kbn/core-capabilities-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-common'] --- import kbnCoreCapabilitiesCommonObj from './kbn_core_capabilities_common.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server.mdx b/api_docs/kbn_core_capabilities_server.mdx index cf9a67511dbd2..d743ec1c4df86 100644 --- a/api_docs/kbn_core_capabilities_server.mdx +++ b/api_docs/kbn_core_capabilities_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server title: "@kbn/core-capabilities-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server'] --- import kbnCoreCapabilitiesServerObj from './kbn_core_capabilities_server.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server_mocks.mdx b/api_docs/kbn_core_capabilities_server_mocks.mdx index 29018e77c93a7..5155e9c1c9f9a 100644 --- a/api_docs/kbn_core_capabilities_server_mocks.mdx +++ b/api_docs/kbn_core_capabilities_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server-mocks title: "@kbn/core-capabilities-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server-mocks'] --- import kbnCoreCapabilitiesServerMocksObj from './kbn_core_capabilities_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser.mdx b/api_docs/kbn_core_chrome_browser.mdx index 7e248c277a53a..53713cab702a6 100644 --- a/api_docs/kbn_core_chrome_browser.mdx +++ b/api_docs/kbn_core_chrome_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser title: "@kbn/core-chrome-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser'] --- import kbnCoreChromeBrowserObj from './kbn_core_chrome_browser.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser_mocks.mdx b/api_docs/kbn_core_chrome_browser_mocks.mdx index c11368f3af3d9..d95e1c0e3b6c4 100644 --- a/api_docs/kbn_core_chrome_browser_mocks.mdx +++ b/api_docs/kbn_core_chrome_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser-mocks title: "@kbn/core-chrome-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser-mocks'] --- import kbnCoreChromeBrowserMocksObj from './kbn_core_chrome_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_config_server_internal.mdx b/api_docs/kbn_core_config_server_internal.mdx index 4ae9332297a09..87cad7a4a7aa6 100644 --- a/api_docs/kbn_core_config_server_internal.mdx +++ b/api_docs/kbn_core_config_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-config-server-internal title: "@kbn/core-config-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-config-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-config-server-internal'] --- import kbnCoreConfigServerInternalObj from './kbn_core_config_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser.mdx b/api_docs/kbn_core_custom_branding_browser.mdx index 2facaddb22fdc..1606de8053efa 100644 --- a/api_docs/kbn_core_custom_branding_browser.mdx +++ b/api_docs/kbn_core_custom_branding_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser title: "@kbn/core-custom-branding-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser'] --- import kbnCoreCustomBrandingBrowserObj from './kbn_core_custom_branding_browser.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_internal.mdx b/api_docs/kbn_core_custom_branding_browser_internal.mdx index ab719f09e2c63..b8a9bdf6327c4 100644 --- a/api_docs/kbn_core_custom_branding_browser_internal.mdx +++ b/api_docs/kbn_core_custom_branding_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-internal title: "@kbn/core-custom-branding-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-internal'] --- import kbnCoreCustomBrandingBrowserInternalObj from './kbn_core_custom_branding_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_mocks.mdx b/api_docs/kbn_core_custom_branding_browser_mocks.mdx index b2f84279964e6..881b13f962fc2 100644 --- a/api_docs/kbn_core_custom_branding_browser_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-mocks title: "@kbn/core-custom-branding-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-mocks'] --- import kbnCoreCustomBrandingBrowserMocksObj from './kbn_core_custom_branding_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_common.mdx b/api_docs/kbn_core_custom_branding_common.mdx index d70220d140f0e..00ff36e5a9b12 100644 --- a/api_docs/kbn_core_custom_branding_common.mdx +++ b/api_docs/kbn_core_custom_branding_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-common title: "@kbn/core-custom-branding-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-common'] --- import kbnCoreCustomBrandingCommonObj from './kbn_core_custom_branding_common.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server.mdx b/api_docs/kbn_core_custom_branding_server.mdx index 5d8aa78a7d4a6..ebe7dcce10cb3 100644 --- a/api_docs/kbn_core_custom_branding_server.mdx +++ b/api_docs/kbn_core_custom_branding_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server title: "@kbn/core-custom-branding-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server'] --- import kbnCoreCustomBrandingServerObj from './kbn_core_custom_branding_server.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_internal.mdx b/api_docs/kbn_core_custom_branding_server_internal.mdx index 6007871fbeb4f..003d60dc54ce7 100644 --- a/api_docs/kbn_core_custom_branding_server_internal.mdx +++ b/api_docs/kbn_core_custom_branding_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-internal title: "@kbn/core-custom-branding-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-internal'] --- import kbnCoreCustomBrandingServerInternalObj from './kbn_core_custom_branding_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_mocks.mdx b/api_docs/kbn_core_custom_branding_server_mocks.mdx index b12a9b9373f49..3830daddbd95c 100644 --- a/api_docs/kbn_core_custom_branding_server_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-mocks title: "@kbn/core-custom-branding-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-mocks'] --- import kbnCoreCustomBrandingServerMocksObj from './kbn_core_custom_branding_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index fc28e7abd3ced..b2f5c372de502 100644 --- a/api_docs/kbn_core_deprecations_browser.mdx +++ b/api_docs/kbn_core_deprecations_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser title: "@kbn/core-deprecations-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser'] --- import kbnCoreDeprecationsBrowserObj from './kbn_core_deprecations_browser.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_internal.mdx b/api_docs/kbn_core_deprecations_browser_internal.mdx index d8e9e2e70cb95..ff470228c7f7b 100644 --- a/api_docs/kbn_core_deprecations_browser_internal.mdx +++ b/api_docs/kbn_core_deprecations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-internal title: "@kbn/core-deprecations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-internal'] --- import kbnCoreDeprecationsBrowserInternalObj from './kbn_core_deprecations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_mocks.mdx b/api_docs/kbn_core_deprecations_browser_mocks.mdx index 06793bda57298..c1836d2822c32 100644 --- a/api_docs/kbn_core_deprecations_browser_mocks.mdx +++ b/api_docs/kbn_core_deprecations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-mocks title: "@kbn/core-deprecations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-mocks'] --- import kbnCoreDeprecationsBrowserMocksObj from './kbn_core_deprecations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_common.mdx b/api_docs/kbn_core_deprecations_common.mdx index 97b6a368d6205..2128e6eb6213f 100644 --- a/api_docs/kbn_core_deprecations_common.mdx +++ b/api_docs/kbn_core_deprecations_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-common title: "@kbn/core-deprecations-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-common'] --- import kbnCoreDeprecationsCommonObj from './kbn_core_deprecations_common.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server.mdx b/api_docs/kbn_core_deprecations_server.mdx index 06a9e3bc8b2aa..4d2b587d1b6b4 100644 --- a/api_docs/kbn_core_deprecations_server.mdx +++ b/api_docs/kbn_core_deprecations_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server title: "@kbn/core-deprecations-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server'] --- import kbnCoreDeprecationsServerObj from './kbn_core_deprecations_server.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_internal.mdx b/api_docs/kbn_core_deprecations_server_internal.mdx index 484362bc9717c..33f6ac490860a 100644 --- a/api_docs/kbn_core_deprecations_server_internal.mdx +++ b/api_docs/kbn_core_deprecations_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-internal title: "@kbn/core-deprecations-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-internal'] --- import kbnCoreDeprecationsServerInternalObj from './kbn_core_deprecations_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_mocks.mdx b/api_docs/kbn_core_deprecations_server_mocks.mdx index 2b1496836fd14..a3923e66a070d 100644 --- a/api_docs/kbn_core_deprecations_server_mocks.mdx +++ b/api_docs/kbn_core_deprecations_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-mocks title: "@kbn/core-deprecations-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-mocks'] --- import kbnCoreDeprecationsServerMocksObj from './kbn_core_deprecations_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index da4db3f2e5a39..6682062f66a04 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] --- import kbnCoreDocLinksBrowserObj from './kbn_core_doc_links_browser.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index 4deae23e311bb..7e412e4d84656 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] --- import kbnCoreDocLinksBrowserMocksObj from './kbn_core_doc_links_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index a96a8402e95b2..c1c0498587d6d 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] --- import kbnCoreDocLinksServerObj from './kbn_core_doc_links_server.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index 7dafc9670937a..e79ddfb62ae5f 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] --- import kbnCoreDocLinksServerMocksObj from './kbn_core_doc_links_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx index 00e21b7c8c4cd..2225a4cc87d5f 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-internal title: "@kbn/core-elasticsearch-client-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-internal'] --- import kbnCoreElasticsearchClientServerInternalObj from './kbn_core_elasticsearch_client_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx index 61096379c1a35..05b4dfd3bcec0 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-mocks title: "@kbn/core-elasticsearch-client-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-mocks'] --- import kbnCoreElasticsearchClientServerMocksObj from './kbn_core_elasticsearch_client_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server.mdx b/api_docs/kbn_core_elasticsearch_server.mdx index 914a607ba4510..eab8ac0954ccd 100644 --- a/api_docs/kbn_core_elasticsearch_server.mdx +++ b/api_docs/kbn_core_elasticsearch_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server title: "@kbn/core-elasticsearch-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server'] --- import kbnCoreElasticsearchServerObj from './kbn_core_elasticsearch_server.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_internal.mdx b/api_docs/kbn_core_elasticsearch_server_internal.mdx index f9b7fc07ef57f..58d4827d4d876 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-internal title: "@kbn/core-elasticsearch-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-internal'] --- import kbnCoreElasticsearchServerInternalObj from './kbn_core_elasticsearch_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_server_mocks.mdx index f45c0e2ba281b..edef4939e24fd 100644 --- a/api_docs/kbn_core_elasticsearch_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-mocks title: "@kbn/core-elasticsearch-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-mocks'] --- import kbnCoreElasticsearchServerMocksObj from './kbn_core_elasticsearch_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_internal.mdx b/api_docs/kbn_core_environment_server_internal.mdx index fe2b0d418efe9..90101269c587d 100644 --- a/api_docs/kbn_core_environment_server_internal.mdx +++ b/api_docs/kbn_core_environment_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-internal title: "@kbn/core-environment-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-internal'] --- import kbnCoreEnvironmentServerInternalObj from './kbn_core_environment_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_mocks.mdx b/api_docs/kbn_core_environment_server_mocks.mdx index de245988d874d..9197b11c59c00 100644 --- a/api_docs/kbn_core_environment_server_mocks.mdx +++ b/api_docs/kbn_core_environment_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-mocks title: "@kbn/core-environment-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-mocks'] --- import kbnCoreEnvironmentServerMocksObj from './kbn_core_environment_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser.mdx b/api_docs/kbn_core_execution_context_browser.mdx index 2e24c70d937cc..73e12d30cd33f 100644 --- a/api_docs/kbn_core_execution_context_browser.mdx +++ b/api_docs/kbn_core_execution_context_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser title: "@kbn/core-execution-context-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser'] --- import kbnCoreExecutionContextBrowserObj from './kbn_core_execution_context_browser.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_internal.mdx b/api_docs/kbn_core_execution_context_browser_internal.mdx index 3eeefff838d59..cf67d90eb24a3 100644 --- a/api_docs/kbn_core_execution_context_browser_internal.mdx +++ b/api_docs/kbn_core_execution_context_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-internal title: "@kbn/core-execution-context-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-internal'] --- import kbnCoreExecutionContextBrowserInternalObj from './kbn_core_execution_context_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_mocks.mdx b/api_docs/kbn_core_execution_context_browser_mocks.mdx index 6aa09d853c97b..ebca881c0be5b 100644 --- a/api_docs/kbn_core_execution_context_browser_mocks.mdx +++ b/api_docs/kbn_core_execution_context_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-mocks title: "@kbn/core-execution-context-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-mocks'] --- import kbnCoreExecutionContextBrowserMocksObj from './kbn_core_execution_context_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_common.mdx b/api_docs/kbn_core_execution_context_common.mdx index 74f788fde9c4a..3b4a650d211d3 100644 --- a/api_docs/kbn_core_execution_context_common.mdx +++ b/api_docs/kbn_core_execution_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-common title: "@kbn/core-execution-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-common'] --- import kbnCoreExecutionContextCommonObj from './kbn_core_execution_context_common.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server.mdx b/api_docs/kbn_core_execution_context_server.mdx index 4d9bad9a99e2a..ca1b8247c53c4 100644 --- a/api_docs/kbn_core_execution_context_server.mdx +++ b/api_docs/kbn_core_execution_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server title: "@kbn/core-execution-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server'] --- import kbnCoreExecutionContextServerObj from './kbn_core_execution_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_internal.mdx b/api_docs/kbn_core_execution_context_server_internal.mdx index f3bcc14e633c8..0ad6538d70d66 100644 --- a/api_docs/kbn_core_execution_context_server_internal.mdx +++ b/api_docs/kbn_core_execution_context_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-internal title: "@kbn/core-execution-context-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-internal'] --- import kbnCoreExecutionContextServerInternalObj from './kbn_core_execution_context_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_mocks.mdx b/api_docs/kbn_core_execution_context_server_mocks.mdx index 5fc3117502e5c..b5cf62d6ae0d7 100644 --- a/api_docs/kbn_core_execution_context_server_mocks.mdx +++ b/api_docs/kbn_core_execution_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-mocks title: "@kbn/core-execution-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-mocks'] --- import kbnCoreExecutionContextServerMocksObj from './kbn_core_execution_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser.mdx b/api_docs/kbn_core_fatal_errors_browser.mdx index 1627580848aa5..0676526d3dbf0 100644 --- a/api_docs/kbn_core_fatal_errors_browser.mdx +++ b/api_docs/kbn_core_fatal_errors_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser title: "@kbn/core-fatal-errors-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser'] --- import kbnCoreFatalErrorsBrowserObj from './kbn_core_fatal_errors_browser.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx index df4b66973facb..2e5ce4430dfde 100644 --- a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx +++ b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser-mocks title: "@kbn/core-fatal-errors-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser-mocks'] --- import kbnCoreFatalErrorsBrowserMocksObj from './kbn_core_fatal_errors_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser.mdx b/api_docs/kbn_core_http_browser.mdx index a612f81fdcfd3..1bd082b34cdd5 100644 --- a/api_docs/kbn_core_http_browser.mdx +++ b/api_docs/kbn_core_http_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser title: "@kbn/core-http-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser'] --- import kbnCoreHttpBrowserObj from './kbn_core_http_browser.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_internal.mdx b/api_docs/kbn_core_http_browser_internal.mdx index f2e5236be83a0..cda9e42bafd7c 100644 --- a/api_docs/kbn_core_http_browser_internal.mdx +++ b/api_docs/kbn_core_http_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-internal title: "@kbn/core-http-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-internal'] --- import kbnCoreHttpBrowserInternalObj from './kbn_core_http_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_mocks.mdx b/api_docs/kbn_core_http_browser_mocks.mdx index f6eee4cf9a021..21bbb2be7b2d1 100644 --- a/api_docs/kbn_core_http_browser_mocks.mdx +++ b/api_docs/kbn_core_http_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-mocks title: "@kbn/core-http-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-mocks'] --- import kbnCoreHttpBrowserMocksObj from './kbn_core_http_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_common.mdx b/api_docs/kbn_core_http_common.mdx index c376893e3d624..c8fab3a573a8e 100644 --- a/api_docs/kbn_core_http_common.mdx +++ b/api_docs/kbn_core_http_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-common title: "@kbn/core-http-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-common'] --- import kbnCoreHttpCommonObj from './kbn_core_http_common.devdocs.json'; diff --git a/api_docs/kbn_core_http_context_server_mocks.mdx b/api_docs/kbn_core_http_context_server_mocks.mdx index b362fe116e723..08cdc48bd3ea4 100644 --- a/api_docs/kbn_core_http_context_server_mocks.mdx +++ b/api_docs/kbn_core_http_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-context-server-mocks title: "@kbn/core-http-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-context-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-context-server-mocks'] --- import kbnCoreHttpContextServerMocksObj from './kbn_core_http_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_request_handler_context_server.mdx b/api_docs/kbn_core_http_request_handler_context_server.mdx index eeb0f260e6409..6af22f0888884 100644 --- a/api_docs/kbn_core_http_request_handler_context_server.mdx +++ b/api_docs/kbn_core_http_request_handler_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-request-handler-context-server title: "@kbn/core-http-request-handler-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-request-handler-context-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-request-handler-context-server'] --- import kbnCoreHttpRequestHandlerContextServerObj from './kbn_core_http_request_handler_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server.mdx b/api_docs/kbn_core_http_resources_server.mdx index d26c55f0fb0df..0aecd1a7d302f 100644 --- a/api_docs/kbn_core_http_resources_server.mdx +++ b/api_docs/kbn_core_http_resources_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server title: "@kbn/core-http-resources-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server'] --- import kbnCoreHttpResourcesServerObj from './kbn_core_http_resources_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_internal.mdx b/api_docs/kbn_core_http_resources_server_internal.mdx index 21c62de395267..dab5d7e38b5c9 100644 --- a/api_docs/kbn_core_http_resources_server_internal.mdx +++ b/api_docs/kbn_core_http_resources_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-internal title: "@kbn/core-http-resources-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-internal'] --- import kbnCoreHttpResourcesServerInternalObj from './kbn_core_http_resources_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_mocks.mdx b/api_docs/kbn_core_http_resources_server_mocks.mdx index d211d7411945e..ff081c38aa591 100644 --- a/api_docs/kbn_core_http_resources_server_mocks.mdx +++ b/api_docs/kbn_core_http_resources_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-mocks title: "@kbn/core-http-resources-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-mocks'] --- import kbnCoreHttpResourcesServerMocksObj from './kbn_core_http_resources_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_internal.mdx b/api_docs/kbn_core_http_router_server_internal.mdx index 4250092d3f05a..35d2a8ed5998e 100644 --- a/api_docs/kbn_core_http_router_server_internal.mdx +++ b/api_docs/kbn_core_http_router_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-internal title: "@kbn/core-http-router-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-internal'] --- import kbnCoreHttpRouterServerInternalObj from './kbn_core_http_router_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_mocks.mdx b/api_docs/kbn_core_http_router_server_mocks.mdx index 08f56b9a1ac97..d1a4660f7f4d5 100644 --- a/api_docs/kbn_core_http_router_server_mocks.mdx +++ b/api_docs/kbn_core_http_router_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-mocks title: "@kbn/core-http-router-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-mocks'] --- import kbnCoreHttpRouterServerMocksObj from './kbn_core_http_router_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_server.devdocs.json b/api_docs/kbn_core_http_server.devdocs.json index 230ca9ab5e1f1..a7075e5be6a9f 100644 --- a/api_docs/kbn_core_http_server.devdocs.json +++ b/api_docs/kbn_core_http_server.devdocs.json @@ -14860,6 +14860,10 @@ "plugin": "cloudSecurityPosture", "path": "x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/bulk_action.ts" }, + { + "plugin": "dataVisualizer", + "path": "x-pack/plugins/data_visualizer/server/routes.ts" + }, { "plugin": "ecsDataQualityDashboard", "path": "x-pack/plugins/ecs_data_quality_dashboard/server/routes/get_unallowed_field_values.ts" diff --git a/api_docs/kbn_core_http_server.mdx b/api_docs/kbn_core_http_server.mdx index 12f1edda40a9e..76e5eb520904f 100644 --- a/api_docs/kbn_core_http_server.mdx +++ b/api_docs/kbn_core_http_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server title: "@kbn/core-http-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server'] --- import kbnCoreHttpServerObj from './kbn_core_http_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_internal.mdx b/api_docs/kbn_core_http_server_internal.mdx index 82af6b2aefb75..e937c3fc1ddb0 100644 --- a/api_docs/kbn_core_http_server_internal.mdx +++ b/api_docs/kbn_core_http_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-internal title: "@kbn/core-http-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-internal'] --- import kbnCoreHttpServerInternalObj from './kbn_core_http_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_mocks.mdx b/api_docs/kbn_core_http_server_mocks.mdx index b2ae7d0501047..7753581fb96b1 100644 --- a/api_docs/kbn_core_http_server_mocks.mdx +++ b/api_docs/kbn_core_http_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-mocks title: "@kbn/core-http-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-mocks'] --- import kbnCoreHttpServerMocksObj from './kbn_core_http_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser.mdx b/api_docs/kbn_core_i18n_browser.mdx index fbfeee4e498df..193c353704d9e 100644 --- a/api_docs/kbn_core_i18n_browser.mdx +++ b/api_docs/kbn_core_i18n_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser title: "@kbn/core-i18n-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser'] --- import kbnCoreI18nBrowserObj from './kbn_core_i18n_browser.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser_mocks.mdx b/api_docs/kbn_core_i18n_browser_mocks.mdx index 07d9ee7b67473..6c814d7ce06e6 100644 --- a/api_docs/kbn_core_i18n_browser_mocks.mdx +++ b/api_docs/kbn_core_i18n_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser-mocks title: "@kbn/core-i18n-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser-mocks'] --- import kbnCoreI18nBrowserMocksObj from './kbn_core_i18n_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server.mdx b/api_docs/kbn_core_i18n_server.mdx index 18fa18602d6a1..49e165ebbecd8 100644 --- a/api_docs/kbn_core_i18n_server.mdx +++ b/api_docs/kbn_core_i18n_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server title: "@kbn/core-i18n-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server'] --- import kbnCoreI18nServerObj from './kbn_core_i18n_server.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_internal.mdx b/api_docs/kbn_core_i18n_server_internal.mdx index 850d7e006f30f..2e0f3ce02543e 100644 --- a/api_docs/kbn_core_i18n_server_internal.mdx +++ b/api_docs/kbn_core_i18n_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-internal title: "@kbn/core-i18n-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-internal'] --- import kbnCoreI18nServerInternalObj from './kbn_core_i18n_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_mocks.mdx b/api_docs/kbn_core_i18n_server_mocks.mdx index 0972b169c5a25..545044bf52ffa 100644 --- a/api_docs/kbn_core_i18n_server_mocks.mdx +++ b/api_docs/kbn_core_i18n_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-mocks title: "@kbn/core-i18n-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-mocks'] --- import kbnCoreI18nServerMocksObj from './kbn_core_i18n_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index 2b50f03d992a4..0fd0fa460dddb 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] --- import kbnCoreInjectedMetadataBrowserMocksObj from './kbn_core_injected_metadata_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_internal.mdx b/api_docs/kbn_core_integrations_browser_internal.mdx index 42b46c147d31e..cc96782959ee9 100644 --- a/api_docs/kbn_core_integrations_browser_internal.mdx +++ b/api_docs/kbn_core_integrations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-internal title: "@kbn/core-integrations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-internal'] --- import kbnCoreIntegrationsBrowserInternalObj from './kbn_core_integrations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_mocks.mdx b/api_docs/kbn_core_integrations_browser_mocks.mdx index 44c1b89989262..f6fe47ac21d11 100644 --- a/api_docs/kbn_core_integrations_browser_mocks.mdx +++ b/api_docs/kbn_core_integrations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-mocks title: "@kbn/core-integrations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-mocks'] --- import kbnCoreIntegrationsBrowserMocksObj from './kbn_core_integrations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser.mdx b/api_docs/kbn_core_lifecycle_browser.mdx index e36ba5bb25295..75d0fe03b17cc 100644 --- a/api_docs/kbn_core_lifecycle_browser.mdx +++ b/api_docs/kbn_core_lifecycle_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser title: "@kbn/core-lifecycle-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser'] --- import kbnCoreLifecycleBrowserObj from './kbn_core_lifecycle_browser.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser_mocks.mdx b/api_docs/kbn_core_lifecycle_browser_mocks.mdx index 08f8b2d90a84f..f3b8c71159e6c 100644 --- a/api_docs/kbn_core_lifecycle_browser_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser-mocks title: "@kbn/core-lifecycle-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser-mocks'] --- import kbnCoreLifecycleBrowserMocksObj from './kbn_core_lifecycle_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server.mdx b/api_docs/kbn_core_lifecycle_server.mdx index f627928546900..e16f639b13af3 100644 --- a/api_docs/kbn_core_lifecycle_server.mdx +++ b/api_docs/kbn_core_lifecycle_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server title: "@kbn/core-lifecycle-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server'] --- import kbnCoreLifecycleServerObj from './kbn_core_lifecycle_server.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server_mocks.mdx b/api_docs/kbn_core_lifecycle_server_mocks.mdx index 8189b61131732..6ffed34ebd619 100644 --- a/api_docs/kbn_core_lifecycle_server_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server-mocks title: "@kbn/core-lifecycle-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server-mocks'] --- import kbnCoreLifecycleServerMocksObj from './kbn_core_lifecycle_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_browser_mocks.mdx b/api_docs/kbn_core_logging_browser_mocks.mdx index a59624cb0fd83..6e120d259e69d 100644 --- a/api_docs/kbn_core_logging_browser_mocks.mdx +++ b/api_docs/kbn_core_logging_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-browser-mocks title: "@kbn/core-logging-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-browser-mocks'] --- import kbnCoreLoggingBrowserMocksObj from './kbn_core_logging_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_common_internal.mdx b/api_docs/kbn_core_logging_common_internal.mdx index 2c3e194d00012..c99f19f834dc5 100644 --- a/api_docs/kbn_core_logging_common_internal.mdx +++ b/api_docs/kbn_core_logging_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-common-internal title: "@kbn/core-logging-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-common-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-common-internal'] --- import kbnCoreLoggingCommonInternalObj from './kbn_core_logging_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index 0032ef1fd1c51..00c26cf52fb06 100644 --- a/api_docs/kbn_core_logging_server.mdx +++ b/api_docs/kbn_core_logging_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server title: "@kbn/core-logging-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server'] --- import kbnCoreLoggingServerObj from './kbn_core_logging_server.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_internal.mdx b/api_docs/kbn_core_logging_server_internal.mdx index fffc8316e29ef..a2224af004975 100644 --- a/api_docs/kbn_core_logging_server_internal.mdx +++ b/api_docs/kbn_core_logging_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-internal title: "@kbn/core-logging-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-internal'] --- import kbnCoreLoggingServerInternalObj from './kbn_core_logging_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_mocks.mdx b/api_docs/kbn_core_logging_server_mocks.mdx index a7649ca3c12c4..f3f51eaed7399 100644 --- a/api_docs/kbn_core_logging_server_mocks.mdx +++ b/api_docs/kbn_core_logging_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-mocks title: "@kbn/core-logging-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-mocks'] --- import kbnCoreLoggingServerMocksObj from './kbn_core_logging_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_internal.mdx b/api_docs/kbn_core_metrics_collectors_server_internal.mdx index cb58b91d8279b..91a777e524285 100644 --- a/api_docs/kbn_core_metrics_collectors_server_internal.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-internal title: "@kbn/core-metrics-collectors-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-internal'] --- import kbnCoreMetricsCollectorsServerInternalObj from './kbn_core_metrics_collectors_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx index a540b9c91e0fe..f04a4a69dc7cd 100644 --- a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-mocks title: "@kbn/core-metrics-collectors-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-mocks'] --- import kbnCoreMetricsCollectorsServerMocksObj from './kbn_core_metrics_collectors_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server.mdx b/api_docs/kbn_core_metrics_server.mdx index e864cb1853d72..40db72a5cca5b 100644 --- a/api_docs/kbn_core_metrics_server.mdx +++ b/api_docs/kbn_core_metrics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server title: "@kbn/core-metrics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server'] --- import kbnCoreMetricsServerObj from './kbn_core_metrics_server.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_internal.mdx b/api_docs/kbn_core_metrics_server_internal.mdx index f1e1c04e375f0..987bbe7199823 100644 --- a/api_docs/kbn_core_metrics_server_internal.mdx +++ b/api_docs/kbn_core_metrics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-internal title: "@kbn/core-metrics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-internal'] --- import kbnCoreMetricsServerInternalObj from './kbn_core_metrics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_mocks.mdx b/api_docs/kbn_core_metrics_server_mocks.mdx index 9149c2a0260ce..f0bd3ae2f9c16 100644 --- a/api_docs/kbn_core_metrics_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-mocks title: "@kbn/core-metrics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-mocks'] --- import kbnCoreMetricsServerMocksObj from './kbn_core_metrics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_mount_utils_browser.mdx b/api_docs/kbn_core_mount_utils_browser.mdx index 28b259f64a7e0..629a8a8b0c33b 100644 --- a/api_docs/kbn_core_mount_utils_browser.mdx +++ b/api_docs/kbn_core_mount_utils_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-mount-utils-browser title: "@kbn/core-mount-utils-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-mount-utils-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-mount-utils-browser'] --- import kbnCoreMountUtilsBrowserObj from './kbn_core_mount_utils_browser.devdocs.json'; diff --git a/api_docs/kbn_core_node_server.mdx b/api_docs/kbn_core_node_server.mdx index c5f37a08e2077..60b68fdbfc68b 100644 --- a/api_docs/kbn_core_node_server.mdx +++ b/api_docs/kbn_core_node_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server title: "@kbn/core-node-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server'] --- import kbnCoreNodeServerObj from './kbn_core_node_server.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_internal.mdx b/api_docs/kbn_core_node_server_internal.mdx index d8c4209967acd..462fd470809ba 100644 --- a/api_docs/kbn_core_node_server_internal.mdx +++ b/api_docs/kbn_core_node_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-internal title: "@kbn/core-node-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-internal'] --- import kbnCoreNodeServerInternalObj from './kbn_core_node_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_mocks.mdx b/api_docs/kbn_core_node_server_mocks.mdx index 29c98e37a41b4..921b9dc9e1bbc 100644 --- a/api_docs/kbn_core_node_server_mocks.mdx +++ b/api_docs/kbn_core_node_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-mocks title: "@kbn/core-node-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-mocks'] --- import kbnCoreNodeServerMocksObj from './kbn_core_node_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser.mdx b/api_docs/kbn_core_notifications_browser.mdx index 4942c22792c4b..40cc1e2bc1c89 100644 --- a/api_docs/kbn_core_notifications_browser.mdx +++ b/api_docs/kbn_core_notifications_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser title: "@kbn/core-notifications-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser'] --- import kbnCoreNotificationsBrowserObj from './kbn_core_notifications_browser.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_internal.mdx b/api_docs/kbn_core_notifications_browser_internal.mdx index 183059e2b9c44..c996d8e1352e8 100644 --- a/api_docs/kbn_core_notifications_browser_internal.mdx +++ b/api_docs/kbn_core_notifications_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-internal title: "@kbn/core-notifications-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-internal'] --- import kbnCoreNotificationsBrowserInternalObj from './kbn_core_notifications_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_mocks.mdx b/api_docs/kbn_core_notifications_browser_mocks.mdx index 65d79f76f326b..b403be9b87ccc 100644 --- a/api_docs/kbn_core_notifications_browser_mocks.mdx +++ b/api_docs/kbn_core_notifications_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-mocks title: "@kbn/core-notifications-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-mocks'] --- import kbnCoreNotificationsBrowserMocksObj from './kbn_core_notifications_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser.mdx b/api_docs/kbn_core_overlays_browser.mdx index f14305177173d..f36f423ececc9 100644 --- a/api_docs/kbn_core_overlays_browser.mdx +++ b/api_docs/kbn_core_overlays_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser title: "@kbn/core-overlays-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser'] --- import kbnCoreOverlaysBrowserObj from './kbn_core_overlays_browser.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_internal.mdx b/api_docs/kbn_core_overlays_browser_internal.mdx index 68fbe4722aa42..ac138745d54bf 100644 --- a/api_docs/kbn_core_overlays_browser_internal.mdx +++ b/api_docs/kbn_core_overlays_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-internal title: "@kbn/core-overlays-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-internal'] --- import kbnCoreOverlaysBrowserInternalObj from './kbn_core_overlays_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_mocks.mdx b/api_docs/kbn_core_overlays_browser_mocks.mdx index de475e9439f19..7bebb45195bb6 100644 --- a/api_docs/kbn_core_overlays_browser_mocks.mdx +++ b/api_docs/kbn_core_overlays_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-mocks title: "@kbn/core-overlays-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-mocks'] --- import kbnCoreOverlaysBrowserMocksObj from './kbn_core_overlays_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser.mdx b/api_docs/kbn_core_plugins_browser.mdx index aa25bdb614f52..10cfba81a031b 100644 --- a/api_docs/kbn_core_plugins_browser.mdx +++ b/api_docs/kbn_core_plugins_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser title: "@kbn/core-plugins-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser'] --- import kbnCorePluginsBrowserObj from './kbn_core_plugins_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser_mocks.mdx b/api_docs/kbn_core_plugins_browser_mocks.mdx index c40f4014e7dee..053581b9baeae 100644 --- a/api_docs/kbn_core_plugins_browser_mocks.mdx +++ b/api_docs/kbn_core_plugins_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser-mocks title: "@kbn/core-plugins-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser-mocks'] --- import kbnCorePluginsBrowserMocksObj from './kbn_core_plugins_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_contracts_browser.mdx b/api_docs/kbn_core_plugins_contracts_browser.mdx index dce50c7b8d4b4..84b177f971a31 100644 --- a/api_docs/kbn_core_plugins_contracts_browser.mdx +++ b/api_docs/kbn_core_plugins_contracts_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-contracts-browser title: "@kbn/core-plugins-contracts-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-contracts-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-contracts-browser'] --- import kbnCorePluginsContractsBrowserObj from './kbn_core_plugins_contracts_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_contracts_server.mdx b/api_docs/kbn_core_plugins_contracts_server.mdx index a4994bb77f7cf..22370e6949e96 100644 --- a/api_docs/kbn_core_plugins_contracts_server.mdx +++ b/api_docs/kbn_core_plugins_contracts_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-contracts-server title: "@kbn/core-plugins-contracts-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-contracts-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-contracts-server'] --- import kbnCorePluginsContractsServerObj from './kbn_core_plugins_contracts_server.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server.mdx b/api_docs/kbn_core_plugins_server.mdx index 773b22d72fcd4..cd05db32d8be9 100644 --- a/api_docs/kbn_core_plugins_server.mdx +++ b/api_docs/kbn_core_plugins_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server title: "@kbn/core-plugins-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server'] --- import kbnCorePluginsServerObj from './kbn_core_plugins_server.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server_mocks.mdx b/api_docs/kbn_core_plugins_server_mocks.mdx index 2fc858e27c4e1..617d71964d1bd 100644 --- a/api_docs/kbn_core_plugins_server_mocks.mdx +++ b/api_docs/kbn_core_plugins_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server-mocks title: "@kbn/core-plugins-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server-mocks'] --- import kbnCorePluginsServerMocksObj from './kbn_core_plugins_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index 02bff0d8eb5ba..c4bc9e752e45d 100644 --- a/api_docs/kbn_core_preboot_server.mdx +++ b/api_docs/kbn_core_preboot_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server title: "@kbn/core-preboot-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server'] --- import kbnCorePrebootServerObj from './kbn_core_preboot_server.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server_mocks.mdx b/api_docs/kbn_core_preboot_server_mocks.mdx index 37b5fc25c742d..dba8c170343ef 100644 --- a/api_docs/kbn_core_preboot_server_mocks.mdx +++ b/api_docs/kbn_core_preboot_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server-mocks title: "@kbn/core-preboot-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server-mocks'] --- import kbnCorePrebootServerMocksObj from './kbn_core_preboot_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_browser_mocks.mdx b/api_docs/kbn_core_rendering_browser_mocks.mdx index 8b50f4429447b..02a5ede291405 100644 --- a/api_docs/kbn_core_rendering_browser_mocks.mdx +++ b/api_docs/kbn_core_rendering_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-browser-mocks title: "@kbn/core-rendering-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-browser-mocks'] --- import kbnCoreRenderingBrowserMocksObj from './kbn_core_rendering_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_internal.mdx b/api_docs/kbn_core_rendering_server_internal.mdx index 34f73f83f0084..685d1f49367f2 100644 --- a/api_docs/kbn_core_rendering_server_internal.mdx +++ b/api_docs/kbn_core_rendering_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-internal title: "@kbn/core-rendering-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-internal'] --- import kbnCoreRenderingServerInternalObj from './kbn_core_rendering_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_mocks.mdx b/api_docs/kbn_core_rendering_server_mocks.mdx index 7e1df39b46b9e..2e682e6ad1713 100644 --- a/api_docs/kbn_core_rendering_server_mocks.mdx +++ b/api_docs/kbn_core_rendering_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-mocks title: "@kbn/core-rendering-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-mocks'] --- import kbnCoreRenderingServerMocksObj from './kbn_core_rendering_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_root_server_internal.mdx b/api_docs/kbn_core_root_server_internal.mdx index 1cef382393e48..03f401cafde13 100644 --- a/api_docs/kbn_core_root_server_internal.mdx +++ b/api_docs/kbn_core_root_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-root-server-internal title: "@kbn/core-root-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-root-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-root-server-internal'] --- import kbnCoreRootServerInternalObj from './kbn_core_root_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_browser.mdx b/api_docs/kbn_core_saved_objects_api_browser.mdx index c2f86bc71c50a..d46a3cbb206c5 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.mdx +++ b/api_docs/kbn_core_saved_objects_api_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-browser title: "@kbn/core-saved-objects-api-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-browser'] --- import kbnCoreSavedObjectsApiBrowserObj from './kbn_core_saved_objects_api_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server.mdx b/api_docs/kbn_core_saved_objects_api_server.mdx index 849a6191f315c..3af095dc0555b 100644 --- a/api_docs/kbn_core_saved_objects_api_server.mdx +++ b/api_docs/kbn_core_saved_objects_api_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server title: "@kbn/core-saved-objects-api-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server'] --- import kbnCoreSavedObjectsApiServerObj from './kbn_core_saved_objects_api_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx index a2414bf2257d1..bea06555ff846 100644 --- a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-mocks title: "@kbn/core-saved-objects-api-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-mocks'] --- import kbnCoreSavedObjectsApiServerMocksObj from './kbn_core_saved_objects_api_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_internal.mdx b/api_docs/kbn_core_saved_objects_base_server_internal.mdx index a9556624a28db..675ae0e61abff 100644 --- a/api_docs/kbn_core_saved_objects_base_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-internal title: "@kbn/core-saved-objects-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-internal'] --- import kbnCoreSavedObjectsBaseServerInternalObj from './kbn_core_saved_objects_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx index e819f6d63ee60..2f2989e030210 100644 --- a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-mocks title: "@kbn/core-saved-objects-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-mocks'] --- import kbnCoreSavedObjectsBaseServerMocksObj from './kbn_core_saved_objects_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser.mdx b/api_docs/kbn_core_saved_objects_browser.mdx index 72d3e66799c0e..53d9de40560d3 100644 --- a/api_docs/kbn_core_saved_objects_browser.mdx +++ b/api_docs/kbn_core_saved_objects_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser title: "@kbn/core-saved-objects-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser'] --- import kbnCoreSavedObjectsBrowserObj from './kbn_core_saved_objects_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_internal.mdx b/api_docs/kbn_core_saved_objects_browser_internal.mdx index dde0423a7b1f0..acbb45108ca0b 100644 --- a/api_docs/kbn_core_saved_objects_browser_internal.mdx +++ b/api_docs/kbn_core_saved_objects_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-internal title: "@kbn/core-saved-objects-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-internal'] --- import kbnCoreSavedObjectsBrowserInternalObj from './kbn_core_saved_objects_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_mocks.mdx b/api_docs/kbn_core_saved_objects_browser_mocks.mdx index fb21471738800..8409b60b78631 100644 --- a/api_docs/kbn_core_saved_objects_browser_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-mocks title: "@kbn/core-saved-objects-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-mocks'] --- import kbnCoreSavedObjectsBrowserMocksObj from './kbn_core_saved_objects_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_common.mdx b/api_docs/kbn_core_saved_objects_common.mdx index 8f585e5ef712f..11560a453300b 100644 --- a/api_docs/kbn_core_saved_objects_common.mdx +++ b/api_docs/kbn_core_saved_objects_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-common title: "@kbn/core-saved-objects-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-common'] --- import kbnCoreSavedObjectsCommonObj from './kbn_core_saved_objects_common.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx index fdc82f9342200..c83f9349b6264 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-internal title: "@kbn/core-saved-objects-import-export-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-internal'] --- import kbnCoreSavedObjectsImportExportServerInternalObj from './kbn_core_saved_objects_import_export_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx index 1de73758515db..17e4b0cb86896 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-mocks title: "@kbn/core-saved-objects-import-export-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-mocks'] --- import kbnCoreSavedObjectsImportExportServerMocksObj from './kbn_core_saved_objects_import_export_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx index d4a1266600e85..dca851069c374 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-internal title: "@kbn/core-saved-objects-migration-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-internal'] --- import kbnCoreSavedObjectsMigrationServerInternalObj from './kbn_core_saved_objects_migration_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx index 75ddbd88cf648..39eb8def4328f 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-mocks title: "@kbn/core-saved-objects-migration-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-mocks'] --- import kbnCoreSavedObjectsMigrationServerMocksObj from './kbn_core_saved_objects_migration_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server.mdx b/api_docs/kbn_core_saved_objects_server.mdx index cce430fed1399..dd3aaa56a5d98 100644 --- a/api_docs/kbn_core_saved_objects_server.mdx +++ b/api_docs/kbn_core_saved_objects_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server title: "@kbn/core-saved-objects-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server'] --- import kbnCoreSavedObjectsServerObj from './kbn_core_saved_objects_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_internal.mdx b/api_docs/kbn_core_saved_objects_server_internal.mdx index 72f69ff5d1f65..082c7b99ffcd9 100644 --- a/api_docs/kbn_core_saved_objects_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-internal title: "@kbn/core-saved-objects-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-internal'] --- import kbnCoreSavedObjectsServerInternalObj from './kbn_core_saved_objects_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_mocks.mdx b/api_docs/kbn_core_saved_objects_server_mocks.mdx index 471aff347ea74..4e90cfc1e6e15 100644 --- a/api_docs/kbn_core_saved_objects_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-mocks title: "@kbn/core-saved-objects-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-mocks'] --- import kbnCoreSavedObjectsServerMocksObj from './kbn_core_saved_objects_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_utils_server.mdx b/api_docs/kbn_core_saved_objects_utils_server.mdx index 512129b09266e..02bf834756c06 100644 --- a/api_docs/kbn_core_saved_objects_utils_server.mdx +++ b/api_docs/kbn_core_saved_objects_utils_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-utils-server title: "@kbn/core-saved-objects-utils-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-utils-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-utils-server'] --- import kbnCoreSavedObjectsUtilsServerObj from './kbn_core_saved_objects_utils_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_common.mdx b/api_docs/kbn_core_status_common.mdx index 5b8d798c407d0..f5b390b4d6438 100644 --- a/api_docs/kbn_core_status_common.mdx +++ b/api_docs/kbn_core_status_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common title: "@kbn/core-status-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common'] --- import kbnCoreStatusCommonObj from './kbn_core_status_common.devdocs.json'; diff --git a/api_docs/kbn_core_status_common_internal.devdocs.json b/api_docs/kbn_core_status_common_internal.devdocs.json index 819813f5db06c..6128be9e72179 100644 --- a/api_docs/kbn_core_status_common_internal.devdocs.json +++ b/api_docs/kbn_core_status_common_internal.devdocs.json @@ -75,6 +75,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "@kbn/core-status-common-internal", + "id": "def-common.ServerVersion.build_flavor", + "type": "CompoundType", + "tags": [], + "label": "build_flavor", + "description": [], + "signature": [ + "\"serverless\" | \"traditional\"" + ], + "path": "packages/core/status/core-status-common-internal/src/status.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "@kbn/core-status-common-internal", "id": "def-common.ServerVersion.build_date", diff --git a/api_docs/kbn_core_status_common_internal.mdx b/api_docs/kbn_core_status_common_internal.mdx index cce9d26f94913..f97d0bf41d4cf 100644 --- a/api_docs/kbn_core_status_common_internal.mdx +++ b/api_docs/kbn_core_status_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common-internal title: "@kbn/core-status-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common-internal'] --- import kbnCoreStatusCommonInternalObj from './kbn_core_status_common_internal.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 20 | 0 | 19 | 0 | +| 21 | 0 | 20 | 0 | ## Common diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx index 7fd60d80240de..cec55ecaf5a10 100644 --- a/api_docs/kbn_core_status_server.mdx +++ b/api_docs/kbn_core_status_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server title: "@kbn/core-status-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server'] --- import kbnCoreStatusServerObj from './kbn_core_status_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_internal.mdx b/api_docs/kbn_core_status_server_internal.mdx index 463681e928ae6..6be3bc50dccf0 100644 --- a/api_docs/kbn_core_status_server_internal.mdx +++ b/api_docs/kbn_core_status_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-internal title: "@kbn/core-status-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-internal'] --- import kbnCoreStatusServerInternalObj from './kbn_core_status_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_mocks.mdx b/api_docs/kbn_core_status_server_mocks.mdx index dce6072b82290..4deb8b0633b17 100644 --- a/api_docs/kbn_core_status_server_mocks.mdx +++ b/api_docs/kbn_core_status_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-mocks title: "@kbn/core-status-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-mocks'] --- import kbnCoreStatusServerMocksObj from './kbn_core_status_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx index 99627f59d3149..7b3335ffa7aa1 100644 --- a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx +++ b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-deprecations-getters title: "@kbn/core-test-helpers-deprecations-getters" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-deprecations-getters plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-deprecations-getters'] --- import kbnCoreTestHelpersDeprecationsGettersObj from './kbn_core_test_helpers_deprecations_getters.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx index 346820e660d43..f705307de1c28 100644 --- a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx +++ b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-http-setup-browser title: "@kbn/core-test-helpers-http-setup-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-http-setup-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-http-setup-browser'] --- import kbnCoreTestHelpersHttpSetupBrowserObj from './kbn_core_test_helpers_http_setup_browser.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_kbn_server.mdx b/api_docs/kbn_core_test_helpers_kbn_server.mdx index 552509037b92f..78385197b30dd 100644 --- a/api_docs/kbn_core_test_helpers_kbn_server.mdx +++ b/api_docs/kbn_core_test_helpers_kbn_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-kbn-server title: "@kbn/core-test-helpers-kbn-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-kbn-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-kbn-server'] --- import kbnCoreTestHelpersKbnServerObj from './kbn_core_test_helpers_kbn_server.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_model_versions.mdx b/api_docs/kbn_core_test_helpers_model_versions.mdx index 8adfff27843f4..fb6feaa12d5d6 100644 --- a/api_docs/kbn_core_test_helpers_model_versions.mdx +++ b/api_docs/kbn_core_test_helpers_model_versions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-model-versions title: "@kbn/core-test-helpers-model-versions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-model-versions plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-model-versions'] --- import kbnCoreTestHelpersModelVersionsObj from './kbn_core_test_helpers_model_versions.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx index 3c59ed1ca2557..567f43a631ec1 100644 --- a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx +++ b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-so-type-serializer title: "@kbn/core-test-helpers-so-type-serializer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-so-type-serializer plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-so-type-serializer'] --- import kbnCoreTestHelpersSoTypeSerializerObj from './kbn_core_test_helpers_so_type_serializer.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_test_utils.mdx b/api_docs/kbn_core_test_helpers_test_utils.mdx index a97bf61389c41..8cdf04449d6f4 100644 --- a/api_docs/kbn_core_test_helpers_test_utils.mdx +++ b/api_docs/kbn_core_test_helpers_test_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-test-utils title: "@kbn/core-test-helpers-test-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-test-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-test-utils'] --- import kbnCoreTestHelpersTestUtilsObj from './kbn_core_test_helpers_test_utils.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index 47bd2019d6f23..38376bd6baa48 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] --- import kbnCoreThemeBrowserObj from './kbn_core_theme_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index 546aa1e3ad155..7169a88a58466 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] --- import kbnCoreThemeBrowserMocksObj from './kbn_core_theme_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser.mdx b/api_docs/kbn_core_ui_settings_browser.mdx index 445a4607c33b7..c40abe664606b 100644 --- a/api_docs/kbn_core_ui_settings_browser.mdx +++ b/api_docs/kbn_core_ui_settings_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser title: "@kbn/core-ui-settings-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser'] --- import kbnCoreUiSettingsBrowserObj from './kbn_core_ui_settings_browser.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_internal.mdx b/api_docs/kbn_core_ui_settings_browser_internal.mdx index bdc5bac248b26..2f563c6c9428d 100644 --- a/api_docs/kbn_core_ui_settings_browser_internal.mdx +++ b/api_docs/kbn_core_ui_settings_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-internal title: "@kbn/core-ui-settings-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-internal'] --- import kbnCoreUiSettingsBrowserInternalObj from './kbn_core_ui_settings_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_mocks.mdx b/api_docs/kbn_core_ui_settings_browser_mocks.mdx index 065d2b61009fd..64eb47baa59cd 100644 --- a/api_docs/kbn_core_ui_settings_browser_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-mocks title: "@kbn/core-ui-settings-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-mocks'] --- import kbnCoreUiSettingsBrowserMocksObj from './kbn_core_ui_settings_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_common.mdx b/api_docs/kbn_core_ui_settings_common.mdx index fa9458171836b..750b73ad06c37 100644 --- a/api_docs/kbn_core_ui_settings_common.mdx +++ b/api_docs/kbn_core_ui_settings_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-common title: "@kbn/core-ui-settings-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-common'] --- import kbnCoreUiSettingsCommonObj from './kbn_core_ui_settings_common.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server.mdx b/api_docs/kbn_core_ui_settings_server.mdx index 2424aa3084e03..db4a9c18429f0 100644 --- a/api_docs/kbn_core_ui_settings_server.mdx +++ b/api_docs/kbn_core_ui_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server title: "@kbn/core-ui-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server'] --- import kbnCoreUiSettingsServerObj from './kbn_core_ui_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_internal.mdx b/api_docs/kbn_core_ui_settings_server_internal.mdx index 5519d8549a24d..2c470fb72fbe7 100644 --- a/api_docs/kbn_core_ui_settings_server_internal.mdx +++ b/api_docs/kbn_core_ui_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-internal title: "@kbn/core-ui-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-internal'] --- import kbnCoreUiSettingsServerInternalObj from './kbn_core_ui_settings_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_mocks.mdx b/api_docs/kbn_core_ui_settings_server_mocks.mdx index 0b9921e8b4b8e..8a86bd09b026d 100644 --- a/api_docs/kbn_core_ui_settings_server_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-mocks title: "@kbn/core-ui-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-mocks'] --- import kbnCoreUiSettingsServerMocksObj from './kbn_core_ui_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server.mdx b/api_docs/kbn_core_usage_data_server.mdx index d49d7904218a5..1ea34ec532c0c 100644 --- a/api_docs/kbn_core_usage_data_server.mdx +++ b/api_docs/kbn_core_usage_data_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server title: "@kbn/core-usage-data-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server'] --- import kbnCoreUsageDataServerObj from './kbn_core_usage_data_server.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_internal.mdx b/api_docs/kbn_core_usage_data_server_internal.mdx index 0326c774328d4..2bf75c924dd9b 100644 --- a/api_docs/kbn_core_usage_data_server_internal.mdx +++ b/api_docs/kbn_core_usage_data_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-internal title: "@kbn/core-usage-data-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-internal'] --- import kbnCoreUsageDataServerInternalObj from './kbn_core_usage_data_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_mocks.mdx b/api_docs/kbn_core_usage_data_server_mocks.mdx index e740880bd1efa..2c86d1d129ca6 100644 --- a/api_docs/kbn_core_usage_data_server_mocks.mdx +++ b/api_docs/kbn_core_usage_data_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-mocks title: "@kbn/core-usage-data-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-mocks'] --- import kbnCoreUsageDataServerMocksObj from './kbn_core_usage_data_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_user_settings_server.mdx b/api_docs/kbn_core_user_settings_server.mdx index b5b1ce3403713..dbf9ede0ec2bc 100644 --- a/api_docs/kbn_core_user_settings_server.mdx +++ b/api_docs/kbn_core_user_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-settings-server title: "@kbn/core-user-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-settings-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-settings-server'] --- import kbnCoreUserSettingsServerObj from './kbn_core_user_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_user_settings_server_internal.mdx b/api_docs/kbn_core_user_settings_server_internal.mdx index 32551520e4e30..f8e539a5983c0 100644 --- a/api_docs/kbn_core_user_settings_server_internal.mdx +++ b/api_docs/kbn_core_user_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-settings-server-internal title: "@kbn/core-user-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-settings-server-internal plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-settings-server-internal'] --- import kbnCoreUserSettingsServerInternalObj from './kbn_core_user_settings_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_user_settings_server_mocks.mdx b/api_docs/kbn_core_user_settings_server_mocks.mdx index 9c9d4ff2d66c9..b3b97bbc99e33 100644 --- a/api_docs/kbn_core_user_settings_server_mocks.mdx +++ b/api_docs/kbn_core_user_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-user-settings-server-mocks title: "@kbn/core-user-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-user-settings-server-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-user-settings-server-mocks'] --- import kbnCoreUserSettingsServerMocksObj from './kbn_core_user_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index 52f35791bc0d8..d546f7eb0b8b6 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] --- import kbnCryptoObj from './kbn_crypto.devdocs.json'; diff --git a/api_docs/kbn_crypto_browser.mdx b/api_docs/kbn_crypto_browser.mdx index 2b1aca5dd33a1..0de4ea41bb1e3 100644 --- a/api_docs/kbn_crypto_browser.mdx +++ b/api_docs/kbn_crypto_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto-browser title: "@kbn/crypto-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_custom_icons.mdx b/api_docs/kbn_custom_icons.mdx index d68ec3c8684c4..b63875a2b2e1a 100644 --- a/api_docs/kbn_custom_icons.mdx +++ b/api_docs/kbn_custom_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-custom-icons title: "@kbn/custom-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/custom-icons plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/custom-icons'] --- import kbnCustomIconsObj from './kbn_custom_icons.devdocs.json'; diff --git a/api_docs/kbn_custom_integrations.mdx b/api_docs/kbn_custom_integrations.mdx index c434cfaa99173..23c57769366e0 100644 --- a/api_docs/kbn_custom_integrations.mdx +++ b/api_docs/kbn_custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-custom-integrations title: "@kbn/custom-integrations" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/custom-integrations plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/custom-integrations'] --- import kbnCustomIntegrationsObj from './kbn_custom_integrations.devdocs.json'; diff --git a/api_docs/kbn_cypress_config.mdx b/api_docs/kbn_cypress_config.mdx index 44523ffdea60f..531a2845dcb49 100644 --- a/api_docs/kbn_cypress_config.mdx +++ b/api_docs/kbn_cypress_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cypress-config title: "@kbn/cypress-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cypress-config plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cypress-config'] --- import kbnCypressConfigObj from './kbn_cypress_config.devdocs.json'; diff --git a/api_docs/kbn_data_forge.mdx b/api_docs/kbn_data_forge.mdx index f0f5bcc9a6ab8..f37e53b48e8c9 100644 --- a/api_docs/kbn_data_forge.mdx +++ b/api_docs/kbn_data_forge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-data-forge title: "@kbn/data-forge" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/data-forge plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/data-forge'] --- import kbnDataForgeObj from './kbn_data_forge.devdocs.json'; diff --git a/api_docs/kbn_data_service.mdx b/api_docs/kbn_data_service.mdx index 7bcd2e0327054..5e4534cde7c0d 100644 --- a/api_docs/kbn_data_service.mdx +++ b/api_docs/kbn_data_service.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-data-service title: "@kbn/data-service" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/data-service plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/data-service'] --- import kbnDataServiceObj from './kbn_data_service.devdocs.json'; diff --git a/api_docs/kbn_data_stream_adapter.mdx b/api_docs/kbn_data_stream_adapter.mdx index a34fb29fe90da..77fc9dd7ae6a7 100644 --- a/api_docs/kbn_data_stream_adapter.mdx +++ b/api_docs/kbn_data_stream_adapter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-data-stream-adapter title: "@kbn/data-stream-adapter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/data-stream-adapter plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/data-stream-adapter'] --- import kbnDataStreamAdapterObj from './kbn_data_stream_adapter.devdocs.json'; diff --git a/api_docs/kbn_data_view_utils.mdx b/api_docs/kbn_data_view_utils.mdx index 28e39104f298c..c43c651e325dc 100644 --- a/api_docs/kbn_data_view_utils.mdx +++ b/api_docs/kbn_data_view_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-data-view-utils title: "@kbn/data-view-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/data-view-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/data-view-utils'] --- import kbnDataViewUtilsObj from './kbn_data_view_utils.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 6ff71cdf16d4a..d68867caf0ad0 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/datemath plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] --- import kbnDatemathObj from './kbn_datemath.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_analytics.mdx b/api_docs/kbn_deeplinks_analytics.mdx index 00cc1d8fbbb53..98469aeca9f05 100644 --- a/api_docs/kbn_deeplinks_analytics.mdx +++ b/api_docs/kbn_deeplinks_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-analytics title: "@kbn/deeplinks-analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-analytics plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-analytics'] --- import kbnDeeplinksAnalyticsObj from './kbn_deeplinks_analytics.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_devtools.mdx b/api_docs/kbn_deeplinks_devtools.mdx index 325a4b8832556..c97e5e1f31fa1 100644 --- a/api_docs/kbn_deeplinks_devtools.mdx +++ b/api_docs/kbn_deeplinks_devtools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-devtools title: "@kbn/deeplinks-devtools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-devtools plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-devtools'] --- import kbnDeeplinksDevtoolsObj from './kbn_deeplinks_devtools.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_management.mdx b/api_docs/kbn_deeplinks_management.mdx index b9ac7bcaf6310..0d9c5819384ef 100644 --- a/api_docs/kbn_deeplinks_management.mdx +++ b/api_docs/kbn_deeplinks_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-management title: "@kbn/deeplinks-management" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-management plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-management'] --- import kbnDeeplinksManagementObj from './kbn_deeplinks_management.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_ml.mdx b/api_docs/kbn_deeplinks_ml.mdx index 5ff4c557a9c95..abcfa6f992290 100644 --- a/api_docs/kbn_deeplinks_ml.mdx +++ b/api_docs/kbn_deeplinks_ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-ml title: "@kbn/deeplinks-ml" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-ml plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-ml'] --- import kbnDeeplinksMlObj from './kbn_deeplinks_ml.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_observability.devdocs.json b/api_docs/kbn_deeplinks_observability.devdocs.json index d98b4ba490885..8283e29631a8a 100644 --- a/api_docs/kbn_deeplinks_observability.devdocs.json +++ b/api_docs/kbn_deeplinks_observability.devdocs.json @@ -182,7 +182,14 @@ "\nColumns displayed in the table" ], "signature": [ - "string[] | undefined" + { + "pluginId": "@kbn/deeplinks-observability", + "scope": "common", + "docId": "kibKbnDeeplinksObservabilityPluginApi", + "section": "def-common.GridColumnDisplayOptions", + "text": "GridColumnDisplayOptions" + }, + "[] | undefined" ], "path": "packages/deeplinks/observability/locators/logs_explorer.ts", "deprecated": false, @@ -502,6 +509,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/deeplinks-observability", + "id": "def-common.DocumentFieldGridColumnOptions", + "type": "Type", + "tags": [], + "label": "DocumentFieldGridColumnOptions", + "description": [], + "signature": [ + "{ type: \"document-field\"; field: string; width?: number | undefined; }" + ], + "path": "packages/deeplinks/observability/locators/logs_explorer.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/deeplinks-observability", "id": "def-common.FilterControls", @@ -525,6 +547,35 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/deeplinks-observability", + "id": "def-common.GridColumnDisplayOptions", + "type": "Type", + "tags": [], + "label": "GridColumnDisplayOptions", + "description": [], + "signature": [ + { + "pluginId": "@kbn/deeplinks-observability", + "scope": "common", + "docId": "kibKbnDeeplinksObservabilityPluginApi", + "section": "def-common.DocumentFieldGridColumnOptions", + "text": "DocumentFieldGridColumnOptions" + }, + " | ", + { + "pluginId": "@kbn/deeplinks-observability", + "scope": "common", + "docId": "kibKbnDeeplinksObservabilityPluginApi", + "section": "def-common.SmartFieldGridColumnOptions", + "text": "SmartFieldGridColumnOptions" + } + ], + "path": "packages/deeplinks/observability/locators/logs_explorer.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/deeplinks-observability", "id": "def-common.ListFilterControl", @@ -659,6 +710,21 @@ "deprecated": false, "trackAdoption": false, "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/deeplinks-observability", + "id": "def-common.SmartFieldGridColumnOptions", + "type": "Type", + "tags": [], + "label": "SmartFieldGridColumnOptions", + "description": [], + "signature": [ + "{ type: \"smart-field\"; smartField: \"resource\" | \"content\"; width?: number | undefined; }" + ], + "path": "packages/deeplinks/observability/locators/logs_explorer.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false } ], "objects": [] diff --git a/api_docs/kbn_deeplinks_observability.mdx b/api_docs/kbn_deeplinks_observability.mdx index 679ebd3a0add7..2ac272eb2619f 100644 --- a/api_docs/kbn_deeplinks_observability.mdx +++ b/api_docs/kbn_deeplinks_observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-observability title: "@kbn/deeplinks-observability" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-observability plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-observability'] --- import kbnDeeplinksObservabilityObj from './kbn_deeplinks_observability.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 31 | 0 | 21 | 0 | +| 34 | 0 | 24 | 0 | ## Common diff --git a/api_docs/kbn_deeplinks_search.mdx b/api_docs/kbn_deeplinks_search.mdx index ad3405da94a4b..bbcbbc5f68df4 100644 --- a/api_docs/kbn_deeplinks_search.mdx +++ b/api_docs/kbn_deeplinks_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-deeplinks-search title: "@kbn/deeplinks-search" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/deeplinks-search plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-search'] --- import kbnDeeplinksSearchObj from './kbn_deeplinks_search.devdocs.json'; diff --git a/api_docs/kbn_default_nav_analytics.mdx b/api_docs/kbn_default_nav_analytics.mdx index 4e1e2c5a71ea0..aee098b3b7f88 100644 --- a/api_docs/kbn_default_nav_analytics.mdx +++ b/api_docs/kbn_default_nav_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-analytics title: "@kbn/default-nav-analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-analytics plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-analytics'] --- import kbnDefaultNavAnalyticsObj from './kbn_default_nav_analytics.devdocs.json'; diff --git a/api_docs/kbn_default_nav_devtools.mdx b/api_docs/kbn_default_nav_devtools.mdx index e41a566fe8330..743e5c98149f6 100644 --- a/api_docs/kbn_default_nav_devtools.mdx +++ b/api_docs/kbn_default_nav_devtools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-devtools title: "@kbn/default-nav-devtools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-devtools plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-devtools'] --- import kbnDefaultNavDevtoolsObj from './kbn_default_nav_devtools.devdocs.json'; diff --git a/api_docs/kbn_default_nav_management.mdx b/api_docs/kbn_default_nav_management.mdx index 81289dd94896f..afd1abbc862b5 100644 --- a/api_docs/kbn_default_nav_management.mdx +++ b/api_docs/kbn_default_nav_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-management title: "@kbn/default-nav-management" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-management plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-management'] --- import kbnDefaultNavManagementObj from './kbn_default_nav_management.devdocs.json'; diff --git a/api_docs/kbn_default_nav_ml.mdx b/api_docs/kbn_default_nav_ml.mdx index e6780c0a641ee..f3312bac2d196 100644 --- a/api_docs/kbn_default_nav_ml.mdx +++ b/api_docs/kbn_default_nav_ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-default-nav-ml title: "@kbn/default-nav-ml" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/default-nav-ml plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/default-nav-ml'] --- import kbnDefaultNavMlObj from './kbn_default_nav_ml.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index 52f072784425e..aed134c87c494 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-errors plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] --- import kbnDevCliErrorsObj from './kbn_dev_cli_errors.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index e0ca875007e2d..522fc2f980961 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-runner plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] --- import kbnDevCliRunnerObj from './kbn_dev_cli_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index 6ee34d508a935..d210d46ea4da0 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-proc-runner plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] --- import kbnDevProcRunnerObj from './kbn_dev_proc_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index e2300f64cec1f..99a003339cc51 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; diff --git a/api_docs/kbn_discover_utils.mdx b/api_docs/kbn_discover_utils.mdx index a294b1b364648..15e808284f6c6 100644 --- a/api_docs/kbn_discover_utils.mdx +++ b/api_docs/kbn_discover_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-discover-utils title: "@kbn/discover-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/discover-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/discover-utils'] --- import kbnDiscoverUtilsObj from './kbn_discover_utils.devdocs.json'; diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index 6c0133256e1a6..bf247f42d6d72 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/doc-links plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] --- import kbnDocLinksObj from './kbn_doc_links.devdocs.json'; diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index 5563163397c3f..4d241e9e7a101 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/docs-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] --- import kbnDocsUtilsObj from './kbn_docs_utils.devdocs.json'; diff --git a/api_docs/kbn_dom_drag_drop.mdx b/api_docs/kbn_dom_drag_drop.mdx index e561046d0a619..6bd2d52f42dc4 100644 --- a/api_docs/kbn_dom_drag_drop.mdx +++ b/api_docs/kbn_dom_drag_drop.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dom-drag-drop title: "@kbn/dom-drag-drop" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dom-drag-drop plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dom-drag-drop'] --- import kbnDomDragDropObj from './kbn_dom_drag_drop.devdocs.json'; diff --git a/api_docs/kbn_ebt_tools.mdx b/api_docs/kbn_ebt_tools.mdx index 6a4d8f5d5c467..1a5fbca3918c3 100644 --- a/api_docs/kbn_ebt_tools.mdx +++ b/api_docs/kbn_ebt_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ebt-tools title: "@kbn/ebt-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ebt-tools plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_ecs.mdx b/api_docs/kbn_ecs.mdx index e55daa4491bd5..2c93053c427c3 100644 --- a/api_docs/kbn_ecs.mdx +++ b/api_docs/kbn_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs title: "@kbn/ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs'] --- import kbnEcsObj from './kbn_ecs.devdocs.json'; diff --git a/api_docs/kbn_ecs_data_quality_dashboard.mdx b/api_docs/kbn_ecs_data_quality_dashboard.mdx index 0dbf96f3bb33a..af48c01b178a9 100644 --- a/api_docs/kbn_ecs_data_quality_dashboard.mdx +++ b/api_docs/kbn_ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs-data-quality-dashboard title: "@kbn/ecs-data-quality-dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs-data-quality-dashboard plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs-data-quality-dashboard'] --- import kbnEcsDataQualityDashboardObj from './kbn_ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/kbn_elastic_agent_utils.mdx b/api_docs/kbn_elastic_agent_utils.mdx index 1938fa33966bf..3d52a7f768bab 100644 --- a/api_docs/kbn_elastic_agent_utils.mdx +++ b/api_docs/kbn_elastic_agent_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-elastic-agent-utils title: "@kbn/elastic-agent-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/elastic-agent-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/elastic-agent-utils'] --- import kbnElasticAgentUtilsObj from './kbn_elastic_agent_utils.devdocs.json'; diff --git a/api_docs/kbn_elastic_assistant.mdx b/api_docs/kbn_elastic_assistant.mdx index 51f32807cd412..c5ee6c045acbb 100644 --- a/api_docs/kbn_elastic_assistant.mdx +++ b/api_docs/kbn_elastic_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-elastic-assistant title: "@kbn/elastic-assistant" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/elastic-assistant plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/elastic-assistant'] --- import kbnElasticAssistantObj from './kbn_elastic_assistant.devdocs.json'; diff --git a/api_docs/kbn_elastic_assistant_common.mdx b/api_docs/kbn_elastic_assistant_common.mdx index 0042f37af440d..67c668afc039b 100644 --- a/api_docs/kbn_elastic_assistant_common.mdx +++ b/api_docs/kbn_elastic_assistant_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-elastic-assistant-common title: "@kbn/elastic-assistant-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/elastic-assistant-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/elastic-assistant-common'] --- import kbnElasticAssistantCommonObj from './kbn_elastic_assistant_common.devdocs.json'; diff --git a/api_docs/kbn_es.mdx b/api_docs/kbn_es.mdx index 6d5ec7bcc31ea..f759301cc1861 100644 --- a/api_docs/kbn_es.mdx +++ b/api_docs/kbn_es.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es title: "@kbn/es" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es'] --- import kbnEsObj from './kbn_es.devdocs.json'; diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index 0c03444a4d58d..a8fab1b683990 100644 --- a/api_docs/kbn_es_archiver.mdx +++ b/api_docs/kbn_es_archiver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-archiver title: "@kbn/es-archiver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-archiver plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-archiver'] --- import kbnEsArchiverObj from './kbn_es_archiver.devdocs.json'; diff --git a/api_docs/kbn_es_errors.mdx b/api_docs/kbn_es_errors.mdx index 7fed094aab4ea..862b7d1ecd8d6 100644 --- a/api_docs/kbn_es_errors.mdx +++ b/api_docs/kbn_es_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-errors title: "@kbn/es-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-errors plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-errors'] --- import kbnEsErrorsObj from './kbn_es_errors.devdocs.json'; diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index 53a9a212ec39f..ec3cd4b84883a 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-query plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] --- import kbnEsQueryObj from './kbn_es_query.devdocs.json'; diff --git a/api_docs/kbn_es_types.mdx b/api_docs/kbn_es_types.mdx index 43511f9acdd78..29f4c160cd89e 100644 --- a/api_docs/kbn_es_types.mdx +++ b/api_docs/kbn_es_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-types title: "@kbn/es-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-types'] --- import kbnEsTypesObj from './kbn_es_types.devdocs.json'; diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index 63f4eb4749b36..9f9f75b63e70b 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/eslint-plugin-imports plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_esql_utils.mdx b/api_docs/kbn_esql_utils.mdx index 9c9bdd7bb241b..2b8f56aaf2c67 100644 --- a/api_docs/kbn_esql_utils.mdx +++ b/api_docs/kbn_esql_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-esql-utils title: "@kbn/esql-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/esql-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/esql-utils'] --- import kbnEsqlUtilsObj from './kbn_esql_utils.devdocs.json'; diff --git a/api_docs/kbn_event_annotation_common.mdx b/api_docs/kbn_event_annotation_common.mdx index d7d786256db97..95422718b5e88 100644 --- a/api_docs/kbn_event_annotation_common.mdx +++ b/api_docs/kbn_event_annotation_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-event-annotation-common title: "@kbn/event-annotation-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/event-annotation-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/event-annotation-common'] --- import kbnEventAnnotationCommonObj from './kbn_event_annotation_common.devdocs.json'; diff --git a/api_docs/kbn_event_annotation_components.mdx b/api_docs/kbn_event_annotation_components.mdx index fda3348004c8d..863e7444ac006 100644 --- a/api_docs/kbn_event_annotation_components.mdx +++ b/api_docs/kbn_event_annotation_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-event-annotation-components title: "@kbn/event-annotation-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/event-annotation-components plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/event-annotation-components'] --- import kbnEventAnnotationComponentsObj from './kbn_event_annotation_components.devdocs.json'; diff --git a/api_docs/kbn_expandable_flyout.mdx b/api_docs/kbn_expandable_flyout.mdx index 4363ed3e4e425..1b786996390a7 100644 --- a/api_docs/kbn_expandable_flyout.mdx +++ b/api_docs/kbn_expandable_flyout.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-expandable-flyout title: "@kbn/expandable-flyout" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/expandable-flyout plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/expandable-flyout'] --- import kbnExpandableFlyoutObj from './kbn_expandable_flyout.devdocs.json'; diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index 0218d7f7eaded..8e8d128f99b76 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] --- import kbnFieldTypesObj from './kbn_field_types.devdocs.json'; diff --git a/api_docs/kbn_field_utils.mdx b/api_docs/kbn_field_utils.mdx index b3bfd9889784f..0efc5d1e62031 100644 --- a/api_docs/kbn_field_utils.mdx +++ b/api_docs/kbn_field_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-utils title: "@kbn/field-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-utils'] --- import kbnFieldUtilsObj from './kbn_field_utils.devdocs.json'; diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index 57f1805355cf4..fa168c06193cc 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/find-used-node-modules plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] --- import kbnFindUsedNodeModulesObj from './kbn_find_used_node_modules.devdocs.json'; diff --git a/api_docs/kbn_ftr_common_functional_services.mdx b/api_docs/kbn_ftr_common_functional_services.mdx index 68f5fc6d85cc9..5bbea782e5316 100644 --- a/api_docs/kbn_ftr_common_functional_services.mdx +++ b/api_docs/kbn_ftr_common_functional_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-services title: "@kbn/ftr-common-functional-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-services plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-services'] --- import kbnFtrCommonFunctionalServicesObj from './kbn_ftr_common_functional_services.devdocs.json'; diff --git a/api_docs/kbn_ftr_common_functional_ui_services.mdx b/api_docs/kbn_ftr_common_functional_ui_services.mdx index eab2893cc20ce..290d48a645f7f 100644 --- a/api_docs/kbn_ftr_common_functional_ui_services.mdx +++ b/api_docs/kbn_ftr_common_functional_ui_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-ui-services title: "@kbn/ftr-common-functional-ui-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-ui-services plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-ui-services'] --- import kbnFtrCommonFunctionalUiServicesObj from './kbn_ftr_common_functional_ui_services.devdocs.json'; diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index d51ef4b1f2f9f..f0be8a8f6c940 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] --- import kbnGenerateObj from './kbn_generate.devdocs.json'; diff --git a/api_docs/kbn_generate_console_definitions.mdx b/api_docs/kbn_generate_console_definitions.mdx index 65a4dd253a177..c2a0a284136fc 100644 --- a/api_docs/kbn_generate_console_definitions.mdx +++ b/api_docs/kbn_generate_console_definitions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-console-definitions title: "@kbn/generate-console-definitions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-console-definitions plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-console-definitions'] --- import kbnGenerateConsoleDefinitionsObj from './kbn_generate_console_definitions.devdocs.json'; diff --git a/api_docs/kbn_generate_csv.mdx b/api_docs/kbn_generate_csv.mdx index b5ad08a5e6b67..ab8fa5e933e75 100644 --- a/api_docs/kbn_generate_csv.mdx +++ b/api_docs/kbn_generate_csv.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-csv title: "@kbn/generate-csv" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-csv plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv'] --- import kbnGenerateCsvObj from './kbn_generate_csv.devdocs.json'; diff --git a/api_docs/kbn_guided_onboarding.mdx b/api_docs/kbn_guided_onboarding.mdx index 86cf924d9594a..9a17ba93c5f79 100644 --- a/api_docs/kbn_guided_onboarding.mdx +++ b/api_docs/kbn_guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-guided-onboarding title: "@kbn/guided-onboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/guided-onboarding plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/guided-onboarding'] --- import kbnGuidedOnboardingObj from './kbn_guided_onboarding.devdocs.json'; diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index 873becaaa3c6d..9a5c5d008f19d 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/handlebars plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] --- import kbnHandlebarsObj from './kbn_handlebars.devdocs.json'; diff --git a/api_docs/kbn_hapi_mocks.mdx b/api_docs/kbn_hapi_mocks.mdx index a58b35e04ece3..c6242a47237d2 100644 --- a/api_docs/kbn_hapi_mocks.mdx +++ b/api_docs/kbn_hapi_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-hapi-mocks title: "@kbn/hapi-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/hapi-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_health_gateway_server.mdx b/api_docs/kbn_health_gateway_server.mdx index 0e97adc32653c..64ba9ebb6f1f1 100644 --- a/api_docs/kbn_health_gateway_server.mdx +++ b/api_docs/kbn_health_gateway_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-health-gateway-server title: "@kbn/health-gateway-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/health-gateway-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/health-gateway-server'] --- import kbnHealthGatewayServerObj from './kbn_health_gateway_server.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index 065be3e64ed6f..dac1c7c33c6e8 100644 --- a/api_docs/kbn_home_sample_data_card.mdx +++ b/api_docs/kbn_home_sample_data_card.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-card title: "@kbn/home-sample-data-card" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-card plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-card'] --- import kbnHomeSampleDataCardObj from './kbn_home_sample_data_card.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_tab.mdx b/api_docs/kbn_home_sample_data_tab.mdx index ccf93e82a0582..10e81aea5e1ff 100644 --- a/api_docs/kbn_home_sample_data_tab.mdx +++ b/api_docs/kbn_home_sample_data_tab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-tab title: "@kbn/home-sample-data-tab" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-tab plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-tab'] --- import kbnHomeSampleDataTabObj from './kbn_home_sample_data_tab.devdocs.json'; diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index 5a53175fa1945..d7e2f67c98aec 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_i18n_react.mdx b/api_docs/kbn_i18n_react.mdx index 66fc7bb0560f3..eb78dfb0507ea 100644 --- a/api_docs/kbn_i18n_react.mdx +++ b/api_docs/kbn_i18n_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n-react title: "@kbn/i18n-react" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n-react plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n-react'] --- import kbnI18nReactObj from './kbn_i18n_react.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index 1e39fcce2541b..2d9dc15a7fb0d 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/import-resolver plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] --- import kbnImportResolverObj from './kbn_import_resolver.devdocs.json'; diff --git a/api_docs/kbn_infra_forge.mdx b/api_docs/kbn_infra_forge.mdx index aab201a3c8563..8b6fe79e5d81b 100644 --- a/api_docs/kbn_infra_forge.mdx +++ b/api_docs/kbn_infra_forge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-infra-forge title: "@kbn/infra-forge" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/infra-forge plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/infra-forge'] --- import kbnInfraForgeObj from './kbn_infra_forge.devdocs.json'; diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index 012bb189e5ff8..5fcc0eb62af0e 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/interpreter plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] --- import kbnInterpreterObj from './kbn_interpreter.devdocs.json'; diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index 0f8b980a24240..812cd5ba62b56 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/io-ts-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] --- import kbnIoTsUtilsObj from './kbn_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index 132d9f355ee3d..a70cb5fb9d140 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/jest-serializers plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] --- import kbnJestSerializersObj from './kbn_jest_serializers.devdocs.json'; diff --git a/api_docs/kbn_journeys.mdx b/api_docs/kbn_journeys.mdx index 3cd69c0fc4342..883f838637ef7 100644 --- a/api_docs/kbn_journeys.mdx +++ b/api_docs/kbn_journeys.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-journeys title: "@kbn/journeys" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/journeys plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/journeys'] --- import kbnJourneysObj from './kbn_journeys.devdocs.json'; diff --git a/api_docs/kbn_json_ast.mdx b/api_docs/kbn_json_ast.mdx index 3bcf9f12b6e30..60d52b5b3cd92 100644 --- a/api_docs/kbn_json_ast.mdx +++ b/api_docs/kbn_json_ast.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-json-ast title: "@kbn/json-ast" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/json-ast plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/json-ast'] --- import kbnJsonAstObj from './kbn_json_ast.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index ed78d6a9512fb..600e3dce46524 100644 --- a/api_docs/kbn_kibana_manifest_schema.mdx +++ b/api_docs/kbn_kibana_manifest_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-manifest-schema title: "@kbn/kibana-manifest-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/kibana-manifest-schema plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; diff --git a/api_docs/kbn_language_documentation_popover.devdocs.json b/api_docs/kbn_language_documentation_popover.devdocs.json index d15c3a47f0f0b..19da7e82a44df 100644 --- a/api_docs/kbn_language_documentation_popover.devdocs.json +++ b/api_docs/kbn_language_documentation_popover.devdocs.json @@ -27,7 +27,7 @@ "label": "LanguageDocumentationPopover", "description": [], "signature": [ - "React.NamedExoticComponent & { readonly type: ({ language, sections, buttonProps, searchInDescription, }: DocumentationPopoverProps) => JSX.Element; }" + "React.NamedExoticComponent & { readonly type: ({ language, sections, buttonProps, searchInDescription, linkToDocumentation, }: DocumentationPopoverProps) => JSX.Element; }" ], "path": "packages/kbn-language-documentation-popover/src/components/documentation_popover.tsx", "deprecated": false, @@ -59,7 +59,7 @@ "label": "LanguageDocumentationPopoverContent", "description": [], "signature": [ - "React.NamedExoticComponent & { readonly type: ({ language, sections, searchInDescription }: DocumentationProps) => JSX.Element; }" + "React.NamedExoticComponent & { readonly type: ({ language, sections, searchInDescription, linkToDocumentation, }: DocumentationProps) => JSX.Element; }" ], "path": "packages/kbn-language-documentation-popover/src/components/documentation_content.tsx", "deprecated": false, diff --git a/api_docs/kbn_language_documentation_popover.mdx b/api_docs/kbn_language_documentation_popover.mdx index cb325cbe392e7..1bc5cf588ff44 100644 --- a/api_docs/kbn_language_documentation_popover.mdx +++ b/api_docs/kbn_language_documentation_popover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-language-documentation-popover title: "@kbn/language-documentation-popover" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/language-documentation-popover plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/language-documentation-popover'] --- import kbnLanguageDocumentationPopoverObj from './kbn_language_documentation_popover.devdocs.json'; diff --git a/api_docs/kbn_lens_embeddable_utils.mdx b/api_docs/kbn_lens_embeddable_utils.mdx index 46c888808ca1a..06ecb346029f0 100644 --- a/api_docs/kbn_lens_embeddable_utils.mdx +++ b/api_docs/kbn_lens_embeddable_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-lens-embeddable-utils title: "@kbn/lens-embeddable-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/lens-embeddable-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/lens-embeddable-utils'] --- import kbnLensEmbeddableUtilsObj from './kbn_lens_embeddable_utils.devdocs.json'; diff --git a/api_docs/kbn_lens_formula_docs.mdx b/api_docs/kbn_lens_formula_docs.mdx index 6f06e654f12ac..ccf5a3235b3f6 100644 --- a/api_docs/kbn_lens_formula_docs.mdx +++ b/api_docs/kbn_lens_formula_docs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-lens-formula-docs title: "@kbn/lens-formula-docs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/lens-formula-docs plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/lens-formula-docs'] --- import kbnLensFormulaDocsObj from './kbn_lens_formula_docs.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 905c9ddcffe9e..b765ed7b42bf1 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] --- import kbnLoggingObj from './kbn_logging.devdocs.json'; diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index a228ab8dcffa2..47460c054bac1 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] --- import kbnLoggingMocksObj from './kbn_logging_mocks.devdocs.json'; diff --git a/api_docs/kbn_managed_content_badge.mdx b/api_docs/kbn_managed_content_badge.mdx index 0a00ed61adf2c..acae16ca482e4 100644 --- a/api_docs/kbn_managed_content_badge.mdx +++ b/api_docs/kbn_managed_content_badge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-content-badge title: "@kbn/managed-content-badge" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-content-badge plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-content-badge'] --- import kbnManagedContentBadgeObj from './kbn_managed_content_badge.devdocs.json'; diff --git a/api_docs/kbn_managed_vscode_config.mdx b/api_docs/kbn_managed_vscode_config.mdx index 376460cea2fa2..2b1b2ef9e44ec 100644 --- a/api_docs/kbn_managed_vscode_config.mdx +++ b/api_docs/kbn_managed_vscode_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-vscode-config title: "@kbn/managed-vscode-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-vscode-config plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-vscode-config'] --- import kbnManagedVscodeConfigObj from './kbn_managed_vscode_config.devdocs.json'; diff --git a/api_docs/kbn_management_cards_navigation.mdx b/api_docs/kbn_management_cards_navigation.mdx index 59c942dba2995..2d886fcfd597c 100644 --- a/api_docs/kbn_management_cards_navigation.mdx +++ b/api_docs/kbn_management_cards_navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-cards-navigation title: "@kbn/management-cards-navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-cards-navigation plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-cards-navigation'] --- import kbnManagementCardsNavigationObj from './kbn_management_cards_navigation.devdocs.json'; diff --git a/api_docs/kbn_management_settings_application.mdx b/api_docs/kbn_management_settings_application.mdx index 85ea9b96d8256..cab65c930c2be 100644 --- a/api_docs/kbn_management_settings_application.mdx +++ b/api_docs/kbn_management_settings_application.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-application title: "@kbn/management-settings-application" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-application plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-application'] --- import kbnManagementSettingsApplicationObj from './kbn_management_settings_application.devdocs.json'; diff --git a/api_docs/kbn_management_settings_components_field_category.mdx b/api_docs/kbn_management_settings_components_field_category.mdx index ff5d6dea8b50f..f88167fc18530 100644 --- a/api_docs/kbn_management_settings_components_field_category.mdx +++ b/api_docs/kbn_management_settings_components_field_category.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-components-field-category title: "@kbn/management-settings-components-field-category" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-components-field-category plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-components-field-category'] --- import kbnManagementSettingsComponentsFieldCategoryObj from './kbn_management_settings_components_field_category.devdocs.json'; diff --git a/api_docs/kbn_management_settings_components_field_input.mdx b/api_docs/kbn_management_settings_components_field_input.mdx index 90fb27b1e2bb9..122176aa636fd 100644 --- a/api_docs/kbn_management_settings_components_field_input.mdx +++ b/api_docs/kbn_management_settings_components_field_input.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-components-field-input title: "@kbn/management-settings-components-field-input" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-components-field-input plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-components-field-input'] --- import kbnManagementSettingsComponentsFieldInputObj from './kbn_management_settings_components_field_input.devdocs.json'; diff --git a/api_docs/kbn_management_settings_components_field_row.mdx b/api_docs/kbn_management_settings_components_field_row.mdx index edb11ca03ab36..8b60182f00778 100644 --- a/api_docs/kbn_management_settings_components_field_row.mdx +++ b/api_docs/kbn_management_settings_components_field_row.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-components-field-row title: "@kbn/management-settings-components-field-row" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-components-field-row plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-components-field-row'] --- import kbnManagementSettingsComponentsFieldRowObj from './kbn_management_settings_components_field_row.devdocs.json'; diff --git a/api_docs/kbn_management_settings_components_form.mdx b/api_docs/kbn_management_settings_components_form.mdx index e9faba609ea93..083749a7340d8 100644 --- a/api_docs/kbn_management_settings_components_form.mdx +++ b/api_docs/kbn_management_settings_components_form.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-components-form title: "@kbn/management-settings-components-form" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-components-form plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-components-form'] --- import kbnManagementSettingsComponentsFormObj from './kbn_management_settings_components_form.devdocs.json'; diff --git a/api_docs/kbn_management_settings_field_definition.mdx b/api_docs/kbn_management_settings_field_definition.mdx index a0dac7eb74904..f4ce7bdc6be1f 100644 --- a/api_docs/kbn_management_settings_field_definition.mdx +++ b/api_docs/kbn_management_settings_field_definition.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-field-definition title: "@kbn/management-settings-field-definition" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-field-definition plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-field-definition'] --- import kbnManagementSettingsFieldDefinitionObj from './kbn_management_settings_field_definition.devdocs.json'; diff --git a/api_docs/kbn_management_settings_ids.mdx b/api_docs/kbn_management_settings_ids.mdx index 6dd9c686a2139..4cebc730312e5 100644 --- a/api_docs/kbn_management_settings_ids.mdx +++ b/api_docs/kbn_management_settings_ids.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-ids title: "@kbn/management-settings-ids" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-ids plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-ids'] --- import kbnManagementSettingsIdsObj from './kbn_management_settings_ids.devdocs.json'; diff --git a/api_docs/kbn_management_settings_section_registry.mdx b/api_docs/kbn_management_settings_section_registry.mdx index e10a606cfd471..47a54f4e2edea 100644 --- a/api_docs/kbn_management_settings_section_registry.mdx +++ b/api_docs/kbn_management_settings_section_registry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-section-registry title: "@kbn/management-settings-section-registry" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-section-registry plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-section-registry'] --- import kbnManagementSettingsSectionRegistryObj from './kbn_management_settings_section_registry.devdocs.json'; diff --git a/api_docs/kbn_management_settings_types.mdx b/api_docs/kbn_management_settings_types.mdx index 6b3eda5d4d11e..d8d92acf7d23c 100644 --- a/api_docs/kbn_management_settings_types.mdx +++ b/api_docs/kbn_management_settings_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-types title: "@kbn/management-settings-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-types'] --- import kbnManagementSettingsTypesObj from './kbn_management_settings_types.devdocs.json'; diff --git a/api_docs/kbn_management_settings_utilities.mdx b/api_docs/kbn_management_settings_utilities.mdx index 0d06fa62e786f..738d31d553aaf 100644 --- a/api_docs/kbn_management_settings_utilities.mdx +++ b/api_docs/kbn_management_settings_utilities.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-settings-utilities title: "@kbn/management-settings-utilities" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-settings-utilities plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-settings-utilities'] --- import kbnManagementSettingsUtilitiesObj from './kbn_management_settings_utilities.devdocs.json'; diff --git a/api_docs/kbn_management_storybook_config.mdx b/api_docs/kbn_management_storybook_config.mdx index faa06e322f0c6..362f45842870e 100644 --- a/api_docs/kbn_management_storybook_config.mdx +++ b/api_docs/kbn_management_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-management-storybook-config title: "@kbn/management-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/management-storybook-config plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/management-storybook-config'] --- import kbnManagementStorybookConfigObj from './kbn_management_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index 9756652261749..c7f4bb170ea58 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mapbox-gl plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] --- import kbnMapboxGlObj from './kbn_mapbox_gl.devdocs.json'; diff --git a/api_docs/kbn_maps_vector_tile_utils.mdx b/api_docs/kbn_maps_vector_tile_utils.mdx index f07b3f23721e8..50a22e56d429c 100644 --- a/api_docs/kbn_maps_vector_tile_utils.mdx +++ b/api_docs/kbn_maps_vector_tile_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-maps-vector-tile-utils title: "@kbn/maps-vector-tile-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/maps-vector-tile-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/maps-vector-tile-utils'] --- import kbnMapsVectorTileUtilsObj from './kbn_maps_vector_tile_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_agg_utils.mdx b/api_docs/kbn_ml_agg_utils.mdx index 08ac74052b564..b5c72a5e5b447 100644 --- a/api_docs/kbn_ml_agg_utils.mdx +++ b/api_docs/kbn_ml_agg_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-agg-utils title: "@kbn/ml-agg-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-agg-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-agg-utils'] --- import kbnMlAggUtilsObj from './kbn_ml_agg_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_anomaly_utils.devdocs.json b/api_docs/kbn_ml_anomaly_utils.devdocs.json index f45552d3eb1bb..95a47b1ac3b0d 100644 --- a/api_docs/kbn_ml_anomaly_utils.devdocs.json +++ b/api_docs/kbn_ml_anomaly_utils.devdocs.json @@ -2378,6 +2378,22 @@ "path": "x-pack/packages/ml/anomaly_utils/anomaly_utils.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-anomaly-utils", + "id": "def-common.MlEntityField.cardinality", + "type": "number", + "tags": [], + "label": "cardinality", + "description": [ + "\nOptional cardinality of field" + ], + "signature": [ + "number | undefined" + ], + "path": "x-pack/packages/ml/anomaly_utils/anomaly_utils.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/kbn_ml_anomaly_utils.mdx b/api_docs/kbn_ml_anomaly_utils.mdx index 774067d7e36e4..dea1fd7ea4674 100644 --- a/api_docs/kbn_ml_anomaly_utils.mdx +++ b/api_docs/kbn_ml_anomaly_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-anomaly-utils title: "@kbn/ml-anomaly-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-anomaly-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-anomaly-utils'] --- import kbnMlAnomalyUtilsObj from './kbn_ml_anomaly_utils.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) for questi | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 206 | 3 | 1 | 0 | +| 207 | 3 | 1 | 0 | ## Common diff --git a/api_docs/kbn_ml_cancellable_search.mdx b/api_docs/kbn_ml_cancellable_search.mdx index 4680fe1735266..7579b0721d13f 100644 --- a/api_docs/kbn_ml_cancellable_search.mdx +++ b/api_docs/kbn_ml_cancellable_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-cancellable-search title: "@kbn/ml-cancellable-search" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-cancellable-search plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-cancellable-search'] --- import kbnMlCancellableSearchObj from './kbn_ml_cancellable_search.devdocs.json'; diff --git a/api_docs/kbn_ml_category_validator.mdx b/api_docs/kbn_ml_category_validator.mdx index 0110e170d6d8c..1cddade4a1773 100644 --- a/api_docs/kbn_ml_category_validator.mdx +++ b/api_docs/kbn_ml_category_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-category-validator title: "@kbn/ml-category-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-category-validator plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-category-validator'] --- import kbnMlCategoryValidatorObj from './kbn_ml_category_validator.devdocs.json'; diff --git a/api_docs/kbn_ml_chi2test.mdx b/api_docs/kbn_ml_chi2test.mdx index 36d705f16e8d5..ee713e714ab90 100644 --- a/api_docs/kbn_ml_chi2test.mdx +++ b/api_docs/kbn_ml_chi2test.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-chi2test title: "@kbn/ml-chi2test" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-chi2test plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-chi2test'] --- import kbnMlChi2testObj from './kbn_ml_chi2test.devdocs.json'; diff --git a/api_docs/kbn_ml_data_frame_analytics_utils.mdx b/api_docs/kbn_ml_data_frame_analytics_utils.mdx index 18b082154f11b..b025dd21b2ce5 100644 --- a/api_docs/kbn_ml_data_frame_analytics_utils.mdx +++ b/api_docs/kbn_ml_data_frame_analytics_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-data-frame-analytics-utils title: "@kbn/ml-data-frame-analytics-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-data-frame-analytics-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-data-frame-analytics-utils'] --- import kbnMlDataFrameAnalyticsUtilsObj from './kbn_ml_data_frame_analytics_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_data_grid.mdx b/api_docs/kbn_ml_data_grid.mdx index 285e171db74c1..542c5d25604a7 100644 --- a/api_docs/kbn_ml_data_grid.mdx +++ b/api_docs/kbn_ml_data_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-data-grid title: "@kbn/ml-data-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-data-grid plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-data-grid'] --- import kbnMlDataGridObj from './kbn_ml_data_grid.devdocs.json'; diff --git a/api_docs/kbn_ml_date_picker.mdx b/api_docs/kbn_ml_date_picker.mdx index 176825dee4909..5658c23c67bc0 100644 --- a/api_docs/kbn_ml_date_picker.mdx +++ b/api_docs/kbn_ml_date_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-date-picker title: "@kbn/ml-date-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-date-picker plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-date-picker'] --- import kbnMlDatePickerObj from './kbn_ml_date_picker.devdocs.json'; diff --git a/api_docs/kbn_ml_date_utils.mdx b/api_docs/kbn_ml_date_utils.mdx index 96654663d2dea..c1efb90693797 100644 --- a/api_docs/kbn_ml_date_utils.mdx +++ b/api_docs/kbn_ml_date_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-date-utils title: "@kbn/ml-date-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-date-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-date-utils'] --- import kbnMlDateUtilsObj from './kbn_ml_date_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_error_utils.mdx b/api_docs/kbn_ml_error_utils.mdx index d510a01909ee2..596bb9801689d 100644 --- a/api_docs/kbn_ml_error_utils.mdx +++ b/api_docs/kbn_ml_error_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-error-utils title: "@kbn/ml-error-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-error-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-error-utils'] --- import kbnMlErrorUtilsObj from './kbn_ml_error_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_in_memory_table.mdx b/api_docs/kbn_ml_in_memory_table.mdx index d8f360f77f5ed..ed864133bf9e7 100644 --- a/api_docs/kbn_ml_in_memory_table.mdx +++ b/api_docs/kbn_ml_in_memory_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-in-memory-table title: "@kbn/ml-in-memory-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-in-memory-table plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-in-memory-table'] --- import kbnMlInMemoryTableObj from './kbn_ml_in_memory_table.devdocs.json'; diff --git a/api_docs/kbn_ml_is_defined.mdx b/api_docs/kbn_ml_is_defined.mdx index c84ce256821d1..356a6f54ce0b0 100644 --- a/api_docs/kbn_ml_is_defined.mdx +++ b/api_docs/kbn_ml_is_defined.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-defined title: "@kbn/ml-is-defined" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-defined plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-defined'] --- import kbnMlIsDefinedObj from './kbn_ml_is_defined.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index 5d6af66f4ba23..a108e8f214116 100644 --- a/api_docs/kbn_ml_is_populated_object.mdx +++ b/api_docs/kbn_ml_is_populated_object.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-populated-object title: "@kbn/ml-is-populated-object" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-populated-object plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-populated-object'] --- import kbnMlIsPopulatedObjectObj from './kbn_ml_is_populated_object.devdocs.json'; diff --git a/api_docs/kbn_ml_kibana_theme.mdx b/api_docs/kbn_ml_kibana_theme.mdx index 7da63204ac6ad..85a7c41d8bc5b 100644 --- a/api_docs/kbn_ml_kibana_theme.mdx +++ b/api_docs/kbn_ml_kibana_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-kibana-theme title: "@kbn/ml-kibana-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-kibana-theme plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-kibana-theme'] --- import kbnMlKibanaThemeObj from './kbn_ml_kibana_theme.devdocs.json'; diff --git a/api_docs/kbn_ml_local_storage.mdx b/api_docs/kbn_ml_local_storage.mdx index a3bd012059125..23d8ebabf1a92 100644 --- a/api_docs/kbn_ml_local_storage.mdx +++ b/api_docs/kbn_ml_local_storage.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-local-storage title: "@kbn/ml-local-storage" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-local-storage plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-local-storage'] --- import kbnMlLocalStorageObj from './kbn_ml_local_storage.devdocs.json'; diff --git a/api_docs/kbn_ml_nested_property.mdx b/api_docs/kbn_ml_nested_property.mdx index 391277b2a3e8e..64e7ed25eaaaa 100644 --- a/api_docs/kbn_ml_nested_property.mdx +++ b/api_docs/kbn_ml_nested_property.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-nested-property title: "@kbn/ml-nested-property" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-nested-property plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-nested-property'] --- import kbnMlNestedPropertyObj from './kbn_ml_nested_property.devdocs.json'; diff --git a/api_docs/kbn_ml_number_utils.mdx b/api_docs/kbn_ml_number_utils.mdx index c4fdf69dcbd0d..9fbaeed2b4440 100644 --- a/api_docs/kbn_ml_number_utils.mdx +++ b/api_docs/kbn_ml_number_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-number-utils title: "@kbn/ml-number-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-number-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-number-utils'] --- import kbnMlNumberUtilsObj from './kbn_ml_number_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_query_utils.mdx b/api_docs/kbn_ml_query_utils.mdx index 182b27b80693a..f568f4c154d37 100644 --- a/api_docs/kbn_ml_query_utils.mdx +++ b/api_docs/kbn_ml_query_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-query-utils title: "@kbn/ml-query-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-query-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-query-utils'] --- import kbnMlQueryUtilsObj from './kbn_ml_query_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_random_sampler_utils.mdx b/api_docs/kbn_ml_random_sampler_utils.mdx index ebb6a2c6a8288..5b8e0aa395b5a 100644 --- a/api_docs/kbn_ml_random_sampler_utils.mdx +++ b/api_docs/kbn_ml_random_sampler_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-random-sampler-utils title: "@kbn/ml-random-sampler-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-random-sampler-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-random-sampler-utils'] --- import kbnMlRandomSamplerUtilsObj from './kbn_ml_random_sampler_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_route_utils.mdx b/api_docs/kbn_ml_route_utils.mdx index 817979a11aa55..541c59270faf0 100644 --- a/api_docs/kbn_ml_route_utils.mdx +++ b/api_docs/kbn_ml_route_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-route-utils title: "@kbn/ml-route-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-route-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-route-utils'] --- import kbnMlRouteUtilsObj from './kbn_ml_route_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_runtime_field_utils.mdx b/api_docs/kbn_ml_runtime_field_utils.mdx index 2ae01ab9f2ee5..ea8b528abc18d 100644 --- a/api_docs/kbn_ml_runtime_field_utils.mdx +++ b/api_docs/kbn_ml_runtime_field_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-runtime-field-utils title: "@kbn/ml-runtime-field-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-runtime-field-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-runtime-field-utils'] --- import kbnMlRuntimeFieldUtilsObj from './kbn_ml_runtime_field_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index 351b2223df9a1..73bcce60e8743 100644 --- a/api_docs/kbn_ml_string_hash.mdx +++ b/api_docs/kbn_ml_string_hash.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-string-hash title: "@kbn/ml-string-hash" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-string-hash plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_ml_trained_models_utils.mdx b/api_docs/kbn_ml_trained_models_utils.mdx index 071b1e8d92816..f2c3f09ba9e53 100644 --- a/api_docs/kbn_ml_trained_models_utils.mdx +++ b/api_docs/kbn_ml_trained_models_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-trained-models-utils title: "@kbn/ml-trained-models-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-trained-models-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-trained-models-utils'] --- import kbnMlTrainedModelsUtilsObj from './kbn_ml_trained_models_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_ui_actions.mdx b/api_docs/kbn_ml_ui_actions.mdx index f981f2a24364e..3da36a0186e79 100644 --- a/api_docs/kbn_ml_ui_actions.mdx +++ b/api_docs/kbn_ml_ui_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-ui-actions title: "@kbn/ml-ui-actions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-ui-actions plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-ui-actions'] --- import kbnMlUiActionsObj from './kbn_ml_ui_actions.devdocs.json'; diff --git a/api_docs/kbn_ml_url_state.mdx b/api_docs/kbn_ml_url_state.mdx index b46b1794d4dc6..94576f66e8f61 100644 --- a/api_docs/kbn_ml_url_state.mdx +++ b/api_docs/kbn_ml_url_state.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-url-state title: "@kbn/ml-url-state" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-url-state plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-url-state'] --- import kbnMlUrlStateObj from './kbn_ml_url_state.devdocs.json'; diff --git a/api_docs/kbn_mock_idp_utils.mdx b/api_docs/kbn_mock_idp_utils.mdx index 05675bd96a17d..8fb3c3c8015cb 100644 --- a/api_docs/kbn_mock_idp_utils.mdx +++ b/api_docs/kbn_mock_idp_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mock-idp-utils title: "@kbn/mock-idp-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mock-idp-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mock-idp-utils'] --- import kbnMockIdpUtilsObj from './kbn_mock_idp_utils.devdocs.json'; diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index b5dcc8f0fedbb..2bb9a466be433 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/monaco plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] --- import kbnMonacoObj from './kbn_monaco.devdocs.json'; diff --git a/api_docs/kbn_object_versioning.mdx b/api_docs/kbn_object_versioning.mdx index c80254b71493d..13df55f6e809e 100644 --- a/api_docs/kbn_object_versioning.mdx +++ b/api_docs/kbn_object_versioning.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-object-versioning title: "@kbn/object-versioning" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/object-versioning plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/object-versioning'] --- import kbnObjectVersioningObj from './kbn_object_versioning.devdocs.json'; diff --git a/api_docs/kbn_observability_alert_details.mdx b/api_docs/kbn_observability_alert_details.mdx index 47543a13fa62d..9e97a3c54f074 100644 --- a/api_docs/kbn_observability_alert_details.mdx +++ b/api_docs/kbn_observability_alert_details.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-alert-details title: "@kbn/observability-alert-details" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-alert-details plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-alert-details'] --- import kbnObservabilityAlertDetailsObj from './kbn_observability_alert_details.devdocs.json'; diff --git a/api_docs/kbn_observability_alerting_test_data.mdx b/api_docs/kbn_observability_alerting_test_data.mdx index 0d33b61044a5a..a7d475f2b88a0 100644 --- a/api_docs/kbn_observability_alerting_test_data.mdx +++ b/api_docs/kbn_observability_alerting_test_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-alerting-test-data title: "@kbn/observability-alerting-test-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-alerting-test-data plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-alerting-test-data'] --- import kbnObservabilityAlertingTestDataObj from './kbn_observability_alerting_test_data.devdocs.json'; diff --git a/api_docs/kbn_observability_get_padded_alert_time_range_util.mdx b/api_docs/kbn_observability_get_padded_alert_time_range_util.mdx index 330ea9dfda1d7..8e86483e02111 100644 --- a/api_docs/kbn_observability_get_padded_alert_time_range_util.mdx +++ b/api_docs/kbn_observability_get_padded_alert_time_range_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-get-padded-alert-time-range-util title: "@kbn/observability-get-padded-alert-time-range-util" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-get-padded-alert-time-range-util plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-get-padded-alert-time-range-util'] --- import kbnObservabilityGetPaddedAlertTimeRangeUtilObj from './kbn_observability_get_padded_alert_time_range_util.devdocs.json'; diff --git a/api_docs/kbn_openapi_bundler.mdx b/api_docs/kbn_openapi_bundler.mdx index 03c0bcf965fec..3c883b6c32834 100644 --- a/api_docs/kbn_openapi_bundler.mdx +++ b/api_docs/kbn_openapi_bundler.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-openapi-bundler title: "@kbn/openapi-bundler" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/openapi-bundler plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/openapi-bundler'] --- import kbnOpenapiBundlerObj from './kbn_openapi_bundler.devdocs.json'; diff --git a/api_docs/kbn_openapi_generator.mdx b/api_docs/kbn_openapi_generator.mdx index 64095a40985f4..8181cba20d589 100644 --- a/api_docs/kbn_openapi_generator.mdx +++ b/api_docs/kbn_openapi_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-openapi-generator title: "@kbn/openapi-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/openapi-generator plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/openapi-generator'] --- import kbnOpenapiGeneratorObj from './kbn_openapi_generator.devdocs.json'; diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index 7567697ecd729..1e9169f7951d9 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] --- import kbnOptimizerObj from './kbn_optimizer.devdocs.json'; diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index 49238d50aae42..d180cb75aaf73 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] --- import kbnOptimizerWebpackHelpersObj from './kbn_optimizer_webpack_helpers.devdocs.json'; diff --git a/api_docs/kbn_osquery_io_ts_types.mdx b/api_docs/kbn_osquery_io_ts_types.mdx index 94df5b5c4ffb5..f6cdc4023c1ac 100644 --- a/api_docs/kbn_osquery_io_ts_types.mdx +++ b/api_docs/kbn_osquery_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-osquery-io-ts-types title: "@kbn/osquery-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/osquery-io-ts-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/osquery-io-ts-types'] --- import kbnOsqueryIoTsTypesObj from './kbn_osquery_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_panel_loader.mdx b/api_docs/kbn_panel_loader.mdx index 2acfa54ff357f..7b843f4bc7ede 100644 --- a/api_docs/kbn_panel_loader.mdx +++ b/api_docs/kbn_panel_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-panel-loader title: "@kbn/panel-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/panel-loader plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/panel-loader'] --- import kbnPanelLoaderObj from './kbn_panel_loader.devdocs.json'; diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index 94248a84fd64d..159b048637d13 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] --- import kbnPerformanceTestingDatasetExtractorObj from './kbn_performance_testing_dataset_extractor.devdocs.json'; diff --git a/api_docs/kbn_plugin_check.mdx b/api_docs/kbn_plugin_check.mdx index 8fa09593837ee..f4f15f63189b8 100644 --- a/api_docs/kbn_plugin_check.mdx +++ b/api_docs/kbn_plugin_check.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-check title: "@kbn/plugin-check" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-check plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-check'] --- import kbnPluginCheckObj from './kbn_plugin_check.devdocs.json'; diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index a3f2320baff97..deb008aa21e44 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-generator plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] --- import kbnPluginGeneratorObj from './kbn_plugin_generator.devdocs.json'; diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index 3bcf0ddf1337d..445692ba4dc04 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-helpers plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] --- import kbnPluginHelpersObj from './kbn_plugin_helpers.devdocs.json'; diff --git a/api_docs/kbn_presentation_containers.mdx b/api_docs/kbn_presentation_containers.mdx index 3bd18d6afa2c9..f3e2878644969 100644 --- a/api_docs/kbn_presentation_containers.mdx +++ b/api_docs/kbn_presentation_containers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-presentation-containers title: "@kbn/presentation-containers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/presentation-containers plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/presentation-containers'] --- import kbnPresentationContainersObj from './kbn_presentation_containers.devdocs.json'; diff --git a/api_docs/kbn_presentation_library.mdx b/api_docs/kbn_presentation_library.mdx index 6c622d763a4e7..958c02b35996e 100644 --- a/api_docs/kbn_presentation_library.mdx +++ b/api_docs/kbn_presentation_library.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-presentation-library title: "@kbn/presentation-library" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/presentation-library plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/presentation-library'] --- import kbnPresentationLibraryObj from './kbn_presentation_library.devdocs.json'; diff --git a/api_docs/kbn_presentation_publishing.mdx b/api_docs/kbn_presentation_publishing.mdx index 7e7196d62bf2d..71a1ea60e3f8f 100644 --- a/api_docs/kbn_presentation_publishing.mdx +++ b/api_docs/kbn_presentation_publishing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-presentation-publishing title: "@kbn/presentation-publishing" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/presentation-publishing plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/presentation-publishing'] --- import kbnPresentationPublishingObj from './kbn_presentation_publishing.devdocs.json'; diff --git a/api_docs/kbn_profiling_utils.mdx b/api_docs/kbn_profiling_utils.mdx index 34f387cf84709..c1a03a92d7b75 100644 --- a/api_docs/kbn_profiling_utils.mdx +++ b/api_docs/kbn_profiling_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-profiling-utils title: "@kbn/profiling-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/profiling-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/profiling-utils'] --- import kbnProfilingUtilsObj from './kbn_profiling_utils.devdocs.json'; diff --git a/api_docs/kbn_random_sampling.mdx b/api_docs/kbn_random_sampling.mdx index 3f57ed02c9933..2840a4110c2da 100644 --- a/api_docs/kbn_random_sampling.mdx +++ b/api_docs/kbn_random_sampling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-random-sampling title: "@kbn/random-sampling" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/random-sampling plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/random-sampling'] --- import kbnRandomSamplingObj from './kbn_random_sampling.devdocs.json'; diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index 3f790740f0cd1..eec114bf0b6a3 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-field plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_common.mdx b/api_docs/kbn_react_kibana_context_common.mdx index 486ef63511d1a..5eba525966cf8 100644 --- a/api_docs/kbn_react_kibana_context_common.mdx +++ b/api_docs/kbn_react_kibana_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-common title: "@kbn/react-kibana-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-common'] --- import kbnReactKibanaContextCommonObj from './kbn_react_kibana_context_common.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_render.mdx b/api_docs/kbn_react_kibana_context_render.mdx index fdf93464c5e05..fea80285dd3a4 100644 --- a/api_docs/kbn_react_kibana_context_render.mdx +++ b/api_docs/kbn_react_kibana_context_render.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-render title: "@kbn/react-kibana-context-render" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-render plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-render'] --- import kbnReactKibanaContextRenderObj from './kbn_react_kibana_context_render.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_root.mdx b/api_docs/kbn_react_kibana_context_root.mdx index 57ae6ed612015..7a3635d4cf544 100644 --- a/api_docs/kbn_react_kibana_context_root.mdx +++ b/api_docs/kbn_react_kibana_context_root.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-root title: "@kbn/react-kibana-context-root" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-root plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-root'] --- import kbnReactKibanaContextRootObj from './kbn_react_kibana_context_root.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_styled.mdx b/api_docs/kbn_react_kibana_context_styled.mdx index 1d1c69c0d89d2..0ffeb3e677282 100644 --- a/api_docs/kbn_react_kibana_context_styled.mdx +++ b/api_docs/kbn_react_kibana_context_styled.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-styled title: "@kbn/react-kibana-context-styled" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-styled plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-styled'] --- import kbnReactKibanaContextStyledObj from './kbn_react_kibana_context_styled.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_context_theme.mdx b/api_docs/kbn_react_kibana_context_theme.mdx index 280bff6ef8090..90bd0fea0152a 100644 --- a/api_docs/kbn_react_kibana_context_theme.mdx +++ b/api_docs/kbn_react_kibana_context_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-context-theme title: "@kbn/react-kibana-context-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-context-theme plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-context-theme'] --- import kbnReactKibanaContextThemeObj from './kbn_react_kibana_context_theme.devdocs.json'; diff --git a/api_docs/kbn_react_kibana_mount.mdx b/api_docs/kbn_react_kibana_mount.mdx index 49bf6c3a2c3ac..c6feb7cbbab4c 100644 --- a/api_docs/kbn_react_kibana_mount.mdx +++ b/api_docs/kbn_react_kibana_mount.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-kibana-mount title: "@kbn/react-kibana-mount" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-kibana-mount plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-kibana-mount'] --- import kbnReactKibanaMountObj from './kbn_react_kibana_mount.devdocs.json'; diff --git a/api_docs/kbn_repo_file_maps.mdx b/api_docs/kbn_repo_file_maps.mdx index f0eaa388ddb2f..aa6a2238db25e 100644 --- a/api_docs/kbn_repo_file_maps.mdx +++ b/api_docs/kbn_repo_file_maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-file-maps title: "@kbn/repo-file-maps" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-file-maps plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-file-maps'] --- import kbnRepoFileMapsObj from './kbn_repo_file_maps.devdocs.json'; diff --git a/api_docs/kbn_repo_linter.mdx b/api_docs/kbn_repo_linter.mdx index bd7cccacd1727..0fab175c2d4fa 100644 --- a/api_docs/kbn_repo_linter.mdx +++ b/api_docs/kbn_repo_linter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-linter title: "@kbn/repo-linter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-linter plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-linter'] --- import kbnRepoLinterObj from './kbn_repo_linter.devdocs.json'; diff --git a/api_docs/kbn_repo_path.mdx b/api_docs/kbn_repo_path.mdx index e9377fcac668f..e9cd4f7049895 100644 --- a/api_docs/kbn_repo_path.mdx +++ b/api_docs/kbn_repo_path.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-path title: "@kbn/repo-path" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-path plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-path'] --- import kbnRepoPathObj from './kbn_repo_path.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index bd87e83a30dfa..b77b0494755aa 100644 --- a/api_docs/kbn_repo_source_classifier.mdx +++ b/api_docs/kbn_repo_source_classifier.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-source-classifier title: "@kbn/repo-source-classifier" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-source-classifier plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; diff --git a/api_docs/kbn_reporting_common.mdx b/api_docs/kbn_reporting_common.mdx index 762b1fecab40a..630aff5e2cadf 100644 --- a/api_docs/kbn_reporting_common.mdx +++ b/api_docs/kbn_reporting_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-common title: "@kbn/reporting-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-common'] --- import kbnReportingCommonObj from './kbn_reporting_common.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_csv.mdx b/api_docs/kbn_reporting_export_types_csv.mdx index 91959d82f64b0..9b6fc11be12ab 100644 --- a/api_docs/kbn_reporting_export_types_csv.mdx +++ b/api_docs/kbn_reporting_export_types_csv.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-csv title: "@kbn/reporting-export-types-csv" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-csv plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-csv'] --- import kbnReportingExportTypesCsvObj from './kbn_reporting_export_types_csv.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_csv_common.mdx b/api_docs/kbn_reporting_export_types_csv_common.mdx index c1962febea500..bce9115330ea7 100644 --- a/api_docs/kbn_reporting_export_types_csv_common.mdx +++ b/api_docs/kbn_reporting_export_types_csv_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-csv-common title: "@kbn/reporting-export-types-csv-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-csv-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-csv-common'] --- import kbnReportingExportTypesCsvCommonObj from './kbn_reporting_export_types_csv_common.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_pdf.mdx b/api_docs/kbn_reporting_export_types_pdf.mdx index 376668862c72e..b2e84b89ba455 100644 --- a/api_docs/kbn_reporting_export_types_pdf.mdx +++ b/api_docs/kbn_reporting_export_types_pdf.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-pdf title: "@kbn/reporting-export-types-pdf" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-pdf plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-pdf'] --- import kbnReportingExportTypesPdfObj from './kbn_reporting_export_types_pdf.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_pdf_common.mdx b/api_docs/kbn_reporting_export_types_pdf_common.mdx index 67e0f71c7c96d..8ef3498cfb734 100644 --- a/api_docs/kbn_reporting_export_types_pdf_common.mdx +++ b/api_docs/kbn_reporting_export_types_pdf_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-pdf-common title: "@kbn/reporting-export-types-pdf-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-pdf-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-pdf-common'] --- import kbnReportingExportTypesPdfCommonObj from './kbn_reporting_export_types_pdf_common.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_png.mdx b/api_docs/kbn_reporting_export_types_png.mdx index 9b14ad11f446f..aa0457602972d 100644 --- a/api_docs/kbn_reporting_export_types_png.mdx +++ b/api_docs/kbn_reporting_export_types_png.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-png title: "@kbn/reporting-export-types-png" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-png plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-png'] --- import kbnReportingExportTypesPngObj from './kbn_reporting_export_types_png.devdocs.json'; diff --git a/api_docs/kbn_reporting_export_types_png_common.mdx b/api_docs/kbn_reporting_export_types_png_common.mdx index 391dde6c3a857..2fe1010dd11c3 100644 --- a/api_docs/kbn_reporting_export_types_png_common.mdx +++ b/api_docs/kbn_reporting_export_types_png_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-export-types-png-common title: "@kbn/reporting-export-types-png-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-export-types-png-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-export-types-png-common'] --- import kbnReportingExportTypesPngCommonObj from './kbn_reporting_export_types_png_common.devdocs.json'; diff --git a/api_docs/kbn_reporting_mocks_server.mdx b/api_docs/kbn_reporting_mocks_server.mdx index 4832a30ad3cd2..313ae7f17e449 100644 --- a/api_docs/kbn_reporting_mocks_server.mdx +++ b/api_docs/kbn_reporting_mocks_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-mocks-server title: "@kbn/reporting-mocks-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-mocks-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-mocks-server'] --- import kbnReportingMocksServerObj from './kbn_reporting_mocks_server.devdocs.json'; diff --git a/api_docs/kbn_reporting_public.mdx b/api_docs/kbn_reporting_public.mdx index ff59b8aca02ee..36b0439c77084 100644 --- a/api_docs/kbn_reporting_public.mdx +++ b/api_docs/kbn_reporting_public.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-public title: "@kbn/reporting-public" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-public plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-public'] --- import kbnReportingPublicObj from './kbn_reporting_public.devdocs.json'; diff --git a/api_docs/kbn_reporting_server.mdx b/api_docs/kbn_reporting_server.mdx index 54d69eb52ce60..6774ad197b464 100644 --- a/api_docs/kbn_reporting_server.mdx +++ b/api_docs/kbn_reporting_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-server title: "@kbn/reporting-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-server'] --- import kbnReportingServerObj from './kbn_reporting_server.devdocs.json'; diff --git a/api_docs/kbn_resizable_layout.mdx b/api_docs/kbn_resizable_layout.mdx index e73a8f6a7a9c1..5f99ba7ae1cdf 100644 --- a/api_docs/kbn_resizable_layout.mdx +++ b/api_docs/kbn_resizable_layout.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-resizable-layout title: "@kbn/resizable-layout" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/resizable-layout plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/resizable-layout'] --- import kbnResizableLayoutObj from './kbn_resizable_layout.devdocs.json'; diff --git a/api_docs/kbn_rison.mdx b/api_docs/kbn_rison.mdx index fdb2f18b13f33..c2bdf4be9965e 100644 --- a/api_docs/kbn_rison.mdx +++ b/api_docs/kbn_rison.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rison title: "@kbn/rison" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rison plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rison'] --- import kbnRisonObj from './kbn_rison.devdocs.json'; diff --git a/api_docs/kbn_router_utils.mdx b/api_docs/kbn_router_utils.mdx index b0ef1a9daa8a8..1ca59372b7570 100644 --- a/api_docs/kbn_router_utils.mdx +++ b/api_docs/kbn_router_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-router-utils title: "@kbn/router-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/router-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/router-utils'] --- import kbnRouterUtilsObj from './kbn_router_utils.devdocs.json'; diff --git a/api_docs/kbn_rrule.mdx b/api_docs/kbn_rrule.mdx index 6912366076fa1..4a89a559a333d 100644 --- a/api_docs/kbn_rrule.mdx +++ b/api_docs/kbn_rrule.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rrule title: "@kbn/rrule" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rrule plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rrule'] --- import kbnRruleObj from './kbn_rrule.devdocs.json'; diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index 8ef629c666c19..466e678fb1048 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rule-data-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; diff --git a/api_docs/kbn_saved_objects_settings.mdx b/api_docs/kbn_saved_objects_settings.mdx index 04e814aafa245..51970f6b0b36d 100644 --- a/api_docs/kbn_saved_objects_settings.mdx +++ b/api_docs/kbn_saved_objects_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-saved-objects-settings title: "@kbn/saved-objects-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/saved-objects-settings plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/saved-objects-settings'] --- import kbnSavedObjectsSettingsObj from './kbn_saved_objects_settings.devdocs.json'; diff --git a/api_docs/kbn_search_api_panels.devdocs.json b/api_docs/kbn_search_api_panels.devdocs.json index a55ccad88b62e..c6fd706ade36f 100644 --- a/api_docs/kbn_search_api_panels.devdocs.json +++ b/api_docs/kbn_search_api_panels.devdocs.json @@ -74,7 +74,7 @@ "label": "CodeBox", "description": [], "signature": [ - "({ application, codeSnippet, languageType, languages, assetBasePath, selectedLanguage, setSelectedLanguage, sharePlugin, consoleRequest, }: React.PropsWithChildren) => JSX.Element" + "({ application, codeSnippet, consolePlugin, languageType, languages, assetBasePath, selectedLanguage, setSelectedLanguage, sharePlugin, consoleRequest, }: React.PropsWithChildren) => JSX.Element" ], "path": "packages/kbn-search-api-panels/components/code_box.tsx", "deprecated": false, @@ -85,7 +85,7 @@ "id": "def-common.CodeBox.$1", "type": "CompoundType", "tags": [], - "label": "{\n application,\n codeSnippet,\n languageType,\n languages,\n assetBasePath,\n selectedLanguage,\n setSelectedLanguage,\n sharePlugin,\n consoleRequest,\n}", + "label": "{\n application,\n codeSnippet,\n consolePlugin,\n languageType,\n languages,\n assetBasePath,\n selectedLanguage,\n setSelectedLanguage,\n sharePlugin,\n consoleRequest,\n}", "description": [], "signature": [ "React.PropsWithChildren" @@ -326,7 +326,7 @@ "label": "IngestData", "description": [], "signature": [ - "({ codeSnippet, selectedLanguage, setSelectedLanguage, docLinks, assetBasePath, application, sharePlugin, languages, consoleRequest, additionalIngestionPanel, }: React.PropsWithChildren) => JSX.Element" + "({ codeSnippet, selectedLanguage, setSelectedLanguage, docLinks, assetBasePath, application, consolePlugin, sharePlugin, languages, consoleRequest, additionalIngestionPanel, }: React.PropsWithChildren) => JSX.Element" ], "path": "packages/kbn-search-api-panels/components/ingest_data.tsx", "deprecated": false, @@ -337,7 +337,7 @@ "id": "def-common.IngestData.$1", "type": "CompoundType", "tags": [], - "label": "{\n codeSnippet,\n selectedLanguage,\n setSelectedLanguage,\n docLinks,\n assetBasePath,\n application,\n sharePlugin,\n languages,\n consoleRequest,\n additionalIngestionPanel,\n}", + "label": "{\n codeSnippet,\n selectedLanguage,\n setSelectedLanguage,\n docLinks,\n assetBasePath,\n application,\n consolePlugin,\n sharePlugin,\n languages,\n consoleRequest,\n additionalIngestionPanel,\n}", "description": [], "signature": [ "React.PropsWithChildren" @@ -573,7 +573,7 @@ "label": "TryInConsoleButton", "description": [], "signature": [ - "({ request, application, sharePlugin, }: ", + "({ request, application, consolePlugin, sharePlugin, }: ", { "pluginId": "@kbn/search-api-panels", "scope": "common", @@ -592,7 +592,7 @@ "id": "def-common.TryInConsoleButton.$1", "type": "Object", "tags": [], - "label": "{\n request,\n application,\n sharePlugin,\n}", + "label": "{\n request,\n application,\n consolePlugin,\n sharePlugin,\n}", "description": [], "signature": [ { @@ -1214,6 +1214,27 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.TryInConsoleButtonProps.consolePlugin", + "type": "Object", + "tags": [], + "label": "consolePlugin", + "description": [], + "signature": [ + { + "pluginId": "console", + "scope": "public", + "docId": "kibConsolePluginApi", + "section": "def-public.ConsolePluginStart", + "text": "ConsolePluginStart" + }, + " | undefined" + ], + "path": "packages/kbn-search-api-panels/components/try_in_console_button.tsx", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "@kbn/search-api-panels", "id": "def-common.TryInConsoleButtonProps.sharePlugin", diff --git a/api_docs/kbn_search_api_panels.mdx b/api_docs/kbn_search_api_panels.mdx index 9f1324396706f..812b2c9ddff40 100644 --- a/api_docs/kbn_search_api_panels.mdx +++ b/api_docs/kbn_search_api_panels.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-api-panels title: "@kbn/search-api-panels" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-api-panels plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-api-panels'] --- import kbnSearchApiPanelsObj from './kbn_search_api_panels.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 75 | 0 | 75 | 0 | +| 76 | 0 | 76 | 0 | ## Common diff --git a/api_docs/kbn_search_connectors.devdocs.json b/api_docs/kbn_search_connectors.devdocs.json index c10a612df9100..818eea8ba852b 100644 --- a/api_docs/kbn_search_connectors.devdocs.json +++ b/api_docs/kbn_search_connectors.devdocs.json @@ -676,6 +676,76 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/search-connectors", + "id": "def-common.deleteConnectorSecret", + "type": "Function", + "tags": [], + "label": "deleteConnectorSecret", + "description": [], + "signature": [ + "(client: ", + { + "pluginId": "@kbn/core-elasticsearch-server", + "scope": "common", + "docId": "kibKbnCoreElasticsearchServerPluginApi", + "section": "def-common.ElasticsearchClient", + "text": "ElasticsearchClient" + }, + ", id: string) => Promise<", + { + "pluginId": "@kbn/search-connectors", + "scope": "common", + "docId": "kibKbnSearchConnectorsPluginApi", + "section": "def-common.ConnectorsAPIUpdateResponse", + "text": "ConnectorsAPIUpdateResponse" + }, + ">" + ], + "path": "packages/kbn-search-connectors/lib/delete_connector_secret.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-connectors", + "id": "def-common.deleteConnectorSecret.$1", + "type": "Object", + "tags": [], + "label": "client", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-elasticsearch-server", + "scope": "common", + "docId": "kibKbnCoreElasticsearchServerPluginApi", + "section": "def-common.ElasticsearchClient", + "text": "ElasticsearchClient" + } + ], + "path": "packages/kbn-search-connectors/lib/delete_connector_secret.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/search-connectors", + "id": "def-common.deleteConnectorSecret.$2", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "string" + ], + "path": "packages/kbn-search-connectors/lib/delete_connector_secret.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/search-connectors", "id": "def-common.fetchConnectorById", @@ -2154,6 +2224,91 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/search-connectors", + "id": "def-common.updateConnectorSecret", + "type": "Function", + "tags": [], + "label": "updateConnectorSecret", + "description": [], + "signature": [ + "(client: ", + { + "pluginId": "@kbn/core-elasticsearch-server", + "scope": "common", + "docId": "kibKbnCoreElasticsearchServerPluginApi", + "section": "def-common.ElasticsearchClient", + "text": "ElasticsearchClient" + }, + ", value: string, secretId: string) => Promise<", + { + "pluginId": "@kbn/search-connectors", + "scope": "common", + "docId": "kibKbnSearchConnectorsPluginApi", + "section": "def-common.ConnectorsAPIUpdateResponse", + "text": "ConnectorsAPIUpdateResponse" + }, + ">" + ], + "path": "packages/kbn-search-connectors/lib/update_connector_secret.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-connectors", + "id": "def-common.updateConnectorSecret.$1", + "type": "Object", + "tags": [], + "label": "client", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-elasticsearch-server", + "scope": "common", + "docId": "kibKbnCoreElasticsearchServerPluginApi", + "section": "def-common.ElasticsearchClient", + "text": "ElasticsearchClient" + } + ], + "path": "packages/kbn-search-connectors/lib/update_connector_secret.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/search-connectors", + "id": "def-common.updateConnectorSecret.$2", + "type": "string", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "string" + ], + "path": "packages/kbn-search-connectors/lib/update_connector_secret.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/search-connectors", + "id": "def-common.updateConnectorSecret.$3", + "type": "string", + "tags": [], + "label": "secretId", + "description": [], + "signature": [ + "string" + ], + "path": "packages/kbn-search-connectors/lib/update_connector_secret.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/search-connectors", "id": "def-common.updateConnectorServiceType", diff --git a/api_docs/kbn_search_connectors.mdx b/api_docs/kbn_search_connectors.mdx index 24aedb4a3b174..13b78b12cb609 100644 --- a/api_docs/kbn_search_connectors.mdx +++ b/api_docs/kbn_search_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-connectors title: "@kbn/search-connectors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-connectors plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-connectors'] --- import kbnSearchConnectorsObj from './kbn_search_connectors.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2805 | 0 | 2805 | 0 | +| 2812 | 0 | 2812 | 0 | ## Common diff --git a/api_docs/kbn_search_errors.mdx b/api_docs/kbn_search_errors.mdx index 40f59bcfabf5d..7ceca40db2598 100644 --- a/api_docs/kbn_search_errors.mdx +++ b/api_docs/kbn_search_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-errors title: "@kbn/search-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-errors plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-errors'] --- import kbnSearchErrorsObj from './kbn_search_errors.devdocs.json'; diff --git a/api_docs/kbn_search_index_documents.mdx b/api_docs/kbn_search_index_documents.mdx index 1990cfdeb5163..561c8c72e933d 100644 --- a/api_docs/kbn_search_index_documents.mdx +++ b/api_docs/kbn_search_index_documents.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-index-documents title: "@kbn/search-index-documents" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-index-documents plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-index-documents'] --- import kbnSearchIndexDocumentsObj from './kbn_search_index_documents.devdocs.json'; diff --git a/api_docs/kbn_search_response_warnings.mdx b/api_docs/kbn_search_response_warnings.mdx index d1025cbf86dfd..d3009463483c7 100644 --- a/api_docs/kbn_search_response_warnings.mdx +++ b/api_docs/kbn_search_response_warnings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-search-response-warnings title: "@kbn/search-response-warnings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/search-response-warnings plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-response-warnings'] --- import kbnSearchResponseWarningsObj from './kbn_search_response_warnings.devdocs.json'; diff --git a/api_docs/kbn_security_hardening.devdocs.json b/api_docs/kbn_security_hardening.devdocs.json new file mode 100644 index 0000000000000..b7a4222c46515 --- /dev/null +++ b/api_docs/kbn_security_hardening.devdocs.json @@ -0,0 +1,126 @@ +{ + "id": "@kbn/security-hardening", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [ + { + "parentPluginId": "@kbn/security-hardening", + "id": "def-common.unsafeConsole", + "type": "Object", + "tags": [], + "label": "unsafeConsole", + "description": [], + "path": "packages/kbn-security-hardening/console.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/security-hardening", + "id": "def-common.unsafeConsole.debug", + "type": "Function", + "tags": [], + "label": "debug", + "description": [], + "signature": [ + "{ (...data: any[]): void; (message?: any, ...optionalParams: any[]): void; }" + ], + "path": "packages/kbn-security-hardening/console.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/security-hardening", + "id": "def-common.unsafeConsole.error", + "type": "Function", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "{ (...data: any[]): void; (message?: any, ...optionalParams: any[]): void; }" + ], + "path": "packages/kbn-security-hardening/console.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/security-hardening", + "id": "def-common.unsafeConsole.info", + "type": "Function", + "tags": [], + "label": "info", + "description": [], + "signature": [ + "{ (...data: any[]): void; (message?: any, ...optionalParams: any[]): void; }" + ], + "path": "packages/kbn-security-hardening/console.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/security-hardening", + "id": "def-common.unsafeConsole.log", + "type": "Function", + "tags": [], + "label": "log", + "description": [], + "signature": [ + "{ (...data: any[]): void; (message?: any, ...optionalParams: any[]): void; }" + ], + "path": "packages/kbn-security-hardening/console.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/security-hardening", + "id": "def-common.unsafeConsole.trace", + "type": "Function", + "tags": [], + "label": "trace", + "description": [], + "signature": [ + "{ (...data: any[]): void; (message?: any, ...optionalParams: any[]): void; }" + ], + "path": "packages/kbn-security-hardening/console.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/security-hardening", + "id": "def-common.unsafeConsole.warn", + "type": "Function", + "tags": [], + "label": "warn", + "description": [], + "signature": [ + "{ (...data: any[]): void; (message?: any, ...optionalParams: any[]): void; }" + ], + "path": "packages/kbn-security-hardening/console.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ] + } +} \ No newline at end of file diff --git a/api_docs/kbn_security_hardening.mdx b/api_docs/kbn_security_hardening.mdx new file mode 100644 index 0000000000000..506797a5eb4a9 --- /dev/null +++ b/api_docs/kbn_security_hardening.mdx @@ -0,0 +1,30 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnSecurityHardeningPluginApi +slug: /kibana-dev-docs/api/kbn-security-hardening +title: "@kbn/security-hardening" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/security-hardening plugin +date: 2024-02-11 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-hardening'] +--- +import kbnSecurityHardeningObj from './kbn_security_hardening.devdocs.json'; + + + +Contact [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 7 | 0 | 7 | 0 | + +## Common + +### Objects + + diff --git a/api_docs/kbn_security_plugin_types_common.mdx b/api_docs/kbn_security_plugin_types_common.mdx index 8bbb0930a8218..0eef6f558a0f1 100644 --- a/api_docs/kbn_security_plugin_types_common.mdx +++ b/api_docs/kbn_security_plugin_types_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-plugin-types-common title: "@kbn/security-plugin-types-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-plugin-types-common plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-plugin-types-common'] --- import kbnSecurityPluginTypesCommonObj from './kbn_security_plugin_types_common.devdocs.json'; diff --git a/api_docs/kbn_security_plugin_types_public.mdx b/api_docs/kbn_security_plugin_types_public.mdx index 118471660e70d..bfab62e70c73b 100644 --- a/api_docs/kbn_security_plugin_types_public.mdx +++ b/api_docs/kbn_security_plugin_types_public.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-plugin-types-public title: "@kbn/security-plugin-types-public" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-plugin-types-public plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-plugin-types-public'] --- import kbnSecurityPluginTypesPublicObj from './kbn_security_plugin_types_public.devdocs.json'; diff --git a/api_docs/kbn_security_plugin_types_server.mdx b/api_docs/kbn_security_plugin_types_server.mdx index e61141b0b426d..dde02a75ddaf1 100644 --- a/api_docs/kbn_security_plugin_types_server.mdx +++ b/api_docs/kbn_security_plugin_types_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-plugin-types-server title: "@kbn/security-plugin-types-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-plugin-types-server plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-plugin-types-server'] --- import kbnSecurityPluginTypesServerObj from './kbn_security_plugin_types_server.devdocs.json'; diff --git a/api_docs/kbn_security_solution_features.mdx b/api_docs/kbn_security_solution_features.mdx index 54e7340cc6579..fe05c6a0be8f5 100644 --- a/api_docs/kbn_security_solution_features.mdx +++ b/api_docs/kbn_security_solution_features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-features title: "@kbn/security-solution-features" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-features plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-features'] --- import kbnSecuritySolutionFeaturesObj from './kbn_security_solution_features.devdocs.json'; diff --git a/api_docs/kbn_security_solution_navigation.mdx b/api_docs/kbn_security_solution_navigation.mdx index 6dbbe7716d967..261c3fc9e4622 100644 --- a/api_docs/kbn_security_solution_navigation.mdx +++ b/api_docs/kbn_security_solution_navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-navigation title: "@kbn/security-solution-navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-navigation plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-navigation'] --- import kbnSecuritySolutionNavigationObj from './kbn_security_solution_navigation.devdocs.json'; diff --git a/api_docs/kbn_security_solution_side_nav.mdx b/api_docs/kbn_security_solution_side_nav.mdx index 57f3d6f24ee87..a613c8eb4b097 100644 --- a/api_docs/kbn_security_solution_side_nav.mdx +++ b/api_docs/kbn_security_solution_side_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-side-nav title: "@kbn/security-solution-side-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-side-nav plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-side-nav'] --- import kbnSecuritySolutionSideNavObj from './kbn_security_solution_side_nav.devdocs.json'; diff --git a/api_docs/kbn_security_solution_storybook_config.mdx b/api_docs/kbn_security_solution_storybook_config.mdx index 2d83c4bb266e9..5a315439000d8 100644 --- a/api_docs/kbn_security_solution_storybook_config.mdx +++ b/api_docs/kbn_security_solution_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-storybook-config title: "@kbn/security-solution-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-storybook-config plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-storybook-config'] --- import kbnSecuritySolutionStorybookConfigObj from './kbn_security_solution_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index 35b773bd297f0..7f9929e578c73 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_data_table.mdx b/api_docs/kbn_securitysolution_data_table.mdx index f683ceae72346..b5460a5464635 100644 --- a/api_docs/kbn_securitysolution_data_table.mdx +++ b/api_docs/kbn_securitysolution_data_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-data-table title: "@kbn/securitysolution-data-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-data-table plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-data-table'] --- import kbnSecuritysolutionDataTableObj from './kbn_securitysolution_data_table.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_ecs.mdx b/api_docs/kbn_securitysolution_ecs.mdx index cc3531d6cf540..3255522beb368 100644 --- a/api_docs/kbn_securitysolution_ecs.mdx +++ b/api_docs/kbn_securitysolution_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-ecs title: "@kbn/securitysolution-ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-ecs plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-ecs'] --- import kbnSecuritysolutionEcsObj from './kbn_securitysolution_ecs.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index 66e2708e164f5..4cb64a68c12cc 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-es-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] --- import kbnSecuritysolutionEsUtilsObj from './kbn_securitysolution_es_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_exception_list_components.mdx b/api_docs/kbn_securitysolution_exception_list_components.mdx index d6cd59f427a39..1ad8c772f997f 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.mdx +++ b/api_docs/kbn_securitysolution_exception_list_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-exception-list-components title: "@kbn/securitysolution-exception-list-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-exception-list-components plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-exception-list-components'] --- import kbnSecuritysolutionExceptionListComponentsObj from './kbn_securitysolution_exception_list_components.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_grouping.mdx b/api_docs/kbn_securitysolution_grouping.mdx index 0af21b9b73ea4..7b4f711d98b3e 100644 --- a/api_docs/kbn_securitysolution_grouping.mdx +++ b/api_docs/kbn_securitysolution_grouping.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-grouping title: "@kbn/securitysolution-grouping" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-grouping plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-grouping'] --- import kbnSecuritysolutionGroupingObj from './kbn_securitysolution_grouping.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index 3ee2cecf5d408..72d783a60ebb3 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] --- import kbnSecuritysolutionHookUtilsObj from './kbn_securitysolution_hook_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index e991bd2333ada..336fb7871b796 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] --- import kbnSecuritysolutionIoTsAlertingTypesObj from './kbn_securitysolution_io_ts_alerting_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index e58c5f649e3aa..a17263839c650 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] --- import kbnSecuritysolutionIoTsListTypesObj from './kbn_securitysolution_io_ts_list_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index 571cdd7653e65..c0f02740e526a 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] --- import kbnSecuritysolutionIoTsTypesObj from './kbn_securitysolution_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index 1568546f020a7..027e459095fb3 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] --- import kbnSecuritysolutionIoTsUtilsObj from './kbn_securitysolution_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index 94d99d3f48855..4f2edf63c5cf9 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-api plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] --- import kbnSecuritysolutionListApiObj from './kbn_securitysolution_list_api.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index 9b91fd6f84972..9127038eff14e 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-constants plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] --- import kbnSecuritysolutionListConstantsObj from './kbn_securitysolution_list_constants.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index 568ae81bcf156..2c3bd4c699ef8 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] --- import kbnSecuritysolutionListHooksObj from './kbn_securitysolution_list_hooks.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index bca66ee2fd246..1535b1b1aa3b5 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] --- import kbnSecuritysolutionListUtilsObj from './kbn_securitysolution_list_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index 4752383b3776e..8da99202288cd 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-rules plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] --- import kbnSecuritysolutionRulesObj from './kbn_securitysolution_rules.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index f9901c6a12902..88cd2aeeaa90c 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-t-grid plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] --- import kbnSecuritysolutionTGridObj from './kbn_securitysolution_t_grid.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index 5eae52bb780a9..e665ea0714aab 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] --- import kbnSecuritysolutionUtilsObj from './kbn_securitysolution_utils.devdocs.json'; diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index e8c3056a52793..f959688c30b7c 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-http-tools plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] --- import kbnServerHttpToolsObj from './kbn_server_http_tools.devdocs.json'; diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index bee9f0812f016..0f539fa6d4ab0 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] --- import kbnServerRouteRepositoryObj from './kbn_server_route_repository.devdocs.json'; diff --git a/api_docs/kbn_serverless_common_settings.mdx b/api_docs/kbn_serverless_common_settings.mdx index cdbfd3d68e37b..e36d5bf39f909 100644 --- a/api_docs/kbn_serverless_common_settings.mdx +++ b/api_docs/kbn_serverless_common_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-serverless-common-settings title: "@kbn/serverless-common-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-common-settings plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-common-settings'] --- import kbnServerlessCommonSettingsObj from './kbn_serverless_common_settings.devdocs.json'; diff --git a/api_docs/kbn_serverless_observability_settings.mdx b/api_docs/kbn_serverless_observability_settings.mdx index 2c96dd4d9b872..ff3264d63b9f9 100644 --- a/api_docs/kbn_serverless_observability_settings.mdx +++ b/api_docs/kbn_serverless_observability_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-serverless-observability-settings title: "@kbn/serverless-observability-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-observability-settings plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-observability-settings'] --- import kbnServerlessObservabilitySettingsObj from './kbn_serverless_observability_settings.devdocs.json'; diff --git a/api_docs/kbn_serverless_project_switcher.mdx b/api_docs/kbn_serverless_project_switcher.mdx index 75bd2722d6ab9..88cb9fd90511e 100644 --- a/api_docs/kbn_serverless_project_switcher.mdx +++ b/api_docs/kbn_serverless_project_switcher.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-serverless-project-switcher title: "@kbn/serverless-project-switcher" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-project-switcher plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-project-switcher'] --- import kbnServerlessProjectSwitcherObj from './kbn_serverless_project_switcher.devdocs.json'; diff --git a/api_docs/kbn_serverless_search_settings.mdx b/api_docs/kbn_serverless_search_settings.mdx index 6b9fbf1de092d..ed13dddb93a5a 100644 --- a/api_docs/kbn_serverless_search_settings.mdx +++ b/api_docs/kbn_serverless_search_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-serverless-search-settings title: "@kbn/serverless-search-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-search-settings plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-search-settings'] --- import kbnServerlessSearchSettingsObj from './kbn_serverless_search_settings.devdocs.json'; diff --git a/api_docs/kbn_serverless_security_settings.mdx b/api_docs/kbn_serverless_security_settings.mdx index d929e1e89989f..2f3ece8f56243 100644 --- a/api_docs/kbn_serverless_security_settings.mdx +++ b/api_docs/kbn_serverless_security_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-serverless-security-settings title: "@kbn/serverless-security-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-security-settings plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-security-settings'] --- import kbnServerlessSecuritySettingsObj from './kbn_serverless_security_settings.devdocs.json'; diff --git a/api_docs/kbn_serverless_storybook_config.mdx b/api_docs/kbn_serverless_storybook_config.mdx index 48df79ee00ea0..81df99584c63a 100644 --- a/api_docs/kbn_serverless_storybook_config.mdx +++ b/api_docs/kbn_serverless_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-serverless-storybook-config title: "@kbn/serverless-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/serverless-storybook-config plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/serverless-storybook-config'] --- import kbnServerlessStorybookConfigObj from './kbn_serverless_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_svg.mdx b/api_docs/kbn_shared_svg.mdx index f5d6ddd4b25f2..cb3665c849340 100644 --- a/api_docs/kbn_shared_svg.mdx +++ b/api_docs/kbn_shared_svg.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-svg title: "@kbn/shared-svg" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-svg plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-svg'] --- import kbnSharedSvgObj from './kbn_shared_svg.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_solution.mdx b/api_docs/kbn_shared_ux_avatar_solution.mdx index 53a2ac8605a49..abff430a0a630 100644 --- a/api_docs/kbn_shared_ux_avatar_solution.mdx +++ b/api_docs/kbn_shared_ux_avatar_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-solution title: "@kbn/shared-ux-avatar-solution" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-solution plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-solution'] --- import kbnSharedUxAvatarSolutionObj from './kbn_shared_ux_avatar_solution.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx index 20b99674a4571..81a312e8cf7b9 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen title: "@kbn/shared-ux-button-exit-full-screen" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen'] --- import kbnSharedUxButtonExitFullScreenObj from './kbn_shared_ux_button_exit_full_screen.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index d2db7457735cc..1a5b141aafb8d 100644 --- a/api_docs/kbn_shared_ux_button_toolbar.mdx +++ b/api_docs/kbn_shared_ux_button_toolbar.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-toolbar title: "@kbn/shared-ux-button-toolbar" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-toolbar plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-toolbar'] --- import kbnSharedUxButtonToolbarObj from './kbn_shared_ux_button_toolbar.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data.mdx b/api_docs/kbn_shared_ux_card_no_data.mdx index bbe9827527c08..4e04df271d3ae 100644 --- a/api_docs/kbn_shared_ux_card_no_data.mdx +++ b/api_docs/kbn_shared_ux_card_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data title: "@kbn/shared-ux-card-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data'] --- import kbnSharedUxCardNoDataObj from './kbn_shared_ux_card_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx index ca2d51f33d433..fb37d8c016766 100644 --- a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data-mocks title: "@kbn/shared-ux-card-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data-mocks'] --- import kbnSharedUxCardNoDataMocksObj from './kbn_shared_ux_card_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_chrome_navigation.mdx b/api_docs/kbn_shared_ux_chrome_navigation.mdx index 72984a79628c9..d3b5c89d90fac 100644 --- a/api_docs/kbn_shared_ux_chrome_navigation.mdx +++ b/api_docs/kbn_shared_ux_chrome_navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-chrome-navigation title: "@kbn/shared-ux-chrome-navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-chrome-navigation plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-chrome-navigation'] --- import kbnSharedUxChromeNavigationObj from './kbn_shared_ux_chrome_navigation.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_error_boundary.mdx b/api_docs/kbn_shared_ux_error_boundary.mdx index 7306bfa692ba3..01c8269b37881 100644 --- a/api_docs/kbn_shared_ux_error_boundary.mdx +++ b/api_docs/kbn_shared_ux_error_boundary.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-error-boundary title: "@kbn/shared-ux-error-boundary" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-error-boundary plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-error-boundary'] --- import kbnSharedUxErrorBoundaryObj from './kbn_shared_ux_error_boundary.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_context.mdx b/api_docs/kbn_shared_ux_file_context.mdx index e058d1c8b8290..4ae2078a45260 100644 --- a/api_docs/kbn_shared_ux_file_context.mdx +++ b/api_docs/kbn_shared_ux_file_context.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-context title: "@kbn/shared-ux-file-context" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-context plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-context'] --- import kbnSharedUxFileContextObj from './kbn_shared_ux_file_context.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image.mdx b/api_docs/kbn_shared_ux_file_image.mdx index 992eb3e689b5d..d7acdd1a0a802 100644 --- a/api_docs/kbn_shared_ux_file_image.mdx +++ b/api_docs/kbn_shared_ux_file_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image title: "@kbn/shared-ux-file-image" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image'] --- import kbnSharedUxFileImageObj from './kbn_shared_ux_file_image.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image_mocks.mdx b/api_docs/kbn_shared_ux_file_image_mocks.mdx index 4e6ad6347430c..d482177449b87 100644 --- a/api_docs/kbn_shared_ux_file_image_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_image_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image-mocks title: "@kbn/shared-ux-file-image-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image-mocks'] --- import kbnSharedUxFileImageMocksObj from './kbn_shared_ux_file_image_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_mocks.mdx b/api_docs/kbn_shared_ux_file_mocks.mdx index 63a08508889d2..643f72eb9b24f 100644 --- a/api_docs/kbn_shared_ux_file_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-mocks title: "@kbn/shared-ux-file-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-mocks'] --- import kbnSharedUxFileMocksObj from './kbn_shared_ux_file_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_picker.mdx b/api_docs/kbn_shared_ux_file_picker.mdx index 1795b1df6a204..22cfdd1cf1be2 100644 --- a/api_docs/kbn_shared_ux_file_picker.mdx +++ b/api_docs/kbn_shared_ux_file_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-picker title: "@kbn/shared-ux-file-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-picker plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-picker'] --- import kbnSharedUxFilePickerObj from './kbn_shared_ux_file_picker.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_types.mdx b/api_docs/kbn_shared_ux_file_types.mdx index 29af4c9c2e6a9..8f1dcf9e21600 100644 --- a/api_docs/kbn_shared_ux_file_types.mdx +++ b/api_docs/kbn_shared_ux_file_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-types title: "@kbn/shared-ux-file-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-types'] --- import kbnSharedUxFileTypesObj from './kbn_shared_ux_file_types.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_upload.mdx b/api_docs/kbn_shared_ux_file_upload.mdx index 615fe97f99855..b4c86dab78d98 100644 --- a/api_docs/kbn_shared_ux_file_upload.mdx +++ b/api_docs/kbn_shared_ux_file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-upload title: "@kbn/shared-ux-file-upload" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-upload plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-upload'] --- import kbnSharedUxFileUploadObj from './kbn_shared_ux_file_upload.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_util.mdx b/api_docs/kbn_shared_ux_file_util.mdx index 98bf551cc063e..d015bb6a23a2a 100644 --- a/api_docs/kbn_shared_ux_file_util.mdx +++ b/api_docs/kbn_shared_ux_file_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-util title: "@kbn/shared-ux-file-util" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-util plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-util'] --- import kbnSharedUxFileUtilObj from './kbn_shared_ux_file_util.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app.mdx b/api_docs/kbn_shared_ux_link_redirect_app.mdx index 4672e3cadc3f3..250e44b778b2e 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app title: "@kbn/shared-ux-link-redirect-app" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app'] --- import kbnSharedUxLinkRedirectAppObj from './kbn_shared_ux_link_redirect_app.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx index f224d4bbdbf58..f6ea9746f42ea 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app-mocks title: "@kbn/shared-ux-link-redirect-app-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app-mocks'] --- import kbnSharedUxLinkRedirectAppMocksObj from './kbn_shared_ux_link_redirect_app_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown.mdx b/api_docs/kbn_shared_ux_markdown.mdx index db23219fc66e7..f2a07ae085402 100644 --- a/api_docs/kbn_shared_ux_markdown.mdx +++ b/api_docs/kbn_shared_ux_markdown.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown title: "@kbn/shared-ux-markdown" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown'] --- import kbnSharedUxMarkdownObj from './kbn_shared_ux_markdown.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown_mocks.mdx b/api_docs/kbn_shared_ux_markdown_mocks.mdx index 8414d3ea81a5a..54e55ff7aaf33 100644 --- a/api_docs/kbn_shared_ux_markdown_mocks.mdx +++ b/api_docs/kbn_shared_ux_markdown_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown-mocks title: "@kbn/shared-ux-markdown-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown-mocks'] --- import kbnSharedUxMarkdownMocksObj from './kbn_shared_ux_markdown_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index 08600d4bc6105..0283292e24095 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] --- import kbnSharedUxPageAnalyticsNoDataObj from './kbn_shared_ux_page_analytics_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx index 72b4ed2b8bb39..4d57a1ae152d3 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data-mocks title: "@kbn/shared-ux-page-analytics-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data-mocks'] --- import kbnSharedUxPageAnalyticsNoDataMocksObj from './kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index 59cff789af06f..9d8acab31cfd3 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] --- import kbnSharedUxPageKibanaNoDataObj from './kbn_shared_ux_page_kibana_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx index 381ff98989c67..db915ab0825d8 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data-mocks title: "@kbn/shared-ux-page-kibana-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data-mocks'] --- import kbnSharedUxPageKibanaNoDataMocksObj from './kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template.mdx b/api_docs/kbn_shared_ux_page_kibana_template.mdx index 7f2c7e99f60da..69be404378479 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template title: "@kbn/shared-ux-page-kibana-template" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template'] --- import kbnSharedUxPageKibanaTemplateObj from './kbn_shared_ux_page_kibana_template.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx index d7ec3d8a1248c..51f701d0a1036 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template-mocks title: "@kbn/shared-ux-page-kibana-template-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template-mocks'] --- import kbnSharedUxPageKibanaTemplateMocksObj from './kbn_shared_ux_page_kibana_template_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data.mdx b/api_docs/kbn_shared_ux_page_no_data.mdx index f28cbb449f751..f648075e9573f 100644 --- a/api_docs/kbn_shared_ux_page_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data title: "@kbn/shared-ux-page-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data'] --- import kbnSharedUxPageNoDataObj from './kbn_shared_ux_page_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config.mdx b/api_docs/kbn_shared_ux_page_no_data_config.mdx index 80319b95ccc12..9387f34bc8854 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config title: "@kbn/shared-ux-page-no-data-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config'] --- import kbnSharedUxPageNoDataConfigObj from './kbn_shared_ux_page_no_data_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx index c46e95f44d545..1ecdcea1d7d66 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config-mocks title: "@kbn/shared-ux-page-no-data-config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config-mocks'] --- import kbnSharedUxPageNoDataConfigMocksObj from './kbn_shared_ux_page_no_data_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx index 1c8091f51f630..217a3073eee20 100644 --- a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-mocks title: "@kbn/shared-ux-page-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-mocks'] --- import kbnSharedUxPageNoDataMocksObj from './kbn_shared_ux_page_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_solution_nav.mdx b/api_docs/kbn_shared_ux_page_solution_nav.mdx index e9076403f55a5..eedb8df0dc46b 100644 --- a/api_docs/kbn_shared_ux_page_solution_nav.mdx +++ b/api_docs/kbn_shared_ux_page_solution_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-solution-nav title: "@kbn/shared-ux-page-solution-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-solution-nav plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-solution-nav'] --- import kbnSharedUxPageSolutionNavObj from './kbn_shared_ux_page_solution_nav.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index 360b21120b24b..c6605eb569e0d 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] --- import kbnSharedUxPromptNoDataViewsObj from './kbn_shared_ux_prompt_no_data_views.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx index 8a3f7a4b769c4..fb3d35caf17a8 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views-mocks title: "@kbn/shared-ux-prompt-no-data-views-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views-mocks'] --- import kbnSharedUxPromptNoDataViewsMocksObj from './kbn_shared_ux_prompt_no_data_views_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_not_found.mdx b/api_docs/kbn_shared_ux_prompt_not_found.mdx index fccba7369485f..7c3aa9753eabd 100644 --- a/api_docs/kbn_shared_ux_prompt_not_found.mdx +++ b/api_docs/kbn_shared_ux_prompt_not_found.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-not-found title: "@kbn/shared-ux-prompt-not-found" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-not-found plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-not-found'] --- import kbnSharedUxPromptNotFoundObj from './kbn_shared_ux_prompt_not_found.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx index 82de16bce0f07..dbe6c39e9ee15 100644 --- a/api_docs/kbn_shared_ux_router.mdx +++ b/api_docs/kbn_shared_ux_router.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router title: "@kbn/shared-ux-router" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router'] --- import kbnSharedUxRouterObj from './kbn_shared_ux_router.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router_mocks.mdx b/api_docs/kbn_shared_ux_router_mocks.mdx index 2d669881993a9..64b7837b5157d 100644 --- a/api_docs/kbn_shared_ux_router_mocks.mdx +++ b/api_docs/kbn_shared_ux_router_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router-mocks title: "@kbn/shared-ux-router-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router-mocks plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router-mocks'] --- import kbnSharedUxRouterMocksObj from './kbn_shared_ux_router_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_config.mdx b/api_docs/kbn_shared_ux_storybook_config.mdx index 460a6f08b0aa0..3ec3fe835b67e 100644 --- a/api_docs/kbn_shared_ux_storybook_config.mdx +++ b/api_docs/kbn_shared_ux_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-config title: "@kbn/shared-ux-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-config plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-config'] --- import kbnSharedUxStorybookConfigObj from './kbn_shared_ux_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_mock.mdx b/api_docs/kbn_shared_ux_storybook_mock.mdx index a9a0565ebd1b5..ebf3476e25460 100644 --- a/api_docs/kbn_shared_ux_storybook_mock.mdx +++ b/api_docs/kbn_shared_ux_storybook_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-mock title: "@kbn/shared-ux-storybook-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-mock plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-mock'] --- import kbnSharedUxStorybookMockObj from './kbn_shared_ux_storybook_mock.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index 137443224d0f9..fe135c4048e84 100644 --- a/api_docs/kbn_shared_ux_utility.mdx +++ b/api_docs/kbn_shared_ux_utility.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility title: "@kbn/shared-ux-utility" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-utility plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_slo_schema.mdx b/api_docs/kbn_slo_schema.mdx index 5df4a1b683e46..aa5491b851796 100644 --- a/api_docs/kbn_slo_schema.mdx +++ b/api_docs/kbn_slo_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-slo-schema title: "@kbn/slo-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/slo-schema plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/slo-schema'] --- import kbnSloSchemaObj from './kbn_slo_schema.devdocs.json'; diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index 2d26ddb0e327a..e789cc1b7fecc 100644 --- a/api_docs/kbn_some_dev_log.mdx +++ b/api_docs/kbn_some_dev_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-some-dev-log title: "@kbn/some-dev-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/some-dev-log plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_sort_predicates.mdx b/api_docs/kbn_sort_predicates.mdx index d53154842565a..859e39b25f340 100644 --- a/api_docs/kbn_sort_predicates.mdx +++ b/api_docs/kbn_sort_predicates.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sort-predicates title: "@kbn/sort-predicates" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/sort-predicates plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sort-predicates'] --- import kbnSortPredicatesObj from './kbn_sort_predicates.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index 23b583fd3f147..2754684422913 100644 --- a/api_docs/kbn_std.mdx +++ b/api_docs/kbn_std.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-std title: "@kbn/std" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/std plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/std'] --- import kbnStdObj from './kbn_std.devdocs.json'; diff --git a/api_docs/kbn_stdio_dev_helpers.mdx b/api_docs/kbn_stdio_dev_helpers.mdx index 4b315de93040b..a7476ae8d9b90 100644 --- a/api_docs/kbn_stdio_dev_helpers.mdx +++ b/api_docs/kbn_stdio_dev_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers title: "@kbn/stdio-dev-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/stdio-dev-helpers plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/stdio-dev-helpers'] --- import kbnStdioDevHelpersObj from './kbn_stdio_dev_helpers.devdocs.json'; diff --git a/api_docs/kbn_storybook.mdx b/api_docs/kbn_storybook.mdx index 81cda58b60340..19c4fdbd2d397 100644 --- a/api_docs/kbn_storybook.mdx +++ b/api_docs/kbn_storybook.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-storybook title: "@kbn/storybook" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/storybook plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] --- import kbnStorybookObj from './kbn_storybook.devdocs.json'; diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index e2a14e589aad0..1947e7918e4cc 100644 --- a/api_docs/kbn_telemetry_tools.mdx +++ b/api_docs/kbn_telemetry_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools title: "@kbn/telemetry-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/telemetry-tools plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] --- import kbnTelemetryToolsObj from './kbn_telemetry_tools.devdocs.json'; diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index 75c1741fe9c68..be7c0f3766bf4 100644 --- a/api_docs/kbn_test.mdx +++ b/api_docs/kbn_test.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test title: "@kbn/test" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; diff --git a/api_docs/kbn_test_eui_helpers.mdx b/api_docs/kbn_test_eui_helpers.mdx index 3dbba0f9da53a..bec6382834370 100644 --- a/api_docs/kbn_test_eui_helpers.mdx +++ b/api_docs/kbn_test_eui_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-eui-helpers title: "@kbn/test-eui-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-eui-helpers plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-eui-helpers'] --- import kbnTestEuiHelpersObj from './kbn_test_eui_helpers.devdocs.json'; diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index 644acddd8a2c7..13c65175c981d 100644 --- a/api_docs/kbn_test_jest_helpers.mdx +++ b/api_docs/kbn_test_jest_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers title: "@kbn/test-jest-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-jest-helpers plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] --- import kbnTestJestHelpersObj from './kbn_test_jest_helpers.devdocs.json'; diff --git a/api_docs/kbn_test_subj_selector.mdx b/api_docs/kbn_test_subj_selector.mdx index e106e07806f97..6090e47adad80 100644 --- a/api_docs/kbn_test_subj_selector.mdx +++ b/api_docs/kbn_test_subj_selector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-subj-selector title: "@kbn/test-subj-selector" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-subj-selector plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-subj-selector'] --- import kbnTestSubjSelectorObj from './kbn_test_subj_selector.devdocs.json'; diff --git a/api_docs/kbn_text_based_editor.devdocs.json b/api_docs/kbn_text_based_editor.devdocs.json index 8e1737f376a91..8331afd578b82 100644 --- a/api_docs/kbn_text_based_editor.devdocs.json +++ b/api_docs/kbn_text_based_editor.devdocs.json @@ -43,7 +43,7 @@ "section": "def-common.TimeRange", "text": "TimeRange" }, - " | undefined, dataView: ", + " | undefined, abortController: AbortController | undefined, dataView: ", { "pluginId": "dataViews", "scope": "common", @@ -142,6 +142,21 @@ "id": "def-public.fetchFieldsFromESQL.$4", "type": "Object", "tags": [], + "label": "abortController", + "description": [], + "signature": [ + "AbortController | undefined" + ], + "path": "packages/kbn-text-based-editor/src/fetch_fields_from_esql.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + }, + { + "parentPluginId": "@kbn/text-based-editor", + "id": "def-public.fetchFieldsFromESQL.$5", + "type": "Object", + "tags": [], "label": "dataView", "description": [], "signature": [ @@ -298,7 +313,7 @@ "section": "def-common.AggregateQuery", "text": "AggregateQuery" }, - " | undefined) => void" + " | undefined, abortController?: AbortController | undefined) => Promise" ], "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", "deprecated": false, @@ -325,6 +340,21 @@ "deprecated": false, "trackAdoption": false, "isRequired": false + }, + { + "parentPluginId": "@kbn/text-based-editor", + "id": "def-public.TextBasedLanguagesEditorProps.onTextLangQuerySubmit.$2", + "type": "Object", + "tags": [], + "label": "abortController", + "description": [], + "signature": [ + "AbortController | undefined" + ], + "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": false } ], "returnComment": [] @@ -549,6 +579,22 @@ "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/text-based-editor", + "id": "def-public.TextBasedLanguagesEditorProps.allowQueryCancellation", + "type": "CompoundType", + "tags": [], + "label": "allowQueryCancellation", + "description": [ + "when set to true enables query cancellation" + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/kbn_text_based_editor.mdx b/api_docs/kbn_text_based_editor.mdx index b0fb3bb352a99..9fe9558d87652 100644 --- a/api_docs/kbn_text_based_editor.mdx +++ b/api_docs/kbn_text_based_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-text-based-editor title: "@kbn/text-based-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/text-based-editor plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/text-based-editor'] --- import kbnTextBasedEditorObj from './kbn_text_based_editor.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 27 | 0 | 11 | 0 | +| 30 | 0 | 13 | 0 | ## Client diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index 622af471c1655..22417992b5116 100644 --- a/api_docs/kbn_tooling_log.mdx +++ b/api_docs/kbn_tooling_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log title: "@kbn/tooling-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/tooling-log plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_triggers_actions_ui_types.mdx b/api_docs/kbn_triggers_actions_ui_types.mdx index fe430c80f73d0..af17ece116d06 100644 --- a/api_docs/kbn_triggers_actions_ui_types.mdx +++ b/api_docs/kbn_triggers_actions_ui_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-triggers-actions-ui-types title: "@kbn/triggers-actions-ui-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/triggers-actions-ui-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/triggers-actions-ui-types'] --- import kbnTriggersActionsUiTypesObj from './kbn_triggers_actions_ui_types.devdocs.json'; diff --git a/api_docs/kbn_ts_projects.mdx b/api_docs/kbn_ts_projects.mdx index 53416cc864850..0eb899f355fb1 100644 --- a/api_docs/kbn_ts_projects.mdx +++ b/api_docs/kbn_ts_projects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ts-projects title: "@kbn/ts-projects" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ts-projects plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ts-projects'] --- import kbnTsProjectsObj from './kbn_ts_projects.devdocs.json'; diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index 621a40aae4f3e..23cd5501c5c27 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config title: "@kbn/typed-react-router-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/typed-react-router-config plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/typed-react-router-config'] --- import kbnTypedReactRouterConfigObj from './kbn_typed_react_router_config.devdocs.json'; diff --git a/api_docs/kbn_ui_actions_browser.mdx b/api_docs/kbn_ui_actions_browser.mdx index 124335ac7e4ff..b09bcf180ed35 100644 --- a/api_docs/kbn_ui_actions_browser.mdx +++ b/api_docs/kbn_ui_actions_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-actions-browser title: "@kbn/ui-actions-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-actions-browser plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-actions-browser'] --- import kbnUiActionsBrowserObj from './kbn_ui_actions_browser.devdocs.json'; diff --git a/api_docs/kbn_ui_shared_deps_src.mdx b/api_docs/kbn_ui_shared_deps_src.mdx index 315b924ee1a62..aaba877db9363 100644 --- a/api_docs/kbn_ui_shared_deps_src.mdx +++ b/api_docs/kbn_ui_shared_deps_src.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-shared-deps-src title: "@kbn/ui-shared-deps-src" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-shared-deps-src plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-shared-deps-src'] --- import kbnUiSharedDepsSrcObj from './kbn_ui_shared_deps_src.devdocs.json'; diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index a79dace22912d..7d2fa17c2c4b4 100644 --- a/api_docs/kbn_ui_theme.mdx +++ b/api_docs/kbn_ui_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme title: "@kbn/ui-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-theme plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_unified_data_table.devdocs.json b/api_docs/kbn_unified_data_table.devdocs.json index d6e88999bd256..8c9ffa9f3c1e4 100644 --- a/api_docs/kbn_unified_data_table.devdocs.json +++ b/api_docs/kbn_unified_data_table.devdocs.json @@ -19,6 +19,51 @@ "common": { "classes": [], "functions": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.DataTableRowControl", + "type": "Function", + "tags": [], + "label": "DataTableRowControl", + "description": [], + "signature": [ + "({ children }: { children: React.ReactNode; }) => JSX.Element" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table_row_control.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.DataTableRowControl.$1", + "type": "Object", + "tags": [], + "label": "{ children }", + "description": [], + "path": "packages/kbn-unified-data-table/src/components/data_table_row_control.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.DataTableRowControl.$1.children", + "type": "CompoundType", + "tags": [], + "label": "children", + "description": [], + "signature": [ + "boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | null | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table_row_control.tsx", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/unified-data-table", "id": "def-common.getDisplayedColumns", @@ -326,7 +371,7 @@ "label": "UnifiedDataTable", "description": [], "signature": [ - "({ ariaLabelledBy, columns, columnTypes, showColumnTokens, headerRowHeight, controlColumnIds, dataView, loadingState, onFilter, onResize, onSetColumns, onSort, rows, searchDescription, searchTitle, settings, showTimeCol, showFullScreenButton, sort, useNewFieldsApi, isSortEnabled, isPaginationEnabled, cellActionsTriggerId, className, rowHeightState, onUpdateRowHeight, maxAllowedSampleSize, sampleSizeState, onUpdateSampleSize, isPlainRecord, rowsPerPageState, onUpdateRowsPerPage, onFieldEdited, services, renderCustomGridBody, renderCustomToolbar, trailingControlColumns, totalHits, onFetchMoreRecords, renderDocumentView, setExpandedDoc, expandedDoc, configRowHeight, showMultiFields, maxDocFieldsDisplayed, externalControlColumns, externalAdditionalControls, rowsPerPageOptions, visibleCellActions, externalCustomRenderers, consumer, componentsTourSteps, gridStyleOverride, rowLineHeightOverride, customGridColumnsConfiguration, }: ", + "({ ariaLabelledBy, columns, columnTypes, showColumnTokens, headerRowHeight, controlColumnIds, dataView, loadingState, onFilter, onResize, onSetColumns, onSort, rows, searchDescription, searchTitle, settings, showTimeCol, showFullScreenButton, sort, useNewFieldsApi, isSortEnabled, isPaginationEnabled, cellActionsTriggerId, className, rowHeightState, onUpdateRowHeight, maxAllowedSampleSize, sampleSizeState, onUpdateSampleSize, isPlainRecord, rowsPerPageState, onUpdateRowsPerPage, onFieldEdited, services, renderCustomGridBody, renderCustomToolbar, trailingControlColumns, totalHits, onFetchMoreRecords, renderDocumentView, setExpandedDoc, expandedDoc, configRowHeight, showMultiFields, maxDocFieldsDisplayed, externalControlColumns, externalAdditionalControls, rowsPerPageOptions, visibleCellActions, externalCustomRenderers, consumer, componentsTourSteps, gridStyleOverride, rowLineHeightOverride, customGridColumnsConfiguration, customControlColumnsConfiguration, }: ", { "pluginId": "@kbn/unified-data-table", "scope": "common", @@ -345,7 +390,7 @@ "id": "def-common.UnifiedDataTable.$1", "type": "Object", "tags": [], - "label": "{\n ariaLabelledBy,\n columns,\n columnTypes,\n showColumnTokens,\n headerRowHeight,\n controlColumnIds = CONTROL_COLUMN_IDS_DEFAULT,\n dataView,\n loadingState,\n onFilter,\n onResize,\n onSetColumns,\n onSort,\n rows,\n searchDescription,\n searchTitle,\n settings,\n showTimeCol,\n showFullScreenButton = true,\n sort,\n useNewFieldsApi,\n isSortEnabled = true,\n isPaginationEnabled = true,\n cellActionsTriggerId,\n className,\n rowHeightState,\n onUpdateRowHeight,\n maxAllowedSampleSize,\n sampleSizeState,\n onUpdateSampleSize,\n isPlainRecord = false,\n rowsPerPageState,\n onUpdateRowsPerPage,\n onFieldEdited,\n services,\n renderCustomGridBody,\n renderCustomToolbar,\n trailingControlColumns,\n totalHits,\n onFetchMoreRecords,\n renderDocumentView,\n setExpandedDoc,\n expandedDoc,\n configRowHeight,\n showMultiFields = true,\n maxDocFieldsDisplayed = 50,\n externalControlColumns,\n externalAdditionalControls,\n rowsPerPageOptions,\n visibleCellActions,\n externalCustomRenderers,\n consumer = 'discover',\n componentsTourSteps,\n gridStyleOverride,\n rowLineHeightOverride,\n customGridColumnsConfiguration,\n}", + "label": "{\n ariaLabelledBy,\n columns,\n columnTypes,\n showColumnTokens,\n headerRowHeight,\n controlColumnIds = CONTROL_COLUMN_IDS_DEFAULT,\n dataView,\n loadingState,\n onFilter,\n onResize,\n onSetColumns,\n onSort,\n rows,\n searchDescription,\n searchTitle,\n settings,\n showTimeCol,\n showFullScreenButton = true,\n sort,\n useNewFieldsApi,\n isSortEnabled = true,\n isPaginationEnabled = true,\n cellActionsTriggerId,\n className,\n rowHeightState,\n onUpdateRowHeight,\n maxAllowedSampleSize,\n sampleSizeState,\n onUpdateSampleSize,\n isPlainRecord = false,\n rowsPerPageState,\n onUpdateRowsPerPage,\n onFieldEdited,\n services,\n renderCustomGridBody,\n renderCustomToolbar,\n trailingControlColumns,\n totalHits,\n onFetchMoreRecords,\n renderDocumentView,\n setExpandedDoc,\n expandedDoc,\n configRowHeight,\n showMultiFields = true,\n maxDocFieldsDisplayed = 50,\n externalControlColumns,\n externalAdditionalControls,\n rowsPerPageOptions,\n visibleCellActions,\n externalCustomRenderers,\n consumer = 'discover',\n componentsTourSteps,\n gridStyleOverride,\n rowLineHeightOverride,\n customGridColumnsConfiguration,\n customControlColumnsConfiguration,\n}", "description": [], "signature": [ { @@ -400,6 +445,82 @@ } ], "interfaces": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.ControlColumns", + "type": "Interface", + "tags": [], + "label": "ControlColumns", + "description": [], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.ControlColumns.select", + "type": "Object", + "tags": [], + "label": "select", + "description": [], + "signature": [ + "EuiDataGridControlColumn" + ], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.ControlColumns.openDetails", + "type": "Object", + "tags": [], + "label": "openDetails", + "description": [], + "signature": [ + "EuiDataGridControlColumn" + ], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.ControlColumnsProps", + "type": "Interface", + "tags": [], + "label": "ControlColumnsProps", + "description": [], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.ControlColumnsProps.controlColumns", + "type": "Object", + "tags": [], + "label": "controlColumns", + "description": [], + "signature": [ + { + "pluginId": "@kbn/unified-data-table", + "scope": "common", + "docId": "kibKbnUnifiedDataTablePluginApi", + "section": "def-common.ControlColumns", + "text": "ControlColumns" + } + ], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/unified-data-table", "id": "def-common.CustomGridColumnProps", @@ -1687,6 +1808,29 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.UnifiedDataTableProps.customControlColumnsConfiguration", + "type": "Function", + "tags": [], + "label": "customControlColumnsConfiguration", + "description": [ + "\nAn optional settings to control which columns to render as trailing and leading control columns" + ], + "signature": [ + { + "pluginId": "@kbn/unified-data-table", + "scope": "common", + "docId": "kibKbnUnifiedDataTablePluginApi", + "section": "def-common.CustomControlColumnConfiguration", + "text": "CustomControlColumnConfiguration" + }, + " | undefined" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table.tsx", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "@kbn/unified-data-table", "id": "def-common.UnifiedDataTableProps.consumer", @@ -1904,6 +2048,56 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.CustomControlColumnConfiguration", + "type": "Type", + "tags": [], + "label": "CustomControlColumnConfiguration", + "description": [], + "signature": [ + "(props: ", + { + "pluginId": "@kbn/unified-data-table", + "scope": "common", + "docId": "kibKbnUnifiedDataTablePluginApi", + "section": "def-common.ControlColumnsProps", + "text": "ControlColumnsProps" + }, + ") => { leadingControlColumns: ", + "EuiDataGridControlColumn", + "[]; trailingControlColumns?: ", + "EuiDataGridControlColumn", + "[] | undefined; }" + ], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.CustomControlColumnConfiguration.$1", + "type": "Object", + "tags": [], + "label": "props", + "description": [], + "signature": [ + { + "pluginId": "@kbn/unified-data-table", + "scope": "common", + "docId": "kibKbnUnifiedDataTablePluginApi", + "section": "def-common.ControlColumnsProps", + "text": "ControlColumnsProps" + } + ], + "path": "packages/kbn-unified-data-table/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/unified-data-table", "id": "def-common.CustomGridColumnsConfiguration", @@ -1980,6 +2174,36 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.OPEN_DETAILS", + "type": "string", + "tags": [], + "label": "OPEN_DETAILS", + "description": [], + "signature": [ + "\"openDetails\"" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table_columns.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/unified-data-table", + "id": "def-common.SELECT_ROW", + "type": "string", + "tags": [], + "label": "SELECT_ROW", + "description": [], + "signature": [ + "\"select\"" + ], + "path": "packages/kbn-unified-data-table/src/components/data_table_columns.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/unified-data-table", "id": "def-common.UnifiedDataTableRenderCustomToolbar", diff --git a/api_docs/kbn_unified_data_table.mdx b/api_docs/kbn_unified_data_table.mdx index b2ca4078f000e..166a3be4ea76f 100644 --- a/api_docs/kbn_unified_data_table.mdx +++ b/api_docs/kbn_unified_data_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unified-data-table title: "@kbn/unified-data-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/unified-data-table plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-data-table'] --- import kbnUnifiedDataTableObj from './kbn_unified_data_table.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 117 | 0 | 55 | 1 | +| 130 | 0 | 67 | 1 | ## Common diff --git a/api_docs/kbn_unified_doc_viewer.mdx b/api_docs/kbn_unified_doc_viewer.mdx index 97aca7830d5fd..2f1d5c58f4a1f 100644 --- a/api_docs/kbn_unified_doc_viewer.mdx +++ b/api_docs/kbn_unified_doc_viewer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unified-doc-viewer title: "@kbn/unified-doc-viewer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/unified-doc-viewer plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-doc-viewer'] --- import kbnUnifiedDocViewerObj from './kbn_unified_doc_viewer.devdocs.json'; diff --git a/api_docs/kbn_unified_field_list.mdx b/api_docs/kbn_unified_field_list.mdx index f391ea773eff5..e5427cb6f23cd 100644 --- a/api_docs/kbn_unified_field_list.mdx +++ b/api_docs/kbn_unified_field_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unified-field-list title: "@kbn/unified-field-list" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/unified-field-list plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-field-list'] --- import kbnUnifiedFieldListObj from './kbn_unified_field_list.devdocs.json'; diff --git a/api_docs/kbn_unsaved_changes_badge.mdx b/api_docs/kbn_unsaved_changes_badge.mdx index 3abbb2ea45377..7e846fa63f314 100644 --- a/api_docs/kbn_unsaved_changes_badge.mdx +++ b/api_docs/kbn_unsaved_changes_badge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unsaved-changes-badge title: "@kbn/unsaved-changes-badge" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/unsaved-changes-badge plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unsaved-changes-badge'] --- import kbnUnsavedChangesBadgeObj from './kbn_unsaved_changes_badge.devdocs.json'; diff --git a/api_docs/kbn_use_tracked_promise.mdx b/api_docs/kbn_use_tracked_promise.mdx index 8ebfbe2031e54..cdc2f63da4e58 100644 --- a/api_docs/kbn_use_tracked_promise.mdx +++ b/api_docs/kbn_use_tracked_promise.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-use-tracked-promise title: "@kbn/use-tracked-promise" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/use-tracked-promise plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/use-tracked-promise'] --- import kbnUseTrackedPromiseObj from './kbn_use_tracked_promise.devdocs.json'; diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index 782d00d6fe62a..f94363c48eec7 100644 --- a/api_docs/kbn_user_profile_components.mdx +++ b/api_docs/kbn_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-user-profile-components title: "@kbn/user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/user-profile-components plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/user-profile-components'] --- import kbnUserProfileComponentsObj from './kbn_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_utility_types.mdx b/api_docs/kbn_utility_types.mdx index 97ce29df9137f..436a5392f8891 100644 --- a/api_docs/kbn_utility_types.mdx +++ b/api_docs/kbn_utility_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types title: "@kbn/utility-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types'] --- import kbnUtilityTypesObj from './kbn_utility_types.devdocs.json'; diff --git a/api_docs/kbn_utility_types_jest.mdx b/api_docs/kbn_utility_types_jest.mdx index 90c8a0db6d324..e516425a3766f 100644 --- a/api_docs/kbn_utility_types_jest.mdx +++ b/api_docs/kbn_utility_types_jest.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest title: "@kbn/utility-types-jest" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types-jest plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types-jest'] --- import kbnUtilityTypesJestObj from './kbn_utility_types_jest.devdocs.json'; diff --git a/api_docs/kbn_utils.devdocs.json b/api_docs/kbn_utils.devdocs.json index c2f4eefd1be61..f7fff669338a0 100644 --- a/api_docs/kbn_utils.devdocs.json +++ b/api_docs/kbn_utils.devdocs.json @@ -451,6 +451,41 @@ ], "returnComment": [], "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/utils", + "id": "def-common.prettyPrintAndSortKeys", + "type": "Function", + "tags": [], + "label": "prettyPrintAndSortKeys", + "description": [ + "\nGiven a JS object, will return a JSON.stringified result with consistently\nsorted keys." + ], + "signature": [ + "(object: object) => string" + ], + "path": "packages/kbn-utils/src/json/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/utils", + "id": "def-common.prettyPrintAndSortKeys.$1", + "type": "Uncategorized", + "tags": [], + "label": "object", + "description": [], + "signature": [ + "object" + ], + "path": "packages/kbn-utils/src/json/index.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false } ], "interfaces": [], diff --git a/api_docs/kbn_utils.mdx b/api_docs/kbn_utils.mdx index be4a8165a1c63..eda0fef175a32 100644 --- a/api_docs/kbn_utils.mdx +++ b/api_docs/kbn_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utils title: "@kbn/utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] --- import kbnUtilsObj from './kbn_utils.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kiban | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 24 | 0 | 14 | 0 | +| 26 | 0 | 15 | 0 | ## Common diff --git a/api_docs/kbn_visualization_ui_components.mdx b/api_docs/kbn_visualization_ui_components.mdx index eceeab039ad9e..d8b986a24231f 100644 --- a/api_docs/kbn_visualization_ui_components.mdx +++ b/api_docs/kbn_visualization_ui_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-visualization-ui-components title: "@kbn/visualization-ui-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/visualization-ui-components plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/visualization-ui-components'] --- import kbnVisualizationUiComponentsObj from './kbn_visualization_ui_components.devdocs.json'; diff --git a/api_docs/kbn_visualization_utils.mdx b/api_docs/kbn_visualization_utils.mdx index 5e69d60e1e6e8..0ce36b3676703 100644 --- a/api_docs/kbn_visualization_utils.mdx +++ b/api_docs/kbn_visualization_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-visualization-utils title: "@kbn/visualization-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/visualization-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/visualization-utils'] --- import kbnVisualizationUtilsObj from './kbn_visualization_utils.devdocs.json'; diff --git a/api_docs/kbn_xstate_utils.mdx b/api_docs/kbn_xstate_utils.mdx index 6726f8c356c04..dd0bb4725b965 100644 --- a/api_docs/kbn_xstate_utils.mdx +++ b/api_docs/kbn_xstate_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-xstate-utils title: "@kbn/xstate-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/xstate-utils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/xstate-utils'] --- import kbnXstateUtilsObj from './kbn_xstate_utils.devdocs.json'; diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx index 918b50bdfd850..d5628793560cb 100644 --- a/api_docs/kbn_yarn_lock_validator.mdx +++ b/api_docs/kbn_yarn_lock_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-yarn-lock-validator title: "@kbn/yarn-lock-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/yarn-lock-validator plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator'] --- import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json'; diff --git a/api_docs/kbn_zod_helpers.mdx b/api_docs/kbn_zod_helpers.mdx index 9a13d897879e9..600013a6725f2 100644 --- a/api_docs/kbn_zod_helpers.mdx +++ b/api_docs/kbn_zod_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-zod-helpers title: "@kbn/zod-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/zod-helpers plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/zod-helpers'] --- import kbnZodHelpersObj from './kbn_zod_helpers.devdocs.json'; diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index b10842d919fd0..f80e198556f9d 100644 --- a/api_docs/kibana_overview.mdx +++ b/api_docs/kibana_overview.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaOverview title: "kibanaOverview" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaOverview plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] --- import kibanaOverviewObj from './kibana_overview.devdocs.json'; diff --git a/api_docs/kibana_react.devdocs.json b/api_docs/kibana_react.devdocs.json index 2dbf5cea1cffe..9fb8755ec56d4 100644 --- a/api_docs/kibana_react.devdocs.json +++ b/api_docs/kibana_react.devdocs.json @@ -1163,6 +1163,18 @@ "plugin": "ml", "path": "x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx" }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx" + }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx" + }, + { + "plugin": "ml", + "path": "x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx" + }, { "plugin": "ml", "path": "x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx" diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index fa15a93b184f3..67c24dd346b48 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaReact title: "kibanaReact" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaReact plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaReact'] --- import kibanaReactObj from './kibana_react.devdocs.json'; diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index 748135aea0663..7c7453ed27ea5 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaUtils title: "kibanaUtils" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaUtils plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaUtils'] --- import kibanaUtilsObj from './kibana_utils.devdocs.json'; diff --git a/api_docs/kubernetes_security.mdx b/api_docs/kubernetes_security.mdx index 70eafb707a20d..36708f7385b73 100644 --- a/api_docs/kubernetes_security.mdx +++ b/api_docs/kubernetes_security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity title: "kubernetesSecurity" image: https://source.unsplash.com/400x175/?github description: API docs for the kubernetesSecurity plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index 44ad4585cc74e..c6fc743f9d2f9 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lens title: "lens" image: https://source.unsplash.com/400x175/?github description: API docs for the lens plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index d89fdf7c99069..5deabea535da9 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard title: "licenseApiGuard" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseApiGuard plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseApiGuard'] --- import licenseApiGuardObj from './license_api_guard.devdocs.json'; diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index ed5b3c8507658..910089ce81c35 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseManagement title: "licenseManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseManagement plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] --- import licenseManagementObj from './license_management.devdocs.json'; diff --git a/api_docs/licensing.devdocs.json b/api_docs/licensing.devdocs.json index 0d43c4c2d00dc..5a54a44d81030 100644 --- a/api_docs/licensing.devdocs.json +++ b/api_docs/licensing.devdocs.json @@ -2220,11 +2220,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts" - }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts" }, { "plugin": "securitySolution", diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index dd236acd18f20..18c6df14e999b 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licensing title: "licensing" image: https://source.unsplash.com/400x175/?github description: API docs for the licensing plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/links.mdx b/api_docs/links.mdx index b7300edc6bdbb..c1a05bc008f47 100644 --- a/api_docs/links.mdx +++ b/api_docs/links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/links title: "links" image: https://source.unsplash.com/400x175/?github description: API docs for the links plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'links'] --- import linksObj from './links.devdocs.json'; diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 95cd4b04df419..e64b3bf4774b1 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lists title: "lists" image: https://source.unsplash.com/400x175/?github description: API docs for the lists plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] --- import listsObj from './lists.devdocs.json'; diff --git a/api_docs/logs_explorer.devdocs.json b/api_docs/logs_explorer.devdocs.json index 24910c48f35bc..34497e0d2ceb0 100644 --- a/api_docs/logs_explorer.devdocs.json +++ b/api_docs/logs_explorer.devdocs.json @@ -50,6 +50,53 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "logsExplorer", + "id": "def-public.getDiscoverColumnsWithFallbackFieldsFromDisplayOptions", + "type": "Function", + "tags": [], + "label": "getDiscoverColumnsWithFallbackFieldsFromDisplayOptions", + "description": [], + "signature": [ + "(displayOptions: ", + { + "pluginId": "logsExplorer", + "scope": "common", + "docId": "kibLogsExplorerPluginApi", + "section": "def-common.DisplayOptions", + "text": "DisplayOptions" + }, + ") => string[] | undefined" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/public/utils/convert_discover_app_state.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "logsExplorer", + "id": "def-public.getDiscoverColumnsWithFallbackFieldsFromDisplayOptions.$1", + "type": "Object", + "tags": [], + "label": "displayOptions", + "description": [], + "signature": [ + { + "pluginId": "logsExplorer", + "scope": "common", + "docId": "kibLogsExplorerPluginApi", + "section": "def-common.DisplayOptions", + "text": "DisplayOptions" + } + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/public/utils/convert_discover_app_state.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "logsExplorer", "id": "def-public.getDiscoverFiltersFromState", @@ -58,7 +105,7 @@ "label": "getDiscoverFiltersFromState", "description": [], "signature": [ - "(filters?: ", + "(index: string, filters?: ", { "pluginId": "@kbn/es-query", "scope": "common", @@ -85,6 +132,21 @@ { "parentPluginId": "logsExplorer", "id": "def-public.getDiscoverFiltersFromState.$1", + "type": "string", + "tags": [], + "label": "index", + "description": [], + "signature": [ + "string" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/public/utils/convert_discover_app_state.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "logsExplorer", + "id": "def-public.getDiscoverFiltersFromState.$2", "type": "Array", "tags": [], "label": "filters", @@ -106,7 +168,7 @@ }, { "parentPluginId": "logsExplorer", - "id": "def-public.getDiscoverFiltersFromState.$2", + "id": "def-public.getDiscoverFiltersFromState.$3", "type": "Object", "tags": [], "label": "controls", @@ -329,6 +391,8 @@ "text": "DisplayOptions" }, " & ", + "WithDataTableRecord", + " & ", "WithDiscoverStateContainer", ") | (", "WithDatasetSelection", @@ -353,6 +417,8 @@ "text": "DisplayOptions" }, " & ", + "WithDataTableRecord", + " & ", "WithDiscoverStateContainer", "), any, ", "LogsExplorerControllerEvent", @@ -447,6 +513,8 @@ "text": "DisplayOptions" }, " & ", + "WithDataTableRecord", + " & ", "WithDiscoverStateContainer", ") | (", "WithDatasetSelection", @@ -471,6 +539,8 @@ "text": "DisplayOptions" }, " & ", + "WithDataTableRecord", + " & ", "WithDiscoverStateContainer", "), any, ", "LogsExplorerControllerEvent", @@ -725,6 +795,8 @@ "text": "DisplayOptions" }, " & ", + "WithDataTableRecord", + " & ", "WithDiscoverStateContainer", ") | (", "WithDatasetSelection", @@ -749,6 +821,8 @@ "text": "DisplayOptions" }, " & ", + "WithDataTableRecord", + " & ", "WithDiscoverStateContainer", ")" ], @@ -1426,45 +1500,6 @@ ], "initialIsOpen": false }, - { - "parentPluginId": "logsExplorer", - "id": "def-common.GridColumnDisplayOptions", - "type": "Interface", - "tags": [], - "label": "GridColumnDisplayOptions", - "description": [], - "path": "x-pack/plugins/observability_solution/logs_explorer/common/display_options/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "logsExplorer", - "id": "def-common.GridColumnDisplayOptions.field", - "type": "string", - "tags": [], - "label": "field", - "description": [], - "path": "x-pack/plugins/observability_solution/logs_explorer/common/display_options/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "logsExplorer", - "id": "def-common.GridColumnDisplayOptions.width", - "type": "number", - "tags": [], - "label": "width", - "description": [], - "signature": [ - "number | undefined" - ], - "path": "x-pack/plugins/observability_solution/logs_explorer/common/display_options/types.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, { "parentPluginId": "logsExplorer", "id": "def-common.GridDisplayOptions", @@ -1655,6 +1690,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "logsExplorer", + "id": "def-common.CONTENT_FIELD", + "type": "string", + "tags": [], + "label": "CONTENT_FIELD", + "description": [], + "signature": [ + "\"content\"" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "logsExplorer", "id": "def-common.ControlPanels", @@ -1693,6 +1743,23 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "logsExplorer", + "id": "def-common.GridColumnDisplayOptions", + "type": "Type", + "tags": [], + "label": "GridColumnDisplayOptions", + "description": [], + "signature": [ + "DocumentFieldGridColumnOptions", + " | ", + "SmartFieldGridColumnOptions" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/display_options/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "logsExplorer", "id": "def-common.PartialChartDisplayOptions", @@ -1771,6 +1838,62 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "logsExplorer", + "id": "def-common.CONTENT_FIELD_CONFIGURATION", + "type": "Object", + "tags": [], + "label": "CONTENT_FIELD_CONFIGURATION", + "description": [], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "logsExplorer", + "id": "def-common.CONTENT_FIELD_CONFIGURATION.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "signature": [ + "\"smart-field\"" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "logsExplorer", + "id": "def-common.CONTENT_FIELD_CONFIGURATION.smartField", + "type": "string", + "tags": [], + "label": "smartField", + "description": [], + "signature": [ + "\"content\"" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "logsExplorer", + "id": "def-common.CONTENT_FIELD_CONFIGURATION.fallbackFields", + "type": "Array", + "tags": [], + "label": "fallbackFields", + "description": [], + "signature": [ + "string[]" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "logsExplorer", "id": "def-common.controlPanelConfigs", @@ -2043,6 +2166,115 @@ "deprecated": false, "trackAdoption": false, "initialIsOpen": false + }, + { + "parentPluginId": "logsExplorer", + "id": "def-common.RESOURCE_FIELD_CONFIGURATION", + "type": "Object", + "tags": [], + "label": "RESOURCE_FIELD_CONFIGURATION", + "description": [], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "logsExplorer", + "id": "def-common.RESOURCE_FIELD_CONFIGURATION.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "signature": [ + "\"smart-field\"" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "logsExplorer", + "id": "def-common.RESOURCE_FIELD_CONFIGURATION.smartField", + "type": "string", + "tags": [], + "label": "smartField", + "description": [], + "signature": [ + "\"resource\"" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "logsExplorer", + "id": "def-common.RESOURCE_FIELD_CONFIGURATION.fallbackFields", + "type": "Array", + "tags": [], + "label": "fallbackFields", + "description": [], + "signature": [ + "string[]" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "logsExplorer", + "id": "def-common.RESOURCE_FIELD_CONFIGURATION.width", + "type": "number", + "tags": [], + "label": "width", + "description": [], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "logsExplorer", + "id": "def-common.SMART_FALLBACK_FIELDS", + "type": "Object", + "tags": [], + "label": "SMART_FALLBACK_FIELDS", + "description": [], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "logsExplorer", + "id": "def-common.SMART_FALLBACK_FIELDS.CONTENT_FIELD", + "type": "Object", + "tags": [], + "label": "[CONTENT_FIELD]", + "description": [], + "signature": [ + "SmartFieldGridColumnOptions" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "logsExplorer", + "id": "def-common.SMART_FALLBACK_FIELDS.RESOURCE_FIELD", + "type": "Object", + "tags": [], + "label": "[RESOURCE_FIELD]", + "description": [], + "signature": [ + "SmartFieldGridColumnOptions" + ], + "path": "x-pack/plugins/observability_solution/logs_explorer/common/constants.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false } ] } diff --git a/api_docs/logs_explorer.mdx b/api_docs/logs_explorer.mdx index 385bc61ba95ec..c7a50b424f715 100644 --- a/api_docs/logs_explorer.mdx +++ b/api_docs/logs_explorer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/logsExplorer title: "logsExplorer" image: https://source.unsplash.com/400x175/?github description: API docs for the logsExplorer plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'logsExplorer'] --- import logsExplorerObj from './logs_explorer.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 87 | 0 | 87 | 16 | +| 101 | 0 | 101 | 19 | ## Client diff --git a/api_docs/logs_shared.mdx b/api_docs/logs_shared.mdx index d2f8d50cc8f01..567944d4d2010 100644 --- a/api_docs/logs_shared.mdx +++ b/api_docs/logs_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/logsShared title: "logsShared" image: https://source.unsplash.com/400x175/?github description: API docs for the logsShared plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'logsShared'] --- import logsSharedObj from './logs_shared.devdocs.json'; diff --git a/api_docs/management.mdx b/api_docs/management.mdx index fba676f19aecd..aecaa7118d310 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/management title: "management" image: https://source.unsplash.com/400x175/?github description: API docs for the management plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] --- import managementObj from './management.devdocs.json'; diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index b08020847ad0f..7bff19aea644c 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/maps title: "maps" image: https://source.unsplash.com/400x175/?github description: API docs for the maps plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'maps'] --- import mapsObj from './maps.devdocs.json'; diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index c31e0e69c8098..3380e244d744f 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mapsEms title: "mapsEms" image: https://source.unsplash.com/400x175/?github description: API docs for the mapsEms plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/metrics_data_access.mdx b/api_docs/metrics_data_access.mdx index 625ffebb489f3..b78d550fcfdd8 100644 --- a/api_docs/metrics_data_access.mdx +++ b/api_docs/metrics_data_access.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/metricsDataAccess title: "metricsDataAccess" image: https://source.unsplash.com/400x175/?github description: API docs for the metricsDataAccess plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'metricsDataAccess'] --- import metricsDataAccessObj from './metrics_data_access.devdocs.json'; diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index 7e9e89ae9fe6c..7f937dd4f5f15 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ml title: "ml" image: https://source.unsplash.com/400x175/?github description: API docs for the ml plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; diff --git a/api_docs/mock_idp_plugin.mdx b/api_docs/mock_idp_plugin.mdx index 7926d1a9b9d7c..c93fb96aeb8cf 100644 --- a/api_docs/mock_idp_plugin.mdx +++ b/api_docs/mock_idp_plugin.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mockIdpPlugin title: "mockIdpPlugin" image: https://source.unsplash.com/400x175/?github description: API docs for the mockIdpPlugin plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mockIdpPlugin'] --- import mockIdpPluginObj from './mock_idp_plugin.devdocs.json'; diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index 975e5081aa0c3..1234f9d291dca 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoring title: "monitoring" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoring plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] --- import monitoringObj from './monitoring.devdocs.json'; diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index 7a6ccb760d179..d4e44cb28d177 100644 --- a/api_docs/monitoring_collection.mdx +++ b/api_docs/monitoring_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoringCollection title: "monitoringCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoringCollection plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoringCollection'] --- import monitoringCollectionObj from './monitoring_collection.devdocs.json'; diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index f1e78a2a0d848..09afc9743e473 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/navigation title: "navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the navigation plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'navigation'] --- import navigationObj from './navigation.devdocs.json'; diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index 1ac248f0f8035..edb4f78ecc4b4 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/newsfeed title: "newsfeed" image: https://source.unsplash.com/400x175/?github description: API docs for the newsfeed plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/no_data_page.mdx b/api_docs/no_data_page.mdx index 6b7425d854413..b74fb4c80ba5c 100644 --- a/api_docs/no_data_page.mdx +++ b/api_docs/no_data_page.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/noDataPage title: "noDataPage" image: https://source.unsplash.com/400x175/?github description: API docs for the noDataPage plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'noDataPage'] --- import noDataPageObj from './no_data_page.devdocs.json'; diff --git a/api_docs/notifications.mdx b/api_docs/notifications.mdx index 134a6b479f43e..aad70a736ba6d 100644 --- a/api_docs/notifications.mdx +++ b/api_docs/notifications.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/notifications title: "notifications" image: https://source.unsplash.com/400x175/?github description: API docs for the notifications plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'notifications'] --- import notificationsObj from './notifications.devdocs.json'; diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index 00a2701daf52c..39ea99294d42e 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observability title: "observability" image: https://source.unsplash.com/400x175/?github description: API docs for the observability plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; diff --git a/api_docs/observability_a_i_assistant.devdocs.json b/api_docs/observability_a_i_assistant.devdocs.json index 108c6df21a6db..e2b198012b1d1 100644 --- a/api_docs/observability_a_i_assistant.devdocs.json +++ b/api_docs/observability_a_i_assistant.devdocs.json @@ -798,8 +798,6 @@ }, "[]; connectorId: string; persist: boolean; } & { conversationId?: string | undefined; title?: string | undefined; }; }; }) => Promise<", "Readable", - " | ", - "ChatCompletion", ">; } & ", "ObservabilityAIAssistantRouteCreateOptions", "; \"POST /internal/observability_ai_assistant/chat\": { endpoint: \"POST /internal/observability_ai_assistant/chat\"; params?: ", @@ -1296,8 +1294,6 @@ }, "[]; connectorId: string; persist: boolean; } & { conversationId?: string | undefined; title?: string | undefined; }; }; }) => Promise<", "Readable", - " | ", - "ChatCompletion", ">; } & ", "ObservabilityAIAssistantRouteCreateOptions", "; \"POST /internal/observability_ai_assistant/chat\": { endpoint: \"POST /internal/observability_ai_assistant/chat\"; params?: ", @@ -2034,8 +2030,6 @@ }, "[]; connectorId: string; persist: boolean; } & { conversationId?: string | undefined; title?: string | undefined; }; }; }) => Promise<", "Readable", - " | ", - "ChatCompletion", ">; } & ", "ObservabilityAIAssistantRouteCreateOptions", "; \"POST /internal/observability_ai_assistant/chat\": { endpoint: \"POST /internal/observability_ai_assistant/chat\"; params?: ", @@ -2541,8 +2535,6 @@ }, "[]; connectorId: string; persist: boolean; } & { conversationId?: string | undefined; title?: string | undefined; }; }; }) => Promise<", "Readable", - " | ", - "ChatCompletion", ">; } & ", "ObservabilityAIAssistantRouteCreateOptions", "; \"POST /internal/observability_ai_assistant/chat\": { endpoint: \"POST /internal/observability_ai_assistant/chat\"; params?: ", @@ -3192,8 +3184,6 @@ }, "[]; connectorId: string; persist: boolean; } & { conversationId?: string | undefined; title?: string | undefined; }; }; }) => Promise<", "Readable", - " | ", - "ChatCompletion", ">; } & ", "ObservabilityAIAssistantRouteCreateOptions", "; \"POST /internal/observability_ai_assistant/chat\": { endpoint: \"POST /internal/observability_ai_assistant/chat\"; params?: ", diff --git a/api_docs/observability_a_i_assistant.mdx b/api_docs/observability_a_i_assistant.mdx index f28cde02e43cd..d6cf4a495645c 100644 --- a/api_docs/observability_a_i_assistant.mdx +++ b/api_docs/observability_a_i_assistant.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityAIAssistant title: "observabilityAIAssistant" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityAIAssistant plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityAIAssistant'] --- import observabilityAIAssistantObj from './observability_a_i_assistant.devdocs.json'; diff --git a/api_docs/observability_logs_explorer.mdx b/api_docs/observability_logs_explorer.mdx index d47958c2a8078..9f3d1076e58e6 100644 --- a/api_docs/observability_logs_explorer.mdx +++ b/api_docs/observability_logs_explorer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityLogsExplorer title: "observabilityLogsExplorer" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityLogsExplorer plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityLogsExplorer'] --- import observabilityLogsExplorerObj from './observability_logs_explorer.devdocs.json'; diff --git a/api_docs/observability_onboarding.mdx b/api_docs/observability_onboarding.mdx index ccd4ba146fa33..94409e8c94b9b 100644 --- a/api_docs/observability_onboarding.mdx +++ b/api_docs/observability_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityOnboarding title: "observabilityOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityOnboarding plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityOnboarding'] --- import observabilityOnboardingObj from './observability_onboarding.devdocs.json'; diff --git a/api_docs/observability_shared.devdocs.json b/api_docs/observability_shared.devdocs.json index 3d85c4cdca8b7..1867134f22f2e 100644 --- a/api_docs/observability_shared.devdocs.json +++ b/api_docs/observability_shared.devdocs.json @@ -522,7 +522,7 @@ "label": "FeatureFeedbackButton", "description": [], "signature": [ - "({ formUrl, \"data-test-subj\": dts, onClickCapture, defaultButton, kibanaVersion, isCloudEnv, isServerlessEnv, sanitizedPath, surveyButtonText, }: FeatureFeedbackButtonProps) => JSX.Element" + "({ formUrl, formConfig, \"data-test-subj\": dts, onClickCapture, defaultButton, kibanaVersion, isCloudEnv, isServerlessEnv, sanitizedPath, surveyButtonText, }: FeatureFeedbackButtonProps) => JSX.Element" ], "path": "x-pack/plugins/observability_shared/public/components/feature_feedback_button/feature_feedback_button.tsx", "deprecated": false, @@ -533,7 +533,7 @@ "id": "def-public.FeatureFeedbackButton.$1", "type": "Object", "tags": [], - "label": "{\n formUrl,\n 'data-test-subj': dts,\n onClickCapture,\n defaultButton,\n kibanaVersion,\n isCloudEnv,\n isServerlessEnv,\n sanitizedPath,\n surveyButtonText = (\n \n ),\n}", + "label": "{\n formUrl,\n formConfig,\n 'data-test-subj': dts,\n onClickCapture,\n defaultButton,\n kibanaVersion,\n isCloudEnv,\n isServerlessEnv,\n sanitizedPath,\n surveyButtonText = (\n \n ),\n}", "description": [], "signature": [ "FeatureFeedbackButtonProps" diff --git a/api_docs/observability_shared.mdx b/api_docs/observability_shared.mdx index 6321d889038b1..364fce65ffe2c 100644 --- a/api_docs/observability_shared.mdx +++ b/api_docs/observability_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityShared title: "observabilityShared" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityShared plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityShared'] --- import observabilitySharedObj from './observability_shared.devdocs.json'; diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index aabeb22ebcdc9..129f1950d07dc 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/osquery title: "osquery" image: https://source.unsplash.com/400x175/?github description: API docs for the osquery plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; diff --git a/api_docs/painless_lab.mdx b/api_docs/painless_lab.mdx index e993b1aa9f688..b5bb1b7e213a8 100644 --- a/api_docs/painless_lab.mdx +++ b/api_docs/painless_lab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/painlessLab title: "painlessLab" image: https://source.unsplash.com/400x175/?github description: API docs for the painlessLab plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'painlessLab'] --- import painlessLabObj from './painless_lab.devdocs.json'; diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index 721ce92b3aa70..ce4a78efd494f 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -7,7 +7,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory description: Directory of public APIs available through plugins or packages. -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -15,13 +15,13 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 751 | 643 | 40 | +| 752 | 644 | 40 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 79771 | 228 | 68327 | 1735 | +| 79848 | 228 | 68394 | 1740 | ## Plugin Directory @@ -32,7 +32,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | - | 2 | 0 | 2 | 0 | | | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | - | 2 | 0 | 2 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 69 | 0 | 4 | 1 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 835 | 1 | 804 | 51 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 836 | 1 | 805 | 51 | | | [@elastic/obs-ux-infra_services-team](https://github.com/orgs/elastic/teams/obs-ux-infra_services-team) | The user interface for Elastic APM | 29 | 0 | 29 | 125 | | | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | - | 9 | 0 | 9 | 0 | | | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | Asset manager plugin for entity assets (inventory, topology, etc) | 9 | 0 | 9 | 2 | @@ -49,7 +49,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | cloudFullStory | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | When Kibana runs on Elastic Cloud, this plugin registers FullStory as a shipper for telemetry. | 0 | 0 | 0 | 0 | | cloudLinks | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | Adds the links to the Elastic Cloud console | 0 | 0 | 0 | 0 | | | [@elastic/kibana-cloud-security-posture](https://github.com/orgs/elastic/teams/kibana-cloud-security-posture) | The cloud security posture plugin | 14 | 0 | 2 | 2 | -| | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 29 | 0 | 23 | 0 | +| | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 32 | 0 | 24 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Content management app | 149 | 0 | 125 | 6 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 323 | 0 | 315 | 16 | | crossClusterReplication | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 0 | 0 | 0 | 0 | @@ -64,8 +64,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 943 | 0 | 276 | 4 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | The Data Visualizer tools help you understand your data, by analyzing the metrics and fields in a log file or an existing Elasticsearch index. | 31 | 3 | 25 | 1 | | | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | This plugin introduces the concept of dataset quality, where users can easily get an overview on the datasets they have. | 10 | 0 | 10 | 5 | -| | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 12 | 0 | 10 | 3 | -| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 151 | 0 | 104 | 22 | +| | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 16 | 0 | 10 | 3 | +| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 156 | 0 | 109 | 23 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 37 | 0 | 35 | 2 | | | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | APIs used to assess the quality of data in Elasticsearch indexes | 2 | 0 | 0 | 0 | | | [@elastic/security-generative-ai](https://github.com/orgs/elastic/teams/security-generative-ai) | Server APIs for the Elastic AI Assistant | 41 | 0 | 27 | 0 | @@ -108,7 +108,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 151 | 0 | 111 | 1 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Image embeddable | 3 | 0 | 3 | 1 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 4 | 0 | 4 | 0 | -| | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 223 | 0 | 218 | 4 | +| | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 226 | 0 | 221 | 4 | | | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | This plugin visualizes data from Filebeat and Metricbeat, and integrates with other Observability solutions | 32 | 0 | 29 | 8 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 4 | 0 | 4 | 0 | | inputControlVis | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds Input Control visualization to Kibana | 0 | 0 | 0 | 0 | @@ -125,7 +125,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 117 | 0 | 42 | 10 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | A dashboard panel for creating links to dashboards or external links. | 57 | 0 | 57 | 6 | | | [@elastic/security-detection-engine](https://github.com/orgs/elastic/teams/security-detection-engine) | - | 226 | 0 | 97 | 52 | -| | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | This plugin provides a LogsExplorer component using the Discover customization framework, offering several affordances specifically designed for log consumption. | 87 | 0 | 87 | 16 | +| | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | This plugin provides a LogsExplorer component using the Discover customization framework, offering several affordances specifically designed for log consumption. | 101 | 0 | 101 | 19 | | | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | Exposes the shared components and APIs to access and visualize logs. | 302 | 0 | 276 | 32 | | logstash | [@elastic/logstash](https://github.com/orgs/elastic/teams/logstash) | - | 0 | 0 | 0 | 0 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 45 | 0 | 45 | 7 | @@ -158,7 +158,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 24 | 0 | 19 | 2 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 129 | 2 | 118 | 4 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 25 | 0 | 25 | 0 | -| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 161 | 0 | 147 | 2 | +| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 164 | 0 | 150 | 2 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 87 | 0 | 81 | 3 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 104 | 0 | 56 | 1 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the definition and helper methods around saved searches, used by discover and visualizations. | 78 | 0 | 77 | 3 | @@ -184,7 +184,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 31 | 0 | 26 | 6 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 1 | 0 | 1 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 6 | 0 | 0 | 0 | -| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 24 | 0 | 9 | 0 | +| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 26 | 0 | 10 | 0 | | | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | Elastic threat intelligence helps you see if you are open to or have been subject to current or historical known threats | 30 | 0 | 14 | 5 | | | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 240 | 1 | 196 | 17 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 4 | 0 | 4 | 1 | @@ -256,7 +256,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 37 | 0 | 15 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 2 | 0 | 2 | 0 | | | [@elastic/appex-qa](https://github.com/orgs/elastic/teams/appex-qa) | - | 8 | 0 | 4 | 0 | -| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 211 | 0 | 174 | 8 | +| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 217 | 0 | 180 | 9 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 77 | 0 | 48 | 9 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 24 | 0 | 24 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 140 | 3 | 137 | 18 | @@ -264,7 +264,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 8 | 0 | 8 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 3 | 0 | 3 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 7 | 0 | 7 | 0 | -| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 52 | 0 | 34 | 3 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 50 | 0 | 33 | 3 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 191 | 1 | 125 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 4 | 0 | 0 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 7 | 0 | 7 | 1 | @@ -400,7 +400,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 14 | 0 | 14 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 36 | 0 | 6 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 12 | 0 | 2 | 0 | -| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 20 | 0 | 19 | 0 | +| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 21 | 0 | 20 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 20 | 0 | 3 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 24 | 0 | 24 | 3 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 4 | 0 | 4 | 0 | @@ -439,7 +439,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 5 | 0 | 5 | 0 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 4 | 0 | 4 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 3 | 0 | 3 | 0 | -| | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | - | 31 | 0 | 21 | 0 | +| | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | - | 34 | 0 | 24 | 0 | | | [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | - | 4 | 0 | 4 | 0 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 8 | 0 | 8 | 0 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 8 | 0 | 8 | 0 | @@ -515,7 +515,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-gis](https://github.com/orgs/elastic/teams/kibana-gis) | - | 592 | 1 | 1 | 0 | | | [@elastic/kibana-gis](https://github.com/orgs/elastic/teams/kibana-gis) | - | 2 | 0 | 2 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 95 | 2 | 0 | 0 | -| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 206 | 3 | 1 | 0 | +| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 207 | 3 | 1 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 8 | 0 | 8 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 37 | 0 | 0 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 12 | 0 | 0 | 0 | @@ -587,11 +587,12 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 16 | 0 | 16 | 1 | | | [@elastic/security-detections-response](https://github.com/orgs/elastic/teams/security-detections-response) | - | 120 | 0 | 117 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 2 | 0 | 2 | 0 | -| | [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | - | 75 | 0 | 75 | 0 | -| | [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | - | 2805 | 0 | 2805 | 0 | +| | [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | - | 76 | 0 | 76 | 0 | +| | [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | - | 2812 | 0 | 2812 | 0 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 18 | 1 | 17 | 1 | | | [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | - | 25 | 0 | 25 | 0 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 20 | 0 | 18 | 1 | +| | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 7 | 0 | 7 | 0 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 82 | 0 | 35 | 0 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 44 | 0 | 14 | 0 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 213 | 0 | 114 | 0 | @@ -675,7 +676,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 14 | 0 | 8 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 137 | 5 | 105 | 2 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 1 | 0 | -| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 27 | 0 | 11 | 0 | +| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 30 | 0 | 13 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 72 | 0 | 55 | 0 | | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 11 | 0 | 11 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 39 | 0 | 25 | 1 | @@ -683,7 +684,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 42 | 0 | 28 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 55 | 0 | 46 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 7 | 0 | 6 | 0 | -| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the unified data table which can be integrated into apps | 117 | 0 | 55 | 1 | +| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the unified data table which can be integrated into apps | 130 | 0 | 67 | 1 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 14 | 0 | 13 | 6 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list and field stats which can be integrated into apps | 291 | 0 | 267 | 10 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 13 | 0 | 9 | 0 | @@ -691,7 +692,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 80 | 1 | 21 | 2 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 37 | 0 | 16 | 1 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 2 | 0 | -| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 24 | 0 | 14 | 0 | +| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 26 | 0 | 15 | 0 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 152 | 0 | 149 | 3 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 10 | 0 | 9 | 1 | | | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | - | 13 | 0 | 13 | 0 | diff --git a/api_docs/presentation_panel.mdx b/api_docs/presentation_panel.mdx index e3aee7f0bfb71..9461fcb4e533d 100644 --- a/api_docs/presentation_panel.mdx +++ b/api_docs/presentation_panel.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationPanel title: "presentationPanel" image: https://source.unsplash.com/400x175/?github description: API docs for the presentationPanel plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationPanel'] --- import presentationPanelObj from './presentation_panel.devdocs.json'; diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index e405fca86a3d1..c076663692f30 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationUtil title: "presentationUtil" image: https://source.unsplash.com/400x175/?github description: API docs for the presentationUtil plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] --- import presentationUtilObj from './presentation_util.devdocs.json'; diff --git a/api_docs/profiling.mdx b/api_docs/profiling.mdx index d4cfe28b7e8aa..25fb5f1237e86 100644 --- a/api_docs/profiling.mdx +++ b/api_docs/profiling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profiling title: "profiling" image: https://source.unsplash.com/400x175/?github description: API docs for the profiling plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling'] --- import profilingObj from './profiling.devdocs.json'; diff --git a/api_docs/profiling_data_access.mdx b/api_docs/profiling_data_access.mdx index 0e56d9598fef2..76361cf65c98e 100644 --- a/api_docs/profiling_data_access.mdx +++ b/api_docs/profiling_data_access.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profilingDataAccess title: "profilingDataAccess" image: https://source.unsplash.com/400x175/?github description: API docs for the profilingDataAccess plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profilingDataAccess'] --- import profilingDataAccessObj from './profiling_data_access.devdocs.json'; diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index c55f1a935cf9e..1dd0fc4f5c001 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/remoteClusters title: "remoteClusters" image: https://source.unsplash.com/400x175/?github description: API docs for the remoteClusters plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'remoteClusters'] --- import remoteClustersObj from './remote_clusters.devdocs.json'; diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index d26e76e7ce488..54d9c13804a44 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/reporting title: "reporting" image: https://source.unsplash.com/400x175/?github description: API docs for the reporting plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'reporting'] --- import reportingObj from './reporting.devdocs.json'; diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index ef919e8821540..4b27ac25c2dc4 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/rollup title: "rollup" image: https://source.unsplash.com/400x175/?github description: API docs for the rollup plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 2e198ed3ddc25..4b08037a65458 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ruleRegistry title: "ruleRegistry" image: https://source.unsplash.com/400x175/?github description: API docs for the ruleRegistry plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] --- import ruleRegistryObj from './rule_registry.devdocs.json'; diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index 91d4e323253ab..a862d6d10ecd1 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/runtimeFields title: "runtimeFields" image: https://source.unsplash.com/400x175/?github description: API docs for the runtimeFields plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] --- import runtimeFieldsObj from './runtime_fields.devdocs.json'; diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index 4126438f28ced..b0c1e9e8c32c6 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjects title: "savedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjects plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] --- import savedObjectsObj from './saved_objects.devdocs.json'; diff --git a/api_docs/saved_objects_finder.mdx b/api_docs/saved_objects_finder.mdx index e12526f9c89bc..faf11db8230b6 100644 --- a/api_docs/saved_objects_finder.mdx +++ b/api_docs/saved_objects_finder.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsFinder title: "savedObjectsFinder" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsFinder plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.devdocs.json b/api_docs/saved_objects_management.devdocs.json index b76bc8ee29428..48f11e8af68f7 100644 --- a/api_docs/saved_objects_management.devdocs.json +++ b/api_docs/saved_objects_management.devdocs.json @@ -1543,6 +1543,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "savedObjectsManagement", + "id": "def-public.SavedObjectWithMetadata.managed", + "type": "CompoundType", + "tags": [], + "label": "managed", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/saved_objects_management/common/types/v1.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "savedObjectsManagement", "id": "def-public.SavedObjectWithMetadata.attributes", @@ -2224,6 +2238,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "savedObjectsManagement", + "id": "def-server.SavedObjectWithMetadata.managed", + "type": "CompoundType", + "tags": [], + "label": "managed", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/saved_objects_management/common/types/v1.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "savedObjectsManagement", "id": "def-server.SavedObjectWithMetadata.attributes", @@ -2726,6 +2754,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "savedObjectsManagement", + "id": "def-common.SavedObjectWithMetadata.managed", + "type": "CompoundType", + "tags": [], + "label": "managed", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/saved_objects_management/common/types/v1.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "savedObjectsManagement", "id": "def-common.SavedObjectWithMetadata.attributes", diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index 1ebf4383f706f..42a1670c52345 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement title: "savedObjectsManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsManagement plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] --- import savedObjectsManagementObj from './saved_objects_management.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 161 | 0 | 147 | 2 | +| 164 | 0 | 150 | 2 | ## Client diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index beb0e74f46dd4..0a65ea95e3363 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging title: "savedObjectsTagging" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTagging plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTagging'] --- import savedObjectsTaggingObj from './saved_objects_tagging.devdocs.json'; diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index e9ea1260a755d..becca0b609184 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss title: "savedObjectsTaggingOss" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTaggingOss plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] --- import savedObjectsTaggingOssObj from './saved_objects_tagging_oss.devdocs.json'; diff --git a/api_docs/saved_search.mdx b/api_docs/saved_search.mdx index 413055e82c1ce..501abcdc7a1f4 100644 --- a/api_docs/saved_search.mdx +++ b/api_docs/saved_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedSearch title: "savedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the savedSearch plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedSearch'] --- import savedSearchObj from './saved_search.devdocs.json'; diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index 07273e1cf9127..ecc2efc9eb95d 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotMode title: "screenshotMode" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotMode plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotMode'] --- import screenshotModeObj from './screenshot_mode.devdocs.json'; diff --git a/api_docs/screenshotting.mdx b/api_docs/screenshotting.mdx index a8685fcc16fcd..fb215f080197a 100644 --- a/api_docs/screenshotting.mdx +++ b/api_docs/screenshotting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotting title: "screenshotting" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotting plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] --- import screenshottingObj from './screenshotting.devdocs.json'; diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 3dca4d8a056eb..8da4328b7693f 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/security title: "security" image: https://source.unsplash.com/400x175/?github description: API docs for the security plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] --- import securityObj from './security.devdocs.json'; diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index 851069752f8ad..975edabe486d1 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolution title: "securitySolution" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolution plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] --- import securitySolutionObj from './security_solution.devdocs.json'; diff --git a/api_docs/security_solution_ess.mdx b/api_docs/security_solution_ess.mdx index 7d6805ad32c9b..495e4ac05416c 100644 --- a/api_docs/security_solution_ess.mdx +++ b/api_docs/security_solution_ess.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolutionEss title: "securitySolutionEss" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolutionEss plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolutionEss'] --- import securitySolutionEssObj from './security_solution_ess.devdocs.json'; diff --git a/api_docs/security_solution_serverless.mdx b/api_docs/security_solution_serverless.mdx index 6fb77f7079ab5..268926586119b 100644 --- a/api_docs/security_solution_serverless.mdx +++ b/api_docs/security_solution_serverless.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolutionServerless title: "securitySolutionServerless" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolutionServerless plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolutionServerless'] --- import securitySolutionServerlessObj from './security_solution_serverless.devdocs.json'; diff --git a/api_docs/serverless.mdx b/api_docs/serverless.mdx index 09bf0ab95e78c..b86708a4cda3e 100644 --- a/api_docs/serverless.mdx +++ b/api_docs/serverless.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverless title: "serverless" image: https://source.unsplash.com/400x175/?github description: API docs for the serverless plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'serverless'] --- import serverlessObj from './serverless.devdocs.json'; diff --git a/api_docs/serverless_observability.mdx b/api_docs/serverless_observability.mdx index 80845375fa25e..79d61aa1c9eb9 100644 --- a/api_docs/serverless_observability.mdx +++ b/api_docs/serverless_observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverlessObservability title: "serverlessObservability" image: https://source.unsplash.com/400x175/?github description: API docs for the serverlessObservability plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'serverlessObservability'] --- import serverlessObservabilityObj from './serverless_observability.devdocs.json'; diff --git a/api_docs/serverless_search.mdx b/api_docs/serverless_search.mdx index 5f5879eeabe24..0647271e3016b 100644 --- a/api_docs/serverless_search.mdx +++ b/api_docs/serverless_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverlessSearch title: "serverlessSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the serverlessSearch plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'serverlessSearch'] --- import serverlessSearchObj from './serverless_search.devdocs.json'; diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index 56946b555e2f2..0de3dd4712cc4 100644 --- a/api_docs/session_view.mdx +++ b/api_docs/session_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/sessionView title: "sessionView" image: https://source.unsplash.com/400x175/?github description: API docs for the sessionView plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] --- import sessionViewObj from './session_view.devdocs.json'; diff --git a/api_docs/share.mdx b/api_docs/share.mdx index a8786c8438ef2..079fbc27deb80 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/share title: "share" image: https://source.unsplash.com/400x175/?github description: API docs for the share plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] --- import shareObj from './share.devdocs.json'; diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index 3fce052eddefb..96b65dd0772df 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/snapshotRestore title: "snapshotRestore" image: https://source.unsplash.com/400x175/?github description: API docs for the snapshotRestore plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] --- import snapshotRestoreObj from './snapshot_restore.devdocs.json'; diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 89ec19c9d8155..29e0aff597e29 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/spaces title: "spaces" image: https://source.unsplash.com/400x175/?github description: API docs for the spaces plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] --- import spacesObj from './spaces.devdocs.json'; diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index 145306cf77dec..14dae824d8046 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackAlerts title: "stackAlerts" image: https://source.unsplash.com/400x175/?github description: API docs for the stackAlerts plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] --- import stackAlertsObj from './stack_alerts.devdocs.json'; diff --git a/api_docs/stack_connectors.mdx b/api_docs/stack_connectors.mdx index cef40b7b09a6a..c549c2b70bc05 100644 --- a/api_docs/stack_connectors.mdx +++ b/api_docs/stack_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackConnectors title: "stackConnectors" image: https://source.unsplash.com/400x175/?github description: API docs for the stackConnectors plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackConnectors'] --- import stackConnectorsObj from './stack_connectors.devdocs.json'; diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index a151419fe32f7..d1016105fcacd 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/taskManager title: "taskManager" image: https://source.unsplash.com/400x175/?github description: API docs for the taskManager plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] --- import taskManagerObj from './task_manager.devdocs.json'; diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index b4fcc79079d2c..d6a5aff049962 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetry title: "telemetry" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetry plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetry'] --- import telemetryObj from './telemetry.devdocs.json'; diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index e1b5a553f14c0..e1511ffd12729 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager title: "telemetryCollectionManager" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionManager plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] --- import telemetryCollectionManagerObj from './telemetry_collection_manager.devdocs.json'; diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index d65290e05ca4e..db2a40395132b 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionXpack title: "telemetryCollectionXpack" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionXpack plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionXpack'] --- import telemetryCollectionXpackObj from './telemetry_collection_xpack.devdocs.json'; diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index 09f984ea18369..416ea7a6fb78b 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection title: "telemetryManagementSection" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryManagementSection plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] --- import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json'; diff --git a/api_docs/text_based_languages.devdocs.json b/api_docs/text_based_languages.devdocs.json index 35e649a14f3f2..be42f4c6c2369 100644 --- a/api_docs/text_based_languages.devdocs.json +++ b/api_docs/text_based_languages.devdocs.json @@ -145,7 +145,7 @@ "section": "def-common.AggregateQuery", "text": "AggregateQuery" }, - " | undefined) => void" + " | undefined, abortController?: AbortController | undefined) => Promise" ], "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", "deprecated": false, @@ -172,6 +172,21 @@ "deprecated": false, "trackAdoption": false, "isRequired": false + }, + { + "parentPluginId": "textBasedLanguages", + "id": "def-public.TextBasedLanguagesEditorProps.onTextLangQuerySubmit.$2", + "type": "Object", + "tags": [], + "label": "abortController", + "description": [], + "signature": [ + "AbortController | undefined" + ], + "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": false } ], "returnComment": [] @@ -396,6 +411,22 @@ "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "textBasedLanguages", + "id": "def-public.TextBasedLanguagesEditorProps.allowQueryCancellation", + "type": "CompoundType", + "tags": [], + "label": "allowQueryCancellation", + "description": [ + "when set to true enables query cancellation" + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-text-based-editor/src/text_based_languages_editor.tsx", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/text_based_languages.mdx b/api_docs/text_based_languages.mdx index e6dc10f5a9565..8bd18a9ec990e 100644 --- a/api_docs/text_based_languages.mdx +++ b/api_docs/text_based_languages.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/textBasedLanguages title: "textBasedLanguages" image: https://source.unsplash.com/400x175/?github description: API docs for the textBasedLanguages plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'textBasedLanguages'] --- import textBasedLanguagesObj from './text_based_languages.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/k | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 24 | 0 | 9 | 0 | +| 26 | 0 | 10 | 0 | ## Client diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx index 72fe54da292e5..330842f160ad5 100644 --- a/api_docs/threat_intelligence.mdx +++ b/api_docs/threat_intelligence.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/threatIntelligence title: "threatIntelligence" image: https://source.unsplash.com/400x175/?github description: API docs for the threatIntelligence plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; diff --git a/api_docs/timelines.devdocs.json b/api_docs/timelines.devdocs.json index 287fe4d5c5417..a79de6ce82d32 100644 --- a/api_docs/timelines.devdocs.json +++ b/api_docs/timelines.devdocs.json @@ -1527,6 +1527,14 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/utils.ts" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threatmatch_input/index.tsx" diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index 51379efcc39aa..ed3773d4c25a2 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/timelines title: "timelines" image: https://source.unsplash.com/400x175/?github description: API docs for the timelines plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'timelines'] --- import timelinesObj from './timelines.devdocs.json'; diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx index a5efbd6742623..47127ab6ae9e8 100644 --- a/api_docs/transform.mdx +++ b/api_docs/transform.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/transform title: "transform" image: https://source.unsplash.com/400x175/?github description: API docs for the transform plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.devdocs.json b/api_docs/triggers_actions_ui.devdocs.json index 4a9016b138d68..e89eb79dd824a 100644 --- a/api_docs/triggers_actions_ui.devdocs.json +++ b/api_docs/triggers_actions_ui.devdocs.json @@ -4179,6 +4179,16 @@ "tags": [], "label": "RuleDefinitionProps", "description": [], + "signature": [ + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.RuleDefinitionProps", + "text": "RuleDefinitionProps" + }, + "" + ], "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, "trackAdoption": false, @@ -4199,15 +4209,7 @@ "section": "def-common.SanitizedRule", "text": "SanitizedRule" }, - "<", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.RuleTypeParams", - "text": "RuleTypeParams" - }, - ">, \"alertTypeId\"> & { ruleTypeId: string; }" + ", \"alertTypeId\"> & { ruleTypeId: string; }" ], "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, @@ -7820,11 +7822,43 @@ "label": "getAddRuleFlyout", "description": [], "signature": [ - "(props: Omit<", + "(props: Omit<", "RuleAddProps", - ">, \"actionTypeRegistry\" | \"ruleTypeRegistry\">) => React.ReactElement<", + ", \"actionTypeRegistry\" | \"ruleTypeRegistry\">) => React.ReactElement<", "RuleAddProps", - ">, string | React.JSXElementConstructor>" + ", string | React.JSXElementConstructor>" ], "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", "deprecated": false, @@ -7840,7 +7874,7 @@ "signature": [ "Omit<", "RuleAddProps", - ">, \"actionTypeRegistry\" | \"ruleTypeRegistry\">" + ", \"actionTypeRegistry\" | \"ruleTypeRegistry\">" ], "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", "deprecated": false, @@ -7858,11 +7892,43 @@ "label": "getEditRuleFlyout", "description": [], "signature": [ - "(props: Omit<", + "(props: Omit<", "RuleEditProps", - ">, \"actionTypeRegistry\" | \"ruleTypeRegistry\">) => React.ReactElement<", + ", \"actionTypeRegistry\" | \"ruleTypeRegistry\">) => React.ReactElement<", "RuleEditProps", - ">, string | React.JSXElementConstructor>" + ", string | React.JSXElementConstructor>" ], "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", "deprecated": false, @@ -7878,7 +7944,7 @@ "signature": [ "Omit<", "RuleEditProps", - ">, \"actionTypeRegistry\" | \"ruleTypeRegistry\">" + ", \"actionTypeRegistry\" | \"ruleTypeRegistry\">" ], "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", "deprecated": false, @@ -8416,7 +8482,15 @@ "section": "def-public.RuleDefinitionProps", "text": "RuleDefinitionProps" }, - ") => React.ReactElement<", + "<", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.RuleTypeParams", + "text": "RuleTypeParams" + }, + ">) => React.ReactElement<", { "pluginId": "triggersActionsUi", "scope": "public", @@ -8424,7 +8498,15 @@ "section": "def-public.RuleDefinitionProps", "text": "RuleDefinitionProps" }, - ", string | React.JSXElementConstructor>" + "<", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.RuleTypeParams", + "text": "RuleTypeParams" + }, + ">, string | React.JSXElementConstructor>" ], "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", "deprecated": false, @@ -8444,7 +8526,16 @@ "docId": "kibTriggersActionsUiPluginApi", "section": "def-public.RuleDefinitionProps", "text": "RuleDefinitionProps" - } + }, + "<", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.RuleTypeParams", + "text": "RuleTypeParams" + }, + ">" ], "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", "deprecated": false, diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 7f22a66a59803..19b98a17d504f 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi title: "triggersActionsUi" image: https://source.unsplash.com/400x175/?github description: API docs for the triggersActionsUi plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.devdocs.json'; diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index 1ef2d79fb3bc5..7fa75e12030d4 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActions title: "uiActions" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActions plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActions'] --- import uiActionsObj from './ui_actions.devdocs.json'; diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index 4c659d410fcbe..ad83fbe9b1d0b 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced title: "uiActionsEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActionsEnhanced plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] --- import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json'; diff --git a/api_docs/unified_doc_viewer.mdx b/api_docs/unified_doc_viewer.mdx index 7e89032b71d6b..6187e10618edf 100644 --- a/api_docs/unified_doc_viewer.mdx +++ b/api_docs/unified_doc_viewer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedDocViewer title: "unifiedDocViewer" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedDocViewer plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedDocViewer'] --- import unifiedDocViewerObj from './unified_doc_viewer.devdocs.json'; diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx index 9690c84929005..8a83db3c438a2 100644 --- a/api_docs/unified_histogram.mdx +++ b/api_docs/unified_histogram.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedHistogram title: "unifiedHistogram" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedHistogram plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedHistogram'] --- import unifiedHistogramObj from './unified_histogram.devdocs.json'; diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index 1465ebdda2291..f3c78d7c262de 100644 --- a/api_docs/unified_search.mdx +++ b/api_docs/unified_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch title: "unifiedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] --- import unifiedSearchObj from './unified_search.devdocs.json'; diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index 90172fc462ad1..257612b54d8cd 100644 --- a/api_docs/unified_search_autocomplete.mdx +++ b/api_docs/unified_search_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete title: "unifiedSearch.autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch.autocomplete plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] --- import unifiedSearchAutocompleteObj from './unified_search_autocomplete.devdocs.json'; diff --git a/api_docs/uptime.mdx b/api_docs/uptime.mdx index 79e7ca4b21364..2f60359a244be 100644 --- a/api_docs/uptime.mdx +++ b/api_docs/uptime.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uptime title: "uptime" image: https://source.unsplash.com/400x175/?github description: API docs for the uptime plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uptime'] --- import uptimeObj from './uptime.devdocs.json'; diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index 9e90600856309..15e83c5a9b4b6 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/urlForwarding title: "urlForwarding" image: https://source.unsplash.com/400x175/?github description: API docs for the urlForwarding plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] --- import urlForwardingObj from './url_forwarding.devdocs.json'; diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 0d44be7793e6a..538ad01cb477c 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/usageCollection title: "usageCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the usageCollection plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'usageCollection'] --- import usageCollectionObj from './usage_collection.devdocs.json'; diff --git a/api_docs/ux.mdx b/api_docs/ux.mdx index 36f67fe934a29..9574635e34dfc 100644 --- a/api_docs/ux.mdx +++ b/api_docs/ux.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ux title: "ux" image: https://source.unsplash.com/400x175/?github description: API docs for the ux plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ux'] --- import uxObj from './ux.devdocs.json'; diff --git a/api_docs/vis_default_editor.mdx b/api_docs/vis_default_editor.mdx index 2b55d86ea4cb4..2dfc19664235e 100644 --- a/api_docs/vis_default_editor.mdx +++ b/api_docs/vis_default_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor title: "visDefaultEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the visDefaultEditor plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visDefaultEditor'] --- import visDefaultEditorObj from './vis_default_editor.devdocs.json'; diff --git a/api_docs/vis_type_gauge.mdx b/api_docs/vis_type_gauge.mdx index 2c07f3ec3582e..53fba333ba810 100644 --- a/api_docs/vis_type_gauge.mdx +++ b/api_docs/vis_type_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeGauge title: "visTypeGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeGauge plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeGauge'] --- import visTypeGaugeObj from './vis_type_gauge.devdocs.json'; diff --git a/api_docs/vis_type_heatmap.mdx b/api_docs/vis_type_heatmap.mdx index 2feb7aa00cc8e..4ff167a4997d8 100644 --- a/api_docs/vis_type_heatmap.mdx +++ b/api_docs/vis_type_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap title: "visTypeHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeHeatmap plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeHeatmap'] --- import visTypeHeatmapObj from './vis_type_heatmap.devdocs.json'; diff --git a/api_docs/vis_type_pie.mdx b/api_docs/vis_type_pie.mdx index eb97b5b933dbc..71b25a0e26a2b 100644 --- a/api_docs/vis_type_pie.mdx +++ b/api_docs/vis_type_pie.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypePie title: "visTypePie" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypePie plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypePie'] --- import visTypePieObj from './vis_type_pie.devdocs.json'; diff --git a/api_docs/vis_type_table.mdx b/api_docs/vis_type_table.mdx index 600d5be2346b4..5e594c2320088 100644 --- a/api_docs/vis_type_table.mdx +++ b/api_docs/vis_type_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTable title: "visTypeTable" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTable plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTable'] --- import visTypeTableObj from './vis_type_table.devdocs.json'; diff --git a/api_docs/vis_type_timelion.mdx b/api_docs/vis_type_timelion.mdx index 7fe24b693cb89..f1a0d56ff2c32 100644 --- a/api_docs/vis_type_timelion.mdx +++ b/api_docs/vis_type_timelion.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion title: "visTypeTimelion" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimelion plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimelion'] --- import visTypeTimelionObj from './vis_type_timelion.devdocs.json'; diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index 7ed7ba8f3f4fc..f9993b7e007c8 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries title: "visTypeTimeseries" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimeseries plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimeseries'] --- import visTypeTimeseriesObj from './vis_type_timeseries.devdocs.json'; diff --git a/api_docs/vis_type_vega.mdx b/api_docs/vis_type_vega.mdx index ca95e9f9fa272..c79330542aff8 100644 --- a/api_docs/vis_type_vega.mdx +++ b/api_docs/vis_type_vega.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVega title: "visTypeVega" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVega plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVega'] --- import visTypeVegaObj from './vis_type_vega.devdocs.json'; diff --git a/api_docs/vis_type_vislib.mdx b/api_docs/vis_type_vislib.mdx index bbad04fca0ceb..07a81891983b4 100644 --- a/api_docs/vis_type_vislib.mdx +++ b/api_docs/vis_type_vislib.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVislib title: "visTypeVislib" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVislib plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVislib'] --- import visTypeVislibObj from './vis_type_vislib.devdocs.json'; diff --git a/api_docs/vis_type_xy.mdx b/api_docs/vis_type_xy.mdx index 39c2e0ca0b9a7..a0e4b49c51133 100644 --- a/api_docs/vis_type_xy.mdx +++ b/api_docs/vis_type_xy.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeXy title: "visTypeXy" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeXy plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] --- import visTypeXyObj from './vis_type_xy.devdocs.json'; diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index 186762e92bbc1..a16dab9206ed5 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visualizations title: "visualizations" image: https://source.unsplash.com/400x175/?github description: API docs for the visualizations plugin -date: 2024-02-08 +date: 2024-02-11 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; diff --git a/config/serverless.yml b/config/serverless.yml index 1d98bcca216e7..5147b4961d282 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -136,6 +136,7 @@ uiSettings: # Task Manager xpack.task_manager.allow_reading_invalid_state: false +xpack.task_manager.request_timeouts.update_by_query: 60000 ## TaskManager requeue invalid tasks, supports ZDT xpack.task_manager.requeue_invalid_tasks.enabled: true diff --git a/docs/management/connectors/pre-configured-connectors.asciidoc b/docs/management/connectors/pre-configured-connectors.asciidoc index b7293b6232190..c027220376cdf 100644 --- a/docs/management/connectors/pre-configured-connectors.asciidoc +++ b/docs/management/connectors/pre-configured-connectors.asciidoc @@ -148,7 +148,7 @@ xpack.actions.preconfigured: actionTypeId: .bedrock config: apiUrl: https://bedrock-runtime.us-east-1.amazonaws.com <1> - defaultModel: anthropic.claude-v2 <2> + defaultModel: anthropic.claude-v2:1 <2> secrets: accessKey: key-value <3> secret: secret-value <4> diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index b7d7e8d344a32..2bfde478a494d 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -340,7 +340,7 @@ For a <>, specifies a string f The default model to use for requests, which varies by connector: + -- -* For an <>, current support is for the Anthropic Claude models. Defaults to `anthropic.claude-v2`. +* For an <>, current support is for the Anthropic Claude models. Defaults to `anthropic.claude-v2:1`. * For a <>, it is optional and applicable only when `xpack.actions.preconfigured..config.apiProvider` is `OpenAI`. -- diff --git a/fleet_packages.json b/fleet_packages.json index 79722187a1b0d..bfde442c20d22 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -52,10 +52,10 @@ }, { "name": "synthetics", - "version": "1.1.1" + "version": "1.2.1" }, { "name": "security_detection_engine", - "version": "8.12.3" + "version": "8.12.4" } ] \ No newline at end of file diff --git a/package.json b/package.json index ccd5182e04a55..6485d9c840817 100644 --- a/package.json +++ b/package.json @@ -101,11 +101,11 @@ "@dnd-kit/utilities": "^2.0.0", "@elastic/apm-rum": "^5.16.0", "@elastic/apm-rum-react": "^2.0.2", - "@elastic/charts": "61.2.0", + "@elastic/charts": "63.0.0", "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.9.1-canary.1", "@elastic/ems-client": "8.5.1", - "@elastic/eui": "92.2.1", + "@elastic/eui": "93.0.0", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.1", @@ -676,6 +676,7 @@ "@kbn/search-index-documents": "link:packages/kbn-search-index-documents", "@kbn/search-response-warnings": "link:packages/kbn-search-response-warnings", "@kbn/searchprofiler-plugin": "link:x-pack/plugins/searchprofiler", + "@kbn/security-hardening": "link:packages/kbn-security-hardening", "@kbn/security-plugin": "link:x-pack/plugins/security", "@kbn/security-plugin-types-common": "link:x-pack/packages/security/plugin_types_common", "@kbn/security-plugin-types-public": "link:x-pack/packages/security/plugin_types_public", @@ -881,6 +882,8 @@ "@reduxjs/toolkit": "1.9.7", "@slack/webhook": "^7.0.1", "@smithy/eventstream-codec": "^2.0.12", + "@smithy/eventstream-serde-node": "^2.1.1", + "@smithy/types": "^2.9.1", "@smithy/util-utf8": "^2.0.0", "@tanstack/react-query": "^4.29.12", "@tanstack/react-query-devtools": "^4.29.12", @@ -946,6 +949,7 @@ "diff": "^5.1.0", "elastic-apm-node": "^4.4.0", "email-addresses": "^5.0.0", + "eventsource-parser": "^1.1.1", "execa": "^5.1.1", "expiry-js": "0.1.7", "exponential-backoff": "^3.1.1", @@ -954,6 +958,7 @@ "fast-glob": "^3.3.2", "fflate": "^0.6.9", "file-saver": "^1.3.8", + "flat": "5", "fnv-plus": "^1.3.1", "font-awesome": "4.7.0", "formik": "^2.4.5", @@ -1380,11 +1385,13 @@ "@types/ejs": "^3.0.6", "@types/enzyme": "^3.10.12", "@types/eslint": "^8.44.2", + "@types/event-stream": "^4.0.5", "@types/express": "^4.17.13", "@types/extract-zip": "^1.6.2", "@types/faker": "^5.1.5", "@types/fetch-mock": "^7.3.1", "@types/file-saver": "^2.0.0", + "@types/flat": "^5.0.5", "@types/flot": "^0.0.31", "@types/fnv-plus": "^1.3.0", "@types/geojson": "^7946.0.10", diff --git a/packages/content-management/content_editor/src/components/editor_flyout_content.tsx b/packages/content-management/content_editor/src/components/editor_flyout_content.tsx index a760ec5f5d1a9..007cc58c4e5ad 100644 --- a/packages/content-management/content_editor/src/components/editor_flyout_content.tsx +++ b/packages/content-management/content_editor/src/components/editor_flyout_content.tsx @@ -45,6 +45,7 @@ export interface Props { item: Item; entityName: string; isReadonly?: boolean; + readonlyReason?: string; services: Pick; onSave?: (args: { id: string; @@ -62,6 +63,7 @@ export const ContentEditorFlyoutContent: FC = ({ item, entityName, isReadonly = true, + readonlyReason, services: { TagSelector, TagList, notifyError }, onSave, onCancel, @@ -136,6 +138,12 @@ export const ContentEditorFlyoutContent: FC = ({ ; export type Props = CommonProps; diff --git a/packages/content-management/content_editor/src/components/metadata_form.tsx b/packages/content-management/content_editor/src/components/metadata_form.tsx index 42e06671bc7a8..26b433edcd161 100644 --- a/packages/content-management/content_editor/src/components/metadata_form.tsx +++ b/packages/content-management/content_editor/src/components/metadata_form.tsx @@ -27,6 +27,7 @@ interface Props { isSubmitted: boolean; }; isReadonly: boolean; + readonlyReason: string; tagsReferences: SavedObjectsReference[]; TagList?: Services['TagList']; TagSelector?: Services['TagSelector']; @@ -40,6 +41,7 @@ export const MetadataForm: FC = ({ TagList, TagSelector, isReadonly, + readonlyReason, }) => { const { title, @@ -54,11 +56,6 @@ export const MetadataForm: FC = ({ getWarnings, } = form; - const readOnlyToolTip = i18n.translate( - 'contentManagement.contentEditor.metadataForm.readOnlyToolTip', - { defaultMessage: 'To edit these details, contact your administrator for access.' } - ); - return ( @@ -73,7 +70,7 @@ export const MetadataForm: FC = ({ > = ({ > ; export function useOpenContentEditor() { diff --git a/packages/content-management/table_list_view/src/table_list_view.tsx b/packages/content-management/table_list_view/src/table_list_view.tsx index 081b837c93ed6..7270fd953a83e 100644 --- a/packages/content-management/table_list_view/src/table_list_view.tsx +++ b/packages/content-management/table_list_view/src/table_list_view.tsx @@ -37,7 +37,6 @@ export type TableListViewProps & { title: string; description?: string; @@ -74,7 +73,6 @@ export const TableListView = ({ titleColumnName, additionalRightSideActions, withoutPageTemplateWrapper, - itemIsEditable, }: TableListViewProps) => { const PageTemplate = withoutPageTemplateWrapper ? (React.Fragment as unknown as typeof KibanaPageTemplate) @@ -120,7 +118,6 @@ export const TableListView = ({ id={listingId} contentEditor={contentEditor} titleColumnName={titleColumnName} - itemIsEditable={itemIsEditable} withoutPageTemplateWrapper={withoutPageTemplateWrapper} onFetchSuccess={onFetchSuccess} setPageDataTestSubject={setPageDataTestSubject} diff --git a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx index 20509bab2ee56..24f9e9db2b564 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx @@ -95,11 +95,6 @@ export interface TableListViewTableProps< */ editItem?(item: T): void; - /** - * Handler to set edit action visiblity, and content editor readonly state per item. If not provided all non-managed items are considered editable. Note: Items with the managed property set to true will always be non-editable. - */ - itemIsEditable?(item: T): boolean; - /** * Name for the column containing the "title" value. */ @@ -259,7 +254,6 @@ function TableListViewTableComp({ findItems, createItem, editItem, - itemIsEditable, deleteItems, getDetailViewLink, onClickTitle, @@ -440,14 +434,34 @@ function TableListViewTableComp({ items, }); - const isEditable = useCallback( - (item: T) => { - // If the So is `managed` it is never editable. - if (item.managed) return false; - return itemIsEditable?.(item) ?? true; - }, - [itemIsEditable] - ); + const tableItemsRowActions = useMemo(() => { + return items.reduce((acc, item) => { + const ret = { + ...acc, + [item.id]: rowItemActions ? rowItemActions(item) : undefined, + }; + + if (item.managed) { + ret[item.id] = { + ...ret[item.id], + delete: { + enabled: false, + reason: i18n.translate('contentManagement.tableList.managedItemNoDelete', { + defaultMessage: 'This item is managed by Elastic. It cannot be deleted.', + }), + }, + edit: { + enabled: false, + reason: i18n.translate('contentManagement.tableList.managedItemNoEdit', { + defaultMessage: 'This item is managed by Elastic. Clone it before making changes.', + }), + }, + }; + } + + return ret; + }, {}); + }, [items, rowItemActions]); const inspectItem = useCallback( (item: T) => { @@ -464,7 +478,9 @@ function TableListViewTableComp({ }, entityName, ...contentEditor, - isReadonly: contentEditor.isReadonly || !isEditable(item), + isReadonly: + contentEditor.isReadonly || tableItemsRowActions[item.id]?.edit?.enabled === false, + readonlyReason: tableItemsRowActions[item.id]?.edit?.reason, onSave: contentEditor.onSave && (async (args) => { @@ -475,7 +491,14 @@ function TableListViewTableComp({ }), }); }, - [getTagIdsFromReferences, openContentEditor, entityName, contentEditor, isEditable, fetchItems] + [ + getTagIdsFromReferences, + openContentEditor, + entityName, + contentEditor, + tableItemsRowActions, + fetchItems, + ] ); const tableColumns = useMemo(() => { @@ -549,7 +572,7 @@ function TableListViewTableComp({ ), icon: 'pencil', type: 'icon', - available: (item) => isEditable(item), + available: (item) => Boolean(tableItemsRowActions[item.id]?.edit?.enabled), enabled: (v) => !(v as unknown as { error: string })?.error, onClick: editItem, 'data-test-subj': `edit-action`, @@ -605,7 +628,7 @@ function TableListViewTableComp({ addOrRemoveExcludeTagFilter, addOrRemoveIncludeTagFilter, DateFormatterComp, - isEditable, + tableItemsRowActions, inspectItem, ]); @@ -617,15 +640,6 @@ function TableListViewTableComp({ return selectedIds.map((selectedId) => itemsById[selectedId]); }, [selectedIds, itemsById]); - const tableItemsRowActions = useMemo(() => { - return items.reduce((acc, item) => { - return { - ...acc, - [item.id]: rowItemActions ? rowItemActions(item) : undefined, - }; - }, {}); - }, [items, rowItemActions]); - // ------------ // Callbacks // ------------ diff --git a/packages/content-management/table_list_view_table/src/types.ts b/packages/content-management/table_list_view_table/src/types.ts index c8e734a289451..90729464288be 100644 --- a/packages/content-management/table_list_view_table/src/types.ts +++ b/packages/content-management/table_list_view_table/src/types.ts @@ -13,7 +13,7 @@ export interface Tag { color: string; } -export type TableRowAction = 'delete'; +export type TableRowAction = 'delete' | 'edit'; export type RowActions = { [action in TableRowAction]?: { diff --git a/packages/core/analytics/core-analytics-browser-internal/src/logger.test.ts b/packages/core/analytics/core-analytics-browser-internal/src/logger.test.ts index 2fbe17e3f7d22..429ddcbcd0e35 100644 --- a/packages/core/analytics/core-analytics-browser-internal/src/logger.test.ts +++ b/packages/core/analytics/core-analytics-browser-internal/src/logger.test.ts @@ -7,16 +7,17 @@ */ import type { LogRecord } from '@kbn/logging'; +import { unsafeConsole } from '@kbn/security-hardening'; import { createLogger } from './logger'; describe('createLogger', () => { // Calling `.mockImplementation` on all of them to avoid jest logging the console usage - const logErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - const logWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); - const logInfoSpy = jest.spyOn(console, 'info').mockImplementation(); - const logDebugSpy = jest.spyOn(console, 'debug').mockImplementation(); - const logTraceSpy = jest.spyOn(console, 'trace').mockImplementation(); - const logLogSpy = jest.spyOn(console, 'log').mockImplementation(); + const logErrorSpy = jest.spyOn(unsafeConsole, 'error').mockImplementation(); + const logWarnSpy = jest.spyOn(unsafeConsole, 'warn').mockImplementation(); + const logInfoSpy = jest.spyOn(unsafeConsole, 'info').mockImplementation(); + const logDebugSpy = jest.spyOn(unsafeConsole, 'debug').mockImplementation(); + const logTraceSpy = jest.spyOn(unsafeConsole, 'trace').mockImplementation(); + const logLogSpy = jest.spyOn(unsafeConsole, 'log').mockImplementation(); beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/core/analytics/core-analytics-browser-internal/src/logger.ts b/packages/core/analytics/core-analytics-browser-internal/src/logger.ts index ff403a7d46d2c..7ec3a56da6ed1 100644 --- a/packages/core/analytics/core-analytics-browser-internal/src/logger.ts +++ b/packages/core/analytics/core-analytics-browser-internal/src/logger.ts @@ -7,6 +7,7 @@ */ import type { Logger } from '@kbn/logging'; +import { unsafeConsole } from '@kbn/security-hardening'; /** * Create custom logger until we have a proper logging solution: https://github.com/elastic/kibana/issues/33796 @@ -16,20 +17,20 @@ export function createLogger(isDev: boolean): Logger { // TODO: Replace with a core logger once we implement it in https://github.com/elastic/kibana/issues/33796 // For now, it logs only in dev mode const logger: Logger = { - // eslint-disable-next-line no-console - fatal: (...args) => (isDev ? console.error(...args) : void 0), - // eslint-disable-next-line no-console - error: (...args) => (isDev ? console.error(...args) : void 0), - // eslint-disable-next-line no-console - warn: (...args) => (isDev ? console.warn(...args) : void 0), - // eslint-disable-next-line no-console - info: (...args) => (isDev ? console.info(...args) : void 0), - // eslint-disable-next-line no-console - debug: (...args) => (isDev ? console.debug(...args) : void 0), - // eslint-disable-next-line no-console - trace: (...args) => (isDev ? console.trace(...args) : void 0), - // eslint-disable-next-line no-console - log: (...args) => (isDev ? console.log(...args) : void 0), + // eslint-disable-next-line @kbn/eslint/no_unsafe_console + fatal: (...args) => (isDev ? unsafeConsole.error(...args) : void 0), + // eslint-disable-next-line @kbn/eslint/no_unsafe_console + error: (...args) => (isDev ? unsafeConsole.error(...args) : void 0), + // eslint-disable-next-line @kbn/eslint/no_unsafe_console + warn: (...args) => (isDev ? unsafeConsole.warn(...args) : void 0), + // eslint-disable-next-line @kbn/eslint/no_unsafe_console + info: (...args) => (isDev ? unsafeConsole.info(...args) : void 0), + // eslint-disable-next-line @kbn/eslint/no_unsafe_console + debug: (...args) => (isDev ? unsafeConsole.debug(...args) : void 0), + // eslint-disable-next-line @kbn/eslint/no_unsafe_console + trace: (...args) => (isDev ? unsafeConsole.trace(...args) : void 0), + // eslint-disable-next-line @kbn/eslint/no_unsafe_console + log: (...args) => (isDev ? unsafeConsole.log(...args) : void 0), isLevelEnabled: () => true, get: () => logger, }; diff --git a/packages/core/analytics/core-analytics-browser-internal/tsconfig.json b/packages/core/analytics/core-analytics-browser-internal/tsconfig.json index c6efe4287effc..f2f9d3837719e 100644 --- a/packages/core/analytics/core-analytics-browser-internal/tsconfig.json +++ b/packages/core/analytics/core-analytics-browser-internal/tsconfig.json @@ -13,7 +13,8 @@ "@kbn/core-injected-metadata-browser-internal", "@kbn/core-analytics-browser", "@kbn/core-base-browser-mocks", - "@kbn/core-injected-metadata-browser-mocks" + "@kbn/core-injected-metadata-browser-mocks", + "@kbn/security-hardening" ], "exclude": ["target/**/*"] } diff --git a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx index 8172b705a6ffe..e0eaf9b2144fa 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx @@ -17,6 +17,7 @@ const buildServerVersion = (parts: Partial = {}): ServerVersion = build_number: 9000, build_snapshot: false, build_date: '2023-05-15T23:12:09.000Z', + build_flavor: 'traditional', ...parts, }); diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts index ed6cc186313f3..92aad0f460e70 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts +++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts @@ -21,6 +21,7 @@ const mockedResponse: StatusResponse = { build_number: 12, build_snapshot: false, build_date: '2023-05-15T23:12:09.000Z', + build_flavor: 'traditional', }, status: { overall: { diff --git a/packages/core/i18n/core-i18n-browser-internal/src/__snapshots__/i18n_service.test.tsx.snap b/packages/core/i18n/core-i18n-browser-internal/src/__snapshots__/i18n_service.test.tsx.snap index 78bb5d5d7ea71..50b4b74b0f3e5 100644 --- a/packages/core/i18n/core-i18n-browser-internal/src/__snapshots__/i18n_service.test.tsx.snap +++ b/packages/core/i18n/core-i18n-browser-internal/src/__snapshots__/i18n_service.test.tsx.snap @@ -376,9 +376,9 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiToast.dismissToast": "Dismiss toast", "euiToast.newNotification": "A new notification appears", "euiToast.notification": "Notification", - "euiTourStep.closeTour": "Close tour", - "euiTourStep.endTour": "End tour", - "euiTourStep.skipTour": "Skip tour", + "euiTourFooter.closeTour": "Close tour", + "euiTourFooter.endTour": "End tour", + "euiTourFooter.skipTour": "Skip tour", "euiTourStepIndicator.ariaLabel": [Function], "euiTourStepIndicator.isActive": "active", "euiTourStepIndicator.isComplete": "complete", diff --git a/packages/core/i18n/core-i18n-browser-internal/src/i18n_eui_mapping.tsx b/packages/core/i18n/core-i18n-browser-internal/src/i18n_eui_mapping.tsx index 8710a8640d747..a8c4db74ba406 100644 --- a/packages/core/i18n/core-i18n-browser-internal/src/i18n_eui_mapping.tsx +++ b/packages/core/i18n/core-i18n-browser-internal/src/i18n_eui_mapping.tsx @@ -1710,13 +1710,13 @@ export const getEuiContextMapping = (): EuiTokensObject => { defaultMessage: 'Notification', description: 'ARIA label on an element containing a notification', }), - 'euiTourStep.endTour': i18n.translate('core.euiTourStep.endTour', { + 'euiTourFooter.endTour': i18n.translate('core.euiTourFooter.endTour', { defaultMessage: 'End tour', }), - 'euiTourStep.skipTour': i18n.translate('core.euiTourStep.skipTour', { + 'euiTourFooter.skipTour': i18n.translate('core.euiTourFooter.skipTour', { defaultMessage: 'Skip tour', }), - 'euiTourStep.closeTour': i18n.translate('core.euiTourStep.closeTour', { + 'euiTourFooter.closeTour': i18n.translate('core.euiTourFooter.closeTour', { defaultMessage: 'Close tour', }), 'euiTourStepIndicator.isActive': i18n.translate('core.euiTourStepIndicator.isActive', { diff --git a/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.test.ts b/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.test.ts index 8b8900be8e035..886d100eda6de 100644 --- a/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.test.ts +++ b/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.test.ts @@ -7,10 +7,11 @@ */ import { LogRecord, LogLevel } from '@kbn/logging'; +import { unsafeConsole } from '@kbn/security-hardening'; import { ConsoleAppender } from './console_appender'; test('`append()` correctly formats records and pushes them to console.', () => { - jest.spyOn(global.console, 'log').mockImplementation(() => { + jest.spyOn(unsafeConsole, 'log').mockImplementation(() => { // noop }); @@ -47,10 +48,7 @@ test('`append()` correctly formats records and pushes them to console.', () => { for (const record of records) { appender.append(record); - // eslint-disable-next-line no-console - expect(console.log).toHaveBeenCalledWith(`mock-${JSON.stringify(record)}`); + expect(unsafeConsole.log).toHaveBeenCalledWith(`mock-${JSON.stringify(record)}`); } - - // eslint-disable-next-line no-console - expect(console.log).toHaveBeenCalledTimes(records.length); + expect(unsafeConsole.log).toHaveBeenCalledTimes(records.length); }); diff --git a/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.ts b/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.ts index 4d35f3150b421..44228d602e417 100644 --- a/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.ts +++ b/packages/core/logging/core-logging-browser-internal/src/appenders/console_appender.ts @@ -7,6 +7,7 @@ */ import type { Layout, LogRecord, DisposableAppender } from '@kbn/logging'; +import { unsafeConsole } from '@kbn/security-hardening'; /** * @@ -25,8 +26,8 @@ export class ConsoleAppender implements DisposableAppender { * @param record `LogRecord` instance to be logged. */ public append(record: LogRecord) { - // eslint-disable-next-line no-console - console.log(this.layout.format(record)); + // eslint-disable-next-line @kbn/eslint/no_unsafe_console + unsafeConsole.log(this.layout.format(record)); } /** diff --git a/packages/core/logging/core-logging-browser-internal/src/logging_system.test.ts b/packages/core/logging/core-logging-browser-internal/src/logging_system.test.ts index 61b32002e7ae5..75071169eef5f 100644 --- a/packages/core/logging/core-logging-browser-internal/src/logging_system.test.ts +++ b/packages/core/logging/core-logging-browser-internal/src/logging_system.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { unsafeConsole } from '@kbn/security-hardening'; import { BrowserLoggingSystem } from './logging_system'; describe('', () => { @@ -15,7 +16,7 @@ describe('', () => { let loggingSystem: BrowserLoggingSystem; beforeEach(() => { - mockConsoleLog = jest.spyOn(global.console, 'log').mockReturnValue(undefined); + mockConsoleLog = jest.spyOn(unsafeConsole, 'log').mockReturnValue(undefined); jest.spyOn(global, 'Date').mockImplementation(() => timestamp); loggingSystem = new BrowserLoggingSystem({ logLevel: 'warn' }); }); diff --git a/packages/core/logging/core-logging-browser-internal/tsconfig.json b/packages/core/logging/core-logging-browser-internal/tsconfig.json index d0d9f725a4ee0..89c06b251c39b 100644 --- a/packages/core/logging/core-logging-browser-internal/tsconfig.json +++ b/packages/core/logging/core-logging-browser-internal/tsconfig.json @@ -12,7 +12,8 @@ ], "kbn_references": [ "@kbn/logging", - "@kbn/core-logging-common-internal" + "@kbn/core-logging-common-internal", + "@kbn/security-hardening" ], "exclude": [ "target/**/*", diff --git a/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.test.ts b/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.test.ts index 1e8f742c1ecda..17379cd6d5553 100644 --- a/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.test.ts +++ b/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.test.ts @@ -19,6 +19,7 @@ jest.mock('../../layouts/layouts', () => { }); import { LogRecord, LogLevel } from '@kbn/logging'; +import { unsafeConsole } from '@kbn/security-hardening'; import { ConsoleAppender } from './console_appender'; test('`configSchema` creates correct schema.', () => { @@ -37,7 +38,7 @@ test('`configSchema` creates correct schema.', () => { }); test('`append()` correctly formats records and pushes them to console.', () => { - jest.spyOn(global.console, 'log').mockImplementation(() => { + jest.spyOn(unsafeConsole, 'log').mockImplementation(() => { // noop }); @@ -74,10 +75,7 @@ test('`append()` correctly formats records and pushes them to console.', () => { for (const record of records) { appender.append(record); - // eslint-disable-next-line no-console - expect(console.log).toHaveBeenCalledWith(`mock-${JSON.stringify(record)}`); + expect(unsafeConsole.log).toHaveBeenCalledWith(`mock-${JSON.stringify(record)}`); } - - // eslint-disable-next-line no-console - expect(console.log).toHaveBeenCalledTimes(records.length); + expect(unsafeConsole.log).toHaveBeenCalledTimes(records.length); }); diff --git a/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.ts b/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.ts index 0602ea81289af..45ad47fb062f0 100644 --- a/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.ts +++ b/packages/core/logging/core-logging-server-internal/src/appenders/console/console_appender.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import type { Layout, LogRecord, DisposableAppender } from '@kbn/logging'; +import { unsafeConsole } from '@kbn/security-hardening'; import { Layouts } from '../../layouts/layouts'; const { literal, object } = schema; @@ -34,8 +35,8 @@ export class ConsoleAppender implements DisposableAppender { * @param record `LogRecord` instance to be logged. */ public append(record: LogRecord) { - // eslint-disable-next-line no-console - console.log(this.layout.format(record)); + // eslint-disable-next-line @kbn/eslint/no_unsafe_console + unsafeConsole.log(this.layout.format(record)); } /** diff --git a/packages/core/logging/core-logging-server-internal/src/logging_system.test.ts b/packages/core/logging/core-logging-server-internal/src/logging_system.test.ts index 76fe93d1e614a..4d3b4d73916ef 100644 --- a/packages/core/logging/core-logging-server-internal/src/logging_system.test.ts +++ b/packages/core/logging/core-logging-server-internal/src/logging_system.test.ts @@ -18,10 +18,11 @@ const mockCreateWriteStream = createWriteStream as unknown as jest.Mock { - mockConsoleLog = jest.spyOn(global.console, 'log').mockReturnValue(undefined); + mockConsoleLog = jest.spyOn(unsafeConsole, 'log').mockReturnValue(undefined); jest.spyOn(global, 'Date').mockImplementation(() => timestamp); jest.spyOn(process, 'uptime').mockReturnValue(10); system = new LoggingSystem(); diff --git a/packages/core/logging/core-logging-server-internal/tsconfig.json b/packages/core/logging/core-logging-server-internal/tsconfig.json index 43c80b0fcdcd5..de25a6b65bc5a 100644 --- a/packages/core/logging/core-logging-server-internal/tsconfig.json +++ b/packages/core/logging/core-logging-server-internal/tsconfig.json @@ -22,6 +22,7 @@ "@kbn/utility-types-jest", "@kbn/utility-types", "@kbn/ecs", + "@kbn/security-hardening", ], "exclude": [ "target/**/*", diff --git a/packages/core/status/core-status-common-internal/src/status.ts b/packages/core/status/core-status-common-internal/src/status.ts index 806a3ac56b407..0a3f3ad770fe8 100644 --- a/packages/core/status/core-status-common-internal/src/status.ts +++ b/packages/core/status/core-status-common-internal/src/status.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { BuildFlavor } from '@kbn/config'; import type { ServiceStatusLevelId, ServiceStatus, CoreStatus } from '@kbn/core-status-common'; import type { OpsMetrics } from '@kbn/core-metrics-server'; @@ -34,6 +35,7 @@ export interface ServerVersion { build_hash: string; build_number: number; build_snapshot: boolean; + build_flavor: BuildFlavor; build_date: string; } diff --git a/packages/core/status/core-status-common-internal/tsconfig.json b/packages/core/status/core-status-common-internal/tsconfig.json index c746e7133cd2c..7d31fa090eb0f 100644 --- a/packages/core/status/core-status-common-internal/tsconfig.json +++ b/packages/core/status/core-status-common-internal/tsconfig.json @@ -13,7 +13,8 @@ ], "kbn_references": [ "@kbn/core-status-common", - "@kbn/core-metrics-server" + "@kbn/core-metrics-server", + "@kbn/config" ], "exclude": [ "target/**/*", diff --git a/packages/core/status/core-status-server-internal/src/routes/status.ts b/packages/core/status/core-status-server-internal/src/routes/status.ts index 59c7aa23c51d4..1faa623467b58 100644 --- a/packages/core/status/core-status-server-internal/src/routes/status.ts +++ b/packages/core/status/core-status-server-internal/src/routes/status.ts @@ -156,7 +156,7 @@ const getFullStatusResponse = async ({ }; query: { v8format?: boolean; v7format?: boolean }; }): Promise => { - const { version, buildSha, buildNum, buildDate } = config.packageInfo; + const { version, buildSha, buildNum, buildDate, buildFlavor } = config.packageInfo; const versionWithoutSnapshot = version.replace(SNAPSHOT_POSTFIX, ''); let statusInfo: StatusInfo | LegacyStatusInfo; @@ -186,6 +186,7 @@ const getFullStatusResponse = async ({ build_hash: buildSha, build_number: buildNum, build_snapshot: SNAPSHOT_POSTFIX.test(version), + build_flavor: buildFlavor, build_date: buildDate.toISOString(), }, status: statusInfo, diff --git a/packages/deeplinks/observability/locators/logs_explorer.ts b/packages/deeplinks/observability/locators/logs_explorer.ts index 13ecd80165217..1442e7afe02cd 100644 --- a/packages/deeplinks/observability/locators/logs_explorer.ts +++ b/packages/deeplinks/observability/locators/logs_explorer.ts @@ -27,6 +27,22 @@ export type ListFilterControl = { export const LOGS_EXPLORER_LOCATOR_ID = 'LOGS_EXPLORER_LOCATOR'; +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type DocumentFieldGridColumnOptions = { + type: 'document-field'; + field: string; + width?: number; +}; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type SmartFieldGridColumnOptions = { + type: 'smart-field'; + smartField: 'content' | 'resource'; + width?: number; +}; + +export type GridColumnDisplayOptions = DocumentFieldGridColumnOptions | SmartFieldGridColumnOptions; + export interface LogsExplorerNavigationParams extends SerializableRecord { /** * Optionally set the time range in the time picker. @@ -43,7 +59,7 @@ export interface LogsExplorerNavigationParams extends SerializableRecord { /** * Columns displayed in the table */ - columns?: string[]; + columns?: GridColumnDisplayOptions[]; /** * Optionally apply free-form filters. */ diff --git a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts index 7fdfa8cd9d8ba..3ba3e3a20bb96 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/logs/index.ts @@ -36,6 +36,9 @@ export type LogDocument = Fields & 'cloud.availability_zone'?: string; 'cloud.project.id'?: string; 'cloud.instance.id'?: string; + 'error.stack_trace'?: string; + 'error.exception.stacktrace'?: string; + 'error.log.stacktrace'?: string; }>; class Log extends Serializable { diff --git a/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts b/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts index f6eb4d11c80fc..16e9f6c3be405 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/simple_logs.ts @@ -9,6 +9,9 @@ import { LogDocument, log, generateShortId, generateLongId } from '@kbn/apm-synt import { Scenario } from '../cli/scenario'; import { withClient } from '../lib/utils/with_client'; +const MORE_THAN_1024_CHARS = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; + const scenario: Scenario = async (runOptions) => { return { generate: ({ range, clients: { logsEsClient } }) => { @@ -111,6 +114,7 @@ const scenario: Scenario = async (runOptions) => { .defaults({ 'trace.id': generateShortId(), 'error.message': MESSAGE_LOG_LEVELS[index].message, + 'error.exception.stacktrace': 'Error message in error.exception.stacktrace', 'agent.name': 'nodejs', 'orchestrator.cluster.name': CLUSTER[index].clusterName, 'orchestrator.cluster.id': CLUSTER[index].clusterId, @@ -143,6 +147,7 @@ const scenario: Scenario = async (runOptions) => { .defaults({ 'trace.id': generateShortId(), 'event.original': MESSAGE_LOG_LEVELS[index].message, + 'error.log.stacktrace': 'Error message in error.log.stacktrace', 'agent.name': 'nodejs', 'orchestrator.cluster.name': CLUSTER[index].clusterName, 'orchestrator.cluster.id': CLUSTER[index].clusterId, @@ -186,6 +191,39 @@ const scenario: Scenario = async (runOptions) => { 'cloud.project.id': generateShortId(), 'cloud.instance.id': generateShortId(), 'log.file.path': `/logs/${generateLongId()}/error.txt`, + 'error.stack_trace': 'Error message in error.stack_trace', + }) + .timestamp(timestamp); + }); + }); + + const malformedDocs = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(3) + .fill(0) + .map(() => { + const index = Math.floor(Math.random() * 3); + return log + .create() + .message(MESSAGE_LOG_LEVELS[index].message) + .logLevel(MORE_THAN_1024_CHARS) + .service(SERVICE_NAMES[index]) + .defaults({ + 'trace.id': generateShortId(), + 'agent.name': 'nodejs', + 'orchestrator.cluster.name': CLUSTER[index].clusterName, + 'orchestrator.cluster.id': CLUSTER[index].clusterId, + 'orchestrator.namespace': CLUSTER[index].namespace, + 'container.name': `${SERVICE_NAMES[index]}-${generateShortId()}`, + 'orchestrator.resource.id': generateShortId(), + 'cloud.provider': CLOUD_PROVIDERS[Math.floor(Math.random() * 3)], + 'cloud.region': CLOUD_REGION[index], + 'cloud.availability_zone': MORE_THAN_1024_CHARS, + 'cloud.project.id': generateShortId(), + 'cloud.instance.id': generateShortId(), + 'log.file.path': `/logs/${generateLongId()}/error.txt`, }) .timestamp(timestamp); }); @@ -199,6 +237,7 @@ const scenario: Scenario = async (runOptions) => { logsWithErrorMessage, logsWithEventMessage, logsWithNoMessage, + malformedDocs, ]) ); }, diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 56308a980cc56..4d8c775710f09 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -1,198 +1,9 @@ { - "core-usage-stats": [], - "legacy-url-alias": [ - "disabled", - "resolveCounter", - "sourceId", - "targetId", - "targetNamespace", - "targetType" - ], - "config": [ - "buildNum" - ], - "config-global": [ - "buildNum" - ], - "url": [ - "accessDate", - "createDate", - "slug" - ], - "usage-counters": [ - "domainId" - ], - "task": [ - "attempts", - "enabled", - "ownerId", - "retryAt", - "runAt", - "schedule", - "schedule.interval", - "scheduledAt", - "scope", - "status", - "taskType" - ], - "guided-onboarding-guide-state": [ - "guideId", - "isActive" - ], - "guided-onboarding-plugin-state": [], - "ui-metric": [ - "count" - ], - "application_usage_totals": [], - "application_usage_daily": [ - "timestamp" - ], - "event_loop_delays_daily": [ - "lastUpdatedAt" - ], - "index-pattern": [ - "name", - "title", - "type" - ], - "sample-data-telemetry": [ - "installCount", - "unInstallCount" - ], - "space": [ - "name" - ], - "spaces-usage-stats": [], - "exception-list-agnostic": [ - "_tags", - "comments", - "comments.comment", - "comments.created_at", - "comments.created_by", - "comments.id", - "comments.updated_at", - "comments.updated_by", - "created_at", - "created_by", - "description", - "entries", - "entries.entries", - "entries.entries.field", - "entries.entries.operator", - "entries.entries.type", - "entries.entries.value", - "entries.field", - "entries.list", - "entries.list.id", - "entries.list.type", - "entries.operator", - "entries.type", - "entries.value", - "expire_time", - "immutable", - "item_id", - "list_id", - "list_type", - "meta", - "name", - "os_types", - "tags", - "tie_breaker_id", - "type", - "updated_by", - "version" - ], - "exception-list": [ - "_tags", - "comments", - "comments.comment", - "comments.created_at", - "comments.created_by", - "comments.id", - "comments.updated_at", - "comments.updated_by", - "created_at", - "created_by", - "description", - "entries", - "entries.entries", - "entries.entries.field", - "entries.entries.operator", - "entries.entries.type", - "entries.entries.value", - "entries.field", - "entries.list", - "entries.list.id", - "entries.list.type", - "entries.operator", - "entries.type", - "entries.value", - "expire_time", - "immutable", - "item_id", - "list_id", - "list_type", - "meta", - "name", - "os_types", - "tags", - "tie_breaker_id", - "type", - "updated_by", - "version" - ], - "telemetry": [], - "file": [ - "FileKind", - "Meta", - "Status", - "Updated", - "created", - "extension", - "hash", - "mime_type", - "name", - "size", - "user" - ], - "fileShare": [ - "created", - "name", - "token", - "valid_until" - ], "action": [ "actionTypeId", "name" ], "action_task_params": [], - "connector_token": [ - "connectorId", - "tokenType" - ], - "query": [ - "description", - "title" - ], - "kql-telemetry": [], - "search-session": [ - "created", - "realmName", - "realmType", - "sessionId", - "username" - ], - "search-telemetry": [], - "file-upload-usage-collection-telemetry": [ - "file_upload", - "file_upload.index_creation_count" - ], - "apm-indices": [], - "tag": [ - "color", - "description", - "name" - ], "alert": [ "actions", "actions.actionRef", @@ -265,34 +76,28 @@ "apiKeyId", "createdAt" ], - "rules-settings": [ - "flapping" - ], - "maintenance-window": [ - "enabled", - "events" + "apm-custom-dashboards": [ + "dashboardSavedObjectId", + "kuery", + "serviceEnvironmentFilterEnabled", + "serviceNameFilterEnabled" ], - "graph-workspace": [ - "description", - "kibanaSavedObjectMeta", - "kibanaSavedObjectMeta.searchSourceJSON", - "legacyIndexPatternRef", - "numLinks", - "numVertices", - "title", - "version", - "wsState" + "apm-indices": [], + "apm-server-schema": [ + "schemaJson" ], - "search": [ + "apm-service-group": [ + "color", "description", - "title" + "groupName", + "kuery" ], - "visualization": [ - "description", - "kibanaSavedObjectMeta", - "title", - "version" + "apm-telemetry": [], + "app_search_telemetry": [], + "application_usage_daily": [ + "timestamp" ], + "application_usage_totals": [], "canvas-element": [ "@created", "@timestamp", @@ -312,9 +117,128 @@ "tags", "template_key" ], - "event-annotation-group": [ + "cases": [ + "assignees", + "assignees.uid", + "category", + "closed_at", + "closed_by", + "closed_by.email", + "closed_by.full_name", + "closed_by.profile_uid", + "closed_by.username", + "connector", + "connector.fields", + "connector.fields.key", + "connector.fields.value", + "connector.name", + "connector.type", + "created_at", + "created_by", + "created_by.email", + "created_by.full_name", + "created_by.profile_uid", + "created_by.username", + "customFields", + "customFields.key", + "customFields.type", + "customFields.value", "description", - "title" + "duration", + "external_service", + "external_service.connector_name", + "external_service.external_id", + "external_service.external_title", + "external_service.external_url", + "external_service.pushed_at", + "external_service.pushed_by", + "external_service.pushed_by.email", + "external_service.pushed_by.full_name", + "external_service.pushed_by.profile_uid", + "external_service.pushed_by.username", + "owner", + "settings", + "settings.syncAlerts", + "severity", + "status", + "tags", + "title", + "total_alerts", + "total_comments", + "updated_at", + "updated_by", + "updated_by.email", + "updated_by.full_name", + "updated_by.profile_uid", + "updated_by.username" + ], + "cases-comments": [ + "actions", + "actions.type", + "alertId", + "comment", + "created_at", + "created_by", + "created_by.username", + "externalReferenceAttachmentTypeId", + "owner", + "persistableStateAttachmentTypeId", + "pushed_at", + "type", + "updated_at" + ], + "cases-configure": [ + "closure_type", + "created_at", + "owner" + ], + "cases-connector-mappings": [ + "owner" + ], + "cases-telemetry": [], + "cases-user-actions": [ + "action", + "created_at", + "created_by", + "created_by.username", + "owner", + "payload", + "payload.assignees", + "payload.assignees.uid", + "payload.comment", + "payload.comment.externalReferenceAttachmentTypeId", + "payload.comment.persistableStateAttachmentTypeId", + "payload.comment.type", + "payload.connector", + "payload.connector.type", + "type" + ], + "cloud-security-posture-settings": [ + "rules" + ], + "config": [ + "buildNum" + ], + "config-global": [ + "buildNum" + ], + "connector_token": [ + "connectorId", + "tokenType" + ], + "core-usage-stats": [], + "csp-rule-template": [ + "metadata", + "metadata.benchmark", + "metadata.benchmark.id", + "metadata.benchmark.name", + "metadata.benchmark.posture_type", + "metadata.benchmark.rule_number", + "metadata.benchmark.version", + "metadata.id", + "metadata.name", + "metadata.section", + "metadata.version" ], "dashboard": [ "controlGroupInput", @@ -339,139 +263,206 @@ "title", "version" ], - "links": [ - "description", - "links", - "title" + "endpoint:user-artifact-manifest": [ + "artifacts", + "schemaVersion" ], - "lens": [ + "enterprise_search_telemetry": [], + "epm-packages": [ + "es_index_patterns", + "experimental_data_stream_features", + "experimental_data_stream_features.data_stream", + "experimental_data_stream_features.features", + "experimental_data_stream_features.features.synthetic_source", + "experimental_data_stream_features.features.tsdb", + "install_format_schema_version", + "install_source", + "install_started_at", + "install_status", + "install_version", + "installed_es", + "installed_es.deferred", + "installed_es.id", + "installed_es.type", + "installed_es.version", + "installed_kibana", + "installed_kibana_space_id", + "internal", + "keep_policies_up_to_date", + "latest_install_failed_attempts", + "name", + "package_assets", + "verification_key_id", + "verification_status", + "version" + ], + "epm-packages-assets": [ + "asset_path", + "data_base64", + "data_utf8", + "install_source", + "media_type", + "package_name", + "package_version" + ], + "event-annotation-group": [ "description", - "state", - "title", - "visualizationType" + "title" ], - "lens-ui-telemetry": [ - "count", - "date", - "name", - "type" + "event_loop_delays_daily": [ + "lastUpdatedAt" ], - "map": [ - "bounds", + "exception-list": [ + "_tags", + "comments", + "comments.comment", + "comments.created_at", + "comments.created_by", + "comments.id", + "comments.updated_at", + "comments.updated_by", + "created_at", + "created_by", "description", - "layerListJSON", - "mapStateJSON", - "title", - "uiStateJSON", + "entries", + "entries.entries", + "entries.entries.field", + "entries.entries.operator", + "entries.entries.type", + "entries.entries.value", + "entries.field", + "entries.list", + "entries.list.id", + "entries.list.type", + "entries.operator", + "entries.type", + "entries.value", + "expire_time", + "immutable", + "item_id", + "list_id", + "list_type", + "meta", + "name", + "os_types", + "tags", + "tie_breaker_id", + "type", + "updated_by", "version" ], - "cases-comments": [ - "actions", - "actions.type", - "alertId", - "comment", + "exception-list-agnostic": [ + "_tags", + "comments", + "comments.comment", + "comments.created_at", + "comments.created_by", + "comments.id", + "comments.updated_at", + "comments.updated_by", "created_at", "created_by", - "created_by.username", - "externalReferenceAttachmentTypeId", - "owner", - "persistableStateAttachmentTypeId", - "pushed_at", + "description", + "entries", + "entries.entries", + "entries.entries.field", + "entries.entries.operator", + "entries.entries.type", + "entries.entries.value", + "entries.field", + "entries.list", + "entries.list.id", + "entries.list.type", + "entries.operator", + "entries.type", + "entries.value", + "expire_time", + "immutable", + "item_id", + "list_id", + "list_type", + "meta", + "name", + "os_types", + "tags", + "tie_breaker_id", "type", - "updated_at" + "updated_by", + "version" ], - "cases-configure": [ - "closure_type", - "created_at", - "owner" + "file": [ + "FileKind", + "Meta", + "Status", + "Updated", + "created", + "extension", + "hash", + "mime_type", + "name", + "size", + "user" ], - "cases-connector-mappings": [ - "owner" + "file-upload-usage-collection-telemetry": [ + "file_upload", + "file_upload.index_creation_count" ], - "cases": [ - "assignees", - "assignees.uid", - "category", - "closed_at", - "closed_by", - "closed_by.email", - "closed_by.full_name", - "closed_by.profile_uid", - "closed_by.username", - "connector", - "connector.fields", - "connector.fields.key", - "connector.fields.value", - "connector.name", - "connector.type", - "created_at", - "created_by", - "created_by.email", - "created_by.full_name", - "created_by.profile_uid", - "created_by.username", - "customFields", - "customFields.key", - "customFields.type", - "customFields.value", + "fileShare": [ + "created", + "name", + "token", + "valid_until" + ], + "fleet-fleet-server-host": [ + "host_urls", + "is_default", + "is_internal", + "is_preconfigured", + "name", + "proxy_id" + ], + "fleet-message-signing-keys": [], + "fleet-preconfiguration-deletion-record": [ + "id" + ], + "fleet-proxy": [ + "certificate", + "certificate_authorities", + "certificate_key", + "is_preconfigured", + "name", + "proxy_headers", + "url" + ], + "fleet-uninstall-tokens": [ + "policy_id", + "token_plain" + ], + "graph-workspace": [ "description", - "duration", - "external_service", - "external_service.connector_name", - "external_service.external_id", - "external_service.external_title", - "external_service.external_url", - "external_service.pushed_at", - "external_service.pushed_by", - "external_service.pushed_by.email", - "external_service.pushed_by.full_name", - "external_service.pushed_by.profile_uid", - "external_service.pushed_by.username", - "owner", - "settings", - "settings.syncAlerts", - "severity", - "status", - "tags", + "kibanaSavedObjectMeta", + "kibanaSavedObjectMeta.searchSourceJSON", + "legacyIndexPatternRef", + "numLinks", + "numVertices", "title", - "total_alerts", - "total_comments", - "updated_at", - "updated_by", - "updated_by.email", - "updated_by.full_name", - "updated_by.profile_uid", - "updated_by.username" + "version", + "wsState" ], - "cases-user-actions": [ - "action", - "created_at", - "created_by", - "created_by.username", - "owner", - "payload", - "payload.assignees", - "payload.assignees.uid", - "payload.comment", - "payload.comment.externalReferenceAttachmentTypeId", - "payload.comment.persistableStateAttachmentTypeId", - "payload.comment.type", - "payload.connector", - "payload.connector.type", + "guided-onboarding-guide-state": [ + "guideId", + "isActive" + ], + "guided-onboarding-plugin-state": [], + "index-pattern": [ + "name", + "title", "type" ], - "cases-telemetry": [], "infrastructure-monitoring-log-view": [ "name" ], - "metrics-data-source": [], - "ingest_manager_settings": [ - "fleet_server_hosts", - "has_seen_add_data_notice", - "output_secret_storage_requirements_met", - "prerelease_integrations_enabled", - "secret_storage_requirements_met" - ], + "infrastructure-ui-source": [], "ingest-agent-policies": [ "agent_features", "agent_features.enabled", @@ -499,6 +490,13 @@ "updated_at", "updated_by" ], + "ingest-download-sources": [ + "host", + "is_default", + "name", + "proxy_id", + "source_id" + ], "ingest-outputs": [ "allow_edit", "auth_type", @@ -582,92 +580,89 @@ "updated_by", "vars" ], - "epm-packages": [ - "es_index_patterns", - "experimental_data_stream_features", - "experimental_data_stream_features.data_stream", - "experimental_data_stream_features.features", - "experimental_data_stream_features.features.synthetic_source", - "experimental_data_stream_features.features.tsdb", - "install_format_schema_version", - "install_source", - "install_started_at", - "install_status", - "install_version", - "installed_es", - "installed_es.deferred", - "installed_es.id", - "installed_es.type", - "installed_es.version", - "installed_kibana", - "installed_kibana_space_id", - "internal", - "keep_policies_up_to_date", - "latest_install_failed_attempts", - "name", - "package_assets", - "verification_key_id", - "verification_status", - "version" + "ingest_manager_settings": [ + "fleet_server_hosts", + "has_seen_add_data_notice", + "output_secret_storage_requirements_met", + "prerelease_integrations_enabled", + "secret_storage_requirements_met" ], - "epm-packages-assets": [ - "asset_path", - "data_base64", - "data_utf8", - "install_source", - "media_type", - "package_name", - "package_version" + "inventory-view": [], + "kql-telemetry": [], + "legacy-url-alias": [ + "disabled", + "resolveCounter", + "sourceId", + "targetId", + "targetNamespace", + "targetType" ], - "fleet-preconfiguration-deletion-record": [ - "id" + "lens": [ + "description", + "state", + "title", + "visualizationType" ], - "ingest-download-sources": [ - "host", - "is_default", + "lens-ui-telemetry": [ + "count", + "date", "name", - "proxy_id", - "source_id" + "type" ], - "fleet-fleet-server-host": [ - "host_urls", - "is_default", - "is_internal", - "is_preconfigured", - "name", - "proxy_id" + "links": [ + "description", + "links", + "title" ], - "fleet-proxy": [ - "certificate", - "certificate_authorities", - "certificate_key", - "is_preconfigured", - "name", - "proxy_headers", - "url" + "maintenance-window": [ + "enabled", + "events" ], - "fleet-message-signing-keys": [], - "fleet-uninstall-tokens": [ - "policy_id", - "token_plain" + "map": [ + "bounds", + "description", + "layerListJSON", + "mapStateJSON", + "title", + "uiStateJSON", + "version" ], - "osquery-manager-usage-metric": [ - "count", - "errors" + "metrics-data-source": [], + "metrics-explorer-view": [], + "ml-job": [ + "datafeed_id", + "job_id", + "type" ], - "osquery-saved-query": [ - "created_at", - "created_by", + "ml-module": [ + "datafeeds", + "defaultIndexPattern", "description", - "ecs_mapping", "id", - "interval", - "platform", + "jobs", + "logo", "query", - "timeout", - "updated_at", - "updated_by", - "version" + "tags", + "title", + "type" + ], + "ml-trained-model": [ + "job", + "job.create_time", + "job.job_id", + "model_id" + ], + "monitoring-telemetry": [ + "reportedClusterUuids" + ], + "observability-onboarding-state": [ + "progress", + "state", + "type" + ], + "osquery-manager-usage-metric": [ + "count", + "errors" ], "osquery-pack": [ "created_at", @@ -702,122 +697,65 @@ "shards", "version" ], - "csp-rule-template": [ - "metadata", - "metadata.benchmark", - "metadata.benchmark.id", - "metadata.benchmark.name", - "metadata.benchmark.posture_type", - "metadata.benchmark.rule_number", - "metadata.benchmark.version", - "metadata.id", - "metadata.name", - "metadata.section", - "metadata.version" - ], - "slo": [ - "budgetingMethod", + "osquery-saved-query": [ + "created_at", + "created_by", "description", - "enabled", + "ecs_mapping", "id", - "indicator", - "indicator.params", - "indicator.type", - "name", - "tags", + "interval", + "platform", + "query", + "timeout", + "updated_at", + "updated_by", "version" ], - "threshold-explorer-view": [], - "observability-onboarding-state": [ - "progress", - "state", - "type" - ], - "ml-job": [ - "datafeed_id", - "job_id", - "type" - ], - "ml-trained-model": [ - "job", - "job.create_time", - "job.job_id", - "model_id" + "policy-settings-protection-updates-note": [ + "note" ], - "ml-module": [ - "datafeeds", - "defaultIndexPattern", + "query": [ "description", - "id", - "jobs", - "logo", - "query", - "tags", - "title", - "type" + "title" ], - "uptime-dynamic-settings": [], - "synthetics-privates-locations": [], - "synthetics-monitor": [ - "alert", - "alert.status", - "alert.status.enabled", - "alert.tls", - "alert.tls.enabled", - "custom_heartbeat_id", + "risk-engine-configuration": [ + "dataViewId", "enabled", - "hash", - "hosts", - "id", - "journey_id", - "locations", - "locations.id", - "locations.label", - "name", - "origin", - "project_id", - "schedule", - "schedule.number", - "tags", - "throttling", - "throttling.label", - "type", - "urls" - ], - "uptime-synthetics-api-key": [ - "apiKey" + "filter", + "identifierType", + "interval", + "pageSize", + "range", + "range.end", + "range.start" ], - "synthetics-param": [], - "infrastructure-ui-source": [], - "inventory-view": [], - "metrics-explorer-view": [], - "upgrade-assistant-reindex-operation": [ - "indexName", - "status" + "rules-settings": [ + "flapping" ], - "upgrade-assistant-ml-upgrade-operation": [ - "snapshotId" + "sample-data-telemetry": [ + "installCount", + "unInstallCount" ], - "monitoring-telemetry": [ - "reportedClusterUuids" + "search": [ + "description", + "title" ], - "enterprise_search_telemetry": [], - "app_search_telemetry": [], - "workplace_search_telemetry": [], - "siem-ui-timeline-note": [ + "search-session": [ "created", - "createdBy", - "eventId", - "note", - "updated", - "updatedBy" + "realmName", + "realmType", + "sessionId", + "username" ], - "siem-ui-timeline-pinned-event": [ - "created", - "createdBy", - "eventId", + "search-telemetry": [], + "security-rule": [ + "rule_id", + "version" + ], + "security-solution-signals-migration": [ + "sourceIndex", "updated", - "updatedBy" + "version" ], "siem-detection-engine-rule-actions": [ "actions", @@ -830,10 +768,6 @@ "ruleAlertId", "ruleThrottle" ], - "security-rule": [ - "rule_id", - "version" - ], "siem-ui-timeline": [ "columns", "columns.aggregatable", @@ -933,46 +867,112 @@ "updated", "updatedBy" ], - "endpoint:user-artifact-manifest": [ - "artifacts", - "schemaVersion" + "siem-ui-timeline-note": [ + "created", + "createdBy", + "eventId", + "note", + "updated", + "updatedBy" ], - "security-solution-signals-migration": [ - "sourceIndex", + "siem-ui-timeline-pinned-event": [ + "created", + "createdBy", + "eventId", "updated", - "version" + "updatedBy" ], - "risk-engine-configuration": [ - "dataViewId", + "slo": [ + "budgetingMethod", + "description", "enabled", - "filter", - "identifierType", - "interval", - "pageSize", - "range", - "range.end", - "range.start" + "id", + "indicator", + "indicator.params", + "indicator.type", + "name", + "tags", + "version" ], - "policy-settings-protection-updates-note": [ - "note" + "space": [ + "name" ], - "apm-telemetry": [], - "apm-server-schema": [ - "schemaJson" + "spaces-usage-stats": [], + "synthetics-monitor": [ + "alert", + "alert.status", + "alert.status.enabled", + "alert.tls", + "alert.tls.enabled", + "custom_heartbeat_id", + "enabled", + "hash", + "hosts", + "id", + "journey_id", + "locations", + "locations.id", + "locations.label", + "name", + "origin", + "project_id", + "schedule", + "schedule.number", + "tags", + "throttling", + "throttling.label", + "type", + "urls" ], - "apm-service-group": [ + "synthetics-param": [], + "synthetics-privates-locations": [], + "tag": [ "color", "description", - "groupName", - "kuery" + "name" ], - "apm-custom-dashboards": [ - "dashboardSavedObjectId", - "kuery", - "serviceEnvironmentFilterEnabled", - "serviceNameFilterEnabled" + "task": [ + "attempts", + "enabled", + "ownerId", + "retryAt", + "runAt", + "schedule", + "schedule.interval", + "scheduledAt", + "scope", + "status", + "taskType" ], - "cloud-security-posture-settings": [ - "rules" - ] + "telemetry": [], + "threshold-explorer-view": [], + "ui-metric": [ + "count" + ], + "upgrade-assistant-ml-upgrade-operation": [ + "snapshotId" + ], + "upgrade-assistant-reindex-operation": [ + "indexName", + "status" + ], + "uptime-dynamic-settings": [], + "uptime-synthetics-api-key": [ + "apiKey" + ], + "url": [ + "accessDate", + "createDate", + "slug" + ], + "usage-counters": [ + "domainId" + ], + "visualization": [ + "description", + "kibanaSavedObjectMeta", + "title", + "version" + ], + "workplace_search_telemetry": [] } diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 4768e2605bb0b..758fde639d00f 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -1,1054 +1,907 @@ { - "core-usage-stats": { - "dynamic": false, - "properties": {} - }, - "legacy-url-alias": { + "action": { "dynamic": false, "properties": { - "sourceId": { - "type": "keyword" - }, - "targetNamespace": { - "type": "keyword" - }, - "targetType": { - "type": "keyword" - }, - "targetId": { + "actionTypeId": { "type": "keyword" }, - "resolveCounter": { - "type": "long" - }, - "disabled": { - "type": "boolean" - } - } - }, - "config": { - "dynamic": false, - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "config-global": { - "dynamic": false, - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "url": { - "dynamic": false, - "properties": { - "slug": { - "type": "text", + "name": { "fields": { "keyword": { "type": "keyword" } - } - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" + }, + "type": "text" } } }, - "usage-counters": { + "action_task_params": { "dynamic": false, - "properties": { - "domainId": { - "type": "keyword" - } - } + "properties": {} }, - "task": { + "alert": { "dynamic": false, "properties": { - "taskType": { - "type": "keyword" - }, - "scheduledAt": { - "type": "date" - }, - "runAt": { - "type": "date" - }, - "retryAt": { - "type": "date" - }, - "enabled": { - "type": "boolean" - }, - "schedule": { + "actions": { + "dynamic": false, "properties": { - "interval": { + "actionRef": { "type": "keyword" - } - } - }, - "attempts": { - "type": "integer" - }, - "status": { - "type": "keyword" - }, - "scope": { - "type": "keyword" - }, - "ownerId": { - "type": "keyword" - } - } - }, - "guided-onboarding-guide-state": { - "dynamic": false, - "properties": { - "guideId": { - "type": "keyword" - }, - "isActive": { - "type": "boolean" - } - } - }, - "guided-onboarding-plugin-state": { - "dynamic": false, - "properties": {} - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "application_usage_totals": { - "dynamic": false, - "properties": {} - }, - "application_usage_daily": { - "dynamic": false, - "properties": { - "timestamp": { - "type": "date" - } - } - }, - "event_loop_delays_daily": { - "dynamic": false, - "properties": { - "lastUpdatedAt": { - "type": "date" - } - } - }, - "index-pattern": { - "dynamic": false, - "properties": { - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "name": { - "type": "text", - "fields": { - "keyword": { + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { "type": "keyword" - } - } - } - } - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "space": { - "dynamic": false, - "properties": { - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "spaces-usage-stats": { - "dynamic": false, - "properties": {} - }, - "exception-list-agnostic": { - "properties": { - "_tags": { - "type": "keyword" - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "immutable": { - "type": "boolean" - }, - "list_id": { - "type": "keyword" - }, - "list_type": { - "type": "keyword" - }, - "meta": { - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "type": "text" } }, - "type": "keyword" + "type": "nested" }, - "tags": { - "fields": { - "text": { - "type": "text" - } - }, + "alertTypeId": { "type": "keyword" }, - "tie_breaker_id": { + "consumer": { "type": "keyword" }, - "type": { - "type": "keyword" + "createdAt": { + "type": "date" }, - "updated_by": { + "createdBy": { "type": "keyword" }, - "version": { - "type": "keyword" + "enabled": { + "type": "boolean" }, - "comments": { + "executionStatus": { "properties": { - "comment": { - "type": "keyword" + "error": { + "properties": { + "message": { + "type": "keyword" + }, + "reason": { + "type": "keyword" + } + } }, - "created_at": { - "type": "keyword" + "lastDuration": { + "type": "long" }, - "created_by": { - "type": "keyword" + "lastExecutionDate": { + "type": "date" }, - "id": { - "type": "keyword" + "numberOfTriggeredActions": { + "type": "long" }, - "updated_at": { + "status": { "type": "keyword" }, - "updated_by": { - "type": "keyword" - } - } - }, - "entries": { - "properties": { - "entries": { + "warning": { "properties": { - "field": { - "type": "keyword" - }, - "operator": { - "type": "keyword" - }, - "type": { + "message": { "type": "keyword" }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, + "reason": { "type": "keyword" } } - }, - "field": { - "type": "keyword" - }, - "list": { + } + } + }, + "lastRun": { + "properties": { + "alertsCount": { "properties": { - "id": { - "type": "keyword" + "active": { + "type": "float" }, - "type": { - "type": "keyword" + "ignored": { + "type": "float" + }, + "new": { + "type": "float" + }, + "recovered": { + "type": "float" } } }, - "operator": { - "type": "keyword" - }, - "type": { + "outcome": { "type": "keyword" }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" + "outcomeOrder": { + "type": "float" } } }, - "expire_time": { - "type": "date" - }, - "item_id": { - "type": "keyword" - }, - "os_types": { - "type": "keyword" - } - } - }, - "exception-list": { - "properties": { - "_tags": { - "type": "keyword" - }, - "created_at": { + "legacyId": { "type": "keyword" }, - "created_by": { - "type": "keyword" + "mapped_params": { + "properties": { + "risk_score": { + "type": "float" + }, + "severity": { + "type": "keyword" + } + } }, - "description": { - "type": "keyword" + "monitoring": { + "properties": { + "run": { + "properties": { + "calculated_metrics": { + "properties": { + "p50": { + "type": "long" + }, + "p95": { + "type": "long" + }, + "p99": { + "type": "long" + }, + "success_ratio": { + "type": "float" + } + } + }, + "last_run": { + "properties": { + "metrics": { + "properties": { + "duration": { + "type": "long" + }, + "gap_duration_s": { + "type": "float" + }, + "total_alerts_created": { + "type": "float" + }, + "total_alerts_detected": { + "type": "float" + }, + "total_indexing_duration_ms": { + "type": "long" + }, + "total_search_duration_ms": { + "type": "long" + } + } + }, + "timestamp": { + "type": "date" + } + } + } + } + } + } }, - "immutable": { + "muteAll": { "type": "boolean" }, - "list_id": { - "type": "keyword" - }, - "list_type": { - "type": "keyword" - }, - "meta": { + "mutedInstanceIds": { "type": "keyword" }, "name": { "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - }, - "tags": { - "fields": { - "text": { - "type": "text" + "keyword": { + "normalizer": "lowercase", + "type": "keyword" } }, - "type": "keyword" + "type": "text" }, - "tie_breaker_id": { + "notifyWhen": { "type": "keyword" }, - "type": { - "type": "keyword" + "params": { + "ignore_above": 4096, + "type": "flattened" }, - "updated_by": { - "type": "keyword" + "revision": { + "type": "long" }, - "version": { - "type": "keyword" + "running": { + "type": "boolean" }, - "comments": { + "schedule": { "properties": { - "comment": { - "type": "keyword" - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "updated_at": { - "type": "keyword" - }, - "updated_by": { + "interval": { "type": "keyword" } } }, - "entries": { + "scheduledTaskId": { + "type": "keyword" + }, + "snoozeSchedule": { "properties": { - "entries": { - "properties": { - "field": { - "type": "keyword" - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "field": { - "type": "keyword" - }, - "list": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "operator": { - "type": "keyword" + "duration": { + "type": "long" }, - "type": { + "id": { "type": "keyword" }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" + "skipRecurrences": { + "format": "strict_date_time", + "type": "date" } - } - }, - "expire_time": { - "type": "date" + }, + "type": "nested" }, - "item_id": { + "tags": { "type": "keyword" }, - "os_types": { + "throttle": { "type": "keyword" - } - } - }, - "telemetry": { - "dynamic": false, - "properties": {} - }, - "file": { - "dynamic": false, - "properties": { - "created": { - "type": "date" }, - "Updated": { + "updatedAt": { "type": "date" }, - "name": { - "type": "text" - }, - "user": { - "type": "flattened" - }, - "Status": { - "type": "keyword" - }, - "mime_type": { - "type": "keyword" - }, - "extension": { - "type": "keyword" - }, - "size": { - "type": "long" - }, - "Meta": { - "type": "flattened" - }, - "FileKind": { + "updatedBy": { "type": "keyword" - }, - "hash": { - "dynamic": false, - "properties": {} } } }, - "fileShare": { - "dynamic": false, + "api_key_pending_invalidation": { "properties": { - "created": { - "type": "date" - }, - "valid_until": { - "type": "long" - }, - "token": { + "apiKeyId": { "type": "keyword" }, - "name": { - "type": "keyword" + "createdAt": { + "type": "date" } } }, - "action": { - "dynamic": false, + "apm-custom-dashboards": { "properties": { - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - }, - "actionTypeId": { + "dashboardSavedObjectId": { "type": "keyword" + }, + "kuery": { + "type": "text" + }, + "serviceEnvironmentFilterEnabled": { + "type": "boolean" + }, + "serviceNameFilterEnabled": { + "type": "boolean" } } }, - "action_task_params": { + "apm-indices": { "dynamic": false, "properties": {} }, - "connector_token": { - "dynamic": false, + "apm-server-schema": { "properties": { - "connectorId": { - "type": "keyword" - }, - "tokenType": { - "type": "keyword" + "schemaJson": { + "index": false, + "type": "text" } } }, - "query": { - "dynamic": false, + "apm-service-group": { "properties": { - "title": { + "color": { "type": "text" }, "description": { "type": "text" + }, + "groupName": { + "type": "keyword" + }, + "kuery": { + "type": "text" } } }, - "kql-telemetry": { + "apm-telemetry": { "dynamic": false, "properties": {} }, - "search-session": { + "app_search_telemetry": { "dynamic": false, - "properties": { - "sessionId": { - "type": "keyword" - }, - "created": { - "type": "date" - }, - "realmType": { - "type": "keyword" - }, - "realmName": { - "type": "keyword" - }, - "username": { - "type": "keyword" + "properties": {} + }, + "application_usage_daily": { + "dynamic": false, + "properties": { + "timestamp": { + "type": "date" } } }, - "search-telemetry": { + "application_usage_totals": { "dynamic": false, "properties": {} }, - "file-upload-usage-collection-telemetry": { + "canvas-element": { + "dynamic": false, "properties": { - "file_upload": { - "properties": { - "index_creation_count": { - "type": "long" + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" } - } + }, + "type": "text" } } }, - "apm-indices": { + "canvas-workpad": { "dynamic": false, - "properties": {} - }, - "tag": { "properties": { - "name": { - "type": "text" + "@created": { + "type": "date" }, - "description": { - "type": "text" + "@timestamp": { + "type": "date" }, - "color": { + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, "type": "text" } } }, - "alert": { + "canvas-workpad-template": { "dynamic": false, "properties": { - "enabled": { - "type": "boolean" + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" }, "name": { - "type": "text", "fields": { "keyword": { - "type": "keyword", - "normalizer": "lowercase" + "type": "keyword" } - } + }, + "type": "text" }, "tags": { - "type": "keyword" + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" }, - "alertTypeId": { + "template_key": { "type": "keyword" - }, - "schedule": { + } + } + }, + "cases": { + "dynamic": false, + "properties": { + "assignees": { "properties": { - "interval": { + "uid": { "type": "keyword" } } }, - "consumer": { + "category": { "type": "keyword" }, - "legacyId": { - "type": "keyword" + "closed_at": { + "type": "date" }, - "actions": { - "dynamic": false, - "type": "nested", + "closed_by": { "properties": { - "group": { + "email": { "type": "keyword" }, - "actionRef": { + "full_name": { "type": "keyword" }, - "actionTypeId": { + "profile_uid": { "type": "keyword" - } - } - }, - "params": { - "type": "flattened", - "ignore_above": 4096 - }, - "mapped_params": { - "properties": { - "risk_score": { - "type": "float" }, - "severity": { + "username": { "type": "keyword" } } }, - "scheduledTaskId": { - "type": "keyword" - }, - "createdBy": { - "type": "keyword" - }, - "updatedBy": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - }, - "updatedAt": { - "type": "date" - }, - "throttle": { - "type": "keyword" - }, - "notifyWhen": { - "type": "keyword" - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "monitoring": { + "connector": { "properties": { - "run": { + "fields": { "properties": { - "calculated_metrics": { - "properties": { - "p50": { - "type": "long" - }, - "p95": { - "type": "long" - }, - "p99": { - "type": "long" - }, - "success_ratio": { - "type": "float" - } - } + "key": { + "type": "text" }, - "last_run": { - "properties": { - "timestamp": { - "type": "date" - }, - "metrics": { - "properties": { - "duration": { - "type": "long" - }, - "total_search_duration_ms": { - "type": "long" - }, - "total_indexing_duration_ms": { - "type": "long" - }, - "total_alerts_detected": { - "type": "float" - }, - "total_alerts_created": { - "type": "float" - }, - "gap_duration_s": { - "type": "float" - } - } - } - } + "value": { + "type": "text" } } + }, + "name": { + "type": "text" + }, + "type": { + "type": "keyword" } } }, - "revision": { - "type": "long" + "created_at": { + "type": "date" }, - "snoozeSchedule": { - "type": "nested", + "created_by": { "properties": { - "id": { + "email": { "type": "keyword" }, - "duration": { - "type": "long" + "full_name": { + "type": "keyword" }, - "skipRecurrences": { - "type": "date", - "format": "strict_date_time" + "profile_uid": { + "type": "keyword" + }, + "username": { + "type": "keyword" } } }, - "executionStatus": { + "customFields": { "properties": { - "numberOfTriggeredActions": { - "type": "long" + "key": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "boolean": { + "ignore_malformed": true, + "type": "boolean" + }, + "date": { + "ignore_malformed": true, + "type": "date" + }, + "ip": { + "ignore_malformed": true, + "type": "ip" + }, + "number": { + "ignore_malformed": true, + "type": "long" + }, + "string": { + "type": "text" + } + }, + "type": "keyword" + } + }, + "type": "nested" + }, + "description": { + "type": "text" + }, + "duration": { + "type": "unsigned_long" + }, + "external_service": { + "properties": { + "connector_name": { + "type": "keyword" }, - "status": { + "external_id": { "type": "keyword" }, - "lastExecutionDate": { - "type": "date" + "external_title": { + "type": "text" }, - "lastDuration": { - "type": "long" + "external_url": { + "type": "text" }, - "error": { + "pushed_at": { + "type": "date" + }, + "pushed_by": { "properties": { - "reason": { + "email": { "type": "keyword" }, - "message": { + "full_name": { "type": "keyword" - } - } - }, - "warning": { - "properties": { - "reason": { + }, + "profile_uid": { "type": "keyword" }, - "message": { + "username": { "type": "keyword" } } } } }, - "lastRun": { + "owner": { + "type": "keyword" + }, + "settings": { "properties": { - "outcome": { + "syncAlerts": { + "type": "boolean" + } + } + }, + "severity": { + "type": "short" + }, + "status": { + "type": "short" + }, + "tags": { + "type": "keyword" + }, + "title": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "total_alerts": { + "type": "integer" + }, + "total_comments": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { "type": "keyword" }, - "outcomeOrder": { - "type": "float" + "full_name": { + "type": "keyword" }, - "alertsCount": { - "properties": { - "active": { - "type": "float" - }, - "new": { - "type": "float" - }, - "recovered": { - "type": "float" - }, - "ignored": { - "type": "float" - } - } + "profile_uid": { + "type": "keyword" + }, + "username": { + "type": "keyword" } } - }, - "running": { - "type": "boolean" } } }, - "api_key_pending_invalidation": { + "cases-comments": { + "dynamic": false, "properties": { - "apiKeyId": { + "actions": { + "properties": { + "type": { + "type": "keyword" + } + } + }, + "alertId": { "type": "keyword" }, - "createdAt": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "username": { + "type": "keyword" + } + } + }, + "externalReferenceAttachmentTypeId": { + "type": "keyword" + }, + "owner": { + "type": "keyword" + }, + "persistableStateAttachmentTypeId": { + "type": "keyword" + }, + "pushed_at": { + "type": "date" + }, + "type": { + "type": "keyword" + }, + "updated_at": { "type": "date" } } }, - "rules-settings": { + "cases-configure": { "dynamic": false, "properties": { - "flapping": { - "properties": {} + "closure_type": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "owner": { + "type": "keyword" } } }, - "maintenance-window": { + "cases-connector-mappings": { "dynamic": false, "properties": { - "enabled": { - "type": "boolean" - }, - "events": { - "type": "date_range", - "format": "epoch_millis||strict_date_optional_time" + "owner": { + "type": "keyword" } } }, - "graph-workspace": { + "cases-telemetry": { + "dynamic": false, + "properties": {} + }, + "cases-user-actions": { + "dynamic": false, "properties": { - "description": { - "type": "text" + "action": { + "type": "keyword" }, - "kibanaSavedObjectMeta": { + "created_at": { + "type": "date" + }, + "created_by": { "properties": { - "searchSourceJSON": { - "type": "text" + "username": { + "type": "keyword" } } }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" + "owner": { + "type": "keyword" }, - "wsState": { - "type": "text" + "payload": { + "dynamic": false, + "properties": { + "assignees": { + "properties": { + "uid": { + "type": "keyword" + } + } + }, + "comment": { + "properties": { + "externalReferenceAttachmentTypeId": { + "type": "keyword" + }, + "persistableStateAttachmentTypeId": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "connector": { + "properties": { + "type": { + "type": "keyword" + } + } + } + } }, - "legacyIndexPatternRef": { - "type": "text", - "index": false + "type": { + "type": "keyword" } } }, - "search": { + "cloud-security-posture-settings": { + "dynamic": false, + "properties": {} + }, + "config": { "dynamic": false, "properties": { - "title": { - "type": "text" - }, - "description": { - "type": "text" + "buildNum": { + "type": "keyword" } } }, - "visualization": { + "config-global": { "dynamic": false, "properties": { - "description": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": {} + "buildNum": { + "type": "keyword" } } }, - "event-annotation-group": { + "connector_token": { "dynamic": false, "properties": { - "title": { - "type": "text" + "connectorId": { + "type": "keyword" }, - "description": { - "type": "text" + "tokenType": { + "type": "keyword" + } + } + }, + "core-usage-stats": { + "dynamic": false, + "properties": {} + }, + "csp-rule-template": { + "dynamic": false, + "properties": { + "metadata": { + "properties": { + "benchmark": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "posture_type": { + "type": "keyword" + }, + "rule_number": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + }, + "type": "object" + }, + "id": { + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "section": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "version": { + "type": "keyword" + } + }, + "type": "object" } } }, "dashboard": { "properties": { + "controlGroupInput": { + "properties": { + "chainingSystem": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "controlStyle": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "ignoreParentSettingsJSON": { + "index": false, + "type": "text" + }, + "panelsJSON": { + "index": false, + "type": "text" + } + } + }, "description": { "type": "text" }, "hits": { - "type": "integer", + "doc_values": false, "index": false, - "doc_values": false + "type": "integer" }, "kibanaSavedObjectMeta": { "properties": { "searchSourceJSON": { - "type": "text", - "index": false + "index": false, + "type": "text" } } }, "optionsJSON": { - "type": "text", - "index": false + "index": false, + "type": "text" }, "panelsJSON": { - "type": "text", - "index": false + "index": false, + "type": "text" }, "refreshInterval": { "properties": { "display": { - "type": "keyword", + "doc_values": false, "index": false, - "doc_values": false + "type": "keyword" }, "pause": { - "type": "boolean", + "doc_values": false, "index": false, - "doc_values": false + "type": "boolean" }, "section": { - "type": "integer", + "doc_values": false, "index": false, - "doc_values": false + "type": "integer" }, "value": { - "type": "integer", - "index": false, - "doc_values": false - } - } - }, - "controlGroupInput": { - "properties": { - "controlStyle": { - "type": "keyword", - "index": false, - "doc_values": false - }, - "chainingSystem": { - "type": "keyword", + "doc_values": false, "index": false, - "doc_values": false - }, - "panelsJSON": { - "type": "text", - "index": false - }, - "ignoreParentSettingsJSON": { - "type": "text", - "index": false + "type": "integer" } } }, "timeFrom": { - "type": "keyword", + "doc_values": false, "index": false, - "doc_values": false + "type": "keyword" }, "timeRestore": { - "type": "boolean", + "doc_values": false, "index": false, - "doc_values": false + "type": "boolean" }, "timeTo": { - "type": "keyword", + "doc_values": false, "index": false, - "doc_values": false + "type": "keyword" }, "title": { "type": "text" @@ -1058,536 +911,686 @@ } } }, - "links": { + "endpoint:user-artifact-manifest": { "dynamic": false, "properties": { - "title": { - "type": "text" - }, - "description": { - "type": "text" + "artifacts": { + "type": "nested" }, - "links": { - "dynamic": false, - "properties": {} + "schemaVersion": { + "type": "keyword" } } }, - "lens": { + "enterprise_search_telemetry": { + "dynamic": false, + "properties": {} + }, + "epm-packages": { "properties": { - "title": { - "type": "text" + "es_index_patterns": { + "dynamic": false, + "properties": {} }, - "description": { - "type": "text" + "experimental_data_stream_features": { + "properties": { + "data_stream": { + "type": "keyword" + }, + "features": { + "dynamic": false, + "properties": { + "synthetic_source": { + "type": "boolean" + }, + "tsdb": { + "type": "boolean" + } + }, + "type": "nested" + } + }, + "type": "nested" }, - "visualizationType": { + "install_format_schema_version": { + "type": "version" + }, + "install_source": { "type": "keyword" }, - "state": { - "dynamic": false, - "properties": {} - } - } - }, - "lens-ui-telemetry": { - "properties": { - "name": { + "install_started_at": { + "type": "date" + }, + "install_status": { "type": "keyword" }, - "type": { + "install_version": { "type": "keyword" }, - "date": { - "type": "date" + "installed_es": { + "properties": { + "deferred": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + }, + "type": "nested" }, - "count": { - "type": "integer" - } - } - }, - "map": { - "properties": { - "description": { - "type": "text" + "installed_kibana": { + "dynamic": false, + "properties": {} }, - "title": { - "type": "text" + "installed_kibana_space_id": { + "type": "keyword" }, - "version": { - "type": "integer" + "internal": { + "type": "boolean" }, - "mapStateJSON": { - "type": "text" + "keep_policies_up_to_date": { + "index": false, + "type": "boolean" }, - "layerListJSON": { - "type": "text" + "latest_install_failed_attempts": { + "enabled": false, + "type": "object" }, - "uiStateJSON": { - "type": "text" + "name": { + "type": "keyword" }, - "bounds": { + "package_assets": { "dynamic": false, "properties": {} - } - } - }, - "cases-comments": { - "dynamic": false, - "properties": { - "comment": { - "type": "text" }, - "owner": { + "verification_key_id": { "type": "keyword" }, - "type": { + "verification_status": { "type": "keyword" }, - "actions": { - "properties": { - "type": { - "type": "keyword" - } - } - }, - "alertId": { + "version": { + "type": "keyword" + } + } + }, + "epm-packages-assets": { + "properties": { + "asset_path": { "type": "keyword" }, - "created_at": { - "type": "date" + "data_base64": { + "type": "binary" }, - "created_by": { - "properties": { - "username": { - "type": "keyword" - } - } + "data_utf8": { + "index": false, + "type": "text" }, - "externalReferenceAttachmentTypeId": { + "install_source": { "type": "keyword" }, - "persistableStateAttachmentTypeId": { + "media_type": { "type": "keyword" }, - "pushed_at": { - "type": "date" + "package_name": { + "type": "keyword" }, - "updated_at": { - "type": "date" + "package_version": { + "type": "keyword" } } }, - "cases-configure": { + "event-annotation-group": { "dynamic": false, "properties": { - "created_at": { - "type": "date" - }, - "closure_type": { - "type": "keyword" + "description": { + "type": "text" }, - "owner": { - "type": "keyword" + "title": { + "type": "text" } } }, - "cases-connector-mappings": { + "event_loop_delays_daily": { "dynamic": false, "properties": { - "owner": { - "type": "keyword" + "lastUpdatedAt": { + "type": "date" } } }, - "cases": { - "dynamic": false, + "exception-list": { "properties": { - "assignees": { - "properties": { - "uid": { - "type": "keyword" - } - } - }, - "closed_at": { - "type": "date" + "_tags": { + "type": "keyword" }, - "closed_by": { + "comments": { "properties": { - "username": { + "comment": { "type": "keyword" }, - "full_name": { + "created_at": { "type": "keyword" }, - "email": { + "created_by": { "type": "keyword" }, - "profile_uid": { + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { "type": "keyword" } } }, "created_at": { - "type": "date" + "type": "keyword" }, "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { "properties": { - "username": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { "type": "keyword" }, - "full_name": { + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { "type": "keyword" }, - "email": { + "type": { "type": "keyword" }, - "profile_uid": { + "value": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" } } }, - "duration": { - "type": "unsigned_long" + "expire_time": { + "type": "date" }, - "description": { - "type": "text" + "immutable": { + "type": "boolean" }, - "connector": { - "properties": { - "name": { + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "os_types": { + "type": "keyword" + }, + "tags": { + "fields": { + "text": { "type": "text" + } + }, + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" }, - "type": { + "created_at": { "type": "keyword" }, - "fields": { - "properties": { - "key": { - "type": "text" - }, - "value": { - "type": "text" - } - } + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" } } }, - "external_service": { + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { "properties": { - "pushed_at": { - "type": "date" - }, - "pushed_by": { + "entries": { "properties": { - "username": { + "field": { "type": "keyword" }, - "full_name": { + "operator": { "type": "keyword" }, - "email": { + "type": { "type": "keyword" }, - "profile_uid": { + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { "type": "keyword" } } }, - "connector_name": { + "operator": { "type": "keyword" }, - "external_id": { + "type": { "type": "keyword" }, - "external_title": { - "type": "text" - }, - "external_url": { - "type": "text" + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" } } }, - "owner": { + "expire_time": { + "type": "date" + }, + "immutable": { + "type": "boolean" + }, + "item_id": { "type": "keyword" }, - "title": { - "type": "text", + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { "fields": { - "keyword": { - "type": "keyword" + "text": { + "type": "text" } - } + }, + "type": "keyword" }, - "status": { - "type": "short" + "os_types": { + "type": "keyword" }, "tags": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, - "updated_at": { - "type": "date" + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" }, "updated_by": { - "properties": { - "username": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "email": { - "type": "keyword" - }, - "profile_uid": { - "type": "keyword" - } - } + "type": "keyword" }, - "settings": { - "properties": { - "syncAlerts": { - "type": "boolean" - } - } + "version": { + "type": "keyword" + } + } + }, + "file": { + "dynamic": false, + "properties": { + "FileKind": { + "type": "keyword" }, - "severity": { - "type": "short" + "Meta": { + "type": "flattened" }, - "total_alerts": { - "type": "integer" + "Status": { + "type": "keyword" }, - "total_comments": { - "type": "integer" + "Updated": { + "type": "date" }, - "category": { + "created": { + "type": "date" + }, + "extension": { "type": "keyword" }, - "customFields": { - "type": "nested", + "hash": { + "dynamic": false, + "properties": {} + }, + "mime_type": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "size": { + "type": "long" + }, + "user": { + "type": "flattened" + } + } + }, + "file-upload-usage-collection-telemetry": { + "properties": { + "file_upload": { "properties": { - "key": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "keyword", - "fields": { - "number": { - "type": "long", - "ignore_malformed": true - }, - "boolean": { - "type": "boolean", - "ignore_malformed": true - }, - "string": { - "type": "text" - }, - "date": { - "type": "date", - "ignore_malformed": true - }, - "ip": { - "type": "ip", - "ignore_malformed": true - } - } + "index_creation_count": { + "type": "long" } } } } }, - "cases-user-actions": { + "fileShare": { "dynamic": false, "properties": { - "action": { + "created": { + "type": "date" + }, + "name": { "type": "keyword" }, - "created_at": { - "type": "date" + "token": { + "type": "keyword" }, - "created_by": { - "properties": { - "username": { - "type": "keyword" - } - } + "valid_until": { + "type": "long" + } + } + }, + "fleet-fleet-server-host": { + "properties": { + "host_urls": { + "index": false, + "type": "keyword" }, - "payload": { - "dynamic": false, - "properties": { - "connector": { - "properties": { - "type": { - "type": "keyword" - } - } - }, - "comment": { - "properties": { - "type": { - "type": "keyword" - }, - "externalReferenceAttachmentTypeId": { - "type": "keyword" - }, - "persistableStateAttachmentTypeId": { - "type": "keyword" - } - } - }, - "assignees": { - "properties": { - "uid": { - "type": "keyword" - } - } - } - } + "is_default": { + "type": "boolean" }, - "owner": { + "is_internal": { + "index": false, + "type": "boolean" + }, + "is_preconfigured": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "proxy_id": { + "type": "keyword" + } + } + }, + "fleet-message-signing-keys": { + "dynamic": false, + "properties": {} + }, + "fleet-preconfiguration-deletion-record": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "fleet-proxy": { + "properties": { + "certificate": { + "index": false, + "type": "keyword" + }, + "certificate_authorities": { + "index": false, + "type": "keyword" + }, + "certificate_key": { + "index": false, + "type": "keyword" + }, + "is_preconfigured": { + "type": "boolean" + }, + "name": { "type": "keyword" }, - "type": { + "proxy_headers": { + "index": false, + "type": "text" + }, + "url": { + "index": false, "type": "keyword" } } }, - "cases-telemetry": { - "dynamic": false, - "properties": {} - }, - "infrastructure-monitoring-log-view": { + "fleet-uninstall-tokens": { "dynamic": false, "properties": { - "name": { - "type": "text" + "policy_id": { + "type": "keyword" + }, + "token_plain": { + "type": "keyword" } } }, - "metrics-data-source": { - "dynamic": false, - "properties": {} - }, - "canvas-element": { - "dynamic": false, + "graph-workspace": { "properties": { - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" } } }, - "help": { + "legacyIndexPatternRef": { + "index": false, "type": "text" }, - "content": { - "type": "text" + "numLinks": { + "type": "integer" }, - "image": { + "numVertices": { + "type": "integer" + }, + "title": { "type": "text" }, - "@timestamp": { - "type": "date" + "version": { + "type": "integer" }, - "@created": { - "type": "date" + "wsState": { + "type": "text" } } }, - "canvas-workpad": { + "guided-onboarding-guide-state": { "dynamic": false, "properties": { - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - }, - "@timestamp": { - "type": "date" + "guideId": { + "type": "keyword" }, - "@created": { - "type": "date" + "isActive": { + "type": "boolean" } } }, - "canvas-workpad-template": { + "guided-onboarding-plugin-state": { + "dynamic": false, + "properties": {} + }, + "index-pattern": { "dynamic": false, "properties": { "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - }, - "help": { - "type": "text", "fields": { "keyword": { "type": "keyword" } - } + }, + "type": "text" }, - "tags": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } + "title": { + "type": "text" }, - "template_key": { + "type": { "type": "keyword" } } }, - "ingest_manager_settings": { + "infrastructure-monitoring-log-view": { + "dynamic": false, "properties": { - "fleet_server_hosts": { - "type": "keyword" - }, - "has_seen_add_data_notice": { - "type": "boolean", - "index": false - }, - "prerelease_integrations_enabled": { - "type": "boolean" - }, - "output_secret_storage_requirements_met": { - "type": "boolean" - }, - "secret_storage_requirements_met": { - "type": "boolean" + "name": { + "type": "text" } } }, + "infrastructure-ui-source": { + "dynamic": false, + "properties": {} + }, "ingest-agent-policies": { "properties": { - "name": { - "type": "keyword" + "agent_features": { + "properties": { + "enabled": { + "type": "boolean" + }, + "name": { + "type": "keyword" + } + } }, - "schema_version": { - "type": "version" + "data_output_id": { + "type": "keyword" }, "description": { "type": "text" }, - "namespace": { + "download_source_id": { "type": "keyword" }, - "is_managed": { - "type": "boolean" + "fleet_server_host_id": { + "type": "keyword" + }, + "inactivity_timeout": { + "type": "integer" }, "is_default": { "type": "boolean" @@ -1595,97 +1598,111 @@ "is_default_fleet_server": { "type": "boolean" }, - "status": { + "is_managed": { + "type": "boolean" + }, + "is_preconfigured": { "type": "keyword" }, - "unenroll_timeout": { - "type": "integer" + "is_protected": { + "type": "boolean" }, - "inactivity_timeout": { - "type": "integer" + "keep_monitoring_alive": { + "type": "boolean" }, - "updated_at": { - "type": "date" + "monitoring_enabled": { + "index": false, + "type": "keyword" }, - "updated_by": { + "monitoring_output_id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { "type": "keyword" }, + "overrides": { + "index": false, + "type": "flattened" + }, "revision": { "type": "integer" }, - "monitoring_enabled": { - "type": "keyword", - "index": false + "schema_version": { + "type": "version" }, - "is_preconfigured": { + "status": { "type": "keyword" }, - "data_output_id": { - "type": "keyword" + "unenroll_timeout": { + "type": "integer" }, - "monitoring_output_id": { - "type": "keyword" + "updated_at": { + "type": "date" }, - "download_source_id": { + "updated_by": { "type": "keyword" - }, - "fleet_server_host_id": { + } + } + }, + "ingest-download-sources": { + "properties": { + "host": { "type": "keyword" }, - "agent_features": { - "properties": { - "name": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - } - } - }, - "is_protected": { + "is_default": { "type": "boolean" }, - "overrides": { - "type": "flattened", - "index": false + "name": { + "type": "keyword" }, - "keep_monitoring_alive": { - "type": "boolean" + "proxy_id": { + "type": "keyword" + }, + "source_id": { + "index": false, + "type": "keyword" } } }, "ingest-outputs": { "properties": { - "output_id": { - "type": "keyword", - "index": false - }, - "name": { - "type": "keyword" + "allow_edit": { + "enabled": false }, - "type": { + "auth_type": { "type": "keyword" }, - "is_default": { - "type": "boolean" + "broker_ack_reliability": { + "type": "text" }, - "is_default_monitoring": { - "type": "boolean" + "broker_buffer_size": { + "type": "integer" }, - "hosts": { - "type": "keyword" + "broker_timeout": { + "type": "integer" }, "ca_sha256": { - "type": "keyword", - "index": false + "index": false, + "type": "keyword" }, "ca_trusted_fingerprint": { - "type": "keyword", - "index": false + "index": false, + "type": "keyword" + }, + "channel_buffer_size": { + "type": "integer" + }, + "client_id": { + "type": "keyword" + }, + "compression": { + "type": "keyword" }, - "service_token": { - "type": "keyword", - "index": false + "compression_level": { + "type": "integer" }, "config": { "type": "flattened" @@ -1693,64 +1710,70 @@ "config_yaml": { "type": "text" }, - "is_preconfigured": { - "type": "boolean", - "index": false - }, - "is_internal": { - "type": "boolean", - "index": false - }, - "ssl": { - "type": "binary" - }, - "proxy_id": { + "connection_type": { "type": "keyword" }, - "shipper": { + "hash": { "dynamic": false, - "properties": {} + "properties": { + "hash": { + "type": "text" + }, + "random": { + "type": "boolean" + } + } }, - "allow_edit": { - "enabled": false + "headers": { + "dynamic": false, + "properties": { + "key": { + "type": "text" + }, + "value": { + "type": "text" + } + } }, - "version": { + "hosts": { "type": "keyword" }, - "key": { - "type": "keyword" + "is_default": { + "type": "boolean" }, - "compression": { - "type": "keyword" + "is_default_monitoring": { + "type": "boolean" }, - "compression_level": { - "type": "integer" + "is_internal": { + "index": false, + "type": "boolean" }, - "client_id": { + "is_preconfigured": { + "index": false, + "type": "boolean" + }, + "key": { "type": "keyword" }, - "auth_type": { + "name": { "type": "keyword" }, - "connection_type": { + "output_id": { + "index": false, "type": "keyword" }, - "username": { + "partition": { "type": "keyword" }, "password": { - "type": "text", - "index": false + "index": false, + "type": "text" }, - "sasl": { - "dynamic": false, - "properties": { - "mechanism": { - "type": "text" - } - } + "preset": { + "index": false, + "type": "keyword" }, - "partition": { + "proxy_id": { "type": "keyword" }, "random": { @@ -1761,6 +1784,9 @@ } } }, + "required_acks": { + "type": "integer" + }, "round_robin": { "dynamic": false, "properties": { @@ -1769,69 +1795,26 @@ } } }, - "hash": { + "sasl": { "dynamic": false, "properties": { - "hash": { + "mechanism": { "type": "text" - }, - "random": { - "type": "boolean" } } }, - "topics": { + "secrets": { "dynamic": false, "properties": { - "topic": { - "type": "keyword" - }, - "when": { + "password": { "dynamic": false, "properties": { - "type": { - "type": "text" - }, - "condition": { - "type": "text" + "id": { + "type": "keyword" } } - } - } - }, - "headers": { - "dynamic": false, - "properties": { - "key": { - "type": "text" }, - "value": { - "type": "text" - } - } - }, - "timeout": { - "type": "integer" - }, - "broker_timeout": { - "type": "integer" - }, - "broker_ack_reliability": { - "type": "text" - }, - "broker_buffer_size": { - "type": "integer" - }, - "required_acks": { - "type": "integer" - }, - "channel_buffer_size": { - "type": "integer" - }, - "secrets": { - "dynamic": false, - "properties": { - "password": { + "service_token": { "dynamic": false, "properties": { "id": { @@ -1851,41 +1834,82 @@ } } } + } + } + }, + "service_token": { + "index": false, + "type": "keyword" + }, + "shipper": { + "dynamic": false, + "properties": {} + }, + "ssl": { + "type": "binary" + }, + "timeout": { + "type": "integer" + }, + "topics": { + "dynamic": false, + "properties": { + "topic": { + "type": "keyword" }, - "service_token": { + "when": { "dynamic": false, "properties": { - "id": { - "type": "keyword" + "condition": { + "type": "text" + }, + "type": { + "type": "text" } } } } }, - "preset": { - "type": "keyword", - "index": false + "type": { + "type": "keyword" + }, + "username": { + "type": "keyword" + }, + "version": { + "type": "keyword" } } }, "ingest-package-policies": { "properties": { - "name": { + "created_at": { + "type": "date" + }, + "created_by": { "type": "keyword" }, "description": { "type": "text" }, - "namespace": { - "type": "keyword" + "elasticsearch": { + "dynamic": false, + "properties": {} }, "enabled": { "type": "boolean" }, + "inputs": { + "dynamic": false, + "properties": {} + }, "is_managed": { "type": "boolean" }, - "policy_id": { + "name": { + "type": "keyword" + }, + "namespace": { "type": "keyword" }, "package": { @@ -1901,16 +1925,11 @@ } } }, - "elasticsearch": { - "dynamic": false, - "properties": {} - }, - "vars": { - "type": "flattened" + "policy_id": { + "type": "keyword" }, - "inputs": { - "dynamic": false, - "properties": {} + "revision": { + "type": "integer" }, "secret_references": { "properties": { @@ -1919,235 +1938,295 @@ } } }, - "revision": { - "type": "integer" + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + } + }, + "ingest_manager_settings": { + "properties": { + "fleet_server_hosts": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "output_secret_storage_requirements_met": { + "type": "boolean" + }, + "prerelease_integrations_enabled": { + "type": "boolean" + }, + "secret_storage_requirements_met": { + "type": "boolean" + } + } + }, + "inventory-view": { + "dynamic": false, + "properties": {} + }, + "kql-telemetry": { + "dynamic": false, + "properties": {} + }, + "legacy-url-alias": { + "dynamic": false, + "properties": { + "disabled": { + "type": "boolean" + }, + "resolveCounter": { + "type": "long" }, - "updated_at": { - "type": "date" + "sourceId": { + "type": "keyword" }, - "updated_by": { + "targetId": { "type": "keyword" }, - "created_at": { - "type": "date" + "targetNamespace": { + "type": "keyword" }, - "created_by": { + "targetType": { "type": "keyword" } } }, - "epm-packages": { + "lens": { "properties": { - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "internal": { - "type": "boolean" - }, - "keep_policies_up_to_date": { - "type": "boolean", - "index": false + "description": { + "type": "text" }, - "es_index_patterns": { + "state": { "dynamic": false, "properties": {} }, - "verification_status": { - "type": "keyword" - }, - "verification_key_id": { - "type": "keyword" - }, - "installed_es": { - "type": "nested", - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "deferred": { - "type": "boolean" - } - } - }, - "latest_install_failed_attempts": { - "type": "object", - "enabled": false - }, - "installed_kibana": { - "dynamic": false, - "properties": {} + "title": { + "type": "text" }, - "installed_kibana_space_id": { + "visualizationType": { "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" }, - "package_assets": { - "dynamic": false, - "properties": {} - }, - "install_started_at": { + "date": { "type": "date" }, - "install_version": { + "name": { "type": "keyword" }, - "install_status": { + "type": { "type": "keyword" + } + } + }, + "links": { + "dynamic": false, + "properties": { + "description": { + "type": "text" }, - "install_source": { - "type": "keyword" + "links": { + "dynamic": false, + "properties": {} }, - "install_format_schema_version": { - "type": "version" + "title": { + "type": "text" + } + } + }, + "maintenance-window": { + "dynamic": false, + "properties": { + "enabled": { + "type": "boolean" }, - "experimental_data_stream_features": { - "type": "nested", - "properties": { - "data_stream": { - "type": "keyword" - }, - "features": { - "type": "nested", - "dynamic": false, - "properties": { - "synthetic_source": { - "type": "boolean" - }, - "tsdb": { - "type": "boolean" - } - } - } - } + "events": { + "format": "epoch_millis||strict_date_optional_time", + "type": "date_range" } } }, - "epm-packages-assets": { + "map": { "properties": { - "package_name": { - "type": "keyword" + "bounds": { + "dynamic": false, + "properties": {} }, - "package_version": { - "type": "keyword" + "description": { + "type": "text" }, - "install_source": { - "type": "keyword" + "layerListJSON": { + "type": "text" }, - "asset_path": { - "type": "keyword" + "mapStateJSON": { + "type": "text" }, - "media_type": { - "type": "keyword" + "title": { + "type": "text" }, - "data_utf8": { - "type": "text", - "index": false + "uiStateJSON": { + "type": "text" }, - "data_base64": { - "type": "binary" + "version": { + "type": "integer" } } }, - "fleet-preconfiguration-deletion-record": { + "metrics-data-source": { + "dynamic": false, + "properties": {} + }, + "metrics-explorer-view": { + "dynamic": false, + "properties": {} + }, + "ml-job": { "properties": { - "id": { + "datafeed_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "job_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "type": { "type": "keyword" } } }, - "ingest-download-sources": { + "ml-module": { + "dynamic": false, "properties": { - "source_id": { - "type": "keyword", - "index": false + "datafeeds": { + "type": "object" }, - "name": { - "type": "keyword" + "defaultIndexPattern": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" }, - "is_default": { - "type": "boolean" + "description": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" }, - "host": { - "type": "keyword" + "id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" }, - "proxy_id": { - "type": "keyword" - } - } - }, - "fleet-fleet-server-host": { - "properties": { - "name": { - "type": "keyword" + "jobs": { + "type": "object" }, - "is_default": { - "type": "boolean" + "logo": { + "type": "object" }, - "is_internal": { - "type": "boolean", - "index": false + "query": { + "type": "object" }, - "host_urls": { - "type": "keyword", - "index": false + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" }, - "is_preconfigured": { - "type": "boolean" + "title": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" }, - "proxy_id": { - "type": "keyword" + "type": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" } } }, - "fleet-proxy": { + "ml-trained-model": { "properties": { - "name": { - "type": "keyword" - }, - "url": { - "type": "keyword", - "index": false - }, - "proxy_headers": { - "type": "text", - "index": false - }, - "certificate_authorities": { - "type": "keyword", - "index": false - }, - "certificate": { - "type": "keyword", - "index": false - }, - "certificate_key": { - "type": "keyword", - "index": false + "job": { + "properties": { + "create_time": { + "type": "date" + }, + "job_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } }, - "is_preconfigured": { - "type": "boolean" + "model_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" } } }, - "fleet-message-signing-keys": { - "dynamic": false, - "properties": {} - }, - "fleet-uninstall-tokens": { - "dynamic": false, + "monitoring-telemetry": { "properties": { - "policy_id": { + "reportedClusterUuids": { "type": "keyword" + } + } + }, + "observability-onboarding-state": { + "properties": { + "progress": { + "dynamic": false, + "type": "object" }, - "token_plain": { + "state": { + "dynamic": false, + "type": "object" + }, + "type": { "type": "keyword" } } @@ -2162,237 +2241,58 @@ } } }, - "osquery-saved-query": { - "dynamic": false, + "osquery-pack": { "properties": { - "description": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "query": { - "type": "text" - }, "created_at": { "type": "date" }, "created_by": { - "type": "text" - }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "text" - }, - "interval": { "type": "keyword" }, - "timeout": { - "type": "short" - }, - "ecs_mapping": { - "dynamic": false, - "properties": {} - } - } - }, - "osquery-pack": { - "properties": { "description": { "type": "text" }, - "name": { - "type": "text" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "keyword" - }, "enabled": { "type": "boolean" }, - "shards": { - "dynamic": false, - "properties": {} - }, - "version": { - "type": "long" + "name": { + "type": "text" }, "queries": { "dynamic": false, "properties": { + "ecs_mapping": { + "dynamic": false, + "properties": {} + }, "id": { "type": "keyword" }, - "query": { - "type": "text" - }, "interval": { "type": "text" }, - "timeout": { - "type": "short" - }, "platform": { "type": "keyword" }, - "version": { - "type": "keyword" - }, - "ecs_mapping": { - "dynamic": false, - "properties": {} - } - } - } - } - }, - "osquery-pack-asset": { - "dynamic": false, - "properties": { - "description": { - "type": "text" - }, - "name": { - "type": "text" - }, - "version": { - "type": "long" - }, - "shards": { - "dynamic": false, - "properties": {} - }, - "queries": { - "dynamic": false, - "properties": { - "id": { - "type": "keyword" - }, "query": { "type": "text" }, - "interval": { - "type": "text" - }, "timeout": { "type": "short" }, - "platform": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "ecs_mapping": { - "dynamic": false, - "properties": {} - } - } - } - } - }, - "csp-rule-template": { - "dynamic": false, - "properties": { - "metadata": { - "type": "object", - "properties": { - "name": { - "type": "keyword", - "fields": { - "text": { - "type": "text" - } - } - }, - "id": { - "type": "keyword" - }, - "section": { - "type": "keyword", - "fields": { - "text": { - "type": "text" - } - } - }, "version": { "type": "keyword" - }, - "benchmark": { - "type": "object", - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "posture_type": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "rule_number": { - "type": "keyword" - } - } - } - } - } - } - }, - "cloud-security-posture-settings": { - "dynamic": false, - "properties": {} - }, - "slo": { - "dynamic": false, - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "description": { - "type": "text" - }, - "indicator": { - "properties": { - "type": { - "type": "keyword" - }, - "params": { - "type": "flattened" } } }, - "budgetingMethod": { - "type": "keyword" + "shards": { + "dynamic": false, + "properties": {} }, - "enabled": { - "type": "boolean" + "updated_at": { + "type": "date" }, - "tags": { + "updated_by": { "type": "keyword" }, "version": { @@ -2400,404 +2300,238 @@ } } }, - "threshold-explorer-view": { + "osquery-pack-asset": { "dynamic": false, - "properties": {} - }, - "observability-onboarding-state": { "properties": { - "type": { - "type": "keyword" + "description": { + "type": "text" }, - "state": { - "type": "object", - "dynamic": false + "name": { + "type": "text" }, - "progress": { - "type": "object", - "dynamic": false - } - } - }, - "ml-job": { - "properties": { - "job_id": { - "type": "text", - "fields": { - "keyword": { + "queries": { + "dynamic": false, + "properties": { + "ecs_mapping": { + "dynamic": false, + "properties": {} + }, + "id": { "type": "keyword" - } - } - }, - "datafeed_id": { - "type": "text", - "fields": { - "keyword": { + }, + "interval": { + "type": "text" + }, + "platform": { "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - } - } - }, - "ml-trained-model": { - "properties": { - "model_id": { - "type": "text", - "fields": { - "keyword": { + }, + "query": { + "type": "text" + }, + "timeout": { + "type": "short" + }, + "version": { "type": "keyword" } } }, - "job": { - "properties": { - "job_id": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - }, - "create_time": { - "type": "date" - } - } + "shards": { + "dynamic": false, + "properties": {} + }, + "version": { + "type": "long" } } }, - "ml-module": { + "osquery-saved-query": { "dynamic": false, "properties": { - "id": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } + "created_at": { + "type": "date" }, - "title": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } + "created_by": { + "type": "text" }, "description": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } + "type": "text" }, - "type": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } + "ecs_mapping": { + "dynamic": false, + "properties": {} }, - "logo": { - "type": "object" + "id": { + "type": "keyword" }, - "defaultIndexPattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } + "interval": { + "type": "keyword" + }, + "platform": { + "type": "keyword" }, "query": { - "type": "object" + "type": "text" }, - "jobs": { - "type": "object" + "timeout": { + "type": "short" }, - "datafeeds": { - "type": "object" + "updated_at": { + "type": "date" }, - "tags": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } + "updated_by": { + "type": "text" + }, + "version": { + "type": "keyword" } } }, - "uptime-dynamic-settings": { - "dynamic": false, - "properties": {} + "policy-settings-protection-updates-note": { + "properties": { + "note": { + "index": false, + "type": "text" + } + } }, - "synthetics-privates-locations": { + "query": { "dynamic": false, - "properties": {} + "properties": { + "description": { + "type": "text" + }, + "title": { + "type": "text" + } + } }, - "synthetics-monitor": { + "risk-engine-configuration": { "dynamic": false, "properties": { - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256, - "normalizer": "lowercase" - } - } - }, - "type": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "urls": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "hosts": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "journey_id": { - "type": "keyword" - }, - "project_id": { - "type": "keyword", - "fields": { - "text": { - "type": "text" - } - } - }, - "origin": { - "type": "keyword" - }, - "hash": { - "type": "keyword" - }, - "locations": { - "properties": { - "id": { - "type": "keyword", - "ignore_above": 256, - "fields": { - "text": { - "type": "text" - } - } - }, - "label": { - "type": "text" - } - } - }, - "custom_heartbeat_id": { + "dataViewId": { "type": "keyword" }, - "id": { - "type": "keyword" + "enabled": { + "type": "boolean" }, - "tags": { - "type": "keyword", - "fields": { - "text": { - "type": "text" - } - } + "filter": { + "dynamic": false, + "properties": {} }, - "schedule": { - "properties": { - "number": { - "type": "integer" - } - } + "identifierType": { + "type": "keyword" }, - "enabled": { - "type": "boolean" + "interval": { + "type": "keyword" }, - "alert": { - "properties": { - "status": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "tls": { - "properties": { - "enabled": { - "type": "boolean" - } - } - } - } + "pageSize": { + "type": "integer" }, - "throttling": { + "range": { "properties": { - "label": { + "end": { + "type": "keyword" + }, + "start": { "type": "keyword" } } } } }, - "uptime-synthetics-api-key": { + "rules-settings": { "dynamic": false, "properties": { - "apiKey": { - "type": "binary" + "flapping": { + "properties": {} } } }, - "synthetics-param": { - "dynamic": false, - "properties": {} - }, - "infrastructure-ui-source": { - "dynamic": false, - "properties": {} - }, - "inventory-view": { - "dynamic": false, - "properties": {} - }, - "metrics-explorer-view": { - "dynamic": false, - "properties": {} - }, - "upgrade-assistant-reindex-operation": { - "dynamic": false, + "sample-data-telemetry": { "properties": { - "indexName": { - "type": "keyword" + "installCount": { + "type": "long" }, - "status": { - "type": "integer" + "unInstallCount": { + "type": "long" } } }, - "upgrade-assistant-ml-upgrade-operation": { + "search": { "dynamic": false, "properties": { - "snapshotId": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } + "description": { + "type": "text" + }, + "title": { + "type": "text" } } }, - "monitoring-telemetry": { + "search-session": { + "dynamic": false, "properties": { - "reportedClusterUuids": { + "created": { + "type": "date" + }, + "realmName": { + "type": "keyword" + }, + "realmType": { + "type": "keyword" + }, + "sessionId": { + "type": "keyword" + }, + "username": { "type": "keyword" } } }, - "enterprise_search_telemetry": { - "dynamic": false, - "properties": {} - }, - "app_search_telemetry": { + "search-telemetry": { "dynamic": false, "properties": {} }, - "workplace_search_telemetry": { + "security-rule": { "dynamic": false, - "properties": {} - }, - "siem-ui-timeline-note": { "properties": { - "eventId": { + "rule_id": { "type": "keyword" }, - "note": { - "type": "text" - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" + "version": { + "type": "long" } } }, - "siem-ui-timeline-pinned-event": { + "security-solution-signals-migration": { + "dynamic": false, "properties": { - "eventId": { + "sourceIndex": { "type": "keyword" }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, "updated": { "type": "date" }, - "updatedBy": { - "type": "text" + "version": { + "type": "long" } } }, "siem-detection-engine-rule-actions": { "properties": { - "alertThrottle": { - "type": "keyword" - }, - "ruleAlertId": { - "type": "keyword" - }, - "ruleThrottle": { - "type": "keyword" - }, "actions": { "properties": { "actionRef": { "type": "keyword" }, - "group": { + "action_type_id": { "type": "keyword" }, - "id": { + "group": { "type": "keyword" }, - "action_type_id": { + "id": { "type": "keyword" }, "params": { @@ -2805,17 +2539,15 @@ "properties": {} } } - } - } - }, - "security-rule": { - "dynamic": false, - "properties": { - "rule_id": { + }, + "alertThrottle": { "type": "keyword" }, - "version": { - "type": "long" + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" } } }, @@ -2838,10 +2570,10 @@ "example": { "type": "text" }, - "indexes": { + "id": { "type": "keyword" }, - "id": { + "indexes": { "type": "keyword" }, "name": { @@ -2858,13 +2590,54 @@ } } }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, "dataProviders": { "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "text" + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } }, "enabled": { "type": "boolean" @@ -2872,71 +2645,46 @@ "excluded": { "type": "boolean" }, + "id": { + "type": "keyword" + }, "kqlQuery": { "type": "text" }, - "type": { + "name": { "type": "text" }, "queryMatch": { "properties": { - "field": { - "type": "text" - }, "displayField": { "type": "text" }, - "value": { - "type": "text" - }, "displayValue": { "type": "text" }, - "operator": { - "type": "text" - } - } - }, - "and": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { + "field": { "type": "text" }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "kqlQuery": { + "operator": { "type": "text" }, - "type": { + "value": { "type": "text" - }, - "queryMatch": { - "properties": { - "field": { - "type": "text" - }, - "displayField": { - "type": "text" - }, - "value": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "operator": { - "type": "text" - } - } } } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" } } }, @@ -2948,16 +2696,16 @@ "eventCategoryField": { "type": "text" }, - "tiebreakerField": { + "query": { "type": "text" }, - "timestampField": { + "size": { "type": "text" }, - "query": { + "tiebreakerField": { "type": "text" }, - "size": { + "timestampField": { "type": "text" } } @@ -2970,22 +2718,28 @@ }, "favorite": { "properties": { - "keySearch": { - "type": "text" + "favoriteDate": { + "type": "date" }, "fullName": { "type": "text" }, - "userName": { + "keySearch": { "type": "text" }, - "favoriteDate": { - "type": "date" + "userName": { + "type": "text" } } }, "filters": { "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, "meta": { "properties": { "alias": { @@ -3015,23 +2769,17 @@ "params": { "type": "text" }, + "relation": { + "type": "keyword" + }, "type": { "type": "keyword" }, "value": { "type": "text" - }, - "relation": { - "type": "keyword" } } }, - "exists": { - "type": "text" - }, - "match_all": { - "type": "text" - }, "missing": { "type": "text" }, @@ -3058,186 +2806,438 @@ "properties": { "kuery": { "properties": { - "kind": { - "type": "keyword" - }, "expression": { "type": "text" + }, + "kind": { + "type": "keyword" } } }, "serializedQuery": { "type": "text" } - } + } + } + } + }, + "savedSearchId": { + "type": "text" + }, + "sort": { + "dynamic": false, + "properties": { + "columnId": { + "type": "keyword" + }, + "columnType": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "slo": { + "dynamic": false, + "properties": { + "budgetingMethod": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "indicator": { + "properties": { + "params": { + "type": "flattened" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "text" + }, + "tags": { + "type": "keyword" + }, + "version": { + "type": "long" + } + } + }, + "space": { + "dynamic": false, + "properties": { + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaces-usage-stats": { + "dynamic": false, + "properties": {} + }, + "synthetics-monitor": { + "dynamic": false, + "properties": { + "alert": { + "properties": { + "status": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "tls": { + "properties": { + "enabled": { + "type": "boolean" + } + } + } + } + }, + "custom_heartbeat_id": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "hash": { + "type": "keyword" + }, + "hosts": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "id": { + "type": "keyword" + }, + "journey_id": { + "type": "keyword" + }, + "locations": { + "properties": { + "id": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 256, + "type": "keyword" + }, + "label": { + "type": "text" } } }, - "title": { - "type": "text" - }, - "templateTimelineId": { + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "normalizer": "lowercase", + "type": "keyword" + } + }, "type": "text" }, - "templateTimelineVersion": { - "type": "integer" + "origin": { + "type": "keyword" }, - "timelineType": { + "project_id": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, - "dateRange": { + "schedule": { "properties": { - "start": { - "type": "date" - }, - "end": { - "type": "date" + "number": { + "type": "integer" } } }, - "sort": { - "dynamic": false, + "tags": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "throttling": { "properties": { - "columnId": { - "type": "keyword" - }, - "columnType": { - "type": "keyword" - }, - "sortDirection": { + "label": { "type": "keyword" } } }, - "status": { - "type": "keyword" - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, "type": "text" }, - "savedSearchId": { + "urls": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, "type": "text" } } }, - "endpoint:user-artifact-manifest": { + "synthetics-param": { "dynamic": false, - "properties": { - "schemaVersion": { - "type": "keyword" - }, - "artifacts": { - "type": "nested" - } - } + "properties": {} }, - "security-solution-signals-migration": { + "synthetics-privates-locations": { "dynamic": false, + "properties": {} + }, + "tag": { "properties": { - "sourceIndex": { - "type": "keyword" + "color": { + "type": "text" }, - "updated": { - "type": "date" + "description": { + "type": "text" }, - "version": { - "type": "long" + "name": { + "type": "text" } } }, - "risk-engine-configuration": { + "task": { "dynamic": false, "properties": { - "dataViewId": { - "type": "keyword" + "attempts": { + "type": "integer" }, "enabled": { "type": "boolean" }, - "filter": { - "dynamic": false, - "properties": {} - }, - "identifierType": { + "ownerId": { "type": "keyword" }, - "interval": { - "type": "keyword" + "retryAt": { + "type": "date" }, - "pageSize": { - "type": "integer" + "runAt": { + "type": "date" }, - "range": { + "schedule": { "properties": { - "start": { - "type": "keyword" - }, - "end": { + "interval": { "type": "keyword" } } + }, + "scheduledAt": { + "type": "date" + }, + "scope": { + "type": "keyword" + }, + "status": { + "type": "keyword" + }, + "taskType": { + "type": "keyword" } } }, - "policy-settings-protection-updates-note": { + "telemetry": { + "dynamic": false, + "properties": {} + }, + "threshold-explorer-view": { + "dynamic": false, + "properties": {} + }, + "ui-metric": { "properties": { - "note": { - "type": "text", - "index": false + "count": { + "type": "integer" } } }, - "apm-telemetry": { + "upgrade-assistant-ml-upgrade-operation": { "dynamic": false, - "properties": {} - }, - "apm-server-schema": { "properties": { - "schemaJson": { - "type": "text", - "index": false + "snapshotId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" } } }, - "apm-service-group": { + "upgrade-assistant-reindex-operation": { + "dynamic": false, "properties": { - "groupName": { + "indexName": { "type": "keyword" }, - "kuery": { - "type": "text" + "status": { + "type": "integer" + } + } + }, + "uptime-dynamic-settings": { + "dynamic": false, + "properties": {} + }, + "uptime-synthetics-api-key": { + "dynamic": false, + "properties": { + "apiKey": { + "type": "binary" + } + } + }, + "url": { + "dynamic": false, + "properties": { + "accessDate": { + "type": "date" }, - "description": { - "type": "text" + "createDate": { + "type": "date" }, - "color": { + "slug": { + "fields": { + "keyword": { + "type": "keyword" + } + }, "type": "text" } } }, - "apm-custom-dashboards": { + "usage-counters": { + "dynamic": false, "properties": { - "dashboardSavedObjectId": { + "domainId": { "type": "keyword" - }, - "kuery": { + } + } + }, + "visualization": { + "dynamic": false, + "properties": { + "description": { "type": "text" }, - "serviceEnvironmentFilterEnabled": { - "type": "boolean" + "kibanaSavedObjectMeta": { + "properties": {} }, - "serviceNameFilterEnabled": { - "type": "boolean" + "title": { + "type": "text" + }, + "version": { + "type": "integer" } } + }, + "workplace_search_telemetry": { + "dynamic": false, + "properties": {} } } diff --git a/packages/kbn-check-mappings-update-cli/src/compatibility/current_mappings.ts b/packages/kbn-check-mappings-update-cli/src/compatibility/current_mappings.ts index 5632f3c479d18..30ff02a624a7f 100644 --- a/packages/kbn-check-mappings-update-cli/src/compatibility/current_mappings.ts +++ b/packages/kbn-check-mappings-update-cli/src/compatibility/current_mappings.ts @@ -10,13 +10,14 @@ import Fsp from 'fs/promises'; import Path from 'path'; import type { SavedObjectsTypeMappingDefinitions } from '@kbn/core-saved-objects-base-server-internal'; +import { prettyPrintAndSortKeys } from '@kbn/utils'; -export const CURRENT_MAPPINGS_FILE = Path.resolve(__dirname, '../../current_mappings.json'); +export const CURRENT_MAPPINGS_FILE_PATH = Path.resolve(__dirname, '../../current_mappings.json'); export async function readCurrentMappings(): Promise { let currentMappingsJson; try { - currentMappingsJson = await Fsp.readFile(CURRENT_MAPPINGS_FILE, 'utf8'); + currentMappingsJson = await Fsp.readFile(CURRENT_MAPPINGS_FILE_PATH, 'utf8'); } catch (error) { if (error.code === 'ENOENT') { return {}; @@ -28,6 +29,10 @@ export async function readCurrentMappings(): Promise => { }; export const writeCurrentFields = async (fieldMap: FieldListMap) => { - await writeFile(CURRENT_FIELDS_FILE_PATH, JSON.stringify(fieldMap, null, 2) + '\n', 'utf-8'); + await writeFile(CURRENT_FIELDS_FILE_PATH, prettyPrintAndSortKeys(fieldMap) + '\n', 'utf-8'); }; diff --git a/packages/kbn-check-mappings-update-cli/tsconfig.json b/packages/kbn-check-mappings-update-cli/tsconfig.json index 48973be21d219..b8ae7bad89ebc 100644 --- a/packages/kbn-check-mappings-update-cli/tsconfig.json +++ b/packages/kbn-check-mappings-update-cli/tsconfig.json @@ -28,5 +28,6 @@ "@kbn/safer-lodash-set", "@kbn/tooling-log", "@kbn/core-saved-objects-server", + "@kbn/utils", ] } diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx index 95f4ff5623ea3..f1d9add2c0f09 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx @@ -6,122 +6,115 @@ * Side Public License, v 1. */ -import React, { FC } from 'react'; -import { EuiFlyout, EuiForm } from '@elastic/eui'; +import React, { FC, useState } from 'react'; +import { EuiFlyout, EuiForm, EuiPage, isColorDark } from '@elastic/eui'; import { ComponentStory } from '@storybook/react'; +import { css } from '@emotion/react'; import { CategoricalColorMapping, ColorMappingProps } from '../categorical_color_mapping'; -import { AVAILABLE_PALETTES } from '../palettes'; +import { AVAILABLE_PALETTES, getPalette, NeutralPalette } from '../palettes'; import { DEFAULT_COLOR_MAPPING_CONFIG } from '../config/default_color_mapping'; +import { ColorMapping } from '../config'; +import { getColorFactory } from '../color/color_handling'; +import { ruleMatch } from '../color/rule_matching'; +import { getValidColor } from '../color/color_math'; export default { title: 'Color Mapping', component: CategoricalColorMapping, - decorators: [ - (story: Function) => ( - {}} hideCloseButton> - {story()} - - ), - ], + decorators: [(story: Function) => story()], }; -const Template: ComponentStory> = (args) => ( - -); +const Template: ComponentStory> = (args) => { + const [updatedModel, setUpdateModel] = useState( + DEFAULT_COLOR_MAPPING_CONFIG + ); + + const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette); + + const colorFactory = getColorFactory(updatedModel, getPaletteFn, false, args.data); + + return ( + +
    + {args.data.type === 'categories' && + args.data.categories.map((c, i) => { + const match = updatedModel.assignments.some(({ rule }) => { + return ruleMatch(rule, c); + }); + const color = colorFactory(c); + const isDark = isColorDark(...getValidColor(color).rgb()); + return ( +
  1. + {c} +
  2. + ); + })} +
+ {}} + hideCloseButton + ownFocus={false} + > + + + + +
+ ); +}; export const Default = Template.bind({}); Default.args = { model: { ...DEFAULT_COLOR_MAPPING_CONFIG, - assignmentMode: 'manual', + colorMode: { - type: 'gradient', - steps: [ - { - type: 'categorical', - colorIndex: 0, - paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId, - touched: false, - }, - { - type: 'categorical', - colorIndex: 1, - paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId, - touched: false, - }, - { - type: 'categorical', - colorIndex: 2, - paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId, - touched: false, - }, - ], - sort: 'asc', + type: 'categorical', }, - assignments: [ - { - rule: { - type: 'matchExactly', - values: ['this is', 'a multi-line combobox that is very long and that will be truncated'], - }, - color: { - type: 'gradient', - }, - touched: false, - }, - { - rule: { - type: 'matchExactly', - values: ['b', ['double', 'value']], - }, - color: { - type: 'gradient', - }, - touched: false, - }, - { - rule: { - type: 'matchExactly', - values: ['c'], - }, - color: { - type: 'gradient', - }, - touched: false, - }, + specialAssignments: [ { rule: { - type: 'matchExactly', - values: [ - 'this is', - 'a multi-line wrap', - 'combo box', - 'test combo', - '3 lines', - ['double', 'value'], - ], + type: 'other', }, color: { - type: 'gradient', + type: 'loop', }, touched: false, }, ], + assignments: [], }, isDarkMode: false, data: { type: 'categories', categories: [ - 'a', - 'b', - 'c', - 'd', - 'this is', - 'a multi-line wrap', - 'combo box', - 'test combo', - '3 lines', + 'US', + 'Mexico', + 'Brasil', + 'Canada', + 'Italy', + 'Germany', + 'France', + 'Spain', + 'UK', + 'Portugal', + 'Greece', + 'Sweden', + 'Finland', ], }, diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx index fe8374d7dcdcd..ccc955d2b8947 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx @@ -13,8 +13,9 @@ import { AVAILABLE_PALETTES } from './palettes'; import { DEFAULT_COLOR_MAPPING_CONFIG } from './config/default_color_mapping'; import { MULTI_FIELD_KEY_SEPARATOR } from '@kbn/data-plugin/common'; -const AUTO_ASSIGN_SWITCH = '[data-test-subj="lns-colorMapping-autoAssignSwitch"]'; const ASSIGNMENTS_LIST = '[data-test-subj="lns-colorMapping-assignmentsList"]'; +const ASSIGNMENTS_PROMPT = '[data-test-subj="lns-colorMapping-assignmentsPrompt"]'; +const ASSIGNMENTS_PROMPT_ADD_ALL = '[data-test-subj="lns-colorMapping-assignmentsPromptAddAll"]'; const ASSIGNMENT_ITEM = (i: number) => `[data-test-subj="lns-colorMapping-assignmentsItem${i}"]`; describe('color mapping', () => { @@ -35,19 +36,12 @@ describe('color mapping', () => { /> ); - expect(component.find(AUTO_ASSIGN_SWITCH).hostNodes().prop('aria-checked')).toEqual(true); - expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual( - dataInput.categories.length - ); - dataInput.categories.forEach((category, index) => { - const assignment = component.find(ASSIGNMENT_ITEM(index)).hostNodes(); - expect(assignment.text()).toEqual(category); - expect(assignment.hasClass('euiComboBox-isDisabled')).toEqual(true); - }); + // empty list prompt visible + expect(component.find(ASSIGNMENTS_PROMPT)).toBeTruthy(); expect(onModelUpdateFn).not.toBeCalled(); }); - it('switch to manual assignments', () => { + it('Add all terms to assignments', () => { const dataInput: ColorMappingInputData = { type: 'categories', categories: ['categoryA', 'categoryB'], @@ -63,9 +57,8 @@ describe('color mapping', () => { specialTokens={new Map()} /> ); - component.find(AUTO_ASSIGN_SWITCH).hostNodes().simulate('click'); + component.find(ASSIGNMENTS_PROMPT_ADD_ALL).hostNodes().simulate('click'); expect(onModelUpdateFn).toBeCalledTimes(1); - expect(component.find(AUTO_ASSIGN_SWITCH).hostNodes().prop('aria-checked')).toEqual(false); expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual( dataInput.categories.length ); @@ -97,6 +90,7 @@ describe('color mapping', () => { } /> ); + component.find(ASSIGNMENTS_PROMPT_ADD_ALL).hostNodes().simulate('click'); expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual( dataInput.categories.length ); diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts index 93896394daf41..f8631ed5768da 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts @@ -23,6 +23,7 @@ import { ColorMapping } from '../config'; describe('Color mapping - color generation', () => { const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette); + it('returns EUI light colors from default config', () => { const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, false, { type: 'categories', @@ -31,18 +32,36 @@ describe('Color mapping - color generation', () => { expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); - // if the category is not available in the `categories` list then a default neutral color is used - expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + // if the category is not available in the `categories` list then a default netural is used + // this is an edge case and ideally never happen + expect(colorFactory('not_available_1')).toBe( + NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX] + ); }); - it('returns max number of colors defined in palette, use other color otherwise', () => { + // currently there is no difference in the two colors, but this could change in the future + // this test will catch the change + it('returns EUI dark colors from default config', () => { + const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, true, { + type: 'categories', + categories: ['catA', 'catB', 'catC'], + }); + expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); + expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); + expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); + // if the category is not available in the `categories` list then a default netural is used + // this is an edge case and ideally never happen + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]); + }); + + it('by default loops colors defined in palette', () => { const twoColorPalette: ColorMapping.CategoricalPalette = { id: 'twoColors', name: 'twoColors', colorCount: 2, type: 'categorical', - getColor(valueInRange, isDarkMode) { - return ['red', 'blue'][valueInRange]; + getColor(indexInRange, isDarkMode, loop) { + return ['red', 'blue'][loop ? indexInRange % 2 : indexInRange]; }, }; @@ -53,6 +72,17 @@ describe('Color mapping - color generation', () => { const colorFactory = getColorFactory( { ...DEFAULT_COLOR_MAPPING_CONFIG, + specialAssignments: [ + { + color: { + type: 'loop', + }, + rule: { + type: 'other', + }, + touched: false, + }, + ], paletteId: twoColorPalette.id, }, simplifiedGetPaletteGn, @@ -64,23 +94,58 @@ describe('Color mapping - color generation', () => { ); expect(colorFactory('cat1')).toBe('#ff0000'); expect(colorFactory('cat2')).toBe('#0000ff'); - // return a palette color only up to the max number of color in the palette - expect(colorFactory('cat3')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); - expect(colorFactory('cat4')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + // the palette will loop depending on the number of colors available + expect(colorFactory('cat3')).toBe('#ff0000'); + expect(colorFactory('cat4')).toBe('#0000ff'); }); - // currently there is no difference in the two colors, but this could change in the future - // this test will catch the change - it('returns EUI dark colors from default config', () => { - const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, true, { - type: 'categories', - categories: ['catA', 'catB', 'catC'], - }); - expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); - expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); - expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); - // if the category is not available in the `categories` list then a default neutral color is used - expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]); + it('returns the unassigned color if configured statically', () => { + const twoColorPalette: ColorMapping.CategoricalPalette = { + id: 'twoColors', + name: 'twoColors', + colorCount: 2, + type: 'categorical', + getColor(indexInRange, darkMode, loop) { + return ['red', 'blue'][loop ? indexInRange % 2 : indexInRange]; + }, + }; + + const simplifiedGetPaletteGn = getPalette( + new Map([[twoColorPalette.id, twoColorPalette]]), + NeutralPalette + ); + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + specialAssignments: [ + { + color: { + type: 'categorical', + paletteId: NeutralPalette.id, + colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + }, + rule: { + type: 'other', + }, + touched: false, + }, + ], + paletteId: twoColorPalette.id, + }, + simplifiedGetPaletteGn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3', 'cat4'], + } + ); + expect(colorFactory('cat1')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + expect(colorFactory('cat2')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + expect(colorFactory('cat3')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + expect(colorFactory('cat4')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + // if the category is not available in the `categories` list then a default netural is used + // this is an edge case and ideally never happen + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); }); it('handles special tokens, multi-field categories and non-trimmed whitespaces', () => { @@ -89,19 +154,19 @@ describe('Color mapping - color generation', () => { categories: ['__other__', ['fieldA', 'fieldB'], '__empty__', ' with-whitespaces '], }); expect(colorFactory('__other__')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); - expect(colorFactory(['fieldA', 'fieldB'])).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); + // expect(colorFactory(['fieldA', 'fieldB'])).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); expect(colorFactory('__empty__')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); expect(colorFactory(' with-whitespaces ')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[3]); }); - it('ignores configured assignments in auto mode', () => { + it('ignores configured assignments in loop mode', () => { const colorFactory = getColorFactory( { ...DEFAULT_COLOR_MAPPING_CONFIG, assignments: [ { color: { type: 'colorCode', colorCode: 'red' }, - rule: { type: 'matchExactly', values: ['assignmentToIgnore'] }, + rule: { type: 'matchExactly', values: ['configuredAssignment'] }, touched: false, }, ], @@ -110,19 +175,19 @@ describe('Color mapping - color generation', () => { false, { type: 'categories', - categories: ['catA', 'catB', 'assignmentToIgnore'], + categories: ['catA', 'catB', 'configuredAssignment', 'nextCat'], } ); expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); - expect(colorFactory('assignmentToIgnore')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); + expect(colorFactory('configuredAssignment')).toBe('red'); + expect(colorFactory('nextCat')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); }); it('color with auto rule are assigned in order of the configured data input', () => { const colorFactory = getColorFactory( { ...DEFAULT_COLOR_MAPPING_CONFIG, - assignmentMode: 'manual', assignments: [ { color: { type: 'colorCode', colorCode: 'red' }, @@ -154,7 +219,8 @@ describe('Color mapping - color generation', () => { expect(colorFactory('redCat')).toBe('red'); // this matches with the second availabe "auto" rule expect(colorFactory('greenCat')).toBe('green'); - // if the category is not available in the `categories` list then a default neutral color is used + // if the category is not available in the `categories` list then a default netural is used + // this is an edge case and ideally never happen expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); }); @@ -188,7 +254,7 @@ describe('Color mapping - color generation', () => { expect(toHex(colorFactory('cat3'))).toBe('#cce8e0'); }); - it('returns sequential gradient colors from lighter to darker [asc, lightMode]', () => { + it('sequential gradient colors from lighter to darker [asc, lightMode]', () => { const colorFactory = getColorFactory( { ...DEFAULT_COLOR_MAPPING_CONFIG, @@ -212,10 +278,59 @@ describe('Color mapping - color generation', () => { categories: ['cat1', 'cat2', 'cat3'], } ); + // light green expect(toHex(colorFactory('cat1'))).toBe('#cce8e0'); + // mid green point expect(toHex(colorFactory('cat2'))).toBe('#93cebc'); + // initial gradient color + expect(toHex(colorFactory('cat3'))).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); + }); + it('sequential gradients and static color from lighter to darker [asc, lightMode]', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + assignments: [ + { color: { type: 'gradient' }, rule: { type: 'auto' }, touched: false }, + { color: { type: 'gradient' }, rule: { type: 'auto' }, touched: false }, + ], + + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + touched: false, + }, + ], + sort: 'asc', + }, + specialAssignments: [ + { + color: { + type: 'categorical', + colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + paletteId: NeutralPalette.id, + }, + rule: { + type: 'other', + }, + touched: false, + }, + ], + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3'], + } + ); + expect(toHex(colorFactory('cat1'))).toBe('#cce8e0'); + expect(toHex(colorFactory('cat2'))).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); // this matches exactly with the initial step selected - expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); + expect(toHex(colorFactory('cat3'))).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); }); it('returns 2 colors gradient [desc, lightMode]', () => { @@ -287,8 +402,8 @@ describe('Color mapping - color generation', () => { expect(toHex(colorFactory('cat1'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[2])); // EUI pink expect(toHex(colorFactory('cat2'))).toBe(NEUTRAL_COLOR_DARK[0]); // NEUTRAL LIGHT GRAY expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); // EUI green - expect(toHex(colorFactory('not available cat'))).toBe( - toHex(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]) - ); // check the other + // if the category is not available in the `categories` list then a default netural is used + // this is an edge case and ideally never happen + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]); }); }); diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts index 795f94b740e9b..8867b07572308 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts @@ -6,25 +6,32 @@ * Side Public License, v 1. */ import chroma from 'chroma-js'; +import { findLast } from 'lodash'; import { ColorMapping } from '../config'; import { changeAlpha, combineColors, getValidColor } from './color_math'; -import { generateAutoAssignmentsForCategories } from '../config/assignment_from_categories'; -import { getPalette } from '../palettes'; +import { getPalette, NeutralPalette } from '../palettes'; import { ColorMappingInputData } from '../categorical_color_mapping'; import { ruleMatch } from './rule_matching'; import { GradientColorMode } from '../config/types'; +import { + DEFAULT_NEUTRAL_PALETTE_INDEX, + DEFAULT_OTHER_ASSIGNMENT_INDEX, +} from '../config/default_color_mapping'; export function getAssignmentColor( colorMode: ColorMapping.Config['colorMode'], - color: ColorMapping.Config['assignments'][number]['color'], + color: + | ColorMapping.Config['assignments'][number]['color'] + | (ColorMapping.LoopColor & { paletteId: string; colorIndex: number }), getPaletteFn: ReturnType, isDarkMode: boolean, index: number, total: number -) { +): string { switch (color.type) { case 'colorCode': case 'categorical': + case 'loop': return getColor(color, getPaletteFn, isDarkMode); case 'gradient': { if (colorMode.type === 'categorical') { @@ -37,31 +44,28 @@ export function getAssignmentColor( } export function getColor( - color: ColorMapping.ColorCode | ColorMapping.CategoricalColor, + color: + | ColorMapping.ColorCode + | ColorMapping.CategoricalColor + | (ColorMapping.LoopColor & { paletteId: string; colorIndex: number }), getPaletteFn: ReturnType, isDarkMode: boolean -) { +): string { return color.type === 'colorCode' ? color.colorCode - : getValidColor(getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode)).hex(); + : getValidColor( + getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode, true) + ).hex(); } export function getColorFactory( - model: ColorMapping.Config, + { assignments, specialAssignments, colorMode, paletteId }: ColorMapping.Config, getPaletteFn: ReturnType, isDarkMode: boolean, data: ColorMappingInputData ): (category: string | string[]) => string { - const palette = getPaletteFn(model.paletteId); - // generate on-the-fly assignments in auto-mode based on current data. - // This simplify the code by always using assignments, even if there is no real static assigmnets - const assignments = - model.assignmentMode === 'auto' - ? generateAutoAssignmentsForCategories(data, palette, model.colorMode) - : model.assignments; - // find auto-assigned colors - const autoAssignedColors = + const autoByOrderAssignments = data.type === 'categories' ? assignments.filter((a) => { return ( @@ -71,75 +75,90 @@ export function getColorFactory( : []; // find all categories that doesn't match with an assignment - const nonAssignedCategories = + const notAssignedCategories = data.type === 'categories' ? data.categories.filter((category) => { return !assignments.some(({ rule }) => ruleMatch(rule, category)); }) : []; + const lastCategorical = findLast(assignments, (d) => { + return d.color.type === 'categorical'; + }); + const nextCategoricalIndex = + lastCategorical?.color.type === 'categorical' ? lastCategorical.color.colorIndex + 1 : 0; + return (category: string | string[]) => { if (typeof category === 'string' || Array.isArray(category)) { - const nonAssignedCategoryIndex = nonAssignedCategories.indexOf(category); + const nonAssignedCategoryIndex = notAssignedCategories.indexOf(category); - // return color for a non assigned category + // this category is not assigned to a specific color if (nonAssignedCategoryIndex > -1) { - if (nonAssignedCategoryIndex < autoAssignedColors.length) { + // if the category order is within current number of auto-assigned items pick the defined color + if (nonAssignedCategoryIndex < autoByOrderAssignments.length) { const autoAssignmentIndex = assignments.findIndex( - (d) => d === autoAssignedColors[nonAssignedCategoryIndex] + (d) => d === autoByOrderAssignments[nonAssignedCategoryIndex] ); return getAssignmentColor( - model.colorMode, - autoAssignedColors[nonAssignedCategoryIndex].color, + colorMode, + autoByOrderAssignments[nonAssignedCategoryIndex].color, getPaletteFn, isDarkMode, autoAssignmentIndex, assignments.length ); } - // if no auto-assign color rule/color is available then use the other color - // TODO: the specialAssignment[0] position is arbitrary, we should fix it better - return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode); - } + const totalColorsIfGradient = assignments.length || notAssignedCategories.length; + const indexIfGradient = + (nonAssignedCategoryIndex - autoByOrderAssignments.length) % totalColorsIfGradient; - // find the assignment where the category matches the rule - const matchingAssignmentIndex = assignments.findIndex(({ rule }) => { - return ruleMatch(rule, category); - }); - - // return the assigned color - if (matchingAssignmentIndex > -1) { - const assignment = assignments[matchingAssignmentIndex]; + // if no auto-assign color rule/color is available then use the color looping palette return getAssignmentColor( - model.colorMode, - assignment.color, + colorMode, + // TODO: the specialAssignment[0] position is arbitrary, we should fix it better + specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color.type === 'loop' + ? colorMode.type === 'gradient' + ? { type: 'gradient' } + : { + type: 'loop', + // those are applied here and depends on the current non-assigned category - auto-assignment list + colorIndex: + nonAssignedCategoryIndex - autoByOrderAssignments.length + nextCategoricalIndex, + paletteId, + } + : specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color, getPaletteFn, isDarkMode, - matchingAssignmentIndex, - assignments.length + indexIfGradient, + totalColorsIfGradient ); } - // if no assign color rule/color is available then use the other color - // TODO: the specialAssignment[0] position is arbitrary, we should fix it better - return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode); - } else { - const matchingAssignmentIndex = assignments.findIndex(({ rule }) => { - return ruleMatch(rule, category); - }); + } + // find the assignment where the category matches the rule + const matchingAssignmentIndex = assignments.findIndex(({ rule }) => { + return ruleMatch(rule, category); + }); - if (matchingAssignmentIndex > -1) { - const assignment = assignments[matchingAssignmentIndex]; - return getAssignmentColor( - model.colorMode, - assignment.color, - getPaletteFn, - isDarkMode, - matchingAssignmentIndex, - assignments.length - ); - } - return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode); + if (matchingAssignmentIndex > -1) { + const assignment = assignments[matchingAssignmentIndex]; + return getAssignmentColor( + colorMode, + assignment.color, + getPaletteFn, + isDarkMode, + matchingAssignmentIndex, + assignments.length + ); } + return getColor( + { + type: 'categorical', + paletteId: NeutralPalette.id, + colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + }, + getPaletteFn, + isDarkMode + ); }; } diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts index 0d844ca26e27e..a157c7927747c 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts @@ -23,7 +23,7 @@ export function ruleMatch( } return rule.values.includes(`${value}`); case 'matchExactlyCI': - return rule.values.some((d) => d.toLowerCase() === `${value}`); + return rule.values.some((d) => d.toLowerCase() === `${value}`.toLowerCase()); case 'range': // TODO: color by value not yet possible in all charts in elastic-charts return typeof value === 'number' ? rangeMatch(rule, value) : false; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx index 896f2ea392884..89c4375d4bc10 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx @@ -31,8 +31,6 @@ export function Assignment({ disableDelete, index, total, - canPickColor, - editable, palette, colorMode, getPaletteFn, @@ -48,8 +46,6 @@ export function Assignment({ disableDelete: boolean; palette: ColorMapping.CategoricalPalette; getPaletteFn: ReturnType; - canPickColor: boolean; - editable: boolean; isDarkMode: boolean; specialTokens: Map; assignmentValuesCounter: Map; @@ -57,18 +53,12 @@ export function Assignment({ const dispatch = useDispatch(); return ( - + { const rule: ColorMapping.RuleRange = { type: 'range', diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx index 1f57e731e84c0..bfef7a270e1a0 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx @@ -15,7 +15,6 @@ import { ColorMapping } from '../../config'; export const Match: React.FC<{ index: number; - editable: boolean; rule: | ColorMapping.RuleAuto | ColorMapping.RuleMatchExactly @@ -25,7 +24,7 @@ export const Match: React.FC<{ options: Array; specialTokens: Map; assignmentValuesCounter: Map; -}> = ({ index, rule, updateValue, editable, options, specialTokens, assignmentValuesCounter }) => { +}> = ({ index, rule, updateValue, options, specialTokens, assignmentValuesCounter }) => { const duplicateWarning = i18n.translate( 'coloring.colorMapping.assignments.duplicateCategoryWarning', { @@ -75,7 +74,6 @@ export const Match: React.FC<{ { - if (selectedOptions.findIndex((option) => option.label.toLowerCase() === label) === -1) { + if (selectedOptions.findIndex((option) => option.label === label) === -1) { updateValue([...selectedOptions, { label, value: label }].map((d) => d.value)); } }} + isCaseSensitive isClearable={false} compressed /> diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx index 70f2cf49609e0..e006a7b23543f 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx @@ -12,9 +12,8 @@ import { ColorMapping } from '../../config'; export const Range: React.FC<{ rule: ColorMapping.RuleRange; - editable: boolean; updateValue: (min: number, max: number, minInclusive: boolean, maxInclusive: boolean) => void; -}> = ({ rule, updateValue, editable }) => { +}> = ({ rule, updateValue }) => { const minValid = rule.min <= rule.max; const maxValid = rule.max >= rule.min; @@ -34,7 +33,6 @@ export const Range: React.FC<{ placeholder="min" value={rule.min} isInvalid={!minValid} - disabled={!editable} onChange={(e) => updateValue(+e.currentTarget.value, rule.max, rule.minInclusive, rule.maxInclusive) } @@ -54,7 +52,6 @@ export const Range: React.FC<{ } placeholder="max" - disabled={!editable} value={rule.max} onChange={(e) => updateValue(rule.min, +e.currentTarget.value, rule.minInclusive, rule.maxInclusive) diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx index 29ede59e37f41..fff892fc9cc7b 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx @@ -6,17 +6,16 @@ * Side Public License, v 1. */ -import { EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import React from 'react'; -import { i18n } from '@kbn/i18n'; import { ColorMapping } from '../../config'; import { getPalette } from '../../palettes'; import { ColorSwatch } from '../color_picker/color_swatch'; import { updateSpecialAssignmentColor } from '../../state/color_mapping'; +import { ColorCode, CategoricalColor } from '../../config/types'; export function SpecialAssignment({ - assignment, + assignmentColor, index, palette, getPaletteFn, @@ -25,55 +24,31 @@ export function SpecialAssignment({ }: { isDarkMode: boolean; index: number; - assignment: ColorMapping.Config['specialAssignments'][number]; + assignmentColor: CategoricalColor | ColorCode; palette: ColorMapping.CategoricalPalette; getPaletteFn: ReturnType; total: number; }) { const dispatch = useDispatch(); - const canPickColor = true; return ( - - - { - dispatch( - updateSpecialAssignmentColor({ - assignmentIndex: index, - color, - }) - ); - }} - /> - - - - - + { + dispatch( + updateSpecialAssignmentColor({ + assignmentIndex: index, + color, + }) + ); + }} + /> ); } diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx index e1e8a08aa6b22..f576daa2096cc 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx @@ -107,7 +107,7 @@ export function ColorPicker({ style={{ paddingBottom: 8 }} > {i18n.translate('coloring.colorMapping.colorPicker.removeGradientColorButtonLabel', { - defaultMessage: 'Remove color step', + defaultMessage: 'Remove color stop', })} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx index 8ddc56d2476c7..34ffbefeca30f 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx @@ -29,11 +29,8 @@ import { getValidColor } from '../../color/color_math'; interface ColorPickerSwatchProps { colorMode: ColorMapping.Config['colorMode']; - assignmentColor: - | ColorMapping.Config['assignments'][number]['color'] - | ColorMapping.Config['specialAssignments'][number]['color']; + assignmentColor: ColorMapping.Config['assignments'][number]['color']; getPaletteFn: ReturnType; - canPickColor: boolean; index: number; total: number; palette: ColorMapping.CategoricalPalette; @@ -46,7 +43,6 @@ export const ColorSwatch = ({ colorMode, assignmentColor, getPaletteFn, - canPickColor, index, total, palette, @@ -71,7 +67,7 @@ export const ColorSwatch = ({ ); const colorIsDark = isColorDark(...getValidColor(colorHex).rgb()); const euiTheme = useEuiTheme(); - return canPickColor && assignmentColor.type !== 'gradient' ? ( + return assignmentColor.type !== 'gradient' ? ( ) : ( @@ -121,7 +117,7 @@ export const ColorSwatch = ({ style={{ // the color swatch can't pickup colors written in rgb/css standard backgroundColor: colorHex, - cursor: canPickColor ? 'pointer' : 'not-allowed', + cursor: 'pointer', width: 32, height: 32, }} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx index 21aa18a49f9dc..77a8273654b89 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx @@ -35,16 +35,16 @@ export function PaletteColors({ selectColor: (color: ColorMapping.CategoricalColor | ColorMapping.ColorCode) => void; }) { const colors = Array.from({ length: palette.colorCount }, (d, i) => { - return palette.getColor(i, isDarkMode); + return palette.getColor(i, isDarkMode, false); }); const neutralColors = Array.from({ length: NeutralPalette.colorCount }, (d, i) => { - return NeutralPalette.getColor(i, isDarkMode); + return NeutralPalette.getColor(i, isDarkMode, false); }); const originalColor = color.type === 'categorical' ? color.paletteId === NeutralPalette.id - ? NeutralPalette.getColor(color.colorIndex, isDarkMode) - : getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode) + ? NeutralPalette.getColor(color.colorIndex, isDarkMode, false) + : getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode, false) : color.colorCode; return ( <> diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx index 84f6786922f44..28f155b85fe9b 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx @@ -48,7 +48,8 @@ export function RGBPicker({ customColorMappingColor.type === 'categorical' ? getPaletteFn(customColorMappingColor.paletteId).getColor( customColorMappingColor.colorIndex, - isDarkMode + isDarkMode, + false ) : customColorMappingColor.colorCode; @@ -142,7 +143,7 @@ export function RGBPicker({ if (chromajs.valid(textColor)) { setCustomColorMappingColor({ type: 'colorCode', - colorCode: chromajs(textColor).hex(), + colorCode: textColor, }); } }} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/assigments.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/assigments.tsx new file mode 100644 index 0000000000000..f525311b8afb3 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/assigments.tsx @@ -0,0 +1,329 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiNotificationBadge, + EuiPanel, + EuiPopover, + EuiText, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { i18n } from '@kbn/i18n'; +import { useDispatch, useSelector } from 'react-redux'; +import { findLast } from 'lodash'; +import { Assignment } from '../assignment/assignment'; +import { + addNewAssignment, + addNewAssignments, + removeAllAssignments, +} from '../../state/color_mapping'; +import { selectColorMode, selectComputedAssignments, selectPalette } from '../../state/selectors'; +import { ColorMappingInputData } from '../../categorical_color_mapping'; +import { ColorMapping } from '../../config'; +import { getPalette, NeutralPalette } from '../../palettes'; +import { ruleMatch } from '../../color/rule_matching'; + +export function AssignmentsConfig({ + data, + palettes, + isDarkMode, + specialTokens, +}: { + palettes: Map; + data: ColorMappingInputData; + isDarkMode: boolean; + /** map between original and formatted tokens used to handle special cases, like the Other bucket and the empty bucket */ + specialTokens: Map; +}) { + const [showOtherActions, setShowOtherActions] = useState(false); + + const dispatch = useDispatch(); + const getPaletteFn = getPalette(palettes, NeutralPalette); + const palette = useSelector(selectPalette(getPaletteFn)); + const colorMode = useSelector(selectColorMode); + const assignments = useSelector(selectComputedAssignments); + + const unmatchingCategories = useMemo(() => { + return data.type === 'categories' + ? data.categories.filter((category) => { + return !assignments.some(({ rule }) => ruleMatch(rule, category)); + }) + : []; + }, [data, assignments]); + + const assignmentValuesCounter = assignments.reduce>( + (acc, assignment) => { + const values = assignment.rule.type === 'matchExactly' ? assignment.rule.values : []; + values.forEach((value) => { + acc.set(value, (acc.get(value) ?? 0) + 1); + }); + return acc; + }, + new Map() + ); + + const onClickAddNewAssignment = useCallback(() => { + const lastCategorical = findLast(assignments, (d) => { + return d.color.type === 'categorical'; + }); + const nextCategoricalIndex = + lastCategorical?.color.type === 'categorical' ? lastCategorical.color.colorIndex + 1 : 0; + dispatch( + addNewAssignment({ + rule: + data.type === 'categories' + ? { + type: 'matchExactly', + values: [], + } + : { type: 'range', min: 0, max: 0, minInclusive: true, maxInclusive: true }, + color: + colorMode.type === 'categorical' + ? { + type: 'categorical', + paletteId: palette.id, + colorIndex: nextCategoricalIndex % palette.colorCount, + } + : { type: 'gradient' }, + touched: false, + }) + ); + }, [assignments, colorMode.type, data.type, dispatch, palette.colorCount, palette.id]); + + const onClickAddAllCurrentCategories = useCallback(() => { + if (data.type === 'categories') { + const lastCategorical = findLast(assignments, (d) => { + return d.color.type === 'categorical'; + }); + const nextCategoricalIndex = + lastCategorical?.color.type === 'categorical' ? lastCategorical.color.colorIndex + 1 : 0; + + const newAssignments: ColorMapping.Config['assignments'] = unmatchingCategories.map( + (c, i) => { + return { + rule: { + type: 'matchExactly', + values: [c], + }, + color: + colorMode.type === 'categorical' + ? { + type: 'categorical', + paletteId: palette.id, + colorIndex: (nextCategoricalIndex + i) % palette.colorCount, + } + : { type: 'gradient' }, + touched: false, + }; + } + ); + dispatch(addNewAssignments(newAssignments)); + } + }, [ + dispatch, + assignments, + colorMode.type, + data.type, + palette.colorCount, + palette.id, + unmatchingCategories, + ]); + + return ( + +
+ + {assignments.map((assignment, i) => { + return ( + + ); + })} + {assignments.length === 0 && ( + +

+ {i18n.translate( + 'coloring.colorMapping.container.mapValuesPromptDescription.mapValuesPromptDetail', + { + defaultMessage: + 'Add new assignments to begin associating terms in your data with specified colors.', + } + )} +

+ + } + actions={[ + + {i18n.translate('coloring.colorMapping.container.AddAssignmentButtonLabel', { + defaultMessage: 'Add assignment', + })} + , + + {i18n.translate('coloring.colorMapping.container.mapValueButtonLabel', { + defaultMessage: 'Add all unassigned terms', + })} + , + ]} + /> + )} +
+
+ {assignments.length > 0 && } +
+ {assignments.length > 0 && ( + + + {i18n.translate('coloring.colorMapping.container.AddAssignmentButtonLabel', { + defaultMessage: 'Add assignment', + })} + + {data.type === 'categories' && ( + setShowOtherActions(true)} + /> + } + isOpen={showOtherActions} + closePopover={() => setShowOtherActions(false)} + panelPaddingSize="xs" + anchorPosition="downRight" + ownFocus + > + { + setShowOtherActions(false); + requestAnimationFrame(() => { + onClickAddAllCurrentCategories(); + }); + }} + disabled={unmatchingCategories.length === 0} + > + + + {i18n.translate( + 'coloring.colorMapping.container.mapCurrentValuesButtonLabel', + { + defaultMessage: 'Add all unsassigned terms', + } + )} + + {unmatchingCategories.length > 0 && ( + + + {unmatchingCategories.length} + + + )} + + , + } + onClick={() => { + setShowOtherActions(false); + dispatch(removeAllAssignments()); + }} + color="danger" + > + {i18n.translate( + 'coloring.colorMapping.container.clearAllAssignmentsButtonLabel', + { + defaultMessage: 'Clear all assignments', + } + )} + , + ]} + /> + + )} + + )} +
+
+ ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx index 748f17fa45842..e3de3c0a24261 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx @@ -8,56 +8,28 @@ import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFormLabel, - EuiHorizontalRule, - EuiPanel, - EuiSwitch, - EuiText, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; -import { Assignment } from '../assignment/assignment'; -import { SpecialAssignment } from '../assignment/special_assignment'; import { PaletteSelector } from '../palette_selector/palette_selector'; -import { - RootState, - addNewAssignment, - assignAutomatically, - assignStatically, - changeGradientSortOrder, -} from '../../state/color_mapping'; -import { generateAutoAssignmentsForCategories } from '../../config/assignment_from_categories'; +import { changeGradientSortOrder } from '../../state/color_mapping'; import { ColorMapping } from '../../config'; import { getPalette } from '../../palettes'; -import { getUnusedColorForNewAssignment } from '../../config/assignments'; -import { - selectColorMode, - selectPalette, - selectSpecialAssignments, - selectIsAutoAssignmentMode, -} from '../../state/selectors'; +import { selectColorMode, selectComputedAssignments, selectPalette } from '../../state/selectors'; import { ColorMappingInputData } from '../../categorical_color_mapping'; import { Gradient } from '../palette_selector/gradient'; import { NeutralPalette } from '../../palettes/neutral'; +import { ScaleMode } from '../palette_selector/scale'; +import { UnassignedTermsConfig } from './unassigned_terms_config'; +import { AssignmentsConfig } from './assigments'; -export const MAX_ASSIGNABLE_COLORS = 10; - -function selectComputedAssignments( - data: ColorMappingInputData, - palette: ColorMapping.CategoricalPalette, - colorMode: ColorMapping.Config['colorMode'] -) { - return (state: RootState) => - state.colorMapping.assignmentMode === 'auto' - ? generateAutoAssignmentsForCategories(data, palette, colorMode) - : state.colorMapping.assignments; -} -export function Container(props: { +export function Container({ + data, + palettes, + isDarkMode, + specialTokens, +}: { palettes: Map; data: ColorMappingInputData; isDarkMode: boolean; @@ -66,187 +38,99 @@ export function Container(props: { }) { const dispatch = useDispatch(); - const getPaletteFn = getPalette(props.palettes, NeutralPalette); + const getPaletteFn = getPalette(palettes, NeutralPalette); const palette = useSelector(selectPalette(getPaletteFn)); const colorMode = useSelector(selectColorMode); - const autoAssignmentMode = useSelector(selectIsAutoAssignmentMode); - const assignments = useSelector(selectComputedAssignments(props.data, palette, colorMode)); - const specialAssignments = useSelector(selectSpecialAssignments); - - const canAddNewAssignment = !autoAssignmentMode && assignments.length < MAX_ASSIGNABLE_COLORS; - - const assignmentValuesCounter = assignments.reduce>( - (acc, assignment) => { - const values = assignment.rule.type === 'matchExactly' ? assignment.rule.values : []; - values.forEach((value) => { - acc.set(value, (acc.get(value) ?? 0) + 1); - }); - return acc; - }, - new Map() - ); + const assignments = useSelector(selectComputedAssignments); return ( - - - - + - + - - {i18n.translate('coloring.colorMapping.container.mappingAssignmentHeader', { - defaultMessage: 'Mapping assignments', - })} - + - - {i18n.translate('coloring.colorMapping.container.autoAssignLabel', { - defaultMessage: 'Auto assign', - })} - - } - checked={autoAssignmentMode} - compressed - onChange={() => { - if (autoAssignmentMode) { - dispatch(assignStatically(assignments)); - } else { - dispatch(assignAutomatically()); - } - }} - /> + - - + {colorMode.type === 'gradient' && ( +
- {colorMode.type !== 'gradient' ? null : ( - - )} - {assignments.map((assignment, i) => { - return ( -
- -
- ); - })} -
- - - - - {props.data.type === 'categories' && - specialAssignments.map((assignment, i) => { - return ( - - ); - })} - - -
- - - { - dispatch( - addNewAssignment({ - rule: - props.data.type === 'categories' - ? { - type: 'matchExactly', - values: [], - } - : { type: 'range', min: 0, max: 0, minInclusive: true, maxInclusive: true }, - color: getUnusedColorForNewAssignment(palette, colorMode, assignments), - touched: false, - }) - ); - }} - disabled={!canAddNewAssignment} - css={css` - margin-right: 8px; - `} - > - {i18n.translate('coloring.colorMapping.container.addAssignmentButtonLabel', { - defaultMessage: 'Add assignment', + - {colorMode.type === 'gradient' && ( - + { dispatch(changeGradientSortOrder(colorMode.sort === 'asc' ? 'desc' : 'asc')); }} - > - {i18n.translate('coloring.colorMapping.container.invertGradientButtonLabel', { - defaultMessage: 'Invert gradient', - })} - - )} - - + /> +
+ + + + + + )} + + + + + + {assignments.length > 0 && ( + + + + )} ); } diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/unassigned_terms_config.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/unassigned_terms_config.tsx new file mode 100644 index 0000000000000..405437a34e35b --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/unassigned_terms_config.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { + EuiButtonGroup, + EuiButtonGroupOptionProps, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useDispatch, useSelector } from 'react-redux'; +import { css } from '@emotion/react'; +import { updateSpecialAssignmentColor } from '../../state/color_mapping'; +import { getPalette, NeutralPalette } from '../../palettes'; +import { + DEFAULT_NEUTRAL_PALETTE_INDEX, + DEFAULT_OTHER_ASSIGNMENT_INDEX, +} from '../../config/default_color_mapping'; +import { SpecialAssignment } from '../assignment/special_assignment'; +import { ColorMapping } from '../../config'; +import { selectColorMode, selectPalette, selectSpecialAssignments } from '../../state/selectors'; +import { ColorMappingInputData } from '../../categorical_color_mapping'; + +export function UnassignedTermsConfig({ + palettes, + data, + isDarkMode, +}: { + palettes: Map; + data: ColorMappingInputData; + isDarkMode: boolean; +}) { + const dispatch = useDispatch(); + + const getPaletteFn = getPalette(palettes, NeutralPalette); + + const palette = useSelector(selectPalette(getPaletteFn)); + const colorMode = useSelector(selectColorMode); + const specialAssignments = useSelector(selectSpecialAssignments); + const otherAssignment = specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX]; + + const colorModes: EuiButtonGroupOptionProps[] = [ + { + id: 'loop', + label: + colorMode.type === 'gradient' + ? i18n.translate( + 'coloring.colorMapping.container.unassignedTermsMode.ReuseGradientLabel', + { + defaultMessage: 'Gradient', + } + ) + : i18n.translate('coloring.colorMapping.container.unassignedTermsMode.ReuseColorsLabel', { + defaultMessage: 'Color palette', + }), + }, + { + id: 'static', + label: i18n.translate( + 'coloring.colorMapping.container.unassignedTermsMode.SingleColorLabel', + { + defaultMessage: 'Single color', + } + ), + }, + ]; + + return ( + + + + { + dispatch( + updateSpecialAssignmentColor({ + assignmentIndex: 0, + color: + optionId === 'loop' + ? { + type: 'loop', + } + : { + type: 'categorical', + colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + paletteId: NeutralPalette.id, + }, + }) + ); + }} + buttonSize="compressed" + isFullWidth + /> + + + +
+ {data.type === 'categories' && otherAssignment.color.type !== 'loop' && ( + + )} +
+
+
+
+ ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx index c4e22f797deaa..99eda60166ac4 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx @@ -6,331 +6,174 @@ * Side Public License, v 1. */ -import { euiFocusRing, EuiIcon, euiShadowSmall, useEuiTheme } from '@elastic/eui'; import React from 'react'; -import { useDispatch } from 'react-redux'; - import { euiThemeVars } from '@kbn/ui-theme'; import { css } from '@emotion/react'; +import { useDispatch } from 'react-redux'; import { changeAlpha } from '../../color/color_math'; - import { ColorMapping } from '../../config'; -import { ColorSwatch } from '../color_picker/color_swatch'; import { getPalette } from '../../palettes'; - -import { addGradientColorStep, updateGradientColorStep } from '../../state/color_mapping'; -import { colorPickerVisibility } from '../../state/ui'; import { getGradientColorScale } from '../../color/color_handling'; +import { AddStop } from './gradient_add_stop'; +import { ColorSwatch } from '../color_picker/color_swatch'; +import { updateGradientColorStep } from '../../state/color_mapping'; export function Gradient({ paletteId, colorMode, getPaletteFn, isDarkMode, - assignmentsSize, }: { paletteId: string; isDarkMode: boolean; colorMode: ColorMapping.Config['colorMode']; getPaletteFn: ReturnType; - assignmentsSize: number; }) { + const dispatch = useDispatch(); if (colorMode.type === 'categorical') { return null; } + const currentPalette = getPaletteFn(paletteId); const gradientColorScale = getGradientColorScale(colorMode, getPaletteFn, isDarkMode); - const topMostColorStop = + const startStepColor = colorMode.sort === 'asc' ? colorMode.steps.length === 1 ? undefined : colorMode.steps.at(-1) : colorMode.steps.at(0); - const topMostColorStopIndex = + const startStepIndex = colorMode.sort === 'asc' ? colorMode.steps.length === 1 ? NaN : colorMode.steps.length - 1 : 0; - const bottomMostColorStop = + const endStepColor = colorMode.sort === 'asc' ? colorMode.steps.at(0) : colorMode.steps.length === 1 ? undefined : colorMode.steps.at(-1); - const bottomMostColorStopIndex = + const endStepIndex = colorMode.sort === 'asc' ? 0 : colorMode.steps.length === 1 ? NaN : colorMode.steps.length - 1; - const middleMostColorSep = colorMode.steps.length === 3 ? colorMode.steps[1] : undefined; - const middleMostColorStopIndex = colorMode.steps.length === 3 ? 1 : NaN; + const middleStepColor = colorMode.steps.length === 3 ? colorMode.steps[1] : undefined; + const middleStepIndex = colorMode.steps.length === 3 ? 1 : NaN; return ( - <> - {assignmentsSize > 1 && ( -
- )} +
+ +
- {topMostColorStop ? ( - { + dispatch(updateGradientColorStep({ index: startStepIndex, color })); + }} /> ) : ( )}
- {assignmentsSize > 1 && ( -
-
- {middleMostColorSep ? ( - - ) : colorMode.steps.length === 2 ? ( - - ) : undefined} -
-
- )} - {assignmentsSize > 1 && ( -
- )} -
- {bottomMostColorStop ? ( - { + dispatch(updateGradientColorStep({ index: middleStepIndex, color })); + }} /> - ) : ( + ) : colorMode.steps.length === 2 ? ( - )} + ) : undefined}
- - ); -} - -function AddStop({ - colorMode, - currentPalette, - at, -}: { - colorMode: { - type: 'gradient'; - steps: Array<(ColorMapping.CategoricalColor | ColorMapping.ColorCode) & { touched: boolean }>; - }; - currentPalette: ColorMapping.CategoricalPalette; - at: number; -}) { - const euiTheme = useEuiTheme(); - const dispatch = useDispatch(); - return ( - - ); -} - -function ColorStop({ - colorMode, - step, - index, - currentPalette, - getPaletteFn, - isDarkMode, -}: { - colorMode: ColorMapping.GradientColorMode; - step: ColorMapping.CategoricalColor | ColorMapping.ColorCode; - index: number; - currentPalette: ColorMapping.CategoricalPalette; - getPaletteFn: ReturnType; - isDarkMode: boolean; -}) { - const dispatch = useDispatch(); - return ( - { - dispatch( - updateGradientColorStep({ - index, - color, - }) - ); - }} - forType="gradient" - /> +
); } diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient_add_stop.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient_add_stop.tsx new file mode 100644 index 0000000000000..713e780bc32da --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient_add_stop.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; + +import { + euiCanAnimate, + euiFocusRing, + EuiIcon, + euiShadowSmall, + EuiToolTip, + useEuiTheme, +} from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { ColorMapping } from '../../config'; +import { addGradientColorStep } from '../../state/color_mapping'; +import { colorPickerVisibility } from '../../state/ui'; + +export function AddStop({ + colorMode, + currentPalette, + at, +}: { + colorMode: ColorMapping.GradientColorMode; + currentPalette: ColorMapping.CategoricalPalette; + at: number; +}) { + const euiTheme = useEuiTheme(); + const dispatch = useDispatch(); + return ( + <> + + + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx index c9fab3526a786..3db54cea6b108 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx @@ -8,14 +8,7 @@ import React, { useCallback, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { - EuiButtonGroup, - EuiColorPalettePicker, - EuiConfirmModal, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, -} from '@elastic/eui'; +import { EuiColorPalettePicker, EuiConfirmModal, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { RootState, updatePalette } from '../../state/color_mapping'; @@ -33,11 +26,8 @@ export function PaletteSelector({ isDarkMode: boolean; }) { const dispatch = useDispatch(); - const colorMode = useSelector((state: RootState) => state.colorMapping.colorMode); const model = useSelector((state: RootState) => state.colorMapping); - const { paletteId } = model; - const switchPaletteFn = useCallback( (selectedPaletteId: string, preserveColorChanges: boolean) => { dispatch( @@ -45,7 +35,6 @@ export function PaletteSelector({ paletteId: selectedPaletteId, assignments: updateAssignmentsPalette( model.assignments, - model.assignmentMode, model.colorMode, selectedPaletteId, getPaletteFn, @@ -62,37 +51,6 @@ export function PaletteSelector({ [getPaletteFn, model, dispatch] ); - const updateColorMode = useCallback( - (type: 'gradient' | 'categorical', preserveColorChanges: boolean) => { - const updatedColorMode: ColorMapping.Config['colorMode'] = - type === 'gradient' - ? { - type: 'gradient', - steps: [ - { - type: 'categorical', - paletteId, - colorIndex: 0, - touched: false, - }, - ], - sort: 'desc', - } - : { type: 'categorical' }; - - const assignments = updateAssignmentsPalette( - model.assignments, - model.assignmentMode, - updatedColorMode, - paletteId, - getPaletteFn, - preserveColorChanges - ); - dispatch(updatePalette({ paletteId, assignments, colorMode: updatedColorMode })); - }, - [getPaletteFn, model, dispatch, paletteId] - ); - const [preserveModalPaletteId, setPreserveModalPaletteId] = useState(null); const preserveChangesModal = @@ -126,136 +84,44 @@ export function PaletteSelector({ ) : null; - const [colorScaleModalId, setColorScaleModalId] = useState<'gradient' | 'categorical' | null>( - null - ); - - const colorScaleModal = - colorScaleModalId !== null ? ( - { - setColorScaleModalId(null); - }} - onConfirm={() => { - if (colorScaleModalId) updateColorMode(colorScaleModalId, false); - setColorScaleModalId(null); - }} - cancelButtonText={i18n.translate( - 'coloring.colorMapping.colorChangesModal.goBackButtonLabel', - { - defaultMessage: 'Go back', - } - )} - confirmButtonText={i18n.translate( - 'coloring.colorMapping.colorChangesModal.discardButtonLabel', - { - defaultMessage: 'Discard changes', - } - )} - defaultFocusedButton="confirm" - buttonColor="danger" - > -

- {colorScaleModalId === 'categorical' - ? i18n.translate('coloring.colorMapping.colorChangesModal.categoricalModeDescription', { - defaultMessage: `Switching to a categorical mode will discard all your custom color changes`, - }) - : i18n.translate('coloring.colorMapping.colorChangesModal.sequentialModeDescription', { - defaultMessage: `Switching to a sequential mode will discard all your custom color changes`, - })} -

-
- ) : null; - return ( <> {preserveChangesModal} - {colorScaleModal} - - - - d.name !== 'Neutral') - .map((palette) => ({ - 'data-test-subj': `kbnColoring_ColorMapping_Palette-${palette.id}`, - value: palette.id, - title: palette.name, - palette: Array.from({ length: palette.colorCount }, (_, i) => { - return palette.getColor(i, isDarkMode); - }), - type: 'fixed', - }))} - onChange={(selectedPaletteId) => { - const hasChanges = model.assignments.some((a) => a.touched); - const hasGradientChanges = - model.colorMode.type === 'gradient' && - model.colorMode.steps.some((a) => a.touched); - if (hasChanges || hasGradientChanges) { - setPreserveModalPaletteId(selectedPaletteId); - } else { - switchPaletteFn(selectedPaletteId, false); - } - }} - valueOfSelected={model.paletteId} - selectionDisplay={'palette'} - compressed={true} - /> - - - - - { - const hasChanges = model.assignments.some((a) => a.touched); - const hasGradientChanges = - model.colorMode.type === 'gradient' && - model.colorMode.steps.some((a) => a.touched); - - if (hasChanges || hasGradientChanges) { - setColorScaleModalId(id as 'gradient' | 'categorical'); - } else { - updateColorMode(id as 'gradient' | 'categorical', false); - } - }} - isIconOnly - /> - - - + + d.name !== 'Neutral') + .map((palette) => ({ + 'data-test-subj': `kbnColoring_ColorMapping_Palette-${palette.id}`, + value: palette.id, + title: palette.name, + palette: Array.from({ length: palette.colorCount }, (_, i) => { + return palette.getColor(i, isDarkMode, false); + }), + type: 'fixed', + }))} + onChange={(selectedPaletteId) => { + const hasChanges = model.assignments.some((a) => a.touched); + const hasGradientChanges = + model.colorMode.type === 'gradient' && model.colorMode.steps.some((a) => a.touched); + if (hasChanges || hasGradientChanges) { + setPreserveModalPaletteId(selectedPaletteId); + } else { + switchPaletteFn(selectedPaletteId, false); + } + }} + valueOfSelected={model.paletteId} + selectionDisplay={'palette'} + compressed={true} + /> + ); } diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale.tsx new file mode 100644 index 0000000000000..056db47157c60 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { EuiButtonGroup, EuiConfirmModal, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { RootState, updatePalette } from '../../state/color_mapping'; +import { ColorMapping } from '../../config'; +import { updateAssignmentsPalette } from '../../config/assignments'; +import { getPalette } from '../../palettes'; + +export function ScaleMode({ getPaletteFn }: { getPaletteFn: ReturnType }) { + const dispatch = useDispatch(); + const colorMode = useSelector((state: RootState) => state.colorMapping.colorMode); + const model = useSelector((state: RootState) => state.colorMapping); + + const { paletteId } = model; + + const updateColorMode = useCallback( + (type: 'gradient' | 'categorical', preserveColorChanges: boolean) => { + const updatedColorMode: ColorMapping.Config['colorMode'] = + type === 'gradient' + ? { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId, + colorIndex: 0, + touched: false, + }, + ], + sort: 'desc', + } + : { type: 'categorical' }; + + const assignments = updateAssignmentsPalette( + model.assignments, + updatedColorMode, + paletteId, + getPaletteFn, + preserveColorChanges + ); + dispatch(updatePalette({ paletteId, assignments, colorMode: updatedColorMode })); + }, + [getPaletteFn, model, dispatch, paletteId] + ); + + const [colorScaleModalId, setColorScaleModalId] = useState<'gradient' | 'categorical' | null>( + null + ); + + const colorScaleModal = + colorScaleModalId !== null ? ( + { + setColorScaleModalId(null); + }} + onConfirm={() => { + if (colorScaleModalId) updateColorMode(colorScaleModalId, false); + setColorScaleModalId(null); + }} + cancelButtonText={i18n.translate( + 'coloring.colorMapping.colorChangesModal.goBackButtonLabel', + { + defaultMessage: 'Go back', + } + )} + confirmButtonText={i18n.translate( + 'coloring.colorMapping.colorChangesModal.discardButtonLabel', + { + defaultMessage: 'Discard changes', + } + )} + defaultFocusedButton="confirm" + buttonColor="danger" + > +

+ {colorScaleModalId === 'categorical' + ? i18n.translate('coloring.colorMapping.colorChangesModal.categoricalModeDescription', { + defaultMessage: `Switching to a categorical mode will discard all your custom color changes`, + }) + : i18n.translate('coloring.colorMapping.colorChangesModal.sequentialModeDescription', { + defaultMessage: `Switching to a gradient mode will discard all your custom color changes`, + })} +

+
+ ) : null; + + return ( + <> + {colorScaleModal} + + { + const hasChanges = model.assignments.some((a) => a.touched); + const hasGradientChanges = + model.colorMode.type === 'gradient' && model.colorMode.steps.some((a) => a.touched); + + if (hasChanges || hasGradientChanges) { + setColorScaleModalId(id as 'gradient' | 'categorical'); + } else { + updateColorMode(id as 'gradient' | 'categorical', false); + } + }} + /> + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts deleted file mode 100644 index 97c4d17c35e4d..0000000000000 --- a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ColorMapping } from '.'; -import { ColorMappingInputData } from '../categorical_color_mapping'; -import { MAX_ASSIGNABLE_COLORS } from '../components/container/container'; - -export function generateAutoAssignmentsForCategories( - data: ColorMappingInputData, - palette: ColorMapping.CategoricalPalette, - colorMode: ColorMapping.Config['colorMode'] -): ColorMapping.Config['assignments'] { - const isCategorical = colorMode.type === 'categorical'; - - const maxColorAssignable = data.type === 'categories' ? data.categories.length : data.bins; - - const assignableColors = isCategorical - ? Math.min(palette.colorCount, maxColorAssignable) - : Math.min(MAX_ASSIGNABLE_COLORS, maxColorAssignable); - - const autoRules: Array = - data.type === 'categories' - ? data.categories.map((c) => ({ type: 'matchExactly', values: [c] })) - : Array.from({ length: data.bins }, (d, i) => { - const step = (data.max - data.min) / data.bins; - return { - type: 'range', - min: data.max - i * step - step, - max: data.max - i * step, - minInclusive: true, - maxInclusive: false, - }; - }); - - const assignments = autoRules - .slice(0, assignableColors) - .map((rule, colorIndex) => { - if (isCategorical) { - return { - rule, - color: { - type: 'categorical', - paletteId: palette.id, - colorIndex, - }, - touched: false, - }; - } else { - return { - rule, - color: { - type: 'gradient', - }, - touched: false, - }; - } - }); - - return assignments; -} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts index 701baa1b1710b..ce21732122150 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts @@ -7,41 +7,35 @@ */ import type { ColorMapping } from '.'; -import { MAX_ASSIGNABLE_COLORS } from '../components/container/container'; -import { getPalette, NeutralPalette } from '../palettes'; -import { DEFAULT_NEUTRAL_PALETTE_INDEX } from './default_color_mapping'; +import { getPalette } from '../palettes'; export function updateAssignmentsPalette( assignments: ColorMapping.Config['assignments'], - assignmentMode: ColorMapping.Config['assignmentMode'], colorMode: ColorMapping.Config['colorMode'], paletteId: string, getPaletteFn: ReturnType, preserveColorChanges: boolean ): ColorMapping.Config['assignments'] { const palette = getPaletteFn(paletteId); - const maxColors = palette.type === 'categorical' ? palette.colorCount : MAX_ASSIGNABLE_COLORS; - return assignmentMode === 'auto' - ? [] - : assignments.map(({ rule, color, touched }, index) => { - if (preserveColorChanges && touched) { - return { rule, color, touched }; - } else { - const newColor: ColorMapping.Config['assignments'][number]['color'] = - colorMode.type === 'categorical' - ? { - type: 'categorical', - paletteId: index < maxColors ? paletteId : NeutralPalette.id, - colorIndex: index < maxColors ? index : 0, - } - : { type: 'gradient' }; - return { - rule, - color: newColor, - touched: false, - }; - } - }); + return assignments.map(({ rule, color, touched }, index) => { + if (preserveColorChanges && touched) { + return { rule, color, touched }; + } else { + const newColor: ColorMapping.Config['assignments'][number]['color'] = + colorMode.type === 'categorical' + ? { + type: 'categorical', + paletteId, + colorIndex: index % palette.colorCount, + } + : { type: 'gradient' }; + return { + rule, + color: newColor, + touched: false, + }; + } + }); } export function updateColorModePalette( @@ -61,31 +55,3 @@ export function updateColorModePalette( sort: colorMode.sort, }; } - -export function getUnusedColorForNewAssignment( - palette: ColorMapping.CategoricalPalette, - colorMode: ColorMapping.Config['colorMode'], - assignments: ColorMapping.Config['assignments'] -): ColorMapping.Config['assignments'][number]['color'] { - if (colorMode.type === 'categorical') { - // TODO: change the type of color assignment depending on palette - // compute the next unused color index in the palette. - const maxColors = palette.type === 'categorical' ? palette.colorCount : MAX_ASSIGNABLE_COLORS; - const colorIndices = new Set(Array.from({ length: maxColors }, (d, i) => i)); - assignments.forEach(({ color }) => { - if (color.type === 'categorical' && color.paletteId === palette.id) { - colorIndices.delete(color.colorIndex); - } - }); - const paletteForNextUnusedColorIndex = colorIndices.size > 0 ? palette.id : NeutralPalette.id; - const nextUnusedColorIndex = - colorIndices.size > 0 ? [...colorIndices][0] : DEFAULT_NEUTRAL_PALETTE_INDEX; - return { - type: 'categorical', - paletteId: paletteForNextUnusedColorIndex, - colorIndex: nextUnusedColorIndex, - }; - } else { - return { type: 'gradient' }; - } -} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts index e4005770b2883..8a6ae646b7b6b 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts @@ -13,12 +13,12 @@ import { NeutralPalette } from '../palettes/neutral'; import { getColor, getGradientColorScale } from '../color/color_handling'; export const DEFAULT_NEUTRAL_PALETTE_INDEX = 1; +export const DEFAULT_OTHER_ASSIGNMENT_INDEX = 0; /** * The default color mapping used in Kibana, starts with the EUI color palette */ export const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = { - assignmentMode: 'auto', assignments: [], specialAssignments: [ { @@ -26,9 +26,7 @@ export const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = { type: 'other', }, color: { - type: 'categorical', - paletteId: NeutralPalette.id, - colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + type: 'loop', }, touched: false, }, @@ -45,17 +43,26 @@ export function getPaletteColors( ): string[] { const colorMappingModel = colorMappings ?? { ...DEFAULT_COLOR_MAPPING_CONFIG }; const palette = getPalette(AVAILABLE_PALETTES, NeutralPalette)(colorMappingModel.paletteId); - return Array.from({ length: palette.colorCount }, (d, i) => palette.getColor(i, isDarkMode)); + return getPaletteColorsFromPaletteId(isDarkMode, palette.id); +} + +export function getPaletteColorsFromPaletteId( + isDarkMode: boolean, + paletteId: ColorMapping.Config['paletteId'] +): string[] { + const palette = getPalette(AVAILABLE_PALETTES, NeutralPalette)(paletteId); + return Array.from({ length: palette.colorCount }, (d, i) => + palette.getColor(i, isDarkMode, true) + ); } export function getColorsFromMapping( isDarkMode: boolean, colorMappings?: ColorMapping.Config ): string[] { - const { colorMode, paletteId, assignmentMode, assignments, specialAssignments } = - colorMappings ?? { - ...DEFAULT_COLOR_MAPPING_CONFIG, - }; + const { colorMode, paletteId, assignments, specialAssignments } = colorMappings ?? { + ...DEFAULT_COLOR_MAPPING_CONFIG, + }; const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette); if (colorMode.type === 'gradient') { @@ -63,17 +70,23 @@ export function getColorsFromMapping( return Array.from({ length: 6 }, (d, i) => colorScale(i / 6)); } else { const palette = getPaletteFn(paletteId); - if (assignmentMode === 'auto') { - return Array.from({ length: palette.colorCount }, (d, i) => palette.getColor(i, isDarkMode)); - } else { - return [ - ...assignments.map((a) => { - return a.color.type === 'gradient' ? '' : getColor(a.color, getPaletteFn, isDarkMode); - }), - ...specialAssignments.map((a) => { - return getColor(a.color, getPaletteFn, isDarkMode); - }), - ].filter((color) => color !== ''); - } + const otherColors = + specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color.type === 'loop' + ? Array.from({ length: palette.colorCount }, (d, i) => + palette.getColor(i, isDarkMode, true) + ) + : [ + getColor( + specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color, + getPaletteFn, + isDarkMode + ), + ]; + return [ + ...assignments.map((a) => { + return a.color.type === 'gradient' ? '' : getColor(a.color, getPaletteFn, isDarkMode); + }), + ...otherColors, + ].filter((color) => color !== ''); } } diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts index 59cb18435112d..4c62044be9242 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts @@ -30,6 +30,13 @@ export interface GradientColor { type: 'gradient'; } +/** + * An index specified categorical color, coming from paletteId + */ +export interface LoopColor { + type: 'loop'; +} + /** * A special rule that match automatically, in order, all the categories that are not matching a specified rule */ @@ -134,14 +141,13 @@ export interface GradientColorMode { export interface Config { paletteId: string; colorMode: CategoricalColorMode | GradientColorMode; - assignmentMode: 'auto' | 'manual'; assignments: Array< Assignment< RuleAuto | RuleMatchExactly | RuleMatchExactlyCI | RuleRange | RuleRegExp, CategoricalColor | ColorCode | GradientColor > >; - specialAssignments: Array>; + specialAssignments: Array>; } export interface CategoricalPalette { @@ -149,5 +155,5 @@ export interface CategoricalPalette { name: string; type: 'categorical'; colorCount: number; - getColor: (valueInRange: number, isDarkMode: boolean) => string; + getColor: (valueInRange: number, isDarkMode: boolean, loop: boolean) => string; } diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/index.ts b/packages/kbn-coloring/src/shared_components/color_mapping/index.ts index 1b49a2c6a8bf3..7484eabe816ab 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/index.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/index.ts @@ -14,6 +14,7 @@ export * from './color/color_handling'; export { SPECIAL_TOKENS_STRING_CONVERTION } from './color/rule_matching'; export { DEFAULT_COLOR_MAPPING_CONFIG, + DEFAULT_OTHER_ASSIGNMENT_INDEX, getPaletteColors, getColorsFromMapping, } from './config/default_color_mapping'; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts index d93440c5ac5e4..ce13184ff062a 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts @@ -22,7 +22,9 @@ export const ElasticBrandPalette: ColorMapping.CategoricalPalette = { name: 'Elastic Brand', colorCount: ELASTIC_BRAND_PALETTE_COLORS.length, type: 'categorical', - getColor(valueInRange) { - return ELASTIC_BRAND_PALETTE_COLORS[valueInRange]; + getColor(indexInRange, isDarkMode, loop) { + return ELASTIC_BRAND_PALETTE_COLORS[ + loop ? indexInRange % ELASTIC_BRAND_PALETTE_COLORS.length : indexInRange + ]; }, }; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts index ec48793e12819..f9836a400b877 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts @@ -26,7 +26,9 @@ export const EUIAmsterdamColorBlindPalette: ColorMapping.CategoricalPalette = { name: 'Default', colorCount: EUI_AMSTERDAM_PALETTE_COLORS.length, type: 'categorical', - getColor(valueInRange) { - return EUI_AMSTERDAM_PALETTE_COLORS[valueInRange]; + getColor(indexInRange, isDarkMode, loop) { + return EUI_AMSTERDAM_PALETTE_COLORS[ + loop ? indexInRange % EUI_AMSTERDAM_PALETTE_COLORS.length : indexInRange + ]; }, }; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts index 9b576e0b05c66..bb90130a817fe 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts @@ -23,7 +23,9 @@ export const KibanaV7LegacyPalette: ColorMapping.CategoricalPalette = { name: 'Kibana Legacy', colorCount: KIBANA_V7_LEGACY_PALETTE_COLORS.length, type: 'categorical', - getColor(valueInRange) { - return KIBANA_V7_LEGACY_PALETTE_COLORS[valueInRange]; + getColor(indexInRange, isDarkMode, loop) { + return KIBANA_V7_LEGACY_PALETTE_COLORS[ + loop ? indexInRange % KIBANA_V7_LEGACY_PALETTE_COLORS.length : indexInRange + ]; }, }; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts b/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts index 27588aff2b389..704dbedcfec23 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts @@ -9,6 +9,7 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; import type { ColorMapping } from '../config'; +import { DEFAULT_OTHER_ASSIGNMENT_INDEX } from '../config/default_color_mapping'; export interface RootState { colorMapping: ColorMapping.Config; @@ -22,7 +23,6 @@ export interface RootState { } const initialState: RootState['colorMapping'] = { - assignmentMode: 'auto', assignments: [], specialAssignments: [], paletteId: 'eui', @@ -34,7 +34,6 @@ export const colorMappingSlice = createSlice({ initialState, reducers: { updateModel: (state, action: PayloadAction) => { - state.assignmentMode = action.payload.assignmentMode; state.assignments = [...action.payload.assignments]; state.specialAssignments = [...action.payload.specialAssignments]; state.paletteId = action.payload.paletteId; @@ -53,11 +52,9 @@ export const colorMappingSlice = createSlice({ state.colorMode = { ...action.payload.colorMode }; }, assignStatically: (state, action: PayloadAction) => { - state.assignmentMode = 'manual'; state.assignments = [...action.payload]; }, assignAutomatically: (state) => { - state.assignmentMode = 'auto'; state.assignments = []; }, @@ -67,6 +64,9 @@ export const colorMappingSlice = createSlice({ ) => { state.assignments.push({ ...action.payload }); }, + addNewAssignments: (state, action: PayloadAction) => { + state.assignments.push(...action.payload); + }, updateAssignment: ( state, action: PayloadAction<{ @@ -120,6 +120,21 @@ export const colorMappingSlice = createSlice({ }, removeAssignment: (state, action: PayloadAction) => { state.assignments.splice(action.payload, 1); + if (state.assignments.length === 0) { + state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX] = { + ...state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX], + color: { type: 'loop' }, + touched: true, + }; + } + }, + removeAllAssignments: (state) => { + state.assignments = []; + state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX] = { + ...state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX], + color: { type: 'loop' }, + touched: true, + }; }, changeColorMode: (state, action: PayloadAction) => { state.colorMode = { ...action.payload }; @@ -209,11 +224,13 @@ export const { assignStatically, assignAutomatically, addNewAssignment, + addNewAssignments, updateAssignment, updateAssignmentColor, updateSpecialAssignmentColor, updateAssignmentRule, removeAssignment, + removeAllAssignments, changeColorMode, updateGradientColorStep, removeGradientColorStep, diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts b/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts index 69bd57d2d852e..07cfdb9af0a79 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts @@ -18,9 +18,9 @@ export function selectColorMode(state: RootState) { export function selectSpecialAssignments(state: RootState) { return state.colorMapping.specialAssignments; } -export function selectIsAutoAssignmentMode(state: RootState) { - return state.colorMapping.assignmentMode === 'auto'; -} export function selectColorPickerVisibility(state: RootState) { return state.ui.colorPicker; } +export function selectComputedAssignments(state: RootState) { + return state.colorMapping.assignments; +} diff --git a/packages/kbn-es/src/serverless_resources/project_roles/es/roles.yml b/packages/kbn-es/src/serverless_resources/project_roles/es/roles.yml index 85046bce4dc47..404806c4ac2f6 100644 --- a/packages/kbn-es/src/serverless_resources/project_roles/es/roles.yml +++ b/packages/kbn-es/src/serverless_resources/project_roles/es/roles.yml @@ -1,5 +1,5 @@ # ----- -# Source: https://github.com/elastic/project-controller/blob/main/internal/project/esproject/config/roles.yml +# Source: project-controller/blob/main/internal/project/esproject/config/roles.yml # ----- viewer: cluster: ['manage_own_api_key', 'read_pipeline'] @@ -29,6 +29,18 @@ developer: resources: - '*' +# admin role defined in elasticsearch controller +admin: + cluster: ['all'] + indices: + - names: ['*'] + privileges: ['all'] + allow_restricted_indices: false + applications: + - application: '*' + privileges: ['*'] + resources: ['*'] + # temporarily added for testing purpose system_indices_superuser: cluster: ['all'] diff --git a/packages/kbn-es/src/serverless_resources/project_roles/oblt/roles.yml b/packages/kbn-es/src/serverless_resources/project_roles/oblt/roles.yml index bd40fcf282b2a..e0091f5b7d055 100644 --- a/packages/kbn-es/src/serverless_resources/project_roles/oblt/roles.yml +++ b/packages/kbn-es/src/serverless_resources/project_roles/oblt/roles.yml @@ -1,5 +1,5 @@ # ----- -# Source: https://github.com/elastic/project-controller/blob/main/internal/project/security/config/roles.yml +# Source: project-controller/blob/main/internal/project/security/config/roles.yml # ----- viewer: cluster: [] @@ -77,6 +77,18 @@ editor: - '*' run_as: [] +# admin role defined in elasticsearch controller +admin: + cluster: ['all'] + indices: + - names: ['*'] + privileges: ['all'] + allow_restricted_indices: false + applications: + - application: '*' + privileges: ['*'] + resources: ['*'] + # temporarily added for testing purpose system_indices_superuser: cluster: ['all'] diff --git a/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml b/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml index 6fbdd4535bb53..def2ff2cdeb55 100644 --- a/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml +++ b/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml @@ -1,5 +1,5 @@ # ----- -# Source: https://github.com/elastic/project-controller/blob/main/internal/project/security/config/roles.yml +# Source: project-controller/blob/main/internal/project/security/config/roles.yml # ----- # modeled after the t1_analyst minus osquery run saved queries privilege viewer: @@ -724,6 +724,18 @@ endpoint_policy_manager: - feature_visualize.all resources: '*' +# admin role defined in elasticsearch controller +admin: + cluster: ['all'] + indices: + - names: ['*'] + privileges: ['all'] + allow_restricted_indices: false + applications: + - application: '*' + privileges: ['*'] + resources: ['*'] + # temporarily added for testing purpose system_indices_superuser: cluster: ['all'] diff --git a/packages/kbn-eslint-config/.eslintrc.js b/packages/kbn-eslint-config/.eslintrc.js index e9fae3fd1b290..73ae2f6e75b62 100644 --- a/packages/kbn-eslint-config/.eslintrc.js +++ b/packages/kbn-eslint-config/.eslintrc.js @@ -273,6 +273,7 @@ module.exports = { '@kbn/eslint/no_trailing_import_slash': 'error', '@kbn/eslint/no_constructor_args_in_property_initializers': 'error', '@kbn/eslint/no_this_in_property_initializers': 'error', + '@kbn/eslint/no_unsafe_console': 'error', '@kbn/imports/no_unresolvable_imports': 'error', '@kbn/imports/uniform_imports': 'error', '@kbn/imports/no_unused_imports': 'error', diff --git a/packages/kbn-eslint-plugin-eslint/README.mdx b/packages/kbn-eslint-plugin-eslint/README.mdx index 0cbe16599f2c6..2601c5ace4259 100644 --- a/packages/kbn-eslint-plugin-eslint/README.mdx +++ b/packages/kbn-eslint-plugin-eslint/README.mdx @@ -103,4 +103,8 @@ module.exports = { } ] } -``` \ No newline at end of file +``` + +## no_unsafe_console + +Disables the usage of kbn-security-hardening/console/unsafeConsole. \ No newline at end of file diff --git a/packages/kbn-eslint-plugin-eslint/index.js b/packages/kbn-eslint-plugin-eslint/index.js index f099adedabbbe..3d161ec33dda1 100644 --- a/packages/kbn-eslint-plugin-eslint/index.js +++ b/packages/kbn-eslint-plugin-eslint/index.js @@ -17,5 +17,6 @@ module.exports = { no_trailing_import_slash: require('./rules/no_trailing_import_slash'), no_constructor_args_in_property_initializers: require('./rules/no_constructor_args_in_property_initializers'), no_this_in_property_initializers: require('./rules/no_this_in_property_initializers'), + no_unsafe_console: require('./rules/no_unsafe_console'), }, }; diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_unsafe_console.js b/packages/kbn-eslint-plugin-eslint/rules/no_unsafe_console.js new file mode 100644 index 0000000000000..0a7024099594c --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/no_unsafe_console.js @@ -0,0 +1,71 @@ +/* + * 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 tsEstree = require('@typescript-eslint/typescript-estree'); +const esTypes = tsEstree.AST_NODE_TYPES; + +/** @typedef {import("eslint").Rule.RuleModule} Rule */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.Node} Node */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.CallExpression} CallExpression */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.CallExpression} VariableDeclarator */ + +const ERROR_MSG = 'Unexpected unsafeConsole statement.'; + +/** + * @param {CallExpression} node + */ +const isUnsafeConsoleCall = (node) => { + return ( + node.callee.type === esTypes.MemberExpression && + node.callee.property.type === esTypes.Identifier && + node.callee.object.name === 'unsafeConsole' && + node.callee.property.name + ); +}; + +/** + * @param {VariableDeclarator} node + */ +const isUnsafeConsoleObjectPatternDeclarator = (node) => { + return ( + node.id.type === esTypes.ObjectPattern && + node.init && + node.init.type === esTypes.Identifier && + node.init.name === 'unsafeConsole' + ); +}; + +/** @type {Rule} */ +module.exports = { + meta: { + fixable: 'code', + schema: [], + }, + create: (context) => ({ + CallExpression(_) { + const node = /** @type {CallExpression} */ (_); + + if (isUnsafeConsoleCall(node)) { + context.report({ + message: ERROR_MSG, + loc: node.callee.loc, + }); + } + }, + VariableDeclarator(_) { + const node = /** @type {VariableDeclarator} */ (_); + + if (isUnsafeConsoleObjectPatternDeclarator(node)) { + context.report({ + message: ERROR_MSG, + loc: node.init.loc, + }); + } + }, + }), +}; diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_unsafe_console.test.js b/packages/kbn-eslint-plugin-eslint/rules/no_unsafe_console.test.js new file mode 100644 index 0000000000000..95e5250cec3a9 --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/no_unsafe_console.test.js @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { RuleTester } = require('eslint'); +const rule = require('./no_unsafe_console'); +const dedent = require('dedent'); + +const ruleTester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + sourceType: 'module', + ecmaVersion: 2018, + }, +}); + +ruleTester.run('@kbn/eslint/no_unsafe_console', rule, { + valid: [ + { + code: dedent` + unsafeConsole + `, + }, + ], + + invalid: [ + { + code: dedent` + unsafeConsole.debug('something to debug') + `, + errors: [ + { + line: 1, + message: 'Unexpected unsafeConsole statement.', + }, + ], + }, + { + code: dedent` + unsafeConsole.error() + `, + errors: [ + { + line: 1, + message: 'Unexpected unsafeConsole statement.', + }, + ], + }, + { + code: dedent` + unsafeConsole.info('some info') + `, + errors: [ + { + line: 1, + message: 'Unexpected unsafeConsole statement.', + }, + ], + }, + { + code: dedent` + unsafeConsole.log('something to log') + `, + errors: [ + { + line: 1, + message: 'Unexpected unsafeConsole statement.', + }, + ], + }, + { + code: dedent` + unsafeConsole.trace() + `, + errors: [ + { + line: 1, + message: 'Unexpected unsafeConsole statement.', + }, + ], + }, + { + code: dedent` + unsafeConsole.warn('something to warn') + `, + errors: [ + { + line: 1, + message: 'Unexpected unsafeConsole statement.', + }, + ], + }, + { + code: dedent` + unsafeConsole.anyOtherMethodName() + `, + errors: [ + { + line: 1, + message: 'Unexpected unsafeConsole statement.', + }, + ], + }, + { + code: dedent` + const { debug } = unsafeConsole + debug('something to debug') + `, + errors: [ + { + line: 1, + message: 'Unexpected unsafeConsole statement.', + }, + ], + }, + ], +}); diff --git a/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx b/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx index 0f24e233a4f28..daf52aba46727 100644 --- a/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx +++ b/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, - EuiLink, EuiPopoverTitle, EuiText, EuiListGroupItem, @@ -19,6 +18,7 @@ import { EuiFieldSearch, EuiHighlight, EuiSpacer, + EuiLink, } from '@elastic/eui'; import { elementToString } from '../utils/element_to_string'; @@ -38,9 +38,16 @@ interface DocumentationProps { sections?: LanguageDocumentationSections; // if sets to true, allows searching in the markdown description searchInDescription?: boolean; + // if set, a link will appear on the top right corner + linkToDocumentation?: string; } -function DocumentationContent({ language, sections, searchInDescription }: DocumentationProps) { +function DocumentationContent({ + language, + sections, + searchInDescription, + linkToDocumentation, +}: DocumentationProps) { const [selectedSection, setSelectedSection] = useState(); const scrollTargets = useRef>({}); @@ -83,10 +90,28 @@ function DocumentationContent({ language, sections, searchInDescription }: Docum paddingSize="m" data-test-subj="language-documentation-title" > - {i18n.translate('languageDocumentationPopover.header', { - defaultMessage: '{language} reference', - values: { language }, - })} + + + {i18n.translate('languageDocumentationPopover.header', { + defaultMessage: '{language} reference', + values: { language }, + })} + + {linkToDocumentation && ( + + + {i18n.translate('languageDocumentationPopover.documentationLinkLabel', { + defaultMessage: 'View full documentation', + })} + + + )} + ; searchInDescription?: boolean; + linkToDocumentation?: string; } function DocumentationPopover({ @@ -25,6 +32,7 @@ function DocumentationPopover({ sections, buttonProps, searchInDescription, + linkToDocumentation, }: DocumentationPopoverProps) { const [isHelpOpen, setIsHelpOpen] = useState(false); @@ -33,35 +41,42 @@ function DocumentationPopover({ }, [isHelpOpen]); return ( - setIsHelpOpen(false)} - button={ - - - - } + { + setIsHelpOpen(false); + }} > - - + setIsHelpOpen(false)} + button={ + + + + } + > + + + ); } diff --git a/packages/kbn-management/settings/utilities/category/const.ts b/packages/kbn-management/settings/utilities/category/const.ts index 67a17ecb0cc7a..9f9d65a34a04b 100644 --- a/packages/kbn-management/settings/utilities/category/const.ts +++ b/packages/kbn-management/settings/utilities/category/const.ts @@ -22,6 +22,7 @@ export const SECURITY_SOLUTION_CATEGORY = 'securitySolution'; export const TIMELION_CATEGORY = 'timelion'; export const VISUALIZATION_CATEGORY = 'visualization'; export const ENTERPRISE_SEARCH_CATEGORY = 'enterpriseSearch'; +export const DEV_TOOLS_CATEGORY = 'devTools'; export const CATEGORY_ORDER = [ GENERAL_CATEGORY, @@ -39,4 +40,5 @@ export const CATEGORY_ORDER = [ SECURITY_SOLUTION_CATEGORY, TIMELION_CATEGORY, VISUALIZATION_CATEGORY, + DEV_TOOLS_CATEGORY, ]; diff --git a/packages/kbn-management/settings/utilities/category/get_category_name.ts b/packages/kbn-management/settings/utilities/category/get_category_name.ts index 110d207bf5a6c..88756c34e412c 100644 --- a/packages/kbn-management/settings/utilities/category/get_category_name.ts +++ b/packages/kbn-management/settings/utilities/category/get_category_name.ts @@ -11,6 +11,7 @@ import { ACCESSIBILITY_CATEGORY, AUTOCOMPLETE_CATEGORY, BANNER_CATEGORY, + DEV_TOOLS_CATEGORY, DISCOVER_CATEGORY, ENTERPRISE_SEARCH_CATEGORY, GENERAL_CATEGORY, @@ -92,6 +93,9 @@ const names: Record = { [ROLLUPS_CATEGORY]: i18n.translate('management.settings.categoryNames.rollupsLabel', { defaultMessage: 'Rollups', }), + [DEV_TOOLS_CATEGORY]: i18n.translate('management.settings.categoryNames.devToolsLabel', { + defaultMessage: 'Developer Tools', + }), }; export function getCategoryName(category?: string) { diff --git a/packages/kbn-monaco/src/esql/antlr/esql_lexer.g4 b/packages/kbn-monaco/src/esql/antlr/esql_lexer.g4 index 4000c050cb20b..82fba96eeed10 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_lexer.g4 +++ b/packages/kbn-monaco/src/esql/antlr/esql_lexer.g4 @@ -19,7 +19,6 @@ INLINESTATS : I N L I N E S T A T S -> pushMode(EXPRESSION_MODE); KEEP : K E E P -> pushMode(PROJECT_MODE); LIMIT : L I M I T -> pushMode(EXPRESSION_MODE); MV_EXPAND : M V UNDERSCORE E X P A N D -> pushMode(MVEXPAND_MODE); -PROJECT : P R O J E C T -> pushMode(PROJECT_MODE); RENAME : R E N A M E -> pushMode(RENAME_MODE); ROW : R O W -> pushMode(EXPRESSION_MODE); SHOW : S H O W -> pushMode(SHOW_MODE); @@ -218,7 +217,7 @@ FROM_WS : WS -> channel(HIDDEN) ; // -// DROP, KEEP, PROJECT +// DROP, KEEP // mode PROJECT_MODE; PROJECT_PIPE : PIPE -> type(PIPE), popMode; @@ -299,7 +298,8 @@ fragment ENRICH_POLICY_NAME_BODY : ~[\\/?"<>| ,#\t\r\n:] ; ENRICH_POLICY_NAME - : (LETTER | DIGIT) ENRICH_POLICY_NAME_BODY* + // allow prefix for the policy to specify its resolution + : (ENRICH_POLICY_NAME_BODY+ COLON)? ENRICH_POLICY_NAME_BODY+ ; ENRICH_QUOTED_IDENTIFIER diff --git a/packages/kbn-monaco/src/esql/antlr/esql_lexer.interp b/packages/kbn-monaco/src/esql/antlr/esql_lexer.interp index 51248244df48b..94bd0af8ab463 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_lexer.interp +++ b/packages/kbn-monaco/src/esql/antlr/esql_lexer.interp @@ -24,7 +24,6 @@ null null null null -null '|' null null @@ -119,7 +118,6 @@ INLINESTATS KEEP LIMIT MV_EXPAND -PROJECT RENAME ROW SHOW @@ -226,7 +224,6 @@ INLINESTATS KEEP LIMIT MV_EXPAND -PROJECT RENAME ROW SHOW @@ -414,4 +411,4 @@ SHOW_MODE SETTING_MODE atn: -[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 2, 107, 1267, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 4, 38, 9, 38, 4, 39, 9, 39, 4, 40, 9, 40, 4, 41, 9, 41, 4, 42, 9, 42, 4, 43, 9, 43, 4, 44, 9, 44, 4, 45, 9, 45, 4, 46, 9, 46, 4, 47, 9, 47, 4, 48, 9, 48, 4, 49, 9, 49, 4, 50, 9, 50, 4, 51, 9, 51, 4, 52, 9, 52, 4, 53, 9, 53, 4, 54, 9, 54, 4, 55, 9, 55, 4, 56, 9, 56, 4, 57, 9, 57, 4, 58, 9, 58, 4, 59, 9, 59, 4, 60, 9, 60, 4, 61, 9, 61, 4, 62, 9, 62, 4, 63, 9, 63, 4, 64, 9, 64, 4, 65, 9, 65, 4, 66, 9, 66, 4, 67, 9, 67, 4, 68, 9, 68, 4, 69, 9, 69, 4, 70, 9, 70, 4, 71, 9, 71, 4, 72, 9, 72, 4, 73, 9, 73, 4, 74, 9, 74, 4, 75, 9, 75, 4, 76, 9, 76, 4, 77, 9, 77, 4, 78, 9, 78, 4, 79, 9, 79, 4, 80, 9, 80, 4, 81, 9, 81, 4, 82, 9, 82, 4, 83, 9, 83, 4, 84, 9, 84, 4, 85, 9, 85, 4, 86, 9, 86, 4, 87, 9, 87, 4, 88, 9, 88, 4, 89, 9, 89, 4, 90, 9, 90, 4, 91, 9, 91, 4, 92, 9, 92, 4, 93, 9, 93, 4, 94, 9, 94, 4, 95, 9, 95, 4, 96, 9, 96, 4, 97, 9, 97, 4, 98, 9, 98, 4, 99, 9, 99, 4, 100, 9, 100, 4, 101, 9, 101, 4, 102, 9, 102, 4, 103, 9, 103, 4, 104, 9, 104, 4, 105, 9, 105, 4, 106, 9, 106, 4, 107, 9, 107, 4, 108, 9, 108, 4, 109, 9, 109, 4, 110, 9, 110, 4, 111, 9, 111, 4, 112, 9, 112, 4, 113, 9, 113, 4, 114, 9, 114, 4, 115, 9, 115, 4, 116, 9, 116, 4, 117, 9, 117, 4, 118, 9, 118, 4, 119, 9, 119, 4, 120, 9, 120, 4, 121, 9, 121, 4, 122, 9, 122, 4, 123, 9, 123, 4, 124, 9, 124, 4, 125, 9, 125, 4, 126, 9, 126, 4, 127, 9, 127, 4, 128, 9, 128, 4, 129, 9, 129, 4, 130, 9, 130, 4, 131, 9, 131, 4, 132, 9, 132, 4, 133, 9, 133, 4, 134, 9, 134, 4, 135, 9, 135, 4, 136, 9, 136, 4, 137, 9, 137, 4, 138, 9, 138, 4, 139, 9, 139, 4, 140, 9, 140, 4, 141, 9, 141, 4, 142, 9, 142, 4, 143, 9, 143, 4, 144, 9, 144, 4, 145, 9, 145, 4, 146, 9, 146, 4, 147, 9, 147, 4, 148, 9, 148, 4, 149, 9, 149, 4, 150, 9, 150, 4, 151, 9, 151, 4, 152, 9, 152, 4, 153, 9, 153, 4, 154, 9, 154, 4, 155, 9, 155, 4, 156, 9, 156, 4, 157, 9, 157, 4, 158, 9, 158, 4, 159, 9, 159, 4, 160, 9, 160, 4, 161, 9, 161, 4, 162, 9, 162, 4, 163, 9, 163, 4, 164, 9, 164, 4, 165, 9, 165, 4, 166, 9, 166, 4, 167, 9, 167, 4, 168, 9, 168, 4, 169, 9, 169, 4, 170, 9, 170, 4, 171, 9, 171, 4, 172, 9, 172, 4, 173, 9, 173, 4, 174, 9, 174, 4, 175, 9, 175, 4, 176, 9, 176, 4, 177, 9, 177, 4, 178, 9, 178, 4, 179, 9, 179, 4, 180, 9, 180, 4, 181, 9, 181, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 14, 3, 14, 3, 14, 3, 14, 3, 14, 3, 14, 3, 14, 3, 14, 3, 14, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 19, 3, 19, 3, 19, 3, 19, 3, 19, 3, 19, 3, 19, 3, 19, 3, 20, 6, 20, 528, 10, 20, 13, 20, 14, 20, 529, 3, 20, 3, 20, 3, 21, 3, 21, 3, 21, 3, 21, 7, 21, 538, 10, 21, 12, 21, 14, 21, 541, 11, 21, 3, 21, 5, 21, 544, 10, 21, 3, 21, 5, 21, 547, 10, 21, 3, 21, 3, 21, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 7, 22, 556, 10, 22, 12, 22, 14, 22, 559, 11, 22, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 3, 23, 6, 23, 567, 10, 23, 13, 23, 14, 23, 568, 3, 23, 3, 23, 3, 24, 3, 24, 3, 24, 3, 24, 3, 24, 3, 25, 3, 25, 3, 25, 3, 25, 3, 25, 3, 26, 3, 26, 3, 26, 3, 26, 3, 27, 3, 27, 3, 27, 3, 27, 3, 28, 3, 28, 3, 28, 3, 28, 3, 29, 3, 29, 3, 29, 3, 29, 3, 30, 3, 30, 3, 31, 3, 31, 3, 32, 3, 32, 3, 32, 3, 33, 3, 33, 3, 34, 3, 34, 5, 34, 610, 10, 34, 3, 34, 6, 34, 613, 10, 34, 13, 34, 14, 34, 614, 3, 35, 3, 35, 3, 36, 3, 36, 3, 37, 3, 37, 3, 37, 5, 37, 624, 10, 37, 3, 38, 3, 38, 3, 39, 3, 39, 3, 39, 5, 39, 631, 10, 39, 3, 40, 3, 40, 3, 40, 7, 40, 636, 10, 40, 12, 40, 14, 40, 639, 11, 40, 3, 40, 3, 40, 3, 40, 3, 40, 3, 40, 3, 40, 7, 40, 647, 10, 40, 12, 40, 14, 40, 650, 11, 40, 3, 40, 3, 40, 3, 40, 3, 40, 3, 40, 5, 40, 657, 10, 40, 3, 40, 5, 40, 660, 10, 40, 5, 40, 662, 10, 40, 3, 41, 6, 41, 665, 10, 41, 13, 41, 14, 41, 666, 3, 42, 6, 42, 670, 10, 42, 13, 42, 14, 42, 671, 3, 42, 3, 42, 7, 42, 676, 10, 42, 12, 42, 14, 42, 679, 11, 42, 3, 42, 3, 42, 6, 42, 683, 10, 42, 13, 42, 14, 42, 684, 3, 42, 6, 42, 688, 10, 42, 13, 42, 14, 42, 689, 3, 42, 3, 42, 7, 42, 694, 10, 42, 12, 42, 14, 42, 697, 11, 42, 5, 42, 699, 10, 42, 3, 42, 3, 42, 3, 42, 3, 42, 6, 42, 705, 10, 42, 13, 42, 14, 42, 706, 3, 42, 3, 42, 5, 42, 711, 10, 42, 3, 43, 3, 43, 3, 43, 3, 44, 3, 44, 3, 44, 3, 44, 3, 45, 3, 45, 3, 45, 3, 45, 3, 46, 3, 46, 3, 47, 3, 47, 3, 48, 3, 48, 3, 48, 3, 48, 3, 48, 3, 49, 3, 49, 3, 50, 3, 50, 3, 50, 3, 50, 3, 50, 3, 50, 3, 51, 3, 51, 3, 51, 3, 51, 3, 51, 3, 51, 3, 52, 3, 52, 3, 52, 3, 52, 3, 52, 3, 53, 3, 53, 3, 54, 3, 54, 3, 54, 3, 55, 3, 55, 3, 55, 3, 56, 3, 56, 3, 56, 3, 56, 3, 56, 3, 57, 3, 57, 3, 57, 3, 57, 3, 58, 3, 58, 3, 58, 3, 58, 3, 58, 3, 59, 3, 59, 3, 59, 3, 59, 3, 59, 3, 59, 3, 60, 3, 60, 3, 60, 3, 61, 3, 61, 3, 62, 3, 62, 3, 62, 3, 62, 3, 62, 3, 62, 3, 63, 3, 63, 3, 64, 3, 64, 3, 64, 3, 64, 3, 64, 3, 65, 3, 65, 3, 65, 3, 66, 3, 66, 3, 66, 3, 67, 3, 67, 3, 67, 3, 68, 3, 68, 3, 69, 3, 69, 3, 69, 3, 70, 3, 70, 3, 71, 3, 71, 3, 71, 3, 72, 3, 72, 3, 73, 3, 73, 3, 74, 3, 74, 3, 75, 3, 75, 3, 76, 3, 76, 3, 77, 3, 77, 3, 77, 3, 77, 3, 77, 3, 78, 3, 78, 3, 78, 3, 78, 3, 78, 3, 79, 3, 79, 7, 79, 839, 10, 79, 12, 79, 14, 79, 842, 11, 79, 3, 79, 3, 79, 5, 79, 846, 10, 79, 3, 79, 6, 79, 849, 10, 79, 13, 79, 14, 79, 850, 5, 79, 853, 10, 79, 3, 80, 3, 80, 6, 80, 857, 10, 80, 13, 80, 14, 80, 858, 3, 80, 3, 80, 3, 81, 3, 81, 3, 81, 3, 81, 3, 82, 3, 82, 3, 82, 3, 82, 3, 83, 3, 83, 3, 83, 3, 83, 3, 84, 3, 84, 3, 84, 3, 84, 3, 84, 3, 85, 3, 85, 3, 85, 3, 85, 3, 86, 3, 86, 3, 86, 3, 86, 3, 87, 3, 87, 3, 87, 3, 87, 3, 88, 3, 88, 3, 88, 3, 88, 3, 89, 3, 89, 3, 89, 3, 89, 3, 89, 3, 89, 3, 89, 3, 89, 3, 89, 3, 90, 3, 90, 3, 90, 5, 90, 908, 10, 90, 3, 91, 6, 91, 911, 10, 91, 13, 91, 14, 91, 912, 3, 92, 3, 92, 3, 92, 3, 92, 3, 93, 3, 93, 3, 93, 3, 93, 3, 94, 3, 94, 3, 94, 3, 94, 3, 95, 3, 95, 3, 95, 3, 95, 3, 96, 3, 96, 3, 96, 3, 96, 3, 96, 3, 97, 3, 97, 3, 97, 3, 97, 3, 98, 3, 98, 3, 98, 3, 98, 3, 99, 3, 99, 3, 99, 3, 99, 5, 99, 948, 10, 99, 3, 100, 3, 100, 5, 100, 952, 10, 100, 3, 100, 7, 100, 955, 10, 100, 12, 100, 14, 100, 958, 11, 100, 3, 100, 3, 100, 5, 100, 962, 10, 100, 3, 100, 6, 100, 965, 10, 100, 13, 100, 14, 100, 966, 5, 100, 969, 10, 100, 3, 101, 3, 101, 3, 101, 3, 101, 3, 102, 3, 102, 3, 102, 3, 102, 3, 103, 3, 103, 3, 103, 3, 103, 3, 104, 3, 104, 3, 104, 3, 104, 3, 105, 3, 105, 3, 105, 3, 105, 3, 106, 3, 106, 3, 106, 3, 106, 3, 106, 3, 107, 3, 107, 3, 107, 3, 107, 3, 108, 3, 108, 3, 108, 3, 108, 3, 109, 3, 109, 3, 109, 3, 109, 3, 110, 3, 110, 3, 110, 3, 111, 3, 111, 3, 111, 3, 111, 3, 112, 3, 112, 3, 112, 3, 112, 3, 113, 3, 113, 3, 113, 3, 113, 3, 114, 3, 114, 3, 114, 3, 114, 3, 115, 3, 115, 3, 115, 3, 115, 3, 116, 3, 116, 3, 116, 3, 116, 3, 116, 3, 117, 3, 117, 3, 117, 3, 117, 3, 117, 3, 118, 3, 118, 3, 118, 3, 118, 3, 118, 3, 119, 3, 119, 3, 119, 3, 119, 3, 119, 3, 119, 3, 119, 3, 120, 3, 120, 3, 121, 3, 121, 5, 121, 1057, 10, 121, 3, 121, 7, 121, 1060, 10, 121, 12, 121, 14, 121, 1063, 11, 121, 3, 122, 3, 122, 3, 122, 3, 122, 3, 123, 3, 123, 3, 123, 3, 123, 3, 124, 3, 124, 3, 124, 3, 124, 3, 125, 3, 125, 3, 125, 3, 125, 3, 126, 3, 126, 3, 126, 3, 126, 3, 127, 3, 127, 3, 127, 3, 127, 3, 127, 3, 127, 3, 128, 3, 128, 3, 128, 3, 128, 3, 129, 3, 129, 3, 129, 3, 129, 3, 130, 3, 130, 3, 130, 3, 130, 3, 131, 3, 131, 3, 131, 3, 131, 3, 132, 3, 132, 3, 132, 3, 132, 3, 133, 3, 133, 3, 133, 3, 133, 3, 134, 3, 134, 3, 134, 3, 134, 3, 135, 3, 135, 3, 135, 3, 135, 3, 136, 3, 136, 3, 136, 3, 136, 3, 137, 3, 137, 3, 137, 3, 137, 3, 137, 3, 138, 3, 138, 3, 138, 3, 138, 3, 139, 3, 139, 3, 139, 3, 139, 3, 140, 3, 140, 3, 140, 3, 140, 3, 141, 3, 141, 3, 141, 3, 141, 3, 142, 3, 142, 3, 142, 3, 142, 3, 143, 3, 143, 3, 143, 3, 143, 3, 144, 3, 144, 3, 144, 3, 144, 3, 144, 3, 145, 3, 145, 3, 145, 3, 145, 3, 145, 3, 146, 3, 146, 3, 146, 3, 146, 3, 146, 3, 146, 3, 146, 3, 146, 3, 146, 3, 146, 3, 147, 3, 147, 3, 147, 3, 147, 3, 148, 3, 148, 3, 148, 3, 148, 3, 149, 3, 149, 3, 149, 3, 149, 3, 150, 3, 150, 3, 150, 3, 150, 3, 150, 3, 151, 3, 151, 3, 152, 3, 152, 3, 152, 3, 152, 3, 152, 6, 152, 1200, 10, 152, 13, 152, 14, 152, 1201, 3, 153, 3, 153, 3, 153, 3, 153, 3, 154, 3, 154, 3, 154, 3, 154, 3, 155, 3, 155, 3, 155, 3, 155, 3, 156, 3, 156, 3, 157, 3, 157, 3, 158, 3, 158, 3, 159, 3, 159, 3, 160, 3, 160, 3, 161, 3, 161, 3, 162, 3, 162, 3, 163, 3, 163, 3, 164, 3, 164, 3, 165, 3, 165, 3, 166, 3, 166, 3, 167, 3, 167, 3, 168, 3, 168, 3, 169, 3, 169, 3, 170, 3, 170, 3, 171, 3, 171, 3, 172, 3, 172, 3, 173, 3, 173, 3, 174, 3, 174, 3, 175, 3, 175, 3, 176, 3, 176, 3, 177, 3, 177, 3, 178, 3, 178, 3, 179, 3, 179, 3, 180, 3, 180, 3, 181, 3, 181, 4, 557, 648, 2, 2, 182, 13, 2, 3, 15, 2, 4, 17, 2, 5, 19, 2, 6, 21, 2, 7, 23, 2, 8, 25, 2, 9, 27, 2, 10, 29, 2, 11, 31, 2, 12, 33, 2, 13, 35, 2, 14, 37, 2, 15, 39, 2, 16, 41, 2, 17, 43, 2, 18, 45, 2, 19, 47, 2, 20, 49, 2, 21, 51, 2, 22, 53, 2, 23, 55, 2, 24, 57, 2, 2, 59, 2, 2, 61, 2, 25, 63, 2, 26, 65, 2, 27, 67, 2, 28, 69, 2, 2, 71, 2, 2, 73, 2, 2, 75, 2, 2, 77, 2, 2, 79, 2, 2, 81, 2, 2, 83, 2, 2, 85, 2, 2, 87, 2, 2, 89, 2, 29, 91, 2, 30, 93, 2, 31, 95, 2, 32, 97, 2, 33, 99, 2, 34, 101, 2, 35, 103, 2, 36, 105, 2, 37, 107, 2, 38, 109, 2, 39, 111, 2, 40, 113, 2, 41, 115, 2, 42, 117, 2, 43, 119, 2, 44, 121, 2, 45, 123, 2, 46, 125, 2, 47, 127, 2, 48, 129, 2, 49, 131, 2, 50, 133, 2, 51, 135, 2, 52, 137, 2, 53, 139, 2, 54, 141, 2, 55, 143, 2, 56, 145, 2, 57, 147, 2, 58, 149, 2, 59, 151, 2, 60, 153, 2, 61, 155, 2, 62, 157, 2, 63, 159, 2, 64, 161, 2, 65, 163, 2, 66, 165, 2, 67, 167, 2, 68, 169, 2, 69, 171, 2, 70, 173, 2, 71, 175, 2, 72, 177, 2, 2, 179, 2, 2, 181, 2, 2, 183, 2, 2, 185, 2, 2, 187, 2, 73, 189, 2, 2, 191, 2, 74, 193, 2, 2, 195, 2, 75, 197, 2, 76, 199, 2, 77, 201, 2, 2, 203, 2, 2, 205, 2, 2, 207, 2, 2, 209, 2, 78, 211, 2, 2, 213, 2, 2, 215, 2, 79, 217, 2, 80, 219, 2, 81, 221, 2, 2, 223, 2, 2, 225, 2, 2, 227, 2, 2, 229, 2, 82, 231, 2, 2, 233, 2, 2, 235, 2, 83, 237, 2, 84, 239, 2, 85, 241, 2, 2, 243, 2, 2, 245, 2, 86, 247, 2, 87, 249, 2, 2, 251, 2, 88, 253, 2, 2, 255, 2, 2, 257, 2, 89, 259, 2, 90, 261, 2, 91, 263, 2, 2, 265, 2, 2, 267, 2, 2, 269, 2, 2, 271, 2, 2, 273, 2, 2, 275, 2, 2, 277, 2, 92, 279, 2, 93, 281, 2, 94, 283, 2, 2, 285, 2, 2, 287, 2, 2, 289, 2, 2, 291, 2, 95, 293, 2, 96, 295, 2, 97, 297, 2, 2, 299, 2, 98, 301, 2, 99, 303, 2, 100, 305, 2, 101, 307, 2, 102, 309, 2, 2, 311, 2, 103, 313, 2, 104, 315, 2, 105, 317, 2, 106, 319, 2, 107, 321, 2, 2, 323, 2, 2, 325, 2, 2, 327, 2, 2, 329, 2, 2, 331, 2, 2, 333, 2, 2, 335, 2, 2, 337, 2, 2, 339, 2, 2, 341, 2, 2, 343, 2, 2, 345, 2, 2, 347, 2, 2, 349, 2, 2, 351, 2, 2, 353, 2, 2, 355, 2, 2, 357, 2, 2, 359, 2, 2, 361, 2, 2, 363, 2, 2, 365, 2, 2, 367, 2, 2, 369, 2, 2, 371, 2, 2, 13, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 40, 8, 2, 11, 12, 15, 15, 34, 34, 49, 49, 93, 93, 95, 95, 4, 2, 12, 12, 15, 15, 5, 2, 11, 12, 15, 15, 34, 34, 3, 2, 50, 59, 4, 2, 67, 92, 99, 124, 7, 2, 36, 36, 94, 94, 112, 112, 116, 116, 118, 118, 6, 2, 12, 12, 15, 15, 36, 36, 94, 94, 4, 2, 71, 71, 103, 103, 4, 2, 45, 45, 47, 47, 3, 2, 98, 98, 12, 2, 11, 12, 15, 15, 34, 34, 46, 46, 49, 49, 63, 63, 93, 93, 95, 95, 98, 98, 126, 126, 4, 2, 44, 44, 49, 49, 13, 2, 11, 12, 15, 15, 34, 34, 36, 37, 46, 46, 49, 49, 60, 60, 62, 62, 64, 65, 94, 94, 126, 126, 4, 2, 67, 67, 99, 99, 4, 2, 68, 68, 100, 100, 4, 2, 69, 69, 101, 101, 4, 2, 70, 70, 102, 102, 4, 2, 72, 72, 104, 104, 4, 2, 73, 73, 105, 105, 4, 2, 74, 74, 106, 106, 4, 2, 75, 75, 107, 107, 4, 2, 76, 76, 108, 108, 4, 2, 77, 77, 109, 109, 4, 2, 78, 78, 110, 110, 4, 2, 79, 79, 111, 111, 4, 2, 80, 80, 112, 112, 4, 2, 81, 81, 113, 113, 4, 2, 82, 82, 114, 114, 4, 2, 83, 83, 115, 115, 4, 2, 84, 84, 116, 116, 4, 2, 85, 85, 117, 117, 4, 2, 86, 86, 118, 118, 4, 2, 87, 87, 119, 119, 4, 2, 88, 88, 120, 120, 4, 2, 89, 89, 121, 121, 4, 2, 90, 90, 122, 122, 4, 2, 91, 91, 123, 123, 4, 2, 92, 92, 124, 124, 2, 1268, 2, 13, 3, 2, 2, 2, 2, 15, 3, 2, 2, 2, 2, 17, 3, 2, 2, 2, 2, 19, 3, 2, 2, 2, 2, 21, 3, 2, 2, 2, 2, 23, 3, 2, 2, 2, 2, 25, 3, 2, 2, 2, 2, 27, 3, 2, 2, 2, 2, 29, 3, 2, 2, 2, 2, 31, 3, 2, 2, 2, 2, 33, 3, 2, 2, 2, 2, 35, 3, 2, 2, 2, 2, 37, 3, 2, 2, 2, 2, 39, 3, 2, 2, 2, 2, 41, 3, 2, 2, 2, 2, 43, 3, 2, 2, 2, 2, 45, 3, 2, 2, 2, 2, 47, 3, 2, 2, 2, 2, 49, 3, 2, 2, 2, 2, 51, 3, 2, 2, 2, 2, 53, 3, 2, 2, 2, 2, 55, 3, 2, 2, 2, 3, 57, 3, 2, 2, 2, 3, 59, 3, 2, 2, 2, 3, 61, 3, 2, 2, 2, 3, 63, 3, 2, 2, 2, 3, 65, 3, 2, 2, 2, 4, 67, 3, 2, 2, 2, 4, 89, 3, 2, 2, 2, 4, 91, 3, 2, 2, 2, 4, 93, 3, 2, 2, 2, 4, 95, 3, 2, 2, 2, 4, 97, 3, 2, 2, 2, 4, 99, 3, 2, 2, 2, 4, 101, 3, 2, 2, 2, 4, 103, 3, 2, 2, 2, 4, 105, 3, 2, 2, 2, 4, 107, 3, 2, 2, 2, 4, 109, 3, 2, 2, 2, 4, 111, 3, 2, 2, 2, 4, 113, 3, 2, 2, 2, 4, 115, 3, 2, 2, 2, 4, 117, 3, 2, 2, 2, 4, 119, 3, 2, 2, 2, 4, 121, 3, 2, 2, 2, 4, 123, 3, 2, 2, 2, 4, 125, 3, 2, 2, 2, 4, 127, 3, 2, 2, 2, 4, 129, 3, 2, 2, 2, 4, 131, 3, 2, 2, 2, 4, 133, 3, 2, 2, 2, 4, 135, 3, 2, 2, 2, 4, 137, 3, 2, 2, 2, 4, 139, 3, 2, 2, 2, 4, 141, 3, 2, 2, 2, 4, 143, 3, 2, 2, 2, 4, 145, 3, 2, 2, 2, 4, 147, 3, 2, 2, 2, 4, 149, 3, 2, 2, 2, 4, 151, 3, 2, 2, 2, 4, 153, 3, 2, 2, 2, 4, 155, 3, 2, 2, 2, 4, 157, 3, 2, 2, 2, 4, 159, 3, 2, 2, 2, 4, 161, 3, 2, 2, 2, 4, 163, 3, 2, 2, 2, 4, 165, 3, 2, 2, 2, 4, 167, 3, 2, 2, 2, 4, 169, 3, 2, 2, 2, 4, 171, 3, 2, 2, 2, 4, 173, 3, 2, 2, 2, 4, 175, 3, 2, 2, 2, 5, 177, 3, 2, 2, 2, 5, 179, 3, 2, 2, 2, 5, 181, 3, 2, 2, 2, 5, 183, 3, 2, 2, 2, 5, 185, 3, 2, 2, 2, 5, 187, 3, 2, 2, 2, 5, 191, 3, 2, 2, 2, 5, 193, 3, 2, 2, 2, 5, 195, 3, 2, 2, 2, 5, 197, 3, 2, 2, 2, 5, 199, 3, 2, 2, 2, 6, 201, 3, 2, 2, 2, 6, 203, 3, 2, 2, 2, 6, 205, 3, 2, 2, 2, 6, 209, 3, 2, 2, 2, 6, 211, 3, 2, 2, 2, 6, 213, 3, 2, 2, 2, 6, 215, 3, 2, 2, 2, 6, 217, 3, 2, 2, 2, 6, 219, 3, 2, 2, 2, 7, 221, 3, 2, 2, 2, 7, 223, 3, 2, 2, 2, 7, 225, 3, 2, 2, 2, 7, 227, 3, 2, 2, 2, 7, 229, 3, 2, 2, 2, 7, 231, 3, 2, 2, 2, 7, 233, 3, 2, 2, 2, 7, 235, 3, 2, 2, 2, 7, 237, 3, 2, 2, 2, 7, 239, 3, 2, 2, 2, 8, 241, 3, 2, 2, 2, 8, 243, 3, 2, 2, 2, 8, 245, 3, 2, 2, 2, 8, 247, 3, 2, 2, 2, 8, 251, 3, 2, 2, 2, 8, 253, 3, 2, 2, 2, 8, 255, 3, 2, 2, 2, 8, 257, 3, 2, 2, 2, 8, 259, 3, 2, 2, 2, 8, 261, 3, 2, 2, 2, 9, 263, 3, 2, 2, 2, 9, 265, 3, 2, 2, 2, 9, 267, 3, 2, 2, 2, 9, 269, 3, 2, 2, 2, 9, 271, 3, 2, 2, 2, 9, 273, 3, 2, 2, 2, 9, 275, 3, 2, 2, 2, 9, 277, 3, 2, 2, 2, 9, 279, 3, 2, 2, 2, 9, 281, 3, 2, 2, 2, 10, 283, 3, 2, 2, 2, 10, 285, 3, 2, 2, 2, 10, 287, 3, 2, 2, 2, 10, 289, 3, 2, 2, 2, 10, 291, 3, 2, 2, 2, 10, 293, 3, 2, 2, 2, 10, 295, 3, 2, 2, 2, 11, 297, 3, 2, 2, 2, 11, 299, 3, 2, 2, 2, 11, 301, 3, 2, 2, 2, 11, 303, 3, 2, 2, 2, 11, 305, 3, 2, 2, 2, 11, 307, 3, 2, 2, 2, 12, 309, 3, 2, 2, 2, 12, 311, 3, 2, 2, 2, 12, 313, 3, 2, 2, 2, 12, 315, 3, 2, 2, 2, 12, 317, 3, 2, 2, 2, 12, 319, 3, 2, 2, 2, 13, 373, 3, 2, 2, 2, 15, 383, 3, 2, 2, 2, 17, 390, 3, 2, 2, 2, 19, 399, 3, 2, 2, 2, 21, 406, 3, 2, 2, 2, 23, 416, 3, 2, 2, 2, 25, 423, 3, 2, 2, 2, 27, 430, 3, 2, 2, 2, 29, 444, 3, 2, 2, 2, 31, 451, 3, 2, 2, 2, 33, 459, 3, 2, 2, 2, 35, 471, 3, 2, 2, 2, 37, 481, 3, 2, 2, 2, 39, 490, 3, 2, 2, 2, 41, 496, 3, 2, 2, 2, 43, 503, 3, 2, 2, 2, 45, 510, 3, 2, 2, 2, 47, 518, 3, 2, 2, 2, 49, 527, 3, 2, 2, 2, 51, 533, 3, 2, 2, 2, 53, 550, 3, 2, 2, 2, 55, 566, 3, 2, 2, 2, 57, 572, 3, 2, 2, 2, 59, 577, 3, 2, 2, 2, 61, 582, 3, 2, 2, 2, 63, 586, 3, 2, 2, 2, 65, 590, 3, 2, 2, 2, 67, 594, 3, 2, 2, 2, 69, 598, 3, 2, 2, 2, 71, 600, 3, 2, 2, 2, 73, 602, 3, 2, 2, 2, 75, 605, 3, 2, 2, 2, 77, 607, 3, 2, 2, 2, 79, 616, 3, 2, 2, 2, 81, 618, 3, 2, 2, 2, 83, 623, 3, 2, 2, 2, 85, 625, 3, 2, 2, 2, 87, 630, 3, 2, 2, 2, 89, 661, 3, 2, 2, 2, 91, 664, 3, 2, 2, 2, 93, 710, 3, 2, 2, 2, 95, 712, 3, 2, 2, 2, 97, 715, 3, 2, 2, 2, 99, 719, 3, 2, 2, 2, 101, 723, 3, 2, 2, 2, 103, 725, 3, 2, 2, 2, 105, 727, 3, 2, 2, 2, 107, 732, 3, 2, 2, 2, 109, 734, 3, 2, 2, 2, 111, 740, 3, 2, 2, 2, 113, 746, 3, 2, 2, 2, 115, 751, 3, 2, 2, 2, 117, 753, 3, 2, 2, 2, 119, 756, 3, 2, 2, 2, 121, 759, 3, 2, 2, 2, 123, 764, 3, 2, 2, 2, 125, 768, 3, 2, 2, 2, 127, 773, 3, 2, 2, 2, 129, 779, 3, 2, 2, 2, 131, 782, 3, 2, 2, 2, 133, 784, 3, 2, 2, 2, 135, 790, 3, 2, 2, 2, 137, 792, 3, 2, 2, 2, 139, 797, 3, 2, 2, 2, 141, 800, 3, 2, 2, 2, 143, 803, 3, 2, 2, 2, 145, 806, 3, 2, 2, 2, 147, 808, 3, 2, 2, 2, 149, 811, 3, 2, 2, 2, 151, 813, 3, 2, 2, 2, 153, 816, 3, 2, 2, 2, 155, 818, 3, 2, 2, 2, 157, 820, 3, 2, 2, 2, 159, 822, 3, 2, 2, 2, 161, 824, 3, 2, 2, 2, 163, 826, 3, 2, 2, 2, 165, 831, 3, 2, 2, 2, 167, 852, 3, 2, 2, 2, 169, 854, 3, 2, 2, 2, 171, 862, 3, 2, 2, 2, 173, 866, 3, 2, 2, 2, 175, 870, 3, 2, 2, 2, 177, 874, 3, 2, 2, 2, 179, 879, 3, 2, 2, 2, 181, 883, 3, 2, 2, 2, 183, 887, 3, 2, 2, 2, 185, 891, 3, 2, 2, 2, 187, 895, 3, 2, 2, 2, 189, 907, 3, 2, 2, 2, 191, 910, 3, 2, 2, 2, 193, 914, 3, 2, 2, 2, 195, 918, 3, 2, 2, 2, 197, 922, 3, 2, 2, 2, 199, 926, 3, 2, 2, 2, 201, 930, 3, 2, 2, 2, 203, 935, 3, 2, 2, 2, 205, 939, 3, 2, 2, 2, 207, 947, 3, 2, 2, 2, 209, 968, 3, 2, 2, 2, 211, 970, 3, 2, 2, 2, 213, 974, 3, 2, 2, 2, 215, 978, 3, 2, 2, 2, 217, 982, 3, 2, 2, 2, 219, 986, 3, 2, 2, 2, 221, 990, 3, 2, 2, 2, 223, 995, 3, 2, 2, 2, 225, 999, 3, 2, 2, 2, 227, 1003, 3, 2, 2, 2, 229, 1007, 3, 2, 2, 2, 231, 1010, 3, 2, 2, 2, 233, 1014, 3, 2, 2, 2, 235, 1018, 3, 2, 2, 2, 237, 1022, 3, 2, 2, 2, 239, 1026, 3, 2, 2, 2, 241, 1030, 3, 2, 2, 2, 243, 1035, 3, 2, 2, 2, 245, 1040, 3, 2, 2, 2, 247, 1045, 3, 2, 2, 2, 249, 1052, 3, 2, 2, 2, 251, 1056, 3, 2, 2, 2, 253, 1064, 3, 2, 2, 2, 255, 1068, 3, 2, 2, 2, 257, 1072, 3, 2, 2, 2, 259, 1076, 3, 2, 2, 2, 261, 1080, 3, 2, 2, 2, 263, 1084, 3, 2, 2, 2, 265, 1090, 3, 2, 2, 2, 267, 1094, 3, 2, 2, 2, 269, 1098, 3, 2, 2, 2, 271, 1102, 3, 2, 2, 2, 273, 1106, 3, 2, 2, 2, 275, 1110, 3, 2, 2, 2, 277, 1114, 3, 2, 2, 2, 279, 1118, 3, 2, 2, 2, 281, 1122, 3, 2, 2, 2, 283, 1126, 3, 2, 2, 2, 285, 1131, 3, 2, 2, 2, 287, 1135, 3, 2, 2, 2, 289, 1139, 3, 2, 2, 2, 291, 1143, 3, 2, 2, 2, 293, 1147, 3, 2, 2, 2, 295, 1151, 3, 2, 2, 2, 297, 1155, 3, 2, 2, 2, 299, 1160, 3, 2, 2, 2, 301, 1165, 3, 2, 2, 2, 303, 1175, 3, 2, 2, 2, 305, 1179, 3, 2, 2, 2, 307, 1183, 3, 2, 2, 2, 309, 1187, 3, 2, 2, 2, 311, 1192, 3, 2, 2, 2, 313, 1199, 3, 2, 2, 2, 315, 1203, 3, 2, 2, 2, 317, 1207, 3, 2, 2, 2, 319, 1211, 3, 2, 2, 2, 321, 1215, 3, 2, 2, 2, 323, 1217, 3, 2, 2, 2, 325, 1219, 3, 2, 2, 2, 327, 1221, 3, 2, 2, 2, 329, 1223, 3, 2, 2, 2, 331, 1225, 3, 2, 2, 2, 333, 1227, 3, 2, 2, 2, 335, 1229, 3, 2, 2, 2, 337, 1231, 3, 2, 2, 2, 339, 1233, 3, 2, 2, 2, 341, 1235, 3, 2, 2, 2, 343, 1237, 3, 2, 2, 2, 345, 1239, 3, 2, 2, 2, 347, 1241, 3, 2, 2, 2, 349, 1243, 3, 2, 2, 2, 351, 1245, 3, 2, 2, 2, 353, 1247, 3, 2, 2, 2, 355, 1249, 3, 2, 2, 2, 357, 1251, 3, 2, 2, 2, 359, 1253, 3, 2, 2, 2, 361, 1255, 3, 2, 2, 2, 363, 1257, 3, 2, 2, 2, 365, 1259, 3, 2, 2, 2, 367, 1261, 3, 2, 2, 2, 369, 1263, 3, 2, 2, 2, 371, 1265, 3, 2, 2, 2, 373, 374, 5, 327, 159, 2, 374, 375, 5, 337, 164, 2, 375, 376, 5, 357, 174, 2, 376, 377, 5, 357, 174, 2, 377, 378, 5, 329, 160, 2, 378, 379, 5, 325, 158, 2, 379, 380, 5, 359, 175, 2, 380, 381, 3, 2, 2, 2, 381, 382, 8, 2, 2, 2, 382, 14, 3, 2, 2, 2, 383, 384, 5, 327, 159, 2, 384, 385, 5, 355, 173, 2, 385, 386, 5, 349, 170, 2, 386, 387, 5, 351, 171, 2, 387, 388, 3, 2, 2, 2, 388, 389, 8, 3, 3, 2, 389, 16, 3, 2, 2, 2, 390, 391, 5, 329, 160, 2, 391, 392, 5, 347, 169, 2, 392, 393, 5, 355, 173, 2, 393, 394, 5, 337, 164, 2, 394, 395, 5, 325, 158, 2, 395, 396, 5, 335, 163, 2, 396, 397, 3, 2, 2, 2, 397, 398, 8, 4, 4, 2, 398, 18, 3, 2, 2, 2, 399, 400, 5, 329, 160, 2, 400, 401, 5, 363, 177, 2, 401, 402, 5, 321, 156, 2, 402, 403, 5, 343, 167, 2, 403, 404, 3, 2, 2, 2, 404, 405, 8, 5, 2, 2, 405, 20, 3, 2, 2, 2, 406, 407, 5, 329, 160, 2, 407, 408, 5, 367, 179, 2, 408, 409, 5, 351, 171, 2, 409, 410, 5, 343, 167, 2, 410, 411, 5, 321, 156, 2, 411, 412, 5, 337, 164, 2, 412, 413, 5, 347, 169, 2, 413, 414, 3, 2, 2, 2, 414, 415, 8, 6, 5, 2, 415, 22, 3, 2, 2, 2, 416, 417, 5, 331, 161, 2, 417, 418, 5, 355, 173, 2, 418, 419, 5, 349, 170, 2, 419, 420, 5, 345, 168, 2, 420, 421, 3, 2, 2, 2, 421, 422, 8, 7, 6, 2, 422, 24, 3, 2, 2, 2, 423, 424, 5, 333, 162, 2, 424, 425, 5, 355, 173, 2, 425, 426, 5, 349, 170, 2, 426, 427, 5, 341, 166, 2, 427, 428, 3, 2, 2, 2, 428, 429, 8, 8, 2, 2, 429, 26, 3, 2, 2, 2, 430, 431, 5, 337, 164, 2, 431, 432, 5, 347, 169, 2, 432, 433, 5, 343, 167, 2, 433, 434, 5, 337, 164, 2, 434, 435, 5, 347, 169, 2, 435, 436, 5, 329, 160, 2, 436, 437, 5, 357, 174, 2, 437, 438, 5, 359, 175, 2, 438, 439, 5, 321, 156, 2, 439, 440, 5, 359, 175, 2, 440, 441, 5, 357, 174, 2, 441, 442, 3, 2, 2, 2, 442, 443, 8, 9, 2, 2, 443, 28, 3, 2, 2, 2, 444, 445, 5, 341, 166, 2, 445, 446, 5, 329, 160, 2, 446, 447, 5, 329, 160, 2, 447, 448, 5, 351, 171, 2, 448, 449, 3, 2, 2, 2, 449, 450, 8, 10, 3, 2, 450, 30, 3, 2, 2, 2, 451, 452, 5, 343, 167, 2, 452, 453, 5, 337, 164, 2, 453, 454, 5, 345, 168, 2, 454, 455, 5, 337, 164, 2, 455, 456, 5, 359, 175, 2, 456, 457, 3, 2, 2, 2, 457, 458, 8, 11, 2, 2, 458, 32, 3, 2, 2, 2, 459, 460, 5, 345, 168, 2, 460, 461, 5, 363, 177, 2, 461, 462, 5, 85, 38, 2, 462, 463, 5, 329, 160, 2, 463, 464, 5, 367, 179, 2, 464, 465, 5, 351, 171, 2, 465, 466, 5, 321, 156, 2, 466, 467, 5, 347, 169, 2, 467, 468, 5, 327, 159, 2, 468, 469, 3, 2, 2, 2, 469, 470, 8, 12, 7, 2, 470, 34, 3, 2, 2, 2, 471, 472, 5, 351, 171, 2, 472, 473, 5, 355, 173, 2, 473, 474, 5, 349, 170, 2, 474, 475, 5, 339, 165, 2, 475, 476, 5, 329, 160, 2, 476, 477, 5, 325, 158, 2, 477, 478, 5, 359, 175, 2, 478, 479, 3, 2, 2, 2, 479, 480, 8, 13, 3, 2, 480, 36, 3, 2, 2, 2, 481, 482, 5, 355, 173, 2, 482, 483, 5, 329, 160, 2, 483, 484, 5, 347, 169, 2, 484, 485, 5, 321, 156, 2, 485, 486, 5, 345, 168, 2, 486, 487, 5, 329, 160, 2, 487, 488, 3, 2, 2, 2, 488, 489, 8, 14, 8, 2, 489, 38, 3, 2, 2, 2, 490, 491, 5, 355, 173, 2, 491, 492, 5, 349, 170, 2, 492, 493, 5, 365, 178, 2, 493, 494, 3, 2, 2, 2, 494, 495, 8, 15, 2, 2, 495, 40, 3, 2, 2, 2, 496, 497, 5, 357, 174, 2, 497, 498, 5, 335, 163, 2, 498, 499, 5, 349, 170, 2, 499, 500, 5, 365, 178, 2, 500, 501, 3, 2, 2, 2, 501, 502, 8, 16, 9, 2, 502, 42, 3, 2, 2, 2, 503, 504, 5, 357, 174, 2, 504, 505, 5, 349, 170, 2, 505, 506, 5, 355, 173, 2, 506, 507, 5, 359, 175, 2, 507, 508, 3, 2, 2, 2, 508, 509, 8, 17, 2, 2, 509, 44, 3, 2, 2, 2, 510, 511, 5, 357, 174, 2, 511, 512, 5, 359, 175, 2, 512, 513, 5, 321, 156, 2, 513, 514, 5, 359, 175, 2, 514, 515, 5, 357, 174, 2, 515, 516, 3, 2, 2, 2, 516, 517, 8, 18, 2, 2, 517, 46, 3, 2, 2, 2, 518, 519, 5, 365, 178, 2, 519, 520, 5, 335, 163, 2, 520, 521, 5, 329, 160, 2, 521, 522, 5, 355, 173, 2, 522, 523, 5, 329, 160, 2, 523, 524, 3, 2, 2, 2, 524, 525, 8, 19, 2, 2, 525, 48, 3, 2, 2, 2, 526, 528, 10, 2, 2, 2, 527, 526, 3, 2, 2, 2, 528, 529, 3, 2, 2, 2, 529, 527, 3, 2, 2, 2, 529, 530, 3, 2, 2, 2, 530, 531, 3, 2, 2, 2, 531, 532, 8, 20, 2, 2, 532, 50, 3, 2, 2, 2, 533, 534, 7, 49, 2, 2, 534, 535, 7, 49, 2, 2, 535, 539, 3, 2, 2, 2, 536, 538, 10, 3, 2, 2, 537, 536, 3, 2, 2, 2, 538, 541, 3, 2, 2, 2, 539, 537, 3, 2, 2, 2, 539, 540, 3, 2, 2, 2, 540, 543, 3, 2, 2, 2, 541, 539, 3, 2, 2, 2, 542, 544, 7, 15, 2, 2, 543, 542, 3, 2, 2, 2, 543, 544, 3, 2, 2, 2, 544, 546, 3, 2, 2, 2, 545, 547, 7, 12, 2, 2, 546, 545, 3, 2, 2, 2, 546, 547, 3, 2, 2, 2, 547, 548, 3, 2, 2, 2, 548, 549, 8, 21, 10, 2, 549, 52, 3, 2, 2, 2, 550, 551, 7, 49, 2, 2, 551, 552, 7, 44, 2, 2, 552, 557, 3, 2, 2, 2, 553, 556, 5, 53, 22, 2, 554, 556, 11, 2, 2, 2, 555, 553, 3, 2, 2, 2, 555, 554, 3, 2, 2, 2, 556, 559, 3, 2, 2, 2, 557, 558, 3, 2, 2, 2, 557, 555, 3, 2, 2, 2, 558, 560, 3, 2, 2, 2, 559, 557, 3, 2, 2, 2, 560, 561, 7, 44, 2, 2, 561, 562, 7, 49, 2, 2, 562, 563, 3, 2, 2, 2, 563, 564, 8, 22, 10, 2, 564, 54, 3, 2, 2, 2, 565, 567, 9, 4, 2, 2, 566, 565, 3, 2, 2, 2, 567, 568, 3, 2, 2, 2, 568, 566, 3, 2, 2, 2, 568, 569, 3, 2, 2, 2, 569, 570, 3, 2, 2, 2, 570, 571, 8, 23, 10, 2, 571, 56, 3, 2, 2, 2, 572, 573, 5, 163, 77, 2, 573, 574, 3, 2, 2, 2, 574, 575, 8, 24, 11, 2, 575, 576, 8, 24, 12, 2, 576, 58, 3, 2, 2, 2, 577, 578, 5, 67, 29, 2, 578, 579, 3, 2, 2, 2, 579, 580, 8, 25, 13, 2, 580, 581, 8, 25, 14, 2, 581, 60, 3, 2, 2, 2, 582, 583, 5, 55, 23, 2, 583, 584, 3, 2, 2, 2, 584, 585, 8, 26, 10, 2, 585, 62, 3, 2, 2, 2, 586, 587, 5, 51, 21, 2, 587, 588, 3, 2, 2, 2, 588, 589, 8, 27, 10, 2, 589, 64, 3, 2, 2, 2, 590, 591, 5, 53, 22, 2, 591, 592, 3, 2, 2, 2, 592, 593, 8, 28, 10, 2, 593, 66, 3, 2, 2, 2, 594, 595, 7, 126, 2, 2, 595, 596, 3, 2, 2, 2, 596, 597, 8, 29, 14, 2, 597, 68, 3, 2, 2, 2, 598, 599, 9, 5, 2, 2, 599, 70, 3, 2, 2, 2, 600, 601, 9, 6, 2, 2, 601, 72, 3, 2, 2, 2, 602, 603, 7, 94, 2, 2, 603, 604, 9, 7, 2, 2, 604, 74, 3, 2, 2, 2, 605, 606, 10, 8, 2, 2, 606, 76, 3, 2, 2, 2, 607, 609, 9, 9, 2, 2, 608, 610, 9, 10, 2, 2, 609, 608, 3, 2, 2, 2, 609, 610, 3, 2, 2, 2, 610, 612, 3, 2, 2, 2, 611, 613, 5, 69, 30, 2, 612, 611, 3, 2, 2, 2, 613, 614, 3, 2, 2, 2, 614, 612, 3, 2, 2, 2, 614, 615, 3, 2, 2, 2, 615, 78, 3, 2, 2, 2, 616, 617, 7, 66, 2, 2, 617, 80, 3, 2, 2, 2, 618, 619, 7, 98, 2, 2, 619, 82, 3, 2, 2, 2, 620, 624, 10, 11, 2, 2, 621, 622, 7, 98, 2, 2, 622, 624, 7, 98, 2, 2, 623, 620, 3, 2, 2, 2, 623, 621, 3, 2, 2, 2, 624, 84, 3, 2, 2, 2, 625, 626, 7, 97, 2, 2, 626, 86, 3, 2, 2, 2, 627, 631, 5, 71, 31, 2, 628, 631, 5, 69, 30, 2, 629, 631, 5, 85, 38, 2, 630, 627, 3, 2, 2, 2, 630, 628, 3, 2, 2, 2, 630, 629, 3, 2, 2, 2, 631, 88, 3, 2, 2, 2, 632, 637, 7, 36, 2, 2, 633, 636, 5, 73, 32, 2, 634, 636, 5, 75, 33, 2, 635, 633, 3, 2, 2, 2, 635, 634, 3, 2, 2, 2, 636, 639, 3, 2, 2, 2, 637, 635, 3, 2, 2, 2, 637, 638, 3, 2, 2, 2, 638, 640, 3, 2, 2, 2, 639, 637, 3, 2, 2, 2, 640, 662, 7, 36, 2, 2, 641, 642, 7, 36, 2, 2, 642, 643, 7, 36, 2, 2, 643, 644, 7, 36, 2, 2, 644, 648, 3, 2, 2, 2, 645, 647, 10, 3, 2, 2, 646, 645, 3, 2, 2, 2, 647, 650, 3, 2, 2, 2, 648, 649, 3, 2, 2, 2, 648, 646, 3, 2, 2, 2, 649, 651, 3, 2, 2, 2, 650, 648, 3, 2, 2, 2, 651, 652, 7, 36, 2, 2, 652, 653, 7, 36, 2, 2, 653, 654, 7, 36, 2, 2, 654, 656, 3, 2, 2, 2, 655, 657, 7, 36, 2, 2, 656, 655, 3, 2, 2, 2, 656, 657, 3, 2, 2, 2, 657, 659, 3, 2, 2, 2, 658, 660, 7, 36, 2, 2, 659, 658, 3, 2, 2, 2, 659, 660, 3, 2, 2, 2, 660, 662, 3, 2, 2, 2, 661, 632, 3, 2, 2, 2, 661, 641, 3, 2, 2, 2, 662, 90, 3, 2, 2, 2, 663, 665, 5, 69, 30, 2, 664, 663, 3, 2, 2, 2, 665, 666, 3, 2, 2, 2, 666, 664, 3, 2, 2, 2, 666, 667, 3, 2, 2, 2, 667, 92, 3, 2, 2, 2, 668, 670, 5, 69, 30, 2, 669, 668, 3, 2, 2, 2, 670, 671, 3, 2, 2, 2, 671, 669, 3, 2, 2, 2, 671, 672, 3, 2, 2, 2, 672, 673, 3, 2, 2, 2, 673, 677, 5, 107, 49, 2, 674, 676, 5, 69, 30, 2, 675, 674, 3, 2, 2, 2, 676, 679, 3, 2, 2, 2, 677, 675, 3, 2, 2, 2, 677, 678, 3, 2, 2, 2, 678, 711, 3, 2, 2, 2, 679, 677, 3, 2, 2, 2, 680, 682, 5, 107, 49, 2, 681, 683, 5, 69, 30, 2, 682, 681, 3, 2, 2, 2, 683, 684, 3, 2, 2, 2, 684, 682, 3, 2, 2, 2, 684, 685, 3, 2, 2, 2, 685, 711, 3, 2, 2, 2, 686, 688, 5, 69, 30, 2, 687, 686, 3, 2, 2, 2, 688, 689, 3, 2, 2, 2, 689, 687, 3, 2, 2, 2, 689, 690, 3, 2, 2, 2, 690, 698, 3, 2, 2, 2, 691, 695, 5, 107, 49, 2, 692, 694, 5, 69, 30, 2, 693, 692, 3, 2, 2, 2, 694, 697, 3, 2, 2, 2, 695, 693, 3, 2, 2, 2, 695, 696, 3, 2, 2, 2, 696, 699, 3, 2, 2, 2, 697, 695, 3, 2, 2, 2, 698, 691, 3, 2, 2, 2, 698, 699, 3, 2, 2, 2, 699, 700, 3, 2, 2, 2, 700, 701, 5, 77, 34, 2, 701, 711, 3, 2, 2, 2, 702, 704, 5, 107, 49, 2, 703, 705, 5, 69, 30, 2, 704, 703, 3, 2, 2, 2, 705, 706, 3, 2, 2, 2, 706, 704, 3, 2, 2, 2, 706, 707, 3, 2, 2, 2, 707, 708, 3, 2, 2, 2, 708, 709, 5, 77, 34, 2, 709, 711, 3, 2, 2, 2, 710, 669, 3, 2, 2, 2, 710, 680, 3, 2, 2, 2, 710, 687, 3, 2, 2, 2, 710, 702, 3, 2, 2, 2, 711, 94, 3, 2, 2, 2, 712, 713, 5, 323, 157, 2, 713, 714, 5, 369, 180, 2, 714, 96, 3, 2, 2, 2, 715, 716, 5, 321, 156, 2, 716, 717, 5, 347, 169, 2, 717, 718, 5, 327, 159, 2, 718, 98, 3, 2, 2, 2, 719, 720, 5, 321, 156, 2, 720, 721, 5, 357, 174, 2, 721, 722, 5, 325, 158, 2, 722, 100, 3, 2, 2, 2, 723, 724, 7, 63, 2, 2, 724, 102, 3, 2, 2, 2, 725, 726, 7, 46, 2, 2, 726, 104, 3, 2, 2, 2, 727, 728, 5, 327, 159, 2, 728, 729, 5, 329, 160, 2, 729, 730, 5, 357, 174, 2, 730, 731, 5, 325, 158, 2, 731, 106, 3, 2, 2, 2, 732, 733, 7, 48, 2, 2, 733, 108, 3, 2, 2, 2, 734, 735, 5, 331, 161, 2, 735, 736, 5, 321, 156, 2, 736, 737, 5, 343, 167, 2, 737, 738, 5, 357, 174, 2, 738, 739, 5, 329, 160, 2, 739, 110, 3, 2, 2, 2, 740, 741, 5, 331, 161, 2, 741, 742, 5, 337, 164, 2, 742, 743, 5, 355, 173, 2, 743, 744, 5, 357, 174, 2, 744, 745, 5, 359, 175, 2, 745, 112, 3, 2, 2, 2, 746, 747, 5, 343, 167, 2, 747, 748, 5, 321, 156, 2, 748, 749, 5, 357, 174, 2, 749, 750, 5, 359, 175, 2, 750, 114, 3, 2, 2, 2, 751, 752, 7, 42, 2, 2, 752, 116, 3, 2, 2, 2, 753, 754, 5, 337, 164, 2, 754, 755, 5, 347, 169, 2, 755, 118, 3, 2, 2, 2, 756, 757, 5, 337, 164, 2, 757, 758, 5, 357, 174, 2, 758, 120, 3, 2, 2, 2, 759, 760, 5, 343, 167, 2, 760, 761, 5, 337, 164, 2, 761, 762, 5, 341, 166, 2, 762, 763, 5, 329, 160, 2, 763, 122, 3, 2, 2, 2, 764, 765, 5, 347, 169, 2, 765, 766, 5, 349, 170, 2, 766, 767, 5, 359, 175, 2, 767, 124, 3, 2, 2, 2, 768, 769, 5, 347, 169, 2, 769, 770, 5, 361, 176, 2, 770, 771, 5, 343, 167, 2, 771, 772, 5, 343, 167, 2, 772, 126, 3, 2, 2, 2, 773, 774, 5, 347, 169, 2, 774, 775, 5, 361, 176, 2, 775, 776, 5, 343, 167, 2, 776, 777, 5, 343, 167, 2, 777, 778, 5, 357, 174, 2, 778, 128, 3, 2, 2, 2, 779, 780, 5, 349, 170, 2, 780, 781, 5, 355, 173, 2, 781, 130, 3, 2, 2, 2, 782, 783, 7, 65, 2, 2, 783, 132, 3, 2, 2, 2, 784, 785, 5, 355, 173, 2, 785, 786, 5, 343, 167, 2, 786, 787, 5, 337, 164, 2, 787, 788, 5, 341, 166, 2, 788, 789, 5, 329, 160, 2, 789, 134, 3, 2, 2, 2, 790, 791, 7, 43, 2, 2, 791, 136, 3, 2, 2, 2, 792, 793, 5, 359, 175, 2, 793, 794, 5, 355, 173, 2, 794, 795, 5, 361, 176, 2, 795, 796, 5, 329, 160, 2, 796, 138, 3, 2, 2, 2, 797, 798, 7, 63, 2, 2, 798, 799, 7, 63, 2, 2, 799, 140, 3, 2, 2, 2, 800, 801, 7, 63, 2, 2, 801, 802, 7, 128, 2, 2, 802, 142, 3, 2, 2, 2, 803, 804, 7, 35, 2, 2, 804, 805, 7, 63, 2, 2, 805, 144, 3, 2, 2, 2, 806, 807, 7, 62, 2, 2, 807, 146, 3, 2, 2, 2, 808, 809, 7, 62, 2, 2, 809, 810, 7, 63, 2, 2, 810, 148, 3, 2, 2, 2, 811, 812, 7, 64, 2, 2, 812, 150, 3, 2, 2, 2, 813, 814, 7, 64, 2, 2, 814, 815, 7, 63, 2, 2, 815, 152, 3, 2, 2, 2, 816, 817, 7, 45, 2, 2, 817, 154, 3, 2, 2, 2, 818, 819, 7, 47, 2, 2, 819, 156, 3, 2, 2, 2, 820, 821, 7, 44, 2, 2, 821, 158, 3, 2, 2, 2, 822, 823, 7, 49, 2, 2, 823, 160, 3, 2, 2, 2, 824, 825, 7, 39, 2, 2, 825, 162, 3, 2, 2, 2, 826, 827, 7, 93, 2, 2, 827, 828, 3, 2, 2, 2, 828, 829, 8, 77, 2, 2, 829, 830, 8, 77, 2, 2, 830, 164, 3, 2, 2, 2, 831, 832, 7, 95, 2, 2, 832, 833, 3, 2, 2, 2, 833, 834, 8, 78, 14, 2, 834, 835, 8, 78, 14, 2, 835, 166, 3, 2, 2, 2, 836, 840, 5, 71, 31, 2, 837, 839, 5, 87, 39, 2, 838, 837, 3, 2, 2, 2, 839, 842, 3, 2, 2, 2, 840, 838, 3, 2, 2, 2, 840, 841, 3, 2, 2, 2, 841, 853, 3, 2, 2, 2, 842, 840, 3, 2, 2, 2, 843, 846, 5, 85, 38, 2, 844, 846, 5, 79, 35, 2, 845, 843, 3, 2, 2, 2, 845, 844, 3, 2, 2, 2, 846, 848, 3, 2, 2, 2, 847, 849, 5, 87, 39, 2, 848, 847, 3, 2, 2, 2, 849, 850, 3, 2, 2, 2, 850, 848, 3, 2, 2, 2, 850, 851, 3, 2, 2, 2, 851, 853, 3, 2, 2, 2, 852, 836, 3, 2, 2, 2, 852, 845, 3, 2, 2, 2, 853, 168, 3, 2, 2, 2, 854, 856, 5, 81, 36, 2, 855, 857, 5, 83, 37, 2, 856, 855, 3, 2, 2, 2, 857, 858, 3, 2, 2, 2, 858, 856, 3, 2, 2, 2, 858, 859, 3, 2, 2, 2, 859, 860, 3, 2, 2, 2, 860, 861, 5, 81, 36, 2, 861, 170, 3, 2, 2, 2, 862, 863, 5, 51, 21, 2, 863, 864, 3, 2, 2, 2, 864, 865, 8, 81, 10, 2, 865, 172, 3, 2, 2, 2, 866, 867, 5, 53, 22, 2, 867, 868, 3, 2, 2, 2, 868, 869, 8, 82, 10, 2, 869, 174, 3, 2, 2, 2, 870, 871, 5, 55, 23, 2, 871, 872, 3, 2, 2, 2, 872, 873, 8, 83, 10, 2, 873, 176, 3, 2, 2, 2, 874, 875, 5, 67, 29, 2, 875, 876, 3, 2, 2, 2, 876, 877, 8, 84, 13, 2, 877, 878, 8, 84, 14, 2, 878, 178, 3, 2, 2, 2, 879, 880, 5, 163, 77, 2, 880, 881, 3, 2, 2, 2, 881, 882, 8, 85, 11, 2, 882, 180, 3, 2, 2, 2, 883, 884, 5, 165, 78, 2, 884, 885, 3, 2, 2, 2, 885, 886, 8, 86, 15, 2, 886, 182, 3, 2, 2, 2, 887, 888, 5, 103, 47, 2, 888, 889, 3, 2, 2, 2, 889, 890, 8, 87, 16, 2, 890, 184, 3, 2, 2, 2, 891, 892, 5, 101, 46, 2, 892, 893, 3, 2, 2, 2, 893, 894, 8, 88, 17, 2, 894, 186, 3, 2, 2, 2, 895, 896, 5, 345, 168, 2, 896, 897, 5, 329, 160, 2, 897, 898, 5, 359, 175, 2, 898, 899, 5, 321, 156, 2, 899, 900, 5, 327, 159, 2, 900, 901, 5, 321, 156, 2, 901, 902, 5, 359, 175, 2, 902, 903, 5, 321, 156, 2, 903, 188, 3, 2, 2, 2, 904, 908, 10, 12, 2, 2, 905, 906, 7, 49, 2, 2, 906, 908, 10, 13, 2, 2, 907, 904, 3, 2, 2, 2, 907, 905, 3, 2, 2, 2, 908, 190, 3, 2, 2, 2, 909, 911, 5, 189, 90, 2, 910, 909, 3, 2, 2, 2, 911, 912, 3, 2, 2, 2, 912, 910, 3, 2, 2, 2, 912, 913, 3, 2, 2, 2, 913, 192, 3, 2, 2, 2, 914, 915, 5, 169, 80, 2, 915, 916, 3, 2, 2, 2, 916, 917, 8, 92, 18, 2, 917, 194, 3, 2, 2, 2, 918, 919, 5, 51, 21, 2, 919, 920, 3, 2, 2, 2, 920, 921, 8, 93, 10, 2, 921, 196, 3, 2, 2, 2, 922, 923, 5, 53, 22, 2, 923, 924, 3, 2, 2, 2, 924, 925, 8, 94, 10, 2, 925, 198, 3, 2, 2, 2, 926, 927, 5, 55, 23, 2, 927, 928, 3, 2, 2, 2, 928, 929, 8, 95, 10, 2, 929, 200, 3, 2, 2, 2, 930, 931, 5, 67, 29, 2, 931, 932, 3, 2, 2, 2, 932, 933, 8, 96, 13, 2, 933, 934, 8, 96, 14, 2, 934, 202, 3, 2, 2, 2, 935, 936, 5, 107, 49, 2, 936, 937, 3, 2, 2, 2, 937, 938, 8, 97, 19, 2, 938, 204, 3, 2, 2, 2, 939, 940, 5, 103, 47, 2, 940, 941, 3, 2, 2, 2, 941, 942, 8, 98, 16, 2, 942, 206, 3, 2, 2, 2, 943, 948, 5, 71, 31, 2, 944, 948, 5, 69, 30, 2, 945, 948, 5, 85, 38, 2, 946, 948, 5, 157, 74, 2, 947, 943, 3, 2, 2, 2, 947, 944, 3, 2, 2, 2, 947, 945, 3, 2, 2, 2, 947, 946, 3, 2, 2, 2, 948, 208, 3, 2, 2, 2, 949, 952, 5, 71, 31, 2, 950, 952, 5, 157, 74, 2, 951, 949, 3, 2, 2, 2, 951, 950, 3, 2, 2, 2, 952, 956, 3, 2, 2, 2, 953, 955, 5, 207, 99, 2, 954, 953, 3, 2, 2, 2, 955, 958, 3, 2, 2, 2, 956, 954, 3, 2, 2, 2, 956, 957, 3, 2, 2, 2, 957, 969, 3, 2, 2, 2, 958, 956, 3, 2, 2, 2, 959, 962, 5, 85, 38, 2, 960, 962, 5, 79, 35, 2, 961, 959, 3, 2, 2, 2, 961, 960, 3, 2, 2, 2, 962, 964, 3, 2, 2, 2, 963, 965, 5, 207, 99, 2, 964, 963, 3, 2, 2, 2, 965, 966, 3, 2, 2, 2, 966, 964, 3, 2, 2, 2, 966, 967, 3, 2, 2, 2, 967, 969, 3, 2, 2, 2, 968, 951, 3, 2, 2, 2, 968, 961, 3, 2, 2, 2, 969, 210, 3, 2, 2, 2, 970, 971, 5, 209, 100, 2, 971, 972, 3, 2, 2, 2, 972, 973, 8, 101, 20, 2, 973, 212, 3, 2, 2, 2, 974, 975, 5, 169, 80, 2, 975, 976, 3, 2, 2, 2, 976, 977, 8, 102, 18, 2, 977, 214, 3, 2, 2, 2, 978, 979, 5, 51, 21, 2, 979, 980, 3, 2, 2, 2, 980, 981, 8, 103, 10, 2, 981, 216, 3, 2, 2, 2, 982, 983, 5, 53, 22, 2, 983, 984, 3, 2, 2, 2, 984, 985, 8, 104, 10, 2, 985, 218, 3, 2, 2, 2, 986, 987, 5, 55, 23, 2, 987, 988, 3, 2, 2, 2, 988, 989, 8, 105, 10, 2, 989, 220, 3, 2, 2, 2, 990, 991, 5, 67, 29, 2, 991, 992, 3, 2, 2, 2, 992, 993, 8, 106, 13, 2, 993, 994, 8, 106, 14, 2, 994, 222, 3, 2, 2, 2, 995, 996, 5, 101, 46, 2, 996, 997, 3, 2, 2, 2, 997, 998, 8, 107, 17, 2, 998, 224, 3, 2, 2, 2, 999, 1000, 5, 103, 47, 2, 1000, 1001, 3, 2, 2, 2, 1001, 1002, 8, 108, 16, 2, 1002, 226, 3, 2, 2, 2, 1003, 1004, 5, 107, 49, 2, 1004, 1005, 3, 2, 2, 2, 1005, 1006, 8, 109, 19, 2, 1006, 228, 3, 2, 2, 2, 1007, 1008, 5, 321, 156, 2, 1008, 1009, 5, 357, 174, 2, 1009, 230, 3, 2, 2, 2, 1010, 1011, 5, 169, 80, 2, 1011, 1012, 3, 2, 2, 2, 1012, 1013, 8, 111, 18, 2, 1013, 232, 3, 2, 2, 2, 1014, 1015, 5, 209, 100, 2, 1015, 1016, 3, 2, 2, 2, 1016, 1017, 8, 112, 20, 2, 1017, 234, 3, 2, 2, 2, 1018, 1019, 5, 51, 21, 2, 1019, 1020, 3, 2, 2, 2, 1020, 1021, 8, 113, 10, 2, 1021, 236, 3, 2, 2, 2, 1022, 1023, 5, 53, 22, 2, 1023, 1024, 3, 2, 2, 2, 1024, 1025, 8, 114, 10, 2, 1025, 238, 3, 2, 2, 2, 1026, 1027, 5, 55, 23, 2, 1027, 1028, 3, 2, 2, 2, 1028, 1029, 8, 115, 10, 2, 1029, 240, 3, 2, 2, 2, 1030, 1031, 5, 67, 29, 2, 1031, 1032, 3, 2, 2, 2, 1032, 1033, 8, 116, 13, 2, 1033, 1034, 8, 116, 14, 2, 1034, 242, 3, 2, 2, 2, 1035, 1036, 5, 163, 77, 2, 1036, 1037, 3, 2, 2, 2, 1037, 1038, 8, 117, 11, 2, 1038, 1039, 8, 117, 21, 2, 1039, 244, 3, 2, 2, 2, 1040, 1041, 5, 349, 170, 2, 1041, 1042, 5, 347, 169, 2, 1042, 1043, 3, 2, 2, 2, 1043, 1044, 8, 118, 22, 2, 1044, 246, 3, 2, 2, 2, 1045, 1046, 5, 365, 178, 2, 1046, 1047, 5, 337, 164, 2, 1047, 1048, 5, 359, 175, 2, 1048, 1049, 5, 335, 163, 2, 1049, 1050, 3, 2, 2, 2, 1050, 1051, 8, 119, 22, 2, 1051, 248, 3, 2, 2, 2, 1052, 1053, 10, 14, 2, 2, 1053, 250, 3, 2, 2, 2, 1054, 1057, 5, 71, 31, 2, 1055, 1057, 5, 69, 30, 2, 1056, 1054, 3, 2, 2, 2, 1056, 1055, 3, 2, 2, 2, 1057, 1061, 3, 2, 2, 2, 1058, 1060, 5, 249, 120, 2, 1059, 1058, 3, 2, 2, 2, 1060, 1063, 3, 2, 2, 2, 1061, 1059, 3, 2, 2, 2, 1061, 1062, 3, 2, 2, 2, 1062, 252, 3, 2, 2, 2, 1063, 1061, 3, 2, 2, 2, 1064, 1065, 5, 169, 80, 2, 1065, 1066, 3, 2, 2, 2, 1066, 1067, 8, 122, 18, 2, 1067, 254, 3, 2, 2, 2, 1068, 1069, 5, 251, 121, 2, 1069, 1070, 3, 2, 2, 2, 1070, 1071, 8, 123, 23, 2, 1071, 256, 3, 2, 2, 2, 1072, 1073, 5, 51, 21, 2, 1073, 1074, 3, 2, 2, 2, 1074, 1075, 8, 124, 10, 2, 1075, 258, 3, 2, 2, 2, 1076, 1077, 5, 53, 22, 2, 1077, 1078, 3, 2, 2, 2, 1078, 1079, 8, 125, 10, 2, 1079, 260, 3, 2, 2, 2, 1080, 1081, 5, 55, 23, 2, 1081, 1082, 3, 2, 2, 2, 1082, 1083, 8, 126, 10, 2, 1083, 262, 3, 2, 2, 2, 1084, 1085, 5, 67, 29, 2, 1085, 1086, 3, 2, 2, 2, 1086, 1087, 8, 127, 13, 2, 1087, 1088, 8, 127, 14, 2, 1088, 1089, 8, 127, 14, 2, 1089, 264, 3, 2, 2, 2, 1090, 1091, 5, 101, 46, 2, 1091, 1092, 3, 2, 2, 2, 1092, 1093, 8, 128, 17, 2, 1093, 266, 3, 2, 2, 2, 1094, 1095, 5, 103, 47, 2, 1095, 1096, 3, 2, 2, 2, 1096, 1097, 8, 129, 16, 2, 1097, 268, 3, 2, 2, 2, 1098, 1099, 5, 107, 49, 2, 1099, 1100, 3, 2, 2, 2, 1100, 1101, 8, 130, 19, 2, 1101, 270, 3, 2, 2, 2, 1102, 1103, 5, 247, 119, 2, 1103, 1104, 3, 2, 2, 2, 1104, 1105, 8, 131, 24, 2, 1105, 272, 3, 2, 2, 2, 1106, 1107, 5, 209, 100, 2, 1107, 1108, 3, 2, 2, 2, 1108, 1109, 8, 132, 20, 2, 1109, 274, 3, 2, 2, 2, 1110, 1111, 5, 169, 80, 2, 1111, 1112, 3, 2, 2, 2, 1112, 1113, 8, 133, 18, 2, 1113, 276, 3, 2, 2, 2, 1114, 1115, 5, 51, 21, 2, 1115, 1116, 3, 2, 2, 2, 1116, 1117, 8, 134, 10, 2, 1117, 278, 3, 2, 2, 2, 1118, 1119, 5, 53, 22, 2, 1119, 1120, 3, 2, 2, 2, 1120, 1121, 8, 135, 10, 2, 1121, 280, 3, 2, 2, 2, 1122, 1123, 5, 55, 23, 2, 1123, 1124, 3, 2, 2, 2, 1124, 1125, 8, 136, 10, 2, 1125, 282, 3, 2, 2, 2, 1126, 1127, 5, 67, 29, 2, 1127, 1128, 3, 2, 2, 2, 1128, 1129, 8, 137, 13, 2, 1129, 1130, 8, 137, 14, 2, 1130, 284, 3, 2, 2, 2, 1131, 1132, 5, 107, 49, 2, 1132, 1133, 3, 2, 2, 2, 1133, 1134, 8, 138, 19, 2, 1134, 286, 3, 2, 2, 2, 1135, 1136, 5, 169, 80, 2, 1136, 1137, 3, 2, 2, 2, 1137, 1138, 8, 139, 18, 2, 1138, 288, 3, 2, 2, 2, 1139, 1140, 5, 167, 79, 2, 1140, 1141, 3, 2, 2, 2, 1141, 1142, 8, 140, 25, 2, 1142, 290, 3, 2, 2, 2, 1143, 1144, 5, 51, 21, 2, 1144, 1145, 3, 2, 2, 2, 1145, 1146, 8, 141, 10, 2, 1146, 292, 3, 2, 2, 2, 1147, 1148, 5, 53, 22, 2, 1148, 1149, 3, 2, 2, 2, 1149, 1150, 8, 142, 10, 2, 1150, 294, 3, 2, 2, 2, 1151, 1152, 5, 55, 23, 2, 1152, 1153, 3, 2, 2, 2, 1153, 1154, 8, 143, 10, 2, 1154, 296, 3, 2, 2, 2, 1155, 1156, 5, 67, 29, 2, 1156, 1157, 3, 2, 2, 2, 1157, 1158, 8, 144, 13, 2, 1158, 1159, 8, 144, 14, 2, 1159, 298, 3, 2, 2, 2, 1160, 1161, 5, 337, 164, 2, 1161, 1162, 5, 347, 169, 2, 1162, 1163, 5, 331, 161, 2, 1163, 1164, 5, 349, 170, 2, 1164, 300, 3, 2, 2, 2, 1165, 1166, 5, 331, 161, 2, 1166, 1167, 5, 361, 176, 2, 1167, 1168, 5, 347, 169, 2, 1168, 1169, 5, 325, 158, 2, 1169, 1170, 5, 359, 175, 2, 1170, 1171, 5, 337, 164, 2, 1171, 1172, 5, 349, 170, 2, 1172, 1173, 5, 347, 169, 2, 1173, 1174, 5, 357, 174, 2, 1174, 302, 3, 2, 2, 2, 1175, 1176, 5, 51, 21, 2, 1176, 1177, 3, 2, 2, 2, 1177, 1178, 8, 147, 10, 2, 1178, 304, 3, 2, 2, 2, 1179, 1180, 5, 53, 22, 2, 1180, 1181, 3, 2, 2, 2, 1181, 1182, 8, 148, 10, 2, 1182, 306, 3, 2, 2, 2, 1183, 1184, 5, 55, 23, 2, 1184, 1185, 3, 2, 2, 2, 1185, 1186, 8, 149, 10, 2, 1186, 308, 3, 2, 2, 2, 1187, 1188, 5, 165, 78, 2, 1188, 1189, 3, 2, 2, 2, 1189, 1190, 8, 150, 15, 2, 1190, 1191, 8, 150, 14, 2, 1191, 310, 3, 2, 2, 2, 1192, 1193, 7, 60, 2, 2, 1193, 312, 3, 2, 2, 2, 1194, 1200, 5, 79, 35, 2, 1195, 1200, 5, 69, 30, 2, 1196, 1200, 5, 107, 49, 2, 1197, 1200, 5, 71, 31, 2, 1198, 1200, 5, 85, 38, 2, 1199, 1194, 3, 2, 2, 2, 1199, 1195, 3, 2, 2, 2, 1199, 1196, 3, 2, 2, 2, 1199, 1197, 3, 2, 2, 2, 1199, 1198, 3, 2, 2, 2, 1200, 1201, 3, 2, 2, 2, 1201, 1199, 3, 2, 2, 2, 1201, 1202, 3, 2, 2, 2, 1202, 314, 3, 2, 2, 2, 1203, 1204, 5, 51, 21, 2, 1204, 1205, 3, 2, 2, 2, 1205, 1206, 8, 153, 10, 2, 1206, 316, 3, 2, 2, 2, 1207, 1208, 5, 53, 22, 2, 1208, 1209, 3, 2, 2, 2, 1209, 1210, 8, 154, 10, 2, 1210, 318, 3, 2, 2, 2, 1211, 1212, 5, 55, 23, 2, 1212, 1213, 3, 2, 2, 2, 1213, 1214, 8, 155, 10, 2, 1214, 320, 3, 2, 2, 2, 1215, 1216, 9, 15, 2, 2, 1216, 322, 3, 2, 2, 2, 1217, 1218, 9, 16, 2, 2, 1218, 324, 3, 2, 2, 2, 1219, 1220, 9, 17, 2, 2, 1220, 326, 3, 2, 2, 2, 1221, 1222, 9, 18, 2, 2, 1222, 328, 3, 2, 2, 2, 1223, 1224, 9, 9, 2, 2, 1224, 330, 3, 2, 2, 2, 1225, 1226, 9, 19, 2, 2, 1226, 332, 3, 2, 2, 2, 1227, 1228, 9, 20, 2, 2, 1228, 334, 3, 2, 2, 2, 1229, 1230, 9, 21, 2, 2, 1230, 336, 3, 2, 2, 2, 1231, 1232, 9, 22, 2, 2, 1232, 338, 3, 2, 2, 2, 1233, 1234, 9, 23, 2, 2, 1234, 340, 3, 2, 2, 2, 1235, 1236, 9, 24, 2, 2, 1236, 342, 3, 2, 2, 2, 1237, 1238, 9, 25, 2, 2, 1238, 344, 3, 2, 2, 2, 1239, 1240, 9, 26, 2, 2, 1240, 346, 3, 2, 2, 2, 1241, 1242, 9, 27, 2, 2, 1242, 348, 3, 2, 2, 2, 1243, 1244, 9, 28, 2, 2, 1244, 350, 3, 2, 2, 2, 1245, 1246, 9, 29, 2, 2, 1246, 352, 3, 2, 2, 2, 1247, 1248, 9, 30, 2, 2, 1248, 354, 3, 2, 2, 2, 1249, 1250, 9, 31, 2, 2, 1250, 356, 3, 2, 2, 2, 1251, 1252, 9, 32, 2, 2, 1252, 358, 3, 2, 2, 2, 1253, 1254, 9, 33, 2, 2, 1254, 360, 3, 2, 2, 2, 1255, 1256, 9, 34, 2, 2, 1256, 362, 3, 2, 2, 2, 1257, 1258, 9, 35, 2, 2, 1258, 364, 3, 2, 2, 2, 1259, 1260, 9, 36, 2, 2, 1260, 366, 3, 2, 2, 2, 1261, 1262, 9, 37, 2, 2, 1262, 368, 3, 2, 2, 2, 1263, 1264, 9, 38, 2, 2, 1264, 370, 3, 2, 2, 2, 1265, 1266, 9, 39, 2, 2, 1266, 372, 3, 2, 2, 2, 56, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 529, 539, 543, 546, 555, 557, 568, 609, 614, 623, 630, 635, 637, 648, 656, 659, 661, 666, 671, 677, 684, 689, 695, 698, 706, 710, 840, 845, 850, 852, 858, 907, 912, 947, 951, 956, 961, 966, 968, 1056, 1061, 1199, 1201, 26, 7, 4, 2, 7, 6, 2, 7, 8, 2, 7, 3, 2, 7, 5, 2, 7, 10, 2, 7, 7, 2, 7, 11, 2, 2, 3, 2, 9, 66, 2, 7, 2, 2, 9, 28, 2, 6, 2, 2, 9, 67, 2, 9, 36, 2, 9, 35, 2, 9, 69, 2, 9, 38, 2, 9, 78, 2, 7, 12, 2, 7, 9, 2, 9, 88, 2, 9, 87, 2, 9, 68, 2] \ No newline at end of file +[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 2, 106, 1259, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 4, 38, 9, 38, 4, 39, 9, 39, 4, 40, 9, 40, 4, 41, 9, 41, 4, 42, 9, 42, 4, 43, 9, 43, 4, 44, 9, 44, 4, 45, 9, 45, 4, 46, 9, 46, 4, 47, 9, 47, 4, 48, 9, 48, 4, 49, 9, 49, 4, 50, 9, 50, 4, 51, 9, 51, 4, 52, 9, 52, 4, 53, 9, 53, 4, 54, 9, 54, 4, 55, 9, 55, 4, 56, 9, 56, 4, 57, 9, 57, 4, 58, 9, 58, 4, 59, 9, 59, 4, 60, 9, 60, 4, 61, 9, 61, 4, 62, 9, 62, 4, 63, 9, 63, 4, 64, 9, 64, 4, 65, 9, 65, 4, 66, 9, 66, 4, 67, 9, 67, 4, 68, 9, 68, 4, 69, 9, 69, 4, 70, 9, 70, 4, 71, 9, 71, 4, 72, 9, 72, 4, 73, 9, 73, 4, 74, 9, 74, 4, 75, 9, 75, 4, 76, 9, 76, 4, 77, 9, 77, 4, 78, 9, 78, 4, 79, 9, 79, 4, 80, 9, 80, 4, 81, 9, 81, 4, 82, 9, 82, 4, 83, 9, 83, 4, 84, 9, 84, 4, 85, 9, 85, 4, 86, 9, 86, 4, 87, 9, 87, 4, 88, 9, 88, 4, 89, 9, 89, 4, 90, 9, 90, 4, 91, 9, 91, 4, 92, 9, 92, 4, 93, 9, 93, 4, 94, 9, 94, 4, 95, 9, 95, 4, 96, 9, 96, 4, 97, 9, 97, 4, 98, 9, 98, 4, 99, 9, 99, 4, 100, 9, 100, 4, 101, 9, 101, 4, 102, 9, 102, 4, 103, 9, 103, 4, 104, 9, 104, 4, 105, 9, 105, 4, 106, 9, 106, 4, 107, 9, 107, 4, 108, 9, 108, 4, 109, 9, 109, 4, 110, 9, 110, 4, 111, 9, 111, 4, 112, 9, 112, 4, 113, 9, 113, 4, 114, 9, 114, 4, 115, 9, 115, 4, 116, 9, 116, 4, 117, 9, 117, 4, 118, 9, 118, 4, 119, 9, 119, 4, 120, 9, 120, 4, 121, 9, 121, 4, 122, 9, 122, 4, 123, 9, 123, 4, 124, 9, 124, 4, 125, 9, 125, 4, 126, 9, 126, 4, 127, 9, 127, 4, 128, 9, 128, 4, 129, 9, 129, 4, 130, 9, 130, 4, 131, 9, 131, 4, 132, 9, 132, 4, 133, 9, 133, 4, 134, 9, 134, 4, 135, 9, 135, 4, 136, 9, 136, 4, 137, 9, 137, 4, 138, 9, 138, 4, 139, 9, 139, 4, 140, 9, 140, 4, 141, 9, 141, 4, 142, 9, 142, 4, 143, 9, 143, 4, 144, 9, 144, 4, 145, 9, 145, 4, 146, 9, 146, 4, 147, 9, 147, 4, 148, 9, 148, 4, 149, 9, 149, 4, 150, 9, 150, 4, 151, 9, 151, 4, 152, 9, 152, 4, 153, 9, 153, 4, 154, 9, 154, 4, 155, 9, 155, 4, 156, 9, 156, 4, 157, 9, 157, 4, 158, 9, 158, 4, 159, 9, 159, 4, 160, 9, 160, 4, 161, 9, 161, 4, 162, 9, 162, 4, 163, 9, 163, 4, 164, 9, 164, 4, 165, 9, 165, 4, 166, 9, 166, 4, 167, 9, 167, 4, 168, 9, 168, 4, 169, 9, 169, 4, 170, 9, 170, 4, 171, 9, 171, 4, 172, 9, 172, 4, 173, 9, 173, 4, 174, 9, 174, 4, 175, 9, 175, 4, 176, 9, 176, 4, 177, 9, 177, 4, 178, 9, 178, 4, 179, 9, 179, 4, 180, 9, 180, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 3, 14, 3, 14, 3, 14, 3, 14, 3, 14, 3, 14, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 19, 6, 19, 516, 10, 19, 13, 19, 14, 19, 517, 3, 19, 3, 19, 3, 20, 3, 20, 3, 20, 3, 20, 7, 20, 526, 10, 20, 12, 20, 14, 20, 529, 11, 20, 3, 20, 5, 20, 532, 10, 20, 3, 20, 5, 20, 535, 10, 20, 3, 20, 3, 20, 3, 21, 3, 21, 3, 21, 3, 21, 3, 21, 7, 21, 544, 10, 21, 12, 21, 14, 21, 547, 11, 21, 3, 21, 3, 21, 3, 21, 3, 21, 3, 21, 3, 22, 6, 22, 555, 10, 22, 13, 22, 14, 22, 556, 3, 22, 3, 22, 3, 23, 3, 23, 3, 23, 3, 23, 3, 23, 3, 24, 3, 24, 3, 24, 3, 24, 3, 24, 3, 25, 3, 25, 3, 25, 3, 25, 3, 26, 3, 26, 3, 26, 3, 26, 3, 27, 3, 27, 3, 27, 3, 27, 3, 28, 3, 28, 3, 28, 3, 28, 3, 29, 3, 29, 3, 30, 3, 30, 3, 31, 3, 31, 3, 31, 3, 32, 3, 32, 3, 33, 3, 33, 5, 33, 598, 10, 33, 3, 33, 6, 33, 601, 10, 33, 13, 33, 14, 33, 602, 3, 34, 3, 34, 3, 35, 3, 35, 3, 36, 3, 36, 3, 36, 5, 36, 612, 10, 36, 3, 37, 3, 37, 3, 38, 3, 38, 3, 38, 5, 38, 619, 10, 38, 3, 39, 3, 39, 3, 39, 7, 39, 624, 10, 39, 12, 39, 14, 39, 627, 11, 39, 3, 39, 3, 39, 3, 39, 3, 39, 3, 39, 3, 39, 7, 39, 635, 10, 39, 12, 39, 14, 39, 638, 11, 39, 3, 39, 3, 39, 3, 39, 3, 39, 3, 39, 5, 39, 645, 10, 39, 3, 39, 5, 39, 648, 10, 39, 5, 39, 650, 10, 39, 3, 40, 6, 40, 653, 10, 40, 13, 40, 14, 40, 654, 3, 41, 6, 41, 658, 10, 41, 13, 41, 14, 41, 659, 3, 41, 3, 41, 7, 41, 664, 10, 41, 12, 41, 14, 41, 667, 11, 41, 3, 41, 3, 41, 6, 41, 671, 10, 41, 13, 41, 14, 41, 672, 3, 41, 6, 41, 676, 10, 41, 13, 41, 14, 41, 677, 3, 41, 3, 41, 7, 41, 682, 10, 41, 12, 41, 14, 41, 685, 11, 41, 5, 41, 687, 10, 41, 3, 41, 3, 41, 3, 41, 3, 41, 6, 41, 693, 10, 41, 13, 41, 14, 41, 694, 3, 41, 3, 41, 5, 41, 699, 10, 41, 3, 42, 3, 42, 3, 42, 3, 43, 3, 43, 3, 43, 3, 43, 3, 44, 3, 44, 3, 44, 3, 44, 3, 45, 3, 45, 3, 46, 3, 46, 3, 47, 3, 47, 3, 47, 3, 47, 3, 47, 3, 48, 3, 48, 3, 49, 3, 49, 3, 49, 3, 49, 3, 49, 3, 49, 3, 50, 3, 50, 3, 50, 3, 50, 3, 50, 3, 50, 3, 51, 3, 51, 3, 51, 3, 51, 3, 51, 3, 52, 3, 52, 3, 53, 3, 53, 3, 53, 3, 54, 3, 54, 3, 54, 3, 55, 3, 55, 3, 55, 3, 55, 3, 55, 3, 56, 3, 56, 3, 56, 3, 56, 3, 57, 3, 57, 3, 57, 3, 57, 3, 57, 3, 58, 3, 58, 3, 58, 3, 58, 3, 58, 3, 58, 3, 59, 3, 59, 3, 59, 3, 60, 3, 60, 3, 61, 3, 61, 3, 61, 3, 61, 3, 61, 3, 61, 3, 62, 3, 62, 3, 63, 3, 63, 3, 63, 3, 63, 3, 63, 3, 64, 3, 64, 3, 64, 3, 65, 3, 65, 3, 65, 3, 66, 3, 66, 3, 66, 3, 67, 3, 67, 3, 68, 3, 68, 3, 68, 3, 69, 3, 69, 3, 70, 3, 70, 3, 70, 3, 71, 3, 71, 3, 72, 3, 72, 3, 73, 3, 73, 3, 74, 3, 74, 3, 75, 3, 75, 3, 76, 3, 76, 3, 76, 3, 76, 3, 76, 3, 77, 3, 77, 3, 77, 3, 77, 3, 77, 3, 78, 3, 78, 7, 78, 827, 10, 78, 12, 78, 14, 78, 830, 11, 78, 3, 78, 3, 78, 5, 78, 834, 10, 78, 3, 78, 6, 78, 837, 10, 78, 13, 78, 14, 78, 838, 5, 78, 841, 10, 78, 3, 79, 3, 79, 6, 79, 845, 10, 79, 13, 79, 14, 79, 846, 3, 79, 3, 79, 3, 80, 3, 80, 3, 80, 3, 80, 3, 81, 3, 81, 3, 81, 3, 81, 3, 82, 3, 82, 3, 82, 3, 82, 3, 83, 3, 83, 3, 83, 3, 83, 3, 83, 3, 84, 3, 84, 3, 84, 3, 84, 3, 85, 3, 85, 3, 85, 3, 85, 3, 86, 3, 86, 3, 86, 3, 86, 3, 87, 3, 87, 3, 87, 3, 87, 3, 88, 3, 88, 3, 88, 3, 88, 3, 88, 3, 88, 3, 88, 3, 88, 3, 88, 3, 89, 3, 89, 3, 89, 5, 89, 896, 10, 89, 3, 90, 6, 90, 899, 10, 90, 13, 90, 14, 90, 900, 3, 91, 3, 91, 3, 91, 3, 91, 3, 92, 3, 92, 3, 92, 3, 92, 3, 93, 3, 93, 3, 93, 3, 93, 3, 94, 3, 94, 3, 94, 3, 94, 3, 95, 3, 95, 3, 95, 3, 95, 3, 95, 3, 96, 3, 96, 3, 96, 3, 96, 3, 97, 3, 97, 3, 97, 3, 97, 3, 98, 3, 98, 3, 98, 3, 98, 5, 98, 936, 10, 98, 3, 99, 3, 99, 5, 99, 940, 10, 99, 3, 99, 7, 99, 943, 10, 99, 12, 99, 14, 99, 946, 11, 99, 3, 99, 3, 99, 5, 99, 950, 10, 99, 3, 99, 6, 99, 953, 10, 99, 13, 99, 14, 99, 954, 5, 99, 957, 10, 99, 3, 100, 3, 100, 3, 100, 3, 100, 3, 101, 3, 101, 3, 101, 3, 101, 3, 102, 3, 102, 3, 102, 3, 102, 3, 103, 3, 103, 3, 103, 3, 103, 3, 104, 3, 104, 3, 104, 3, 104, 3, 105, 3, 105, 3, 105, 3, 105, 3, 105, 3, 106, 3, 106, 3, 106, 3, 106, 3, 107, 3, 107, 3, 107, 3, 107, 3, 108, 3, 108, 3, 108, 3, 108, 3, 109, 3, 109, 3, 109, 3, 110, 3, 110, 3, 110, 3, 110, 3, 111, 3, 111, 3, 111, 3, 111, 3, 112, 3, 112, 3, 112, 3, 112, 3, 113, 3, 113, 3, 113, 3, 113, 3, 114, 3, 114, 3, 114, 3, 114, 3, 115, 3, 115, 3, 115, 3, 115, 3, 115, 3, 116, 3, 116, 3, 116, 3, 116, 3, 116, 3, 117, 3, 117, 3, 117, 3, 117, 3, 117, 3, 118, 3, 118, 3, 118, 3, 118, 3, 118, 3, 118, 3, 118, 3, 119, 3, 119, 3, 120, 6, 120, 1044, 10, 120, 13, 120, 14, 120, 1045, 3, 120, 3, 120, 5, 120, 1050, 10, 120, 3, 120, 6, 120, 1053, 10, 120, 13, 120, 14, 120, 1054, 3, 121, 3, 121, 3, 121, 3, 121, 3, 122, 3, 122, 3, 122, 3, 122, 3, 123, 3, 123, 3, 123, 3, 123, 3, 124, 3, 124, 3, 124, 3, 124, 3, 125, 3, 125, 3, 125, 3, 125, 3, 126, 3, 126, 3, 126, 3, 126, 3, 126, 3, 126, 3, 127, 3, 127, 3, 127, 3, 127, 3, 128, 3, 128, 3, 128, 3, 128, 3, 129, 3, 129, 3, 129, 3, 129, 3, 130, 3, 130, 3, 130, 3, 130, 3, 131, 3, 131, 3, 131, 3, 131, 3, 132, 3, 132, 3, 132, 3, 132, 3, 133, 3, 133, 3, 133, 3, 133, 3, 134, 3, 134, 3, 134, 3, 134, 3, 135, 3, 135, 3, 135, 3, 135, 3, 136, 3, 136, 3, 136, 3, 136, 3, 136, 3, 137, 3, 137, 3, 137, 3, 137, 3, 138, 3, 138, 3, 138, 3, 138, 3, 139, 3, 139, 3, 139, 3, 139, 3, 140, 3, 140, 3, 140, 3, 140, 3, 141, 3, 141, 3, 141, 3, 141, 3, 142, 3, 142, 3, 142, 3, 142, 3, 143, 3, 143, 3, 143, 3, 143, 3, 143, 3, 144, 3, 144, 3, 144, 3, 144, 3, 144, 3, 145, 3, 145, 3, 145, 3, 145, 3, 145, 3, 145, 3, 145, 3, 145, 3, 145, 3, 145, 3, 146, 3, 146, 3, 146, 3, 146, 3, 147, 3, 147, 3, 147, 3, 147, 3, 148, 3, 148, 3, 148, 3, 148, 3, 149, 3, 149, 3, 149, 3, 149, 3, 149, 3, 150, 3, 150, 3, 151, 3, 151, 3, 151, 3, 151, 3, 151, 6, 151, 1192, 10, 151, 13, 151, 14, 151, 1193, 3, 152, 3, 152, 3, 152, 3, 152, 3, 153, 3, 153, 3, 153, 3, 153, 3, 154, 3, 154, 3, 154, 3, 154, 3, 155, 3, 155, 3, 156, 3, 156, 3, 157, 3, 157, 3, 158, 3, 158, 3, 159, 3, 159, 3, 160, 3, 160, 3, 161, 3, 161, 3, 162, 3, 162, 3, 163, 3, 163, 3, 164, 3, 164, 3, 165, 3, 165, 3, 166, 3, 166, 3, 167, 3, 167, 3, 168, 3, 168, 3, 169, 3, 169, 3, 170, 3, 170, 3, 171, 3, 171, 3, 172, 3, 172, 3, 173, 3, 173, 3, 174, 3, 174, 3, 175, 3, 175, 3, 176, 3, 176, 3, 177, 3, 177, 3, 178, 3, 178, 3, 179, 3, 179, 3, 180, 3, 180, 4, 545, 636, 2, 2, 181, 13, 2, 3, 15, 2, 4, 17, 2, 5, 19, 2, 6, 21, 2, 7, 23, 2, 8, 25, 2, 9, 27, 2, 10, 29, 2, 11, 31, 2, 12, 33, 2, 13, 35, 2, 14, 37, 2, 15, 39, 2, 16, 41, 2, 17, 43, 2, 18, 45, 2, 19, 47, 2, 20, 49, 2, 21, 51, 2, 22, 53, 2, 23, 55, 2, 2, 57, 2, 2, 59, 2, 24, 61, 2, 25, 63, 2, 26, 65, 2, 27, 67, 2, 2, 69, 2, 2, 71, 2, 2, 73, 2, 2, 75, 2, 2, 77, 2, 2, 79, 2, 2, 81, 2, 2, 83, 2, 2, 85, 2, 2, 87, 2, 28, 89, 2, 29, 91, 2, 30, 93, 2, 31, 95, 2, 32, 97, 2, 33, 99, 2, 34, 101, 2, 35, 103, 2, 36, 105, 2, 37, 107, 2, 38, 109, 2, 39, 111, 2, 40, 113, 2, 41, 115, 2, 42, 117, 2, 43, 119, 2, 44, 121, 2, 45, 123, 2, 46, 125, 2, 47, 127, 2, 48, 129, 2, 49, 131, 2, 50, 133, 2, 51, 135, 2, 52, 137, 2, 53, 139, 2, 54, 141, 2, 55, 143, 2, 56, 145, 2, 57, 147, 2, 58, 149, 2, 59, 151, 2, 60, 153, 2, 61, 155, 2, 62, 157, 2, 63, 159, 2, 64, 161, 2, 65, 163, 2, 66, 165, 2, 67, 167, 2, 68, 169, 2, 69, 171, 2, 70, 173, 2, 71, 175, 2, 2, 177, 2, 2, 179, 2, 2, 181, 2, 2, 183, 2, 2, 185, 2, 72, 187, 2, 2, 189, 2, 73, 191, 2, 2, 193, 2, 74, 195, 2, 75, 197, 2, 76, 199, 2, 2, 201, 2, 2, 203, 2, 2, 205, 2, 2, 207, 2, 77, 209, 2, 2, 211, 2, 2, 213, 2, 78, 215, 2, 79, 217, 2, 80, 219, 2, 2, 221, 2, 2, 223, 2, 2, 225, 2, 2, 227, 2, 81, 229, 2, 2, 231, 2, 2, 233, 2, 82, 235, 2, 83, 237, 2, 84, 239, 2, 2, 241, 2, 2, 243, 2, 85, 245, 2, 86, 247, 2, 2, 249, 2, 87, 251, 2, 2, 253, 2, 2, 255, 2, 88, 257, 2, 89, 259, 2, 90, 261, 2, 2, 263, 2, 2, 265, 2, 2, 267, 2, 2, 269, 2, 2, 271, 2, 2, 273, 2, 2, 275, 2, 91, 277, 2, 92, 279, 2, 93, 281, 2, 2, 283, 2, 2, 285, 2, 2, 287, 2, 2, 289, 2, 94, 291, 2, 95, 293, 2, 96, 295, 2, 2, 297, 2, 97, 299, 2, 98, 301, 2, 99, 303, 2, 100, 305, 2, 101, 307, 2, 2, 309, 2, 102, 311, 2, 103, 313, 2, 104, 315, 2, 105, 317, 2, 106, 319, 2, 2, 321, 2, 2, 323, 2, 2, 325, 2, 2, 327, 2, 2, 329, 2, 2, 331, 2, 2, 333, 2, 2, 335, 2, 2, 337, 2, 2, 339, 2, 2, 341, 2, 2, 343, 2, 2, 345, 2, 2, 347, 2, 2, 349, 2, 2, 351, 2, 2, 353, 2, 2, 355, 2, 2, 357, 2, 2, 359, 2, 2, 361, 2, 2, 363, 2, 2, 365, 2, 2, 367, 2, 2, 369, 2, 2, 13, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 40, 8, 2, 11, 12, 15, 15, 34, 34, 49, 49, 93, 93, 95, 95, 4, 2, 12, 12, 15, 15, 5, 2, 11, 12, 15, 15, 34, 34, 3, 2, 50, 59, 4, 2, 67, 92, 99, 124, 7, 2, 36, 36, 94, 94, 112, 112, 116, 116, 118, 118, 6, 2, 12, 12, 15, 15, 36, 36, 94, 94, 4, 2, 71, 71, 103, 103, 4, 2, 45, 45, 47, 47, 3, 2, 98, 98, 12, 2, 11, 12, 15, 15, 34, 34, 46, 46, 49, 49, 63, 63, 93, 93, 95, 95, 98, 98, 126, 126, 4, 2, 44, 44, 49, 49, 13, 2, 11, 12, 15, 15, 34, 34, 36, 37, 46, 46, 49, 49, 60, 60, 62, 62, 64, 65, 94, 94, 126, 126, 4, 2, 67, 67, 99, 99, 4, 2, 68, 68, 100, 100, 4, 2, 69, 69, 101, 101, 4, 2, 70, 70, 102, 102, 4, 2, 72, 72, 104, 104, 4, 2, 73, 73, 105, 105, 4, 2, 74, 74, 106, 106, 4, 2, 75, 75, 107, 107, 4, 2, 76, 76, 108, 108, 4, 2, 77, 77, 109, 109, 4, 2, 78, 78, 110, 110, 4, 2, 79, 79, 111, 111, 4, 2, 80, 80, 112, 112, 4, 2, 81, 81, 113, 113, 4, 2, 82, 82, 114, 114, 4, 2, 83, 83, 115, 115, 4, 2, 84, 84, 116, 116, 4, 2, 85, 85, 117, 117, 4, 2, 86, 86, 118, 118, 4, 2, 87, 87, 119, 119, 4, 2, 88, 88, 120, 120, 4, 2, 89, 89, 121, 121, 4, 2, 90, 90, 122, 122, 4, 2, 91, 91, 123, 123, 4, 2, 92, 92, 124, 124, 2, 1261, 2, 13, 3, 2, 2, 2, 2, 15, 3, 2, 2, 2, 2, 17, 3, 2, 2, 2, 2, 19, 3, 2, 2, 2, 2, 21, 3, 2, 2, 2, 2, 23, 3, 2, 2, 2, 2, 25, 3, 2, 2, 2, 2, 27, 3, 2, 2, 2, 2, 29, 3, 2, 2, 2, 2, 31, 3, 2, 2, 2, 2, 33, 3, 2, 2, 2, 2, 35, 3, 2, 2, 2, 2, 37, 3, 2, 2, 2, 2, 39, 3, 2, 2, 2, 2, 41, 3, 2, 2, 2, 2, 43, 3, 2, 2, 2, 2, 45, 3, 2, 2, 2, 2, 47, 3, 2, 2, 2, 2, 49, 3, 2, 2, 2, 2, 51, 3, 2, 2, 2, 2, 53, 3, 2, 2, 2, 3, 55, 3, 2, 2, 2, 3, 57, 3, 2, 2, 2, 3, 59, 3, 2, 2, 2, 3, 61, 3, 2, 2, 2, 3, 63, 3, 2, 2, 2, 4, 65, 3, 2, 2, 2, 4, 87, 3, 2, 2, 2, 4, 89, 3, 2, 2, 2, 4, 91, 3, 2, 2, 2, 4, 93, 3, 2, 2, 2, 4, 95, 3, 2, 2, 2, 4, 97, 3, 2, 2, 2, 4, 99, 3, 2, 2, 2, 4, 101, 3, 2, 2, 2, 4, 103, 3, 2, 2, 2, 4, 105, 3, 2, 2, 2, 4, 107, 3, 2, 2, 2, 4, 109, 3, 2, 2, 2, 4, 111, 3, 2, 2, 2, 4, 113, 3, 2, 2, 2, 4, 115, 3, 2, 2, 2, 4, 117, 3, 2, 2, 2, 4, 119, 3, 2, 2, 2, 4, 121, 3, 2, 2, 2, 4, 123, 3, 2, 2, 2, 4, 125, 3, 2, 2, 2, 4, 127, 3, 2, 2, 2, 4, 129, 3, 2, 2, 2, 4, 131, 3, 2, 2, 2, 4, 133, 3, 2, 2, 2, 4, 135, 3, 2, 2, 2, 4, 137, 3, 2, 2, 2, 4, 139, 3, 2, 2, 2, 4, 141, 3, 2, 2, 2, 4, 143, 3, 2, 2, 2, 4, 145, 3, 2, 2, 2, 4, 147, 3, 2, 2, 2, 4, 149, 3, 2, 2, 2, 4, 151, 3, 2, 2, 2, 4, 153, 3, 2, 2, 2, 4, 155, 3, 2, 2, 2, 4, 157, 3, 2, 2, 2, 4, 159, 3, 2, 2, 2, 4, 161, 3, 2, 2, 2, 4, 163, 3, 2, 2, 2, 4, 165, 3, 2, 2, 2, 4, 167, 3, 2, 2, 2, 4, 169, 3, 2, 2, 2, 4, 171, 3, 2, 2, 2, 4, 173, 3, 2, 2, 2, 5, 175, 3, 2, 2, 2, 5, 177, 3, 2, 2, 2, 5, 179, 3, 2, 2, 2, 5, 181, 3, 2, 2, 2, 5, 183, 3, 2, 2, 2, 5, 185, 3, 2, 2, 2, 5, 189, 3, 2, 2, 2, 5, 191, 3, 2, 2, 2, 5, 193, 3, 2, 2, 2, 5, 195, 3, 2, 2, 2, 5, 197, 3, 2, 2, 2, 6, 199, 3, 2, 2, 2, 6, 201, 3, 2, 2, 2, 6, 203, 3, 2, 2, 2, 6, 207, 3, 2, 2, 2, 6, 209, 3, 2, 2, 2, 6, 211, 3, 2, 2, 2, 6, 213, 3, 2, 2, 2, 6, 215, 3, 2, 2, 2, 6, 217, 3, 2, 2, 2, 7, 219, 3, 2, 2, 2, 7, 221, 3, 2, 2, 2, 7, 223, 3, 2, 2, 2, 7, 225, 3, 2, 2, 2, 7, 227, 3, 2, 2, 2, 7, 229, 3, 2, 2, 2, 7, 231, 3, 2, 2, 2, 7, 233, 3, 2, 2, 2, 7, 235, 3, 2, 2, 2, 7, 237, 3, 2, 2, 2, 8, 239, 3, 2, 2, 2, 8, 241, 3, 2, 2, 2, 8, 243, 3, 2, 2, 2, 8, 245, 3, 2, 2, 2, 8, 249, 3, 2, 2, 2, 8, 251, 3, 2, 2, 2, 8, 253, 3, 2, 2, 2, 8, 255, 3, 2, 2, 2, 8, 257, 3, 2, 2, 2, 8, 259, 3, 2, 2, 2, 9, 261, 3, 2, 2, 2, 9, 263, 3, 2, 2, 2, 9, 265, 3, 2, 2, 2, 9, 267, 3, 2, 2, 2, 9, 269, 3, 2, 2, 2, 9, 271, 3, 2, 2, 2, 9, 273, 3, 2, 2, 2, 9, 275, 3, 2, 2, 2, 9, 277, 3, 2, 2, 2, 9, 279, 3, 2, 2, 2, 10, 281, 3, 2, 2, 2, 10, 283, 3, 2, 2, 2, 10, 285, 3, 2, 2, 2, 10, 287, 3, 2, 2, 2, 10, 289, 3, 2, 2, 2, 10, 291, 3, 2, 2, 2, 10, 293, 3, 2, 2, 2, 11, 295, 3, 2, 2, 2, 11, 297, 3, 2, 2, 2, 11, 299, 3, 2, 2, 2, 11, 301, 3, 2, 2, 2, 11, 303, 3, 2, 2, 2, 11, 305, 3, 2, 2, 2, 12, 307, 3, 2, 2, 2, 12, 309, 3, 2, 2, 2, 12, 311, 3, 2, 2, 2, 12, 313, 3, 2, 2, 2, 12, 315, 3, 2, 2, 2, 12, 317, 3, 2, 2, 2, 13, 371, 3, 2, 2, 2, 15, 381, 3, 2, 2, 2, 17, 388, 3, 2, 2, 2, 19, 397, 3, 2, 2, 2, 21, 404, 3, 2, 2, 2, 23, 414, 3, 2, 2, 2, 25, 421, 3, 2, 2, 2, 27, 428, 3, 2, 2, 2, 29, 442, 3, 2, 2, 2, 31, 449, 3, 2, 2, 2, 33, 457, 3, 2, 2, 2, 35, 469, 3, 2, 2, 2, 37, 478, 3, 2, 2, 2, 39, 484, 3, 2, 2, 2, 41, 491, 3, 2, 2, 2, 43, 498, 3, 2, 2, 2, 45, 506, 3, 2, 2, 2, 47, 515, 3, 2, 2, 2, 49, 521, 3, 2, 2, 2, 51, 538, 3, 2, 2, 2, 53, 554, 3, 2, 2, 2, 55, 560, 3, 2, 2, 2, 57, 565, 3, 2, 2, 2, 59, 570, 3, 2, 2, 2, 61, 574, 3, 2, 2, 2, 63, 578, 3, 2, 2, 2, 65, 582, 3, 2, 2, 2, 67, 586, 3, 2, 2, 2, 69, 588, 3, 2, 2, 2, 71, 590, 3, 2, 2, 2, 73, 593, 3, 2, 2, 2, 75, 595, 3, 2, 2, 2, 77, 604, 3, 2, 2, 2, 79, 606, 3, 2, 2, 2, 81, 611, 3, 2, 2, 2, 83, 613, 3, 2, 2, 2, 85, 618, 3, 2, 2, 2, 87, 649, 3, 2, 2, 2, 89, 652, 3, 2, 2, 2, 91, 698, 3, 2, 2, 2, 93, 700, 3, 2, 2, 2, 95, 703, 3, 2, 2, 2, 97, 707, 3, 2, 2, 2, 99, 711, 3, 2, 2, 2, 101, 713, 3, 2, 2, 2, 103, 715, 3, 2, 2, 2, 105, 720, 3, 2, 2, 2, 107, 722, 3, 2, 2, 2, 109, 728, 3, 2, 2, 2, 111, 734, 3, 2, 2, 2, 113, 739, 3, 2, 2, 2, 115, 741, 3, 2, 2, 2, 117, 744, 3, 2, 2, 2, 119, 747, 3, 2, 2, 2, 121, 752, 3, 2, 2, 2, 123, 756, 3, 2, 2, 2, 125, 761, 3, 2, 2, 2, 127, 767, 3, 2, 2, 2, 129, 770, 3, 2, 2, 2, 131, 772, 3, 2, 2, 2, 133, 778, 3, 2, 2, 2, 135, 780, 3, 2, 2, 2, 137, 785, 3, 2, 2, 2, 139, 788, 3, 2, 2, 2, 141, 791, 3, 2, 2, 2, 143, 794, 3, 2, 2, 2, 145, 796, 3, 2, 2, 2, 147, 799, 3, 2, 2, 2, 149, 801, 3, 2, 2, 2, 151, 804, 3, 2, 2, 2, 153, 806, 3, 2, 2, 2, 155, 808, 3, 2, 2, 2, 157, 810, 3, 2, 2, 2, 159, 812, 3, 2, 2, 2, 161, 814, 3, 2, 2, 2, 163, 819, 3, 2, 2, 2, 165, 840, 3, 2, 2, 2, 167, 842, 3, 2, 2, 2, 169, 850, 3, 2, 2, 2, 171, 854, 3, 2, 2, 2, 173, 858, 3, 2, 2, 2, 175, 862, 3, 2, 2, 2, 177, 867, 3, 2, 2, 2, 179, 871, 3, 2, 2, 2, 181, 875, 3, 2, 2, 2, 183, 879, 3, 2, 2, 2, 185, 883, 3, 2, 2, 2, 187, 895, 3, 2, 2, 2, 189, 898, 3, 2, 2, 2, 191, 902, 3, 2, 2, 2, 193, 906, 3, 2, 2, 2, 195, 910, 3, 2, 2, 2, 197, 914, 3, 2, 2, 2, 199, 918, 3, 2, 2, 2, 201, 923, 3, 2, 2, 2, 203, 927, 3, 2, 2, 2, 205, 935, 3, 2, 2, 2, 207, 956, 3, 2, 2, 2, 209, 958, 3, 2, 2, 2, 211, 962, 3, 2, 2, 2, 213, 966, 3, 2, 2, 2, 215, 970, 3, 2, 2, 2, 217, 974, 3, 2, 2, 2, 219, 978, 3, 2, 2, 2, 221, 983, 3, 2, 2, 2, 223, 987, 3, 2, 2, 2, 225, 991, 3, 2, 2, 2, 227, 995, 3, 2, 2, 2, 229, 998, 3, 2, 2, 2, 231, 1002, 3, 2, 2, 2, 233, 1006, 3, 2, 2, 2, 235, 1010, 3, 2, 2, 2, 237, 1014, 3, 2, 2, 2, 239, 1018, 3, 2, 2, 2, 241, 1023, 3, 2, 2, 2, 243, 1028, 3, 2, 2, 2, 245, 1033, 3, 2, 2, 2, 247, 1040, 3, 2, 2, 2, 249, 1049, 3, 2, 2, 2, 251, 1056, 3, 2, 2, 2, 253, 1060, 3, 2, 2, 2, 255, 1064, 3, 2, 2, 2, 257, 1068, 3, 2, 2, 2, 259, 1072, 3, 2, 2, 2, 261, 1076, 3, 2, 2, 2, 263, 1082, 3, 2, 2, 2, 265, 1086, 3, 2, 2, 2, 267, 1090, 3, 2, 2, 2, 269, 1094, 3, 2, 2, 2, 271, 1098, 3, 2, 2, 2, 273, 1102, 3, 2, 2, 2, 275, 1106, 3, 2, 2, 2, 277, 1110, 3, 2, 2, 2, 279, 1114, 3, 2, 2, 2, 281, 1118, 3, 2, 2, 2, 283, 1123, 3, 2, 2, 2, 285, 1127, 3, 2, 2, 2, 287, 1131, 3, 2, 2, 2, 289, 1135, 3, 2, 2, 2, 291, 1139, 3, 2, 2, 2, 293, 1143, 3, 2, 2, 2, 295, 1147, 3, 2, 2, 2, 297, 1152, 3, 2, 2, 2, 299, 1157, 3, 2, 2, 2, 301, 1167, 3, 2, 2, 2, 303, 1171, 3, 2, 2, 2, 305, 1175, 3, 2, 2, 2, 307, 1179, 3, 2, 2, 2, 309, 1184, 3, 2, 2, 2, 311, 1191, 3, 2, 2, 2, 313, 1195, 3, 2, 2, 2, 315, 1199, 3, 2, 2, 2, 317, 1203, 3, 2, 2, 2, 319, 1207, 3, 2, 2, 2, 321, 1209, 3, 2, 2, 2, 323, 1211, 3, 2, 2, 2, 325, 1213, 3, 2, 2, 2, 327, 1215, 3, 2, 2, 2, 329, 1217, 3, 2, 2, 2, 331, 1219, 3, 2, 2, 2, 333, 1221, 3, 2, 2, 2, 335, 1223, 3, 2, 2, 2, 337, 1225, 3, 2, 2, 2, 339, 1227, 3, 2, 2, 2, 341, 1229, 3, 2, 2, 2, 343, 1231, 3, 2, 2, 2, 345, 1233, 3, 2, 2, 2, 347, 1235, 3, 2, 2, 2, 349, 1237, 3, 2, 2, 2, 351, 1239, 3, 2, 2, 2, 353, 1241, 3, 2, 2, 2, 355, 1243, 3, 2, 2, 2, 357, 1245, 3, 2, 2, 2, 359, 1247, 3, 2, 2, 2, 361, 1249, 3, 2, 2, 2, 363, 1251, 3, 2, 2, 2, 365, 1253, 3, 2, 2, 2, 367, 1255, 3, 2, 2, 2, 369, 1257, 3, 2, 2, 2, 371, 372, 5, 325, 158, 2, 372, 373, 5, 335, 163, 2, 373, 374, 5, 355, 173, 2, 374, 375, 5, 355, 173, 2, 375, 376, 5, 327, 159, 2, 376, 377, 5, 323, 157, 2, 377, 378, 5, 357, 174, 2, 378, 379, 3, 2, 2, 2, 379, 380, 8, 2, 2, 2, 380, 14, 3, 2, 2, 2, 381, 382, 5, 325, 158, 2, 382, 383, 5, 353, 172, 2, 383, 384, 5, 347, 169, 2, 384, 385, 5, 349, 170, 2, 385, 386, 3, 2, 2, 2, 386, 387, 8, 3, 3, 2, 387, 16, 3, 2, 2, 2, 388, 389, 5, 327, 159, 2, 389, 390, 5, 345, 168, 2, 390, 391, 5, 353, 172, 2, 391, 392, 5, 335, 163, 2, 392, 393, 5, 323, 157, 2, 393, 394, 5, 333, 162, 2, 394, 395, 3, 2, 2, 2, 395, 396, 8, 4, 4, 2, 396, 18, 3, 2, 2, 2, 397, 398, 5, 327, 159, 2, 398, 399, 5, 361, 176, 2, 399, 400, 5, 319, 155, 2, 400, 401, 5, 341, 166, 2, 401, 402, 3, 2, 2, 2, 402, 403, 8, 5, 2, 2, 403, 20, 3, 2, 2, 2, 404, 405, 5, 327, 159, 2, 405, 406, 5, 365, 178, 2, 406, 407, 5, 349, 170, 2, 407, 408, 5, 341, 166, 2, 408, 409, 5, 319, 155, 2, 409, 410, 5, 335, 163, 2, 410, 411, 5, 345, 168, 2, 411, 412, 3, 2, 2, 2, 412, 413, 8, 6, 5, 2, 413, 22, 3, 2, 2, 2, 414, 415, 5, 329, 160, 2, 415, 416, 5, 353, 172, 2, 416, 417, 5, 347, 169, 2, 417, 418, 5, 343, 167, 2, 418, 419, 3, 2, 2, 2, 419, 420, 8, 7, 6, 2, 420, 24, 3, 2, 2, 2, 421, 422, 5, 331, 161, 2, 422, 423, 5, 353, 172, 2, 423, 424, 5, 347, 169, 2, 424, 425, 5, 339, 165, 2, 425, 426, 3, 2, 2, 2, 426, 427, 8, 8, 2, 2, 427, 26, 3, 2, 2, 2, 428, 429, 5, 335, 163, 2, 429, 430, 5, 345, 168, 2, 430, 431, 5, 341, 166, 2, 431, 432, 5, 335, 163, 2, 432, 433, 5, 345, 168, 2, 433, 434, 5, 327, 159, 2, 434, 435, 5, 355, 173, 2, 435, 436, 5, 357, 174, 2, 436, 437, 5, 319, 155, 2, 437, 438, 5, 357, 174, 2, 438, 439, 5, 355, 173, 2, 439, 440, 3, 2, 2, 2, 440, 441, 8, 9, 2, 2, 441, 28, 3, 2, 2, 2, 442, 443, 5, 339, 165, 2, 443, 444, 5, 327, 159, 2, 444, 445, 5, 327, 159, 2, 445, 446, 5, 349, 170, 2, 446, 447, 3, 2, 2, 2, 447, 448, 8, 10, 3, 2, 448, 30, 3, 2, 2, 2, 449, 450, 5, 341, 166, 2, 450, 451, 5, 335, 163, 2, 451, 452, 5, 343, 167, 2, 452, 453, 5, 335, 163, 2, 453, 454, 5, 357, 174, 2, 454, 455, 3, 2, 2, 2, 455, 456, 8, 11, 2, 2, 456, 32, 3, 2, 2, 2, 457, 458, 5, 343, 167, 2, 458, 459, 5, 361, 176, 2, 459, 460, 5, 83, 37, 2, 460, 461, 5, 327, 159, 2, 461, 462, 5, 365, 178, 2, 462, 463, 5, 349, 170, 2, 463, 464, 5, 319, 155, 2, 464, 465, 5, 345, 168, 2, 465, 466, 5, 325, 158, 2, 466, 467, 3, 2, 2, 2, 467, 468, 8, 12, 7, 2, 468, 34, 3, 2, 2, 2, 469, 470, 5, 353, 172, 2, 470, 471, 5, 327, 159, 2, 471, 472, 5, 345, 168, 2, 472, 473, 5, 319, 155, 2, 473, 474, 5, 343, 167, 2, 474, 475, 5, 327, 159, 2, 475, 476, 3, 2, 2, 2, 476, 477, 8, 13, 8, 2, 477, 36, 3, 2, 2, 2, 478, 479, 5, 353, 172, 2, 479, 480, 5, 347, 169, 2, 480, 481, 5, 363, 177, 2, 481, 482, 3, 2, 2, 2, 482, 483, 8, 14, 2, 2, 483, 38, 3, 2, 2, 2, 484, 485, 5, 355, 173, 2, 485, 486, 5, 333, 162, 2, 486, 487, 5, 347, 169, 2, 487, 488, 5, 363, 177, 2, 488, 489, 3, 2, 2, 2, 489, 490, 8, 15, 9, 2, 490, 40, 3, 2, 2, 2, 491, 492, 5, 355, 173, 2, 492, 493, 5, 347, 169, 2, 493, 494, 5, 353, 172, 2, 494, 495, 5, 357, 174, 2, 495, 496, 3, 2, 2, 2, 496, 497, 8, 16, 2, 2, 497, 42, 3, 2, 2, 2, 498, 499, 5, 355, 173, 2, 499, 500, 5, 357, 174, 2, 500, 501, 5, 319, 155, 2, 501, 502, 5, 357, 174, 2, 502, 503, 5, 355, 173, 2, 503, 504, 3, 2, 2, 2, 504, 505, 8, 17, 2, 2, 505, 44, 3, 2, 2, 2, 506, 507, 5, 363, 177, 2, 507, 508, 5, 333, 162, 2, 508, 509, 5, 327, 159, 2, 509, 510, 5, 353, 172, 2, 510, 511, 5, 327, 159, 2, 511, 512, 3, 2, 2, 2, 512, 513, 8, 18, 2, 2, 513, 46, 3, 2, 2, 2, 514, 516, 10, 2, 2, 2, 515, 514, 3, 2, 2, 2, 516, 517, 3, 2, 2, 2, 517, 515, 3, 2, 2, 2, 517, 518, 3, 2, 2, 2, 518, 519, 3, 2, 2, 2, 519, 520, 8, 19, 2, 2, 520, 48, 3, 2, 2, 2, 521, 522, 7, 49, 2, 2, 522, 523, 7, 49, 2, 2, 523, 527, 3, 2, 2, 2, 524, 526, 10, 3, 2, 2, 525, 524, 3, 2, 2, 2, 526, 529, 3, 2, 2, 2, 527, 525, 3, 2, 2, 2, 527, 528, 3, 2, 2, 2, 528, 531, 3, 2, 2, 2, 529, 527, 3, 2, 2, 2, 530, 532, 7, 15, 2, 2, 531, 530, 3, 2, 2, 2, 531, 532, 3, 2, 2, 2, 532, 534, 3, 2, 2, 2, 533, 535, 7, 12, 2, 2, 534, 533, 3, 2, 2, 2, 534, 535, 3, 2, 2, 2, 535, 536, 3, 2, 2, 2, 536, 537, 8, 20, 10, 2, 537, 50, 3, 2, 2, 2, 538, 539, 7, 49, 2, 2, 539, 540, 7, 44, 2, 2, 540, 545, 3, 2, 2, 2, 541, 544, 5, 51, 21, 2, 542, 544, 11, 2, 2, 2, 543, 541, 3, 2, 2, 2, 543, 542, 3, 2, 2, 2, 544, 547, 3, 2, 2, 2, 545, 546, 3, 2, 2, 2, 545, 543, 3, 2, 2, 2, 546, 548, 3, 2, 2, 2, 547, 545, 3, 2, 2, 2, 548, 549, 7, 44, 2, 2, 549, 550, 7, 49, 2, 2, 550, 551, 3, 2, 2, 2, 551, 552, 8, 21, 10, 2, 552, 52, 3, 2, 2, 2, 553, 555, 9, 4, 2, 2, 554, 553, 3, 2, 2, 2, 555, 556, 3, 2, 2, 2, 556, 554, 3, 2, 2, 2, 556, 557, 3, 2, 2, 2, 557, 558, 3, 2, 2, 2, 558, 559, 8, 22, 10, 2, 559, 54, 3, 2, 2, 2, 560, 561, 5, 161, 76, 2, 561, 562, 3, 2, 2, 2, 562, 563, 8, 23, 11, 2, 563, 564, 8, 23, 12, 2, 564, 56, 3, 2, 2, 2, 565, 566, 5, 65, 28, 2, 566, 567, 3, 2, 2, 2, 567, 568, 8, 24, 13, 2, 568, 569, 8, 24, 14, 2, 569, 58, 3, 2, 2, 2, 570, 571, 5, 53, 22, 2, 571, 572, 3, 2, 2, 2, 572, 573, 8, 25, 10, 2, 573, 60, 3, 2, 2, 2, 574, 575, 5, 49, 20, 2, 575, 576, 3, 2, 2, 2, 576, 577, 8, 26, 10, 2, 577, 62, 3, 2, 2, 2, 578, 579, 5, 51, 21, 2, 579, 580, 3, 2, 2, 2, 580, 581, 8, 27, 10, 2, 581, 64, 3, 2, 2, 2, 582, 583, 7, 126, 2, 2, 583, 584, 3, 2, 2, 2, 584, 585, 8, 28, 14, 2, 585, 66, 3, 2, 2, 2, 586, 587, 9, 5, 2, 2, 587, 68, 3, 2, 2, 2, 588, 589, 9, 6, 2, 2, 589, 70, 3, 2, 2, 2, 590, 591, 7, 94, 2, 2, 591, 592, 9, 7, 2, 2, 592, 72, 3, 2, 2, 2, 593, 594, 10, 8, 2, 2, 594, 74, 3, 2, 2, 2, 595, 597, 9, 9, 2, 2, 596, 598, 9, 10, 2, 2, 597, 596, 3, 2, 2, 2, 597, 598, 3, 2, 2, 2, 598, 600, 3, 2, 2, 2, 599, 601, 5, 67, 29, 2, 600, 599, 3, 2, 2, 2, 601, 602, 3, 2, 2, 2, 602, 600, 3, 2, 2, 2, 602, 603, 3, 2, 2, 2, 603, 76, 3, 2, 2, 2, 604, 605, 7, 66, 2, 2, 605, 78, 3, 2, 2, 2, 606, 607, 7, 98, 2, 2, 607, 80, 3, 2, 2, 2, 608, 612, 10, 11, 2, 2, 609, 610, 7, 98, 2, 2, 610, 612, 7, 98, 2, 2, 611, 608, 3, 2, 2, 2, 611, 609, 3, 2, 2, 2, 612, 82, 3, 2, 2, 2, 613, 614, 7, 97, 2, 2, 614, 84, 3, 2, 2, 2, 615, 619, 5, 69, 30, 2, 616, 619, 5, 67, 29, 2, 617, 619, 5, 83, 37, 2, 618, 615, 3, 2, 2, 2, 618, 616, 3, 2, 2, 2, 618, 617, 3, 2, 2, 2, 619, 86, 3, 2, 2, 2, 620, 625, 7, 36, 2, 2, 621, 624, 5, 71, 31, 2, 622, 624, 5, 73, 32, 2, 623, 621, 3, 2, 2, 2, 623, 622, 3, 2, 2, 2, 624, 627, 3, 2, 2, 2, 625, 623, 3, 2, 2, 2, 625, 626, 3, 2, 2, 2, 626, 628, 3, 2, 2, 2, 627, 625, 3, 2, 2, 2, 628, 650, 7, 36, 2, 2, 629, 630, 7, 36, 2, 2, 630, 631, 7, 36, 2, 2, 631, 632, 7, 36, 2, 2, 632, 636, 3, 2, 2, 2, 633, 635, 10, 3, 2, 2, 634, 633, 3, 2, 2, 2, 635, 638, 3, 2, 2, 2, 636, 637, 3, 2, 2, 2, 636, 634, 3, 2, 2, 2, 637, 639, 3, 2, 2, 2, 638, 636, 3, 2, 2, 2, 639, 640, 7, 36, 2, 2, 640, 641, 7, 36, 2, 2, 641, 642, 7, 36, 2, 2, 642, 644, 3, 2, 2, 2, 643, 645, 7, 36, 2, 2, 644, 643, 3, 2, 2, 2, 644, 645, 3, 2, 2, 2, 645, 647, 3, 2, 2, 2, 646, 648, 7, 36, 2, 2, 647, 646, 3, 2, 2, 2, 647, 648, 3, 2, 2, 2, 648, 650, 3, 2, 2, 2, 649, 620, 3, 2, 2, 2, 649, 629, 3, 2, 2, 2, 650, 88, 3, 2, 2, 2, 651, 653, 5, 67, 29, 2, 652, 651, 3, 2, 2, 2, 653, 654, 3, 2, 2, 2, 654, 652, 3, 2, 2, 2, 654, 655, 3, 2, 2, 2, 655, 90, 3, 2, 2, 2, 656, 658, 5, 67, 29, 2, 657, 656, 3, 2, 2, 2, 658, 659, 3, 2, 2, 2, 659, 657, 3, 2, 2, 2, 659, 660, 3, 2, 2, 2, 660, 661, 3, 2, 2, 2, 661, 665, 5, 105, 48, 2, 662, 664, 5, 67, 29, 2, 663, 662, 3, 2, 2, 2, 664, 667, 3, 2, 2, 2, 665, 663, 3, 2, 2, 2, 665, 666, 3, 2, 2, 2, 666, 699, 3, 2, 2, 2, 667, 665, 3, 2, 2, 2, 668, 670, 5, 105, 48, 2, 669, 671, 5, 67, 29, 2, 670, 669, 3, 2, 2, 2, 671, 672, 3, 2, 2, 2, 672, 670, 3, 2, 2, 2, 672, 673, 3, 2, 2, 2, 673, 699, 3, 2, 2, 2, 674, 676, 5, 67, 29, 2, 675, 674, 3, 2, 2, 2, 676, 677, 3, 2, 2, 2, 677, 675, 3, 2, 2, 2, 677, 678, 3, 2, 2, 2, 678, 686, 3, 2, 2, 2, 679, 683, 5, 105, 48, 2, 680, 682, 5, 67, 29, 2, 681, 680, 3, 2, 2, 2, 682, 685, 3, 2, 2, 2, 683, 681, 3, 2, 2, 2, 683, 684, 3, 2, 2, 2, 684, 687, 3, 2, 2, 2, 685, 683, 3, 2, 2, 2, 686, 679, 3, 2, 2, 2, 686, 687, 3, 2, 2, 2, 687, 688, 3, 2, 2, 2, 688, 689, 5, 75, 33, 2, 689, 699, 3, 2, 2, 2, 690, 692, 5, 105, 48, 2, 691, 693, 5, 67, 29, 2, 692, 691, 3, 2, 2, 2, 693, 694, 3, 2, 2, 2, 694, 692, 3, 2, 2, 2, 694, 695, 3, 2, 2, 2, 695, 696, 3, 2, 2, 2, 696, 697, 5, 75, 33, 2, 697, 699, 3, 2, 2, 2, 698, 657, 3, 2, 2, 2, 698, 668, 3, 2, 2, 2, 698, 675, 3, 2, 2, 2, 698, 690, 3, 2, 2, 2, 699, 92, 3, 2, 2, 2, 700, 701, 5, 321, 156, 2, 701, 702, 5, 367, 179, 2, 702, 94, 3, 2, 2, 2, 703, 704, 5, 319, 155, 2, 704, 705, 5, 345, 168, 2, 705, 706, 5, 325, 158, 2, 706, 96, 3, 2, 2, 2, 707, 708, 5, 319, 155, 2, 708, 709, 5, 355, 173, 2, 709, 710, 5, 323, 157, 2, 710, 98, 3, 2, 2, 2, 711, 712, 7, 63, 2, 2, 712, 100, 3, 2, 2, 2, 713, 714, 7, 46, 2, 2, 714, 102, 3, 2, 2, 2, 715, 716, 5, 325, 158, 2, 716, 717, 5, 327, 159, 2, 717, 718, 5, 355, 173, 2, 718, 719, 5, 323, 157, 2, 719, 104, 3, 2, 2, 2, 720, 721, 7, 48, 2, 2, 721, 106, 3, 2, 2, 2, 722, 723, 5, 329, 160, 2, 723, 724, 5, 319, 155, 2, 724, 725, 5, 341, 166, 2, 725, 726, 5, 355, 173, 2, 726, 727, 5, 327, 159, 2, 727, 108, 3, 2, 2, 2, 728, 729, 5, 329, 160, 2, 729, 730, 5, 335, 163, 2, 730, 731, 5, 353, 172, 2, 731, 732, 5, 355, 173, 2, 732, 733, 5, 357, 174, 2, 733, 110, 3, 2, 2, 2, 734, 735, 5, 341, 166, 2, 735, 736, 5, 319, 155, 2, 736, 737, 5, 355, 173, 2, 737, 738, 5, 357, 174, 2, 738, 112, 3, 2, 2, 2, 739, 740, 7, 42, 2, 2, 740, 114, 3, 2, 2, 2, 741, 742, 5, 335, 163, 2, 742, 743, 5, 345, 168, 2, 743, 116, 3, 2, 2, 2, 744, 745, 5, 335, 163, 2, 745, 746, 5, 355, 173, 2, 746, 118, 3, 2, 2, 2, 747, 748, 5, 341, 166, 2, 748, 749, 5, 335, 163, 2, 749, 750, 5, 339, 165, 2, 750, 751, 5, 327, 159, 2, 751, 120, 3, 2, 2, 2, 752, 753, 5, 345, 168, 2, 753, 754, 5, 347, 169, 2, 754, 755, 5, 357, 174, 2, 755, 122, 3, 2, 2, 2, 756, 757, 5, 345, 168, 2, 757, 758, 5, 359, 175, 2, 758, 759, 5, 341, 166, 2, 759, 760, 5, 341, 166, 2, 760, 124, 3, 2, 2, 2, 761, 762, 5, 345, 168, 2, 762, 763, 5, 359, 175, 2, 763, 764, 5, 341, 166, 2, 764, 765, 5, 341, 166, 2, 765, 766, 5, 355, 173, 2, 766, 126, 3, 2, 2, 2, 767, 768, 5, 347, 169, 2, 768, 769, 5, 353, 172, 2, 769, 128, 3, 2, 2, 2, 770, 771, 7, 65, 2, 2, 771, 130, 3, 2, 2, 2, 772, 773, 5, 353, 172, 2, 773, 774, 5, 341, 166, 2, 774, 775, 5, 335, 163, 2, 775, 776, 5, 339, 165, 2, 776, 777, 5, 327, 159, 2, 777, 132, 3, 2, 2, 2, 778, 779, 7, 43, 2, 2, 779, 134, 3, 2, 2, 2, 780, 781, 5, 357, 174, 2, 781, 782, 5, 353, 172, 2, 782, 783, 5, 359, 175, 2, 783, 784, 5, 327, 159, 2, 784, 136, 3, 2, 2, 2, 785, 786, 7, 63, 2, 2, 786, 787, 7, 63, 2, 2, 787, 138, 3, 2, 2, 2, 788, 789, 7, 63, 2, 2, 789, 790, 7, 128, 2, 2, 790, 140, 3, 2, 2, 2, 791, 792, 7, 35, 2, 2, 792, 793, 7, 63, 2, 2, 793, 142, 3, 2, 2, 2, 794, 795, 7, 62, 2, 2, 795, 144, 3, 2, 2, 2, 796, 797, 7, 62, 2, 2, 797, 798, 7, 63, 2, 2, 798, 146, 3, 2, 2, 2, 799, 800, 7, 64, 2, 2, 800, 148, 3, 2, 2, 2, 801, 802, 7, 64, 2, 2, 802, 803, 7, 63, 2, 2, 803, 150, 3, 2, 2, 2, 804, 805, 7, 45, 2, 2, 805, 152, 3, 2, 2, 2, 806, 807, 7, 47, 2, 2, 807, 154, 3, 2, 2, 2, 808, 809, 7, 44, 2, 2, 809, 156, 3, 2, 2, 2, 810, 811, 7, 49, 2, 2, 811, 158, 3, 2, 2, 2, 812, 813, 7, 39, 2, 2, 813, 160, 3, 2, 2, 2, 814, 815, 7, 93, 2, 2, 815, 816, 3, 2, 2, 2, 816, 817, 8, 76, 2, 2, 817, 818, 8, 76, 2, 2, 818, 162, 3, 2, 2, 2, 819, 820, 7, 95, 2, 2, 820, 821, 3, 2, 2, 2, 821, 822, 8, 77, 14, 2, 822, 823, 8, 77, 14, 2, 823, 164, 3, 2, 2, 2, 824, 828, 5, 69, 30, 2, 825, 827, 5, 85, 38, 2, 826, 825, 3, 2, 2, 2, 827, 830, 3, 2, 2, 2, 828, 826, 3, 2, 2, 2, 828, 829, 3, 2, 2, 2, 829, 841, 3, 2, 2, 2, 830, 828, 3, 2, 2, 2, 831, 834, 5, 83, 37, 2, 832, 834, 5, 77, 34, 2, 833, 831, 3, 2, 2, 2, 833, 832, 3, 2, 2, 2, 834, 836, 3, 2, 2, 2, 835, 837, 5, 85, 38, 2, 836, 835, 3, 2, 2, 2, 837, 838, 3, 2, 2, 2, 838, 836, 3, 2, 2, 2, 838, 839, 3, 2, 2, 2, 839, 841, 3, 2, 2, 2, 840, 824, 3, 2, 2, 2, 840, 833, 3, 2, 2, 2, 841, 166, 3, 2, 2, 2, 842, 844, 5, 79, 35, 2, 843, 845, 5, 81, 36, 2, 844, 843, 3, 2, 2, 2, 845, 846, 3, 2, 2, 2, 846, 844, 3, 2, 2, 2, 846, 847, 3, 2, 2, 2, 847, 848, 3, 2, 2, 2, 848, 849, 5, 79, 35, 2, 849, 168, 3, 2, 2, 2, 850, 851, 5, 49, 20, 2, 851, 852, 3, 2, 2, 2, 852, 853, 8, 80, 10, 2, 853, 170, 3, 2, 2, 2, 854, 855, 5, 51, 21, 2, 855, 856, 3, 2, 2, 2, 856, 857, 8, 81, 10, 2, 857, 172, 3, 2, 2, 2, 858, 859, 5, 53, 22, 2, 859, 860, 3, 2, 2, 2, 860, 861, 8, 82, 10, 2, 861, 174, 3, 2, 2, 2, 862, 863, 5, 65, 28, 2, 863, 864, 3, 2, 2, 2, 864, 865, 8, 83, 13, 2, 865, 866, 8, 83, 14, 2, 866, 176, 3, 2, 2, 2, 867, 868, 5, 161, 76, 2, 868, 869, 3, 2, 2, 2, 869, 870, 8, 84, 11, 2, 870, 178, 3, 2, 2, 2, 871, 872, 5, 163, 77, 2, 872, 873, 3, 2, 2, 2, 873, 874, 8, 85, 15, 2, 874, 180, 3, 2, 2, 2, 875, 876, 5, 101, 46, 2, 876, 877, 3, 2, 2, 2, 877, 878, 8, 86, 16, 2, 878, 182, 3, 2, 2, 2, 879, 880, 5, 99, 45, 2, 880, 881, 3, 2, 2, 2, 881, 882, 8, 87, 17, 2, 882, 184, 3, 2, 2, 2, 883, 884, 5, 343, 167, 2, 884, 885, 5, 327, 159, 2, 885, 886, 5, 357, 174, 2, 886, 887, 5, 319, 155, 2, 887, 888, 5, 325, 158, 2, 888, 889, 5, 319, 155, 2, 889, 890, 5, 357, 174, 2, 890, 891, 5, 319, 155, 2, 891, 186, 3, 2, 2, 2, 892, 896, 10, 12, 2, 2, 893, 894, 7, 49, 2, 2, 894, 896, 10, 13, 2, 2, 895, 892, 3, 2, 2, 2, 895, 893, 3, 2, 2, 2, 896, 188, 3, 2, 2, 2, 897, 899, 5, 187, 89, 2, 898, 897, 3, 2, 2, 2, 899, 900, 3, 2, 2, 2, 900, 898, 3, 2, 2, 2, 900, 901, 3, 2, 2, 2, 901, 190, 3, 2, 2, 2, 902, 903, 5, 167, 79, 2, 903, 904, 3, 2, 2, 2, 904, 905, 8, 91, 18, 2, 905, 192, 3, 2, 2, 2, 906, 907, 5, 49, 20, 2, 907, 908, 3, 2, 2, 2, 908, 909, 8, 92, 10, 2, 909, 194, 3, 2, 2, 2, 910, 911, 5, 51, 21, 2, 911, 912, 3, 2, 2, 2, 912, 913, 8, 93, 10, 2, 913, 196, 3, 2, 2, 2, 914, 915, 5, 53, 22, 2, 915, 916, 3, 2, 2, 2, 916, 917, 8, 94, 10, 2, 917, 198, 3, 2, 2, 2, 918, 919, 5, 65, 28, 2, 919, 920, 3, 2, 2, 2, 920, 921, 8, 95, 13, 2, 921, 922, 8, 95, 14, 2, 922, 200, 3, 2, 2, 2, 923, 924, 5, 105, 48, 2, 924, 925, 3, 2, 2, 2, 925, 926, 8, 96, 19, 2, 926, 202, 3, 2, 2, 2, 927, 928, 5, 101, 46, 2, 928, 929, 3, 2, 2, 2, 929, 930, 8, 97, 16, 2, 930, 204, 3, 2, 2, 2, 931, 936, 5, 69, 30, 2, 932, 936, 5, 67, 29, 2, 933, 936, 5, 83, 37, 2, 934, 936, 5, 155, 73, 2, 935, 931, 3, 2, 2, 2, 935, 932, 3, 2, 2, 2, 935, 933, 3, 2, 2, 2, 935, 934, 3, 2, 2, 2, 936, 206, 3, 2, 2, 2, 937, 940, 5, 69, 30, 2, 938, 940, 5, 155, 73, 2, 939, 937, 3, 2, 2, 2, 939, 938, 3, 2, 2, 2, 940, 944, 3, 2, 2, 2, 941, 943, 5, 205, 98, 2, 942, 941, 3, 2, 2, 2, 943, 946, 3, 2, 2, 2, 944, 942, 3, 2, 2, 2, 944, 945, 3, 2, 2, 2, 945, 957, 3, 2, 2, 2, 946, 944, 3, 2, 2, 2, 947, 950, 5, 83, 37, 2, 948, 950, 5, 77, 34, 2, 949, 947, 3, 2, 2, 2, 949, 948, 3, 2, 2, 2, 950, 952, 3, 2, 2, 2, 951, 953, 5, 205, 98, 2, 952, 951, 3, 2, 2, 2, 953, 954, 3, 2, 2, 2, 954, 952, 3, 2, 2, 2, 954, 955, 3, 2, 2, 2, 955, 957, 3, 2, 2, 2, 956, 939, 3, 2, 2, 2, 956, 949, 3, 2, 2, 2, 957, 208, 3, 2, 2, 2, 958, 959, 5, 207, 99, 2, 959, 960, 3, 2, 2, 2, 960, 961, 8, 100, 20, 2, 961, 210, 3, 2, 2, 2, 962, 963, 5, 167, 79, 2, 963, 964, 3, 2, 2, 2, 964, 965, 8, 101, 18, 2, 965, 212, 3, 2, 2, 2, 966, 967, 5, 49, 20, 2, 967, 968, 3, 2, 2, 2, 968, 969, 8, 102, 10, 2, 969, 214, 3, 2, 2, 2, 970, 971, 5, 51, 21, 2, 971, 972, 3, 2, 2, 2, 972, 973, 8, 103, 10, 2, 973, 216, 3, 2, 2, 2, 974, 975, 5, 53, 22, 2, 975, 976, 3, 2, 2, 2, 976, 977, 8, 104, 10, 2, 977, 218, 3, 2, 2, 2, 978, 979, 5, 65, 28, 2, 979, 980, 3, 2, 2, 2, 980, 981, 8, 105, 13, 2, 981, 982, 8, 105, 14, 2, 982, 220, 3, 2, 2, 2, 983, 984, 5, 99, 45, 2, 984, 985, 3, 2, 2, 2, 985, 986, 8, 106, 17, 2, 986, 222, 3, 2, 2, 2, 987, 988, 5, 101, 46, 2, 988, 989, 3, 2, 2, 2, 989, 990, 8, 107, 16, 2, 990, 224, 3, 2, 2, 2, 991, 992, 5, 105, 48, 2, 992, 993, 3, 2, 2, 2, 993, 994, 8, 108, 19, 2, 994, 226, 3, 2, 2, 2, 995, 996, 5, 319, 155, 2, 996, 997, 5, 355, 173, 2, 997, 228, 3, 2, 2, 2, 998, 999, 5, 167, 79, 2, 999, 1000, 3, 2, 2, 2, 1000, 1001, 8, 110, 18, 2, 1001, 230, 3, 2, 2, 2, 1002, 1003, 5, 207, 99, 2, 1003, 1004, 3, 2, 2, 2, 1004, 1005, 8, 111, 20, 2, 1005, 232, 3, 2, 2, 2, 1006, 1007, 5, 49, 20, 2, 1007, 1008, 3, 2, 2, 2, 1008, 1009, 8, 112, 10, 2, 1009, 234, 3, 2, 2, 2, 1010, 1011, 5, 51, 21, 2, 1011, 1012, 3, 2, 2, 2, 1012, 1013, 8, 113, 10, 2, 1013, 236, 3, 2, 2, 2, 1014, 1015, 5, 53, 22, 2, 1015, 1016, 3, 2, 2, 2, 1016, 1017, 8, 114, 10, 2, 1017, 238, 3, 2, 2, 2, 1018, 1019, 5, 65, 28, 2, 1019, 1020, 3, 2, 2, 2, 1020, 1021, 8, 115, 13, 2, 1021, 1022, 8, 115, 14, 2, 1022, 240, 3, 2, 2, 2, 1023, 1024, 5, 161, 76, 2, 1024, 1025, 3, 2, 2, 2, 1025, 1026, 8, 116, 11, 2, 1026, 1027, 8, 116, 21, 2, 1027, 242, 3, 2, 2, 2, 1028, 1029, 5, 347, 169, 2, 1029, 1030, 5, 345, 168, 2, 1030, 1031, 3, 2, 2, 2, 1031, 1032, 8, 117, 22, 2, 1032, 244, 3, 2, 2, 2, 1033, 1034, 5, 363, 177, 2, 1034, 1035, 5, 335, 163, 2, 1035, 1036, 5, 357, 174, 2, 1036, 1037, 5, 333, 162, 2, 1037, 1038, 3, 2, 2, 2, 1038, 1039, 8, 118, 22, 2, 1039, 246, 3, 2, 2, 2, 1040, 1041, 10, 14, 2, 2, 1041, 248, 3, 2, 2, 2, 1042, 1044, 5, 247, 119, 2, 1043, 1042, 3, 2, 2, 2, 1044, 1045, 3, 2, 2, 2, 1045, 1043, 3, 2, 2, 2, 1045, 1046, 3, 2, 2, 2, 1046, 1047, 3, 2, 2, 2, 1047, 1048, 5, 309, 150, 2, 1048, 1050, 3, 2, 2, 2, 1049, 1043, 3, 2, 2, 2, 1049, 1050, 3, 2, 2, 2, 1050, 1052, 3, 2, 2, 2, 1051, 1053, 5, 247, 119, 2, 1052, 1051, 3, 2, 2, 2, 1053, 1054, 3, 2, 2, 2, 1054, 1052, 3, 2, 2, 2, 1054, 1055, 3, 2, 2, 2, 1055, 250, 3, 2, 2, 2, 1056, 1057, 5, 167, 79, 2, 1057, 1058, 3, 2, 2, 2, 1058, 1059, 8, 121, 18, 2, 1059, 252, 3, 2, 2, 2, 1060, 1061, 5, 249, 120, 2, 1061, 1062, 3, 2, 2, 2, 1062, 1063, 8, 122, 23, 2, 1063, 254, 3, 2, 2, 2, 1064, 1065, 5, 49, 20, 2, 1065, 1066, 3, 2, 2, 2, 1066, 1067, 8, 123, 10, 2, 1067, 256, 3, 2, 2, 2, 1068, 1069, 5, 51, 21, 2, 1069, 1070, 3, 2, 2, 2, 1070, 1071, 8, 124, 10, 2, 1071, 258, 3, 2, 2, 2, 1072, 1073, 5, 53, 22, 2, 1073, 1074, 3, 2, 2, 2, 1074, 1075, 8, 125, 10, 2, 1075, 260, 3, 2, 2, 2, 1076, 1077, 5, 65, 28, 2, 1077, 1078, 3, 2, 2, 2, 1078, 1079, 8, 126, 13, 2, 1079, 1080, 8, 126, 14, 2, 1080, 1081, 8, 126, 14, 2, 1081, 262, 3, 2, 2, 2, 1082, 1083, 5, 99, 45, 2, 1083, 1084, 3, 2, 2, 2, 1084, 1085, 8, 127, 17, 2, 1085, 264, 3, 2, 2, 2, 1086, 1087, 5, 101, 46, 2, 1087, 1088, 3, 2, 2, 2, 1088, 1089, 8, 128, 16, 2, 1089, 266, 3, 2, 2, 2, 1090, 1091, 5, 105, 48, 2, 1091, 1092, 3, 2, 2, 2, 1092, 1093, 8, 129, 19, 2, 1093, 268, 3, 2, 2, 2, 1094, 1095, 5, 245, 118, 2, 1095, 1096, 3, 2, 2, 2, 1096, 1097, 8, 130, 24, 2, 1097, 270, 3, 2, 2, 2, 1098, 1099, 5, 207, 99, 2, 1099, 1100, 3, 2, 2, 2, 1100, 1101, 8, 131, 20, 2, 1101, 272, 3, 2, 2, 2, 1102, 1103, 5, 167, 79, 2, 1103, 1104, 3, 2, 2, 2, 1104, 1105, 8, 132, 18, 2, 1105, 274, 3, 2, 2, 2, 1106, 1107, 5, 49, 20, 2, 1107, 1108, 3, 2, 2, 2, 1108, 1109, 8, 133, 10, 2, 1109, 276, 3, 2, 2, 2, 1110, 1111, 5, 51, 21, 2, 1111, 1112, 3, 2, 2, 2, 1112, 1113, 8, 134, 10, 2, 1113, 278, 3, 2, 2, 2, 1114, 1115, 5, 53, 22, 2, 1115, 1116, 3, 2, 2, 2, 1116, 1117, 8, 135, 10, 2, 1117, 280, 3, 2, 2, 2, 1118, 1119, 5, 65, 28, 2, 1119, 1120, 3, 2, 2, 2, 1120, 1121, 8, 136, 13, 2, 1121, 1122, 8, 136, 14, 2, 1122, 282, 3, 2, 2, 2, 1123, 1124, 5, 105, 48, 2, 1124, 1125, 3, 2, 2, 2, 1125, 1126, 8, 137, 19, 2, 1126, 284, 3, 2, 2, 2, 1127, 1128, 5, 167, 79, 2, 1128, 1129, 3, 2, 2, 2, 1129, 1130, 8, 138, 18, 2, 1130, 286, 3, 2, 2, 2, 1131, 1132, 5, 165, 78, 2, 1132, 1133, 3, 2, 2, 2, 1133, 1134, 8, 139, 25, 2, 1134, 288, 3, 2, 2, 2, 1135, 1136, 5, 49, 20, 2, 1136, 1137, 3, 2, 2, 2, 1137, 1138, 8, 140, 10, 2, 1138, 290, 3, 2, 2, 2, 1139, 1140, 5, 51, 21, 2, 1140, 1141, 3, 2, 2, 2, 1141, 1142, 8, 141, 10, 2, 1142, 292, 3, 2, 2, 2, 1143, 1144, 5, 53, 22, 2, 1144, 1145, 3, 2, 2, 2, 1145, 1146, 8, 142, 10, 2, 1146, 294, 3, 2, 2, 2, 1147, 1148, 5, 65, 28, 2, 1148, 1149, 3, 2, 2, 2, 1149, 1150, 8, 143, 13, 2, 1150, 1151, 8, 143, 14, 2, 1151, 296, 3, 2, 2, 2, 1152, 1153, 5, 335, 163, 2, 1153, 1154, 5, 345, 168, 2, 1154, 1155, 5, 329, 160, 2, 1155, 1156, 5, 347, 169, 2, 1156, 298, 3, 2, 2, 2, 1157, 1158, 5, 329, 160, 2, 1158, 1159, 5, 359, 175, 2, 1159, 1160, 5, 345, 168, 2, 1160, 1161, 5, 323, 157, 2, 1161, 1162, 5, 357, 174, 2, 1162, 1163, 5, 335, 163, 2, 1163, 1164, 5, 347, 169, 2, 1164, 1165, 5, 345, 168, 2, 1165, 1166, 5, 355, 173, 2, 1166, 300, 3, 2, 2, 2, 1167, 1168, 5, 49, 20, 2, 1168, 1169, 3, 2, 2, 2, 1169, 1170, 8, 146, 10, 2, 1170, 302, 3, 2, 2, 2, 1171, 1172, 5, 51, 21, 2, 1172, 1173, 3, 2, 2, 2, 1173, 1174, 8, 147, 10, 2, 1174, 304, 3, 2, 2, 2, 1175, 1176, 5, 53, 22, 2, 1176, 1177, 3, 2, 2, 2, 1177, 1178, 8, 148, 10, 2, 1178, 306, 3, 2, 2, 2, 1179, 1180, 5, 163, 77, 2, 1180, 1181, 3, 2, 2, 2, 1181, 1182, 8, 149, 15, 2, 1182, 1183, 8, 149, 14, 2, 1183, 308, 3, 2, 2, 2, 1184, 1185, 7, 60, 2, 2, 1185, 310, 3, 2, 2, 2, 1186, 1192, 5, 77, 34, 2, 1187, 1192, 5, 67, 29, 2, 1188, 1192, 5, 105, 48, 2, 1189, 1192, 5, 69, 30, 2, 1190, 1192, 5, 83, 37, 2, 1191, 1186, 3, 2, 2, 2, 1191, 1187, 3, 2, 2, 2, 1191, 1188, 3, 2, 2, 2, 1191, 1189, 3, 2, 2, 2, 1191, 1190, 3, 2, 2, 2, 1192, 1193, 3, 2, 2, 2, 1193, 1191, 3, 2, 2, 2, 1193, 1194, 3, 2, 2, 2, 1194, 312, 3, 2, 2, 2, 1195, 1196, 5, 49, 20, 2, 1196, 1197, 3, 2, 2, 2, 1197, 1198, 8, 152, 10, 2, 1198, 314, 3, 2, 2, 2, 1199, 1200, 5, 51, 21, 2, 1200, 1201, 3, 2, 2, 2, 1201, 1202, 8, 153, 10, 2, 1202, 316, 3, 2, 2, 2, 1203, 1204, 5, 53, 22, 2, 1204, 1205, 3, 2, 2, 2, 1205, 1206, 8, 154, 10, 2, 1206, 318, 3, 2, 2, 2, 1207, 1208, 9, 15, 2, 2, 1208, 320, 3, 2, 2, 2, 1209, 1210, 9, 16, 2, 2, 1210, 322, 3, 2, 2, 2, 1211, 1212, 9, 17, 2, 2, 1212, 324, 3, 2, 2, 2, 1213, 1214, 9, 18, 2, 2, 1214, 326, 3, 2, 2, 2, 1215, 1216, 9, 9, 2, 2, 1216, 328, 3, 2, 2, 2, 1217, 1218, 9, 19, 2, 2, 1218, 330, 3, 2, 2, 2, 1219, 1220, 9, 20, 2, 2, 1220, 332, 3, 2, 2, 2, 1221, 1222, 9, 21, 2, 2, 1222, 334, 3, 2, 2, 2, 1223, 1224, 9, 22, 2, 2, 1224, 336, 3, 2, 2, 2, 1225, 1226, 9, 23, 2, 2, 1226, 338, 3, 2, 2, 2, 1227, 1228, 9, 24, 2, 2, 1228, 340, 3, 2, 2, 2, 1229, 1230, 9, 25, 2, 2, 1230, 342, 3, 2, 2, 2, 1231, 1232, 9, 26, 2, 2, 1232, 344, 3, 2, 2, 2, 1233, 1234, 9, 27, 2, 2, 1234, 346, 3, 2, 2, 2, 1235, 1236, 9, 28, 2, 2, 1236, 348, 3, 2, 2, 2, 1237, 1238, 9, 29, 2, 2, 1238, 350, 3, 2, 2, 2, 1239, 1240, 9, 30, 2, 2, 1240, 352, 3, 2, 2, 2, 1241, 1242, 9, 31, 2, 2, 1242, 354, 3, 2, 2, 2, 1243, 1244, 9, 32, 2, 2, 1244, 356, 3, 2, 2, 2, 1245, 1246, 9, 33, 2, 2, 1246, 358, 3, 2, 2, 2, 1247, 1248, 9, 34, 2, 2, 1248, 360, 3, 2, 2, 2, 1249, 1250, 9, 35, 2, 2, 1250, 362, 3, 2, 2, 2, 1251, 1252, 9, 36, 2, 2, 1252, 364, 3, 2, 2, 2, 1253, 1254, 9, 37, 2, 2, 1254, 366, 3, 2, 2, 2, 1255, 1256, 9, 38, 2, 2, 1256, 368, 3, 2, 2, 2, 1257, 1258, 9, 39, 2, 2, 1258, 370, 3, 2, 2, 2, 57, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 517, 527, 531, 534, 543, 545, 556, 597, 602, 611, 618, 623, 625, 636, 644, 647, 649, 654, 659, 665, 672, 677, 683, 686, 694, 698, 828, 833, 838, 840, 846, 895, 900, 935, 939, 944, 949, 954, 956, 1045, 1049, 1054, 1191, 1193, 26, 7, 4, 2, 7, 6, 2, 7, 8, 2, 7, 3, 2, 7, 5, 2, 7, 10, 2, 7, 7, 2, 7, 11, 2, 2, 3, 2, 9, 65, 2, 7, 2, 2, 9, 27, 2, 6, 2, 2, 9, 66, 2, 9, 35, 2, 9, 34, 2, 9, 68, 2, 9, 37, 2, 9, 77, 2, 7, 12, 2, 7, 9, 2, 9, 87, 2, 9, 86, 2, 9, 67, 2] \ No newline at end of file diff --git a/packages/kbn-monaco/src/esql/antlr/esql_lexer.tokens b/packages/kbn-monaco/src/esql/antlr/esql_lexer.tokens index 4bdf0572a3b8a..1f49f7e26406b 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_lexer.tokens +++ b/packages/kbn-monaco/src/esql/antlr/esql_lexer.tokens @@ -9,118 +9,117 @@ INLINESTATS=8 KEEP=9 LIMIT=10 MV_EXPAND=11 -PROJECT=12 -RENAME=13 -ROW=14 -SHOW=15 -SORT=16 -STATS=17 -WHERE=18 -UNKNOWN_CMD=19 -LINE_COMMENT=20 -MULTILINE_COMMENT=21 -WS=22 -EXPLAIN_WS=23 -EXPLAIN_LINE_COMMENT=24 -EXPLAIN_MULTILINE_COMMENT=25 -PIPE=26 -STRING=27 -INTEGER_LITERAL=28 -DECIMAL_LITERAL=29 -BY=30 -AND=31 -ASC=32 -ASSIGN=33 -COMMA=34 -DESC=35 -DOT=36 -FALSE=37 -FIRST=38 -LAST=39 -LP=40 -IN=41 -IS=42 -LIKE=43 -NOT=44 -NULL=45 -NULLS=46 -OR=47 -PARAM=48 -RLIKE=49 -RP=50 -TRUE=51 -EQ=52 -CIEQ=53 -NEQ=54 -LT=55 -LTE=56 -GT=57 -GTE=58 -PLUS=59 -MINUS=60 -ASTERISK=61 -SLASH=62 -PERCENT=63 -OPENING_BRACKET=64 -CLOSING_BRACKET=65 -UNQUOTED_IDENTIFIER=66 -QUOTED_IDENTIFIER=67 -EXPR_LINE_COMMENT=68 -EXPR_MULTILINE_COMMENT=69 -EXPR_WS=70 -METADATA=71 -FROM_UNQUOTED_IDENTIFIER=72 -FROM_LINE_COMMENT=73 -FROM_MULTILINE_COMMENT=74 -FROM_WS=75 -UNQUOTED_ID_PATTERN=76 -PROJECT_LINE_COMMENT=77 -PROJECT_MULTILINE_COMMENT=78 -PROJECT_WS=79 -AS=80 -RENAME_LINE_COMMENT=81 -RENAME_MULTILINE_COMMENT=82 -RENAME_WS=83 -ON=84 -WITH=85 -ENRICH_POLICY_NAME=86 -ENRICH_LINE_COMMENT=87 -ENRICH_MULTILINE_COMMENT=88 -ENRICH_WS=89 -ENRICH_FIELD_LINE_COMMENT=90 -ENRICH_FIELD_MULTILINE_COMMENT=91 -ENRICH_FIELD_WS=92 -MVEXPAND_LINE_COMMENT=93 -MVEXPAND_MULTILINE_COMMENT=94 -MVEXPAND_WS=95 -INFO=96 -FUNCTIONS=97 -SHOW_LINE_COMMENT=98 -SHOW_MULTILINE_COMMENT=99 -SHOW_WS=100 -COLON=101 -SETTING=102 -SETTING_LINE_COMMENT=103 -SETTTING_MULTILINE_COMMENT=104 -SETTING_WS=105 -'|'=26 -'='=33 -','=34 -'.'=36 -'('=40 -'?'=48 -')'=50 -'=='=52 -'=~'=53 -'!='=54 -'<'=55 -'<='=56 -'>'=57 -'>='=58 -'+'=59 -'-'=60 -'*'=61 -'/'=62 -'%'=63 -']'=65 -':'=101 +RENAME=12 +ROW=13 +SHOW=14 +SORT=15 +STATS=16 +WHERE=17 +UNKNOWN_CMD=18 +LINE_COMMENT=19 +MULTILINE_COMMENT=20 +WS=21 +EXPLAIN_WS=22 +EXPLAIN_LINE_COMMENT=23 +EXPLAIN_MULTILINE_COMMENT=24 +PIPE=25 +STRING=26 +INTEGER_LITERAL=27 +DECIMAL_LITERAL=28 +BY=29 +AND=30 +ASC=31 +ASSIGN=32 +COMMA=33 +DESC=34 +DOT=35 +FALSE=36 +FIRST=37 +LAST=38 +LP=39 +IN=40 +IS=41 +LIKE=42 +NOT=43 +NULL=44 +NULLS=45 +OR=46 +PARAM=47 +RLIKE=48 +RP=49 +TRUE=50 +EQ=51 +CIEQ=52 +NEQ=53 +LT=54 +LTE=55 +GT=56 +GTE=57 +PLUS=58 +MINUS=59 +ASTERISK=60 +SLASH=61 +PERCENT=62 +OPENING_BRACKET=63 +CLOSING_BRACKET=64 +UNQUOTED_IDENTIFIER=65 +QUOTED_IDENTIFIER=66 +EXPR_LINE_COMMENT=67 +EXPR_MULTILINE_COMMENT=68 +EXPR_WS=69 +METADATA=70 +FROM_UNQUOTED_IDENTIFIER=71 +FROM_LINE_COMMENT=72 +FROM_MULTILINE_COMMENT=73 +FROM_WS=74 +UNQUOTED_ID_PATTERN=75 +PROJECT_LINE_COMMENT=76 +PROJECT_MULTILINE_COMMENT=77 +PROJECT_WS=78 +AS=79 +RENAME_LINE_COMMENT=80 +RENAME_MULTILINE_COMMENT=81 +RENAME_WS=82 +ON=83 +WITH=84 +ENRICH_POLICY_NAME=85 +ENRICH_LINE_COMMENT=86 +ENRICH_MULTILINE_COMMENT=87 +ENRICH_WS=88 +ENRICH_FIELD_LINE_COMMENT=89 +ENRICH_FIELD_MULTILINE_COMMENT=90 +ENRICH_FIELD_WS=91 +MVEXPAND_LINE_COMMENT=92 +MVEXPAND_MULTILINE_COMMENT=93 +MVEXPAND_WS=94 +INFO=95 +FUNCTIONS=96 +SHOW_LINE_COMMENT=97 +SHOW_MULTILINE_COMMENT=98 +SHOW_WS=99 +COLON=100 +SETTING=101 +SETTING_LINE_COMMENT=102 +SETTTING_MULTILINE_COMMENT=103 +SETTING_WS=104 +'|'=25 +'='=32 +','=33 +'.'=35 +'('=39 +'?'=47 +')'=49 +'=='=51 +'=~'=52 +'!='=53 +'<'=54 +'<='=55 +'>'=56 +'>='=57 +'+'=58 +'-'=59 +'*'=60 +'/'=61 +'%'=62 +']'=64 +':'=100 diff --git a/packages/kbn-monaco/src/esql/antlr/esql_lexer.ts b/packages/kbn-monaco/src/esql/antlr/esql_lexer.ts index 1bea52f3e5f29..12f8c8617cd75 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_lexer.ts +++ b/packages/kbn-monaco/src/esql/antlr/esql_lexer.ts @@ -28,100 +28,99 @@ export class esql_lexer extends Lexer { public static readonly KEEP = 9; public static readonly LIMIT = 10; public static readonly MV_EXPAND = 11; - public static readonly PROJECT = 12; - public static readonly RENAME = 13; - public static readonly ROW = 14; - public static readonly SHOW = 15; - public static readonly SORT = 16; - public static readonly STATS = 17; - public static readonly WHERE = 18; - public static readonly UNKNOWN_CMD = 19; - public static readonly LINE_COMMENT = 20; - public static readonly MULTILINE_COMMENT = 21; - public static readonly WS = 22; - public static readonly EXPLAIN_WS = 23; - public static readonly EXPLAIN_LINE_COMMENT = 24; - public static readonly EXPLAIN_MULTILINE_COMMENT = 25; - public static readonly PIPE = 26; - public static readonly STRING = 27; - public static readonly INTEGER_LITERAL = 28; - public static readonly DECIMAL_LITERAL = 29; - public static readonly BY = 30; - public static readonly AND = 31; - public static readonly ASC = 32; - public static readonly ASSIGN = 33; - public static readonly COMMA = 34; - public static readonly DESC = 35; - public static readonly DOT = 36; - public static readonly FALSE = 37; - public static readonly FIRST = 38; - public static readonly LAST = 39; - public static readonly LP = 40; - public static readonly IN = 41; - public static readonly IS = 42; - public static readonly LIKE = 43; - public static readonly NOT = 44; - public static readonly NULL = 45; - public static readonly NULLS = 46; - public static readonly OR = 47; - public static readonly PARAM = 48; - public static readonly RLIKE = 49; - public static readonly RP = 50; - public static readonly TRUE = 51; - public static readonly EQ = 52; - public static readonly CIEQ = 53; - public static readonly NEQ = 54; - public static readonly LT = 55; - public static readonly LTE = 56; - public static readonly GT = 57; - public static readonly GTE = 58; - public static readonly PLUS = 59; - public static readonly MINUS = 60; - public static readonly ASTERISK = 61; - public static readonly SLASH = 62; - public static readonly PERCENT = 63; - public static readonly OPENING_BRACKET = 64; - public static readonly CLOSING_BRACKET = 65; - public static readonly UNQUOTED_IDENTIFIER = 66; - public static readonly QUOTED_IDENTIFIER = 67; - public static readonly EXPR_LINE_COMMENT = 68; - public static readonly EXPR_MULTILINE_COMMENT = 69; - public static readonly EXPR_WS = 70; - public static readonly METADATA = 71; - public static readonly FROM_UNQUOTED_IDENTIFIER = 72; - public static readonly FROM_LINE_COMMENT = 73; - public static readonly FROM_MULTILINE_COMMENT = 74; - public static readonly FROM_WS = 75; - public static readonly UNQUOTED_ID_PATTERN = 76; - public static readonly PROJECT_LINE_COMMENT = 77; - public static readonly PROJECT_MULTILINE_COMMENT = 78; - public static readonly PROJECT_WS = 79; - public static readonly AS = 80; - public static readonly RENAME_LINE_COMMENT = 81; - public static readonly RENAME_MULTILINE_COMMENT = 82; - public static readonly RENAME_WS = 83; - public static readonly ON = 84; - public static readonly WITH = 85; - public static readonly ENRICH_POLICY_NAME = 86; - public static readonly ENRICH_LINE_COMMENT = 87; - public static readonly ENRICH_MULTILINE_COMMENT = 88; - public static readonly ENRICH_WS = 89; - public static readonly ENRICH_FIELD_LINE_COMMENT = 90; - public static readonly ENRICH_FIELD_MULTILINE_COMMENT = 91; - public static readonly ENRICH_FIELD_WS = 92; - public static readonly MVEXPAND_LINE_COMMENT = 93; - public static readonly MVEXPAND_MULTILINE_COMMENT = 94; - public static readonly MVEXPAND_WS = 95; - public static readonly INFO = 96; - public static readonly FUNCTIONS = 97; - public static readonly SHOW_LINE_COMMENT = 98; - public static readonly SHOW_MULTILINE_COMMENT = 99; - public static readonly SHOW_WS = 100; - public static readonly COLON = 101; - public static readonly SETTING = 102; - public static readonly SETTING_LINE_COMMENT = 103; - public static readonly SETTTING_MULTILINE_COMMENT = 104; - public static readonly SETTING_WS = 105; + public static readonly RENAME = 12; + public static readonly ROW = 13; + public static readonly SHOW = 14; + public static readonly SORT = 15; + public static readonly STATS = 16; + public static readonly WHERE = 17; + public static readonly UNKNOWN_CMD = 18; + public static readonly LINE_COMMENT = 19; + public static readonly MULTILINE_COMMENT = 20; + public static readonly WS = 21; + public static readonly EXPLAIN_WS = 22; + public static readonly EXPLAIN_LINE_COMMENT = 23; + public static readonly EXPLAIN_MULTILINE_COMMENT = 24; + public static readonly PIPE = 25; + public static readonly STRING = 26; + public static readonly INTEGER_LITERAL = 27; + public static readonly DECIMAL_LITERAL = 28; + public static readonly BY = 29; + public static readonly AND = 30; + public static readonly ASC = 31; + public static readonly ASSIGN = 32; + public static readonly COMMA = 33; + public static readonly DESC = 34; + public static readonly DOT = 35; + public static readonly FALSE = 36; + public static readonly FIRST = 37; + public static readonly LAST = 38; + public static readonly LP = 39; + public static readonly IN = 40; + public static readonly IS = 41; + public static readonly LIKE = 42; + public static readonly NOT = 43; + public static readonly NULL = 44; + public static readonly NULLS = 45; + public static readonly OR = 46; + public static readonly PARAM = 47; + public static readonly RLIKE = 48; + public static readonly RP = 49; + public static readonly TRUE = 50; + public static readonly EQ = 51; + public static readonly CIEQ = 52; + public static readonly NEQ = 53; + public static readonly LT = 54; + public static readonly LTE = 55; + public static readonly GT = 56; + public static readonly GTE = 57; + public static readonly PLUS = 58; + public static readonly MINUS = 59; + public static readonly ASTERISK = 60; + public static readonly SLASH = 61; + public static readonly PERCENT = 62; + public static readonly OPENING_BRACKET = 63; + public static readonly CLOSING_BRACKET = 64; + public static readonly UNQUOTED_IDENTIFIER = 65; + public static readonly QUOTED_IDENTIFIER = 66; + public static readonly EXPR_LINE_COMMENT = 67; + public static readonly EXPR_MULTILINE_COMMENT = 68; + public static readonly EXPR_WS = 69; + public static readonly METADATA = 70; + public static readonly FROM_UNQUOTED_IDENTIFIER = 71; + public static readonly FROM_LINE_COMMENT = 72; + public static readonly FROM_MULTILINE_COMMENT = 73; + public static readonly FROM_WS = 74; + public static readonly UNQUOTED_ID_PATTERN = 75; + public static readonly PROJECT_LINE_COMMENT = 76; + public static readonly PROJECT_MULTILINE_COMMENT = 77; + public static readonly PROJECT_WS = 78; + public static readonly AS = 79; + public static readonly RENAME_LINE_COMMENT = 80; + public static readonly RENAME_MULTILINE_COMMENT = 81; + public static readonly RENAME_WS = 82; + public static readonly ON = 83; + public static readonly WITH = 84; + public static readonly ENRICH_POLICY_NAME = 85; + public static readonly ENRICH_LINE_COMMENT = 86; + public static readonly ENRICH_MULTILINE_COMMENT = 87; + public static readonly ENRICH_WS = 88; + public static readonly ENRICH_FIELD_LINE_COMMENT = 89; + public static readonly ENRICH_FIELD_MULTILINE_COMMENT = 90; + public static readonly ENRICH_FIELD_WS = 91; + public static readonly MVEXPAND_LINE_COMMENT = 92; + public static readonly MVEXPAND_MULTILINE_COMMENT = 93; + public static readonly MVEXPAND_WS = 94; + public static readonly INFO = 95; + public static readonly FUNCTIONS = 96; + public static readonly SHOW_LINE_COMMENT = 97; + public static readonly SHOW_MULTILINE_COMMENT = 98; + public static readonly SHOW_WS = 99; + public static readonly COLON = 100; + public static readonly SETTING = 101; + public static readonly SETTING_LINE_COMMENT = 102; + public static readonly SETTTING_MULTILINE_COMMENT = 103; + public static readonly SETTING_WS = 104; public static readonly EXPLAIN_MODE = 1; public static readonly EXPRESSION_MODE = 2; public static readonly FROM_MODE = 3; @@ -147,47 +146,47 @@ export class esql_lexer extends Lexer { public static readonly ruleNames: string[] = [ "DISSECT", "DROP", "ENRICH", "EVAL", "EXPLAIN", "FROM", "GROK", "INLINESTATS", - "KEEP", "LIMIT", "MV_EXPAND", "PROJECT", "RENAME", "ROW", "SHOW", "SORT", - "STATS", "WHERE", "UNKNOWN_CMD", "LINE_COMMENT", "MULTILINE_COMMENT", - "WS", "EXPLAIN_OPENING_BRACKET", "EXPLAIN_PIPE", "EXPLAIN_WS", "EXPLAIN_LINE_COMMENT", - "EXPLAIN_MULTILINE_COMMENT", "PIPE", "DIGIT", "LETTER", "ESCAPE_SEQUENCE", - "UNESCAPED_CHARS", "EXPONENT", "ASPERAND", "BACKQUOTE", "BACKQUOTE_BLOCK", - "UNDERSCORE", "UNQUOTED_ID_BODY", "STRING", "INTEGER_LITERAL", "DECIMAL_LITERAL", - "BY", "AND", "ASC", "ASSIGN", "COMMA", "DESC", "DOT", "FALSE", "FIRST", - "LAST", "LP", "IN", "IS", "LIKE", "NOT", "NULL", "NULLS", "OR", "PARAM", - "RLIKE", "RP", "TRUE", "EQ", "CIEQ", "NEQ", "LT", "LTE", "GT", "GTE", - "PLUS", "MINUS", "ASTERISK", "SLASH", "PERCENT", "OPENING_BRACKET", "CLOSING_BRACKET", - "UNQUOTED_IDENTIFIER", "QUOTED_IDENTIFIER", "EXPR_LINE_COMMENT", "EXPR_MULTILINE_COMMENT", - "EXPR_WS", "FROM_PIPE", "FROM_OPENING_BRACKET", "FROM_CLOSING_BRACKET", - "FROM_COMMA", "FROM_ASSIGN", "METADATA", "FROM_UNQUOTED_IDENTIFIER_PART", - "FROM_UNQUOTED_IDENTIFIER", "FROM_QUOTED_IDENTIFIER", "FROM_LINE_COMMENT", - "FROM_MULTILINE_COMMENT", "FROM_WS", "PROJECT_PIPE", "PROJECT_DOT", "PROJECT_COMMA", - "UNQUOTED_ID_BODY_WITH_PATTERN", "UNQUOTED_ID_PATTERN", "PROJECT_UNQUOTED_IDENTIFIER", - "PROJECT_QUOTED_IDENTIFIER", "PROJECT_LINE_COMMENT", "PROJECT_MULTILINE_COMMENT", - "PROJECT_WS", "RENAME_PIPE", "RENAME_ASSIGN", "RENAME_COMMA", "RENAME_DOT", - "AS", "RENAME_QUOTED_IDENTIFIER", "RENAME_UNQUOTED_IDENTIFIER", "RENAME_LINE_COMMENT", - "RENAME_MULTILINE_COMMENT", "RENAME_WS", "ENRICH_PIPE", "ENRICH_OPENING_BRACKET", - "ON", "WITH", "ENRICH_POLICY_NAME_BODY", "ENRICH_POLICY_NAME", "ENRICH_QUOTED_IDENTIFIER", - "ENRICH_MODE_UNQUOTED_VALUE", "ENRICH_LINE_COMMENT", "ENRICH_MULTILINE_COMMENT", - "ENRICH_WS", "ENRICH_FIELD_PIPE", "ENRICH_FIELD_ASSIGN", "ENRICH_FIELD_COMMA", - "ENRICH_FIELD_DOT", "ENRICH_FIELD_WITH", "ENRICH_FIELD_UNQUOTED_IDENTIFIER", - "ENRICH_FIELD_QUOTED_IDENTIFIER", "ENRICH_FIELD_LINE_COMMENT", "ENRICH_FIELD_MULTILINE_COMMENT", - "ENRICH_FIELD_WS", "MVEXPAND_PIPE", "MVEXPAND_DOT", "MVEXPAND_QUOTED_IDENTIFIER", - "MVEXPAND_UNQUOTED_IDENTIFIER", "MVEXPAND_LINE_COMMENT", "MVEXPAND_MULTILINE_COMMENT", - "MVEXPAND_WS", "SHOW_PIPE", "INFO", "FUNCTIONS", "SHOW_LINE_COMMENT", - "SHOW_MULTILINE_COMMENT", "SHOW_WS", "SETTING_CLOSING_BRACKET", "COLON", - "SETTING", "SETTING_LINE_COMMENT", "SETTTING_MULTILINE_COMMENT", "SETTING_WS", - "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", - "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "KEEP", "LIMIT", "MV_EXPAND", "RENAME", "ROW", "SHOW", "SORT", "STATS", + "WHERE", "UNKNOWN_CMD", "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "EXPLAIN_OPENING_BRACKET", + "EXPLAIN_PIPE", "EXPLAIN_WS", "EXPLAIN_LINE_COMMENT", "EXPLAIN_MULTILINE_COMMENT", + "PIPE", "DIGIT", "LETTER", "ESCAPE_SEQUENCE", "UNESCAPED_CHARS", "EXPONENT", + "ASPERAND", "BACKQUOTE", "BACKQUOTE_BLOCK", "UNDERSCORE", "UNQUOTED_ID_BODY", + "STRING", "INTEGER_LITERAL", "DECIMAL_LITERAL", "BY", "AND", "ASC", "ASSIGN", + "COMMA", "DESC", "DOT", "FALSE", "FIRST", "LAST", "LP", "IN", "IS", "LIKE", + "NOT", "NULL", "NULLS", "OR", "PARAM", "RLIKE", "RP", "TRUE", "EQ", "CIEQ", + "NEQ", "LT", "LTE", "GT", "GTE", "PLUS", "MINUS", "ASTERISK", "SLASH", + "PERCENT", "OPENING_BRACKET", "CLOSING_BRACKET", "UNQUOTED_IDENTIFIER", + "QUOTED_IDENTIFIER", "EXPR_LINE_COMMENT", "EXPR_MULTILINE_COMMENT", "EXPR_WS", + "FROM_PIPE", "FROM_OPENING_BRACKET", "FROM_CLOSING_BRACKET", "FROM_COMMA", + "FROM_ASSIGN", "METADATA", "FROM_UNQUOTED_IDENTIFIER_PART", "FROM_UNQUOTED_IDENTIFIER", + "FROM_QUOTED_IDENTIFIER", "FROM_LINE_COMMENT", "FROM_MULTILINE_COMMENT", + "FROM_WS", "PROJECT_PIPE", "PROJECT_DOT", "PROJECT_COMMA", "UNQUOTED_ID_BODY_WITH_PATTERN", + "UNQUOTED_ID_PATTERN", "PROJECT_UNQUOTED_IDENTIFIER", "PROJECT_QUOTED_IDENTIFIER", + "PROJECT_LINE_COMMENT", "PROJECT_MULTILINE_COMMENT", "PROJECT_WS", "RENAME_PIPE", + "RENAME_ASSIGN", "RENAME_COMMA", "RENAME_DOT", "AS", "RENAME_QUOTED_IDENTIFIER", + "RENAME_UNQUOTED_IDENTIFIER", "RENAME_LINE_COMMENT", "RENAME_MULTILINE_COMMENT", + "RENAME_WS", "ENRICH_PIPE", "ENRICH_OPENING_BRACKET", "ON", "WITH", "ENRICH_POLICY_NAME_BODY", + "ENRICH_POLICY_NAME", "ENRICH_QUOTED_IDENTIFIER", "ENRICH_MODE_UNQUOTED_VALUE", + "ENRICH_LINE_COMMENT", "ENRICH_MULTILINE_COMMENT", "ENRICH_WS", "ENRICH_FIELD_PIPE", + "ENRICH_FIELD_ASSIGN", "ENRICH_FIELD_COMMA", "ENRICH_FIELD_DOT", "ENRICH_FIELD_WITH", + "ENRICH_FIELD_UNQUOTED_IDENTIFIER", "ENRICH_FIELD_QUOTED_IDENTIFIER", + "ENRICH_FIELD_LINE_COMMENT", "ENRICH_FIELD_MULTILINE_COMMENT", "ENRICH_FIELD_WS", + "MVEXPAND_PIPE", "MVEXPAND_DOT", "MVEXPAND_QUOTED_IDENTIFIER", "MVEXPAND_UNQUOTED_IDENTIFIER", + "MVEXPAND_LINE_COMMENT", "MVEXPAND_MULTILINE_COMMENT", "MVEXPAND_WS", + "SHOW_PIPE", "INFO", "FUNCTIONS", "SHOW_LINE_COMMENT", "SHOW_MULTILINE_COMMENT", + "SHOW_WS", "SETTING_CLOSING_BRACKET", "COLON", "SETTING", "SETTING_LINE_COMMENT", + "SETTTING_MULTILINE_COMMENT", "SETTING_WS", "A", "B", "C", "D", "E", "F", + "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", + "U", "V", "W", "X", "Y", "Z", ]; private static readonly _LITERAL_NAMES: Array = [ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, - undefined, undefined, undefined, undefined, undefined, "'|'", undefined, - undefined, undefined, undefined, undefined, undefined, "'='", "','", undefined, - "'.'", undefined, undefined, undefined, "'('", undefined, undefined, undefined, + undefined, undefined, undefined, undefined, "'|'", undefined, undefined, + undefined, undefined, undefined, undefined, "'='", "','", undefined, "'.'", + undefined, undefined, undefined, "'('", undefined, undefined, undefined, undefined, undefined, undefined, undefined, "'?'", undefined, "')'", undefined, "'=='", "'=~'", "'!='", "'<'", "'<='", "'>'", "'>='", "'+'", "'-'", "'*'", "'/'", "'%'", undefined, "']'", undefined, undefined, undefined, undefined, @@ -199,8 +198,8 @@ export class esql_lexer extends Lexer { ]; private static readonly _SYMBOLIC_NAMES: Array = [ undefined, "DISSECT", "DROP", "ENRICH", "EVAL", "EXPLAIN", "FROM", "GROK", - "INLINESTATS", "KEEP", "LIMIT", "MV_EXPAND", "PROJECT", "RENAME", "ROW", - "SHOW", "SORT", "STATS", "WHERE", "UNKNOWN_CMD", "LINE_COMMENT", "MULTILINE_COMMENT", + "INLINESTATS", "KEEP", "LIMIT", "MV_EXPAND", "RENAME", "ROW", "SHOW", + "SORT", "STATS", "WHERE", "UNKNOWN_CMD", "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "EXPLAIN_WS", "EXPLAIN_LINE_COMMENT", "EXPLAIN_MULTILINE_COMMENT", "PIPE", "STRING", "INTEGER_LITERAL", "DECIMAL_LITERAL", "BY", "AND", "ASC", "ASSIGN", "COMMA", "DESC", "DOT", "FALSE", "FIRST", "LAST", "LP", "IN", @@ -250,7 +249,7 @@ export class esql_lexer extends Lexer { private static readonly _serializedATNSegments: number = 3; private static readonly _serializedATNSegment0: string = - "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x02k\u04F3\b\x01" + + "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x02j\u04EB\b\x01" + "\b\x01\b\x01\b\x01\b\x01\b\x01\b\x01\b\x01\b\x01\b\x01\b\x01\x04\x02\t" + "\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06\x04\x07\t" + "\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f\t\f\x04\r\t\r\x04\x0E" + @@ -279,602 +278,598 @@ export class esql_lexer extends Lexer { "\t\xA5\x04\xA6\t\xA6\x04\xA7\t\xA7\x04\xA8\t\xA8\x04\xA9\t\xA9\x04\xAA" + "\t\xAA\x04\xAB\t\xAB\x04\xAC\t\xAC\x04\xAD\t\xAD\x04\xAE\t\xAE\x04\xAF" + "\t\xAF\x04\xB0\t\xB0\x04\xB1\t\xB1\x04\xB2\t\xB2\x04\xB3\t\xB3\x04\xB4" + - "\t\xB4\x04\xB5\t\xB5\x03\x02\x03\x02\x03\x02\x03\x02\x03\x02\x03\x02\x03" + - "\x02\x03\x02\x03\x02\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03" + - "\x03\x03\x03\x03\x04\x03\x04\x03\x04\x03\x04\x03\x04\x03\x04\x03\x04\x03" + - "\x04\x03\x04\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03" + - "\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03" + - "\x06\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\b\x03" + - "\b\x03\b\x03\b\x03\b\x03\b\x03\b\x03\t\x03\t\x03\t\x03\t\x03\t\x03\t\x03" + - "\t\x03\t\x03\t\x03\t\x03\t\x03\t\x03\t\x03\t\x03\n\x03\n\x03\n\x03\n\x03" + - "\n\x03\n\x03\n\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\f\x03" + - "\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\r\x03" + - "\r\x03\r\x03\r\x03\r\x03\r\x03\r\x03\r\x03\r\x03\r\x03\x0E\x03\x0E\x03" + - "\x0E\x03\x0E\x03\x0E\x03\x0E\x03\x0E\x03\x0E\x03\x0E\x03\x0F\x03\x0F\x03" + - "\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10\x03" + - "\x10\x03\x10\x03\x11\x03\x11\x03\x11\x03\x11\x03\x11\x03\x11\x03\x11\x03" + - "\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x13\x03" + - "\x13\x03\x13\x03\x13\x03\x13\x03\x13\x03\x13\x03\x13\x03\x14\x06\x14\u0210" + - "\n\x14\r\x14\x0E\x14\u0211\x03\x14\x03\x14\x03\x15\x03\x15\x03\x15\x03" + - "\x15\x07\x15\u021A\n\x15\f\x15\x0E\x15\u021D\v\x15\x03\x15\x05\x15\u0220" + - "\n\x15\x03\x15\x05\x15\u0223\n\x15\x03\x15\x03\x15\x03\x16\x03\x16\x03" + - "\x16\x03\x16\x03\x16\x07\x16\u022C\n\x16\f\x16\x0E\x16\u022F\v\x16\x03" + - "\x16\x03\x16\x03\x16\x03\x16\x03\x16\x03\x17\x06\x17\u0237\n\x17\r\x17" + - "\x0E\x17\u0238\x03\x17\x03\x17\x03\x18\x03\x18\x03\x18\x03\x18\x03\x18" + - "\x03\x19\x03\x19\x03\x19\x03\x19\x03\x19\x03\x1A\x03\x1A\x03\x1A\x03\x1A" + - "\x03\x1B\x03\x1B\x03\x1B\x03\x1B\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1D" + - "\x03\x1D\x03\x1D\x03\x1D\x03\x1E\x03\x1E\x03\x1F\x03\x1F\x03 \x03 \x03" + - " \x03!\x03!\x03\"\x03\"\x05\"\u0262\n\"\x03\"\x06\"\u0265\n\"\r\"\x0E" + - "\"\u0266\x03#\x03#\x03$\x03$\x03%\x03%\x03%\x05%\u0270\n%\x03&\x03&\x03" + - "\'\x03\'\x03\'\x05\'\u0277\n\'\x03(\x03(\x03(\x07(\u027C\n(\f(\x0E(\u027F" + - "\v(\x03(\x03(\x03(\x03(\x03(\x03(\x07(\u0287\n(\f(\x0E(\u028A\v(\x03(" + - "\x03(\x03(\x03(\x03(\x05(\u0291\n(\x03(\x05(\u0294\n(\x05(\u0296\n(\x03" + - ")\x06)\u0299\n)\r)\x0E)\u029A\x03*\x06*\u029E\n*\r*\x0E*\u029F\x03*\x03" + - "*\x07*\u02A4\n*\f*\x0E*\u02A7\v*\x03*\x03*\x06*\u02AB\n*\r*\x0E*\u02AC" + - "\x03*\x06*\u02B0\n*\r*\x0E*\u02B1\x03*\x03*\x07*\u02B6\n*\f*\x0E*\u02B9" + - "\v*\x05*\u02BB\n*\x03*\x03*\x03*\x03*\x06*\u02C1\n*\r*\x0E*\u02C2\x03" + - "*\x03*\x05*\u02C7\n*\x03+\x03+\x03+\x03,\x03,\x03,\x03,\x03-\x03-\x03" + - "-\x03-\x03.\x03.\x03/\x03/\x030\x030\x030\x030\x030\x031\x031\x032\x03" + - "2\x032\x032\x032\x032\x033\x033\x033\x033\x033\x033\x034\x034\x034\x03" + - "4\x034\x035\x035\x036\x036\x036\x037\x037\x037\x038\x038\x038\x038\x03" + - "8\x039\x039\x039\x039\x03:\x03:\x03:\x03:\x03:\x03;\x03;\x03;\x03;\x03" + - ";\x03;\x03<\x03<\x03<\x03=\x03=\x03>\x03>\x03>\x03>\x03>\x03>\x03?\x03" + - "?\x03@\x03@\x03@\x03@\x03@\x03A\x03A\x03A\x03B\x03B\x03B\x03C\x03C\x03" + - "C\x03D\x03D\x03E\x03E\x03E\x03F\x03F\x03G\x03G\x03G\x03H\x03H\x03I\x03" + - "I\x03J\x03J\x03K\x03K\x03L\x03L\x03M\x03M\x03M\x03M\x03M\x03N\x03N\x03" + - "N\x03N\x03N\x03O\x03O\x07O\u0347\nO\fO\x0EO\u034A\vO\x03O\x03O\x05O\u034E" + - "\nO\x03O\x06O\u0351\nO\rO\x0EO\u0352\x05O\u0355\nO\x03P\x03P\x06P\u0359" + - "\nP\rP\x0EP\u035A\x03P\x03P\x03Q\x03Q\x03Q\x03Q\x03R\x03R\x03R\x03R\x03" + - "S\x03S\x03S\x03S\x03T\x03T\x03T\x03T\x03T\x03U\x03U\x03U\x03U\x03V\x03" + - "V\x03V\x03V\x03W\x03W\x03W\x03W\x03X\x03X\x03X\x03X\x03Y\x03Y\x03Y\x03" + - "Y\x03Y\x03Y\x03Y\x03Y\x03Y\x03Z\x03Z\x03Z\x05Z\u038C\nZ\x03[\x06[\u038F" + - "\n[\r[\x0E[\u0390\x03\\\x03\\\x03\\\x03\\\x03]\x03]\x03]\x03]\x03^\x03" + - "^\x03^\x03^\x03_\x03_\x03_\x03_\x03`\x03`\x03`\x03`\x03`\x03a\x03a\x03" + - "a\x03a\x03b\x03b\x03b\x03b\x03c\x03c\x03c\x03c\x05c\u03B4\nc\x03d\x03" + - "d\x05d\u03B8\nd\x03d\x07d\u03BB\nd\fd\x0Ed\u03BE\vd\x03d\x03d\x05d\u03C2" + - "\nd\x03d\x06d\u03C5\nd\rd\x0Ed\u03C6\x05d\u03C9\nd\x03e\x03e\x03e\x03" + - "e\x03f\x03f\x03f\x03f\x03g\x03g\x03g\x03g\x03h\x03h\x03h\x03h\x03i\x03" + - "i\x03i\x03i\x03j\x03j\x03j\x03j\x03j\x03k\x03k\x03k\x03k\x03l\x03l\x03" + - "l\x03l\x03m\x03m\x03m\x03m\x03n\x03n\x03n\x03o\x03o\x03o\x03o\x03p\x03" + - "p\x03p\x03p\x03q\x03q\x03q\x03q\x03r\x03r\x03r\x03r\x03s\x03s\x03s\x03" + - "s\x03t\x03t\x03t\x03t\x03t\x03u\x03u\x03u\x03u\x03u\x03v\x03v\x03v\x03" + - "v\x03v\x03w\x03w\x03w\x03w\x03w\x03w\x03w\x03x\x03x\x03y\x03y\x05y\u0421" + - "\ny\x03y\x07y\u0424\ny\fy\x0Ey\u0427\vy\x03z\x03z\x03z\x03z\x03{\x03{" + - "\x03{\x03{\x03|\x03|\x03|\x03|\x03}\x03}\x03}\x03}\x03~\x03~\x03~\x03" + - "~\x03\x7F\x03\x7F\x03\x7F\x03\x7F\x03\x7F\x03\x7F\x03\x80\x03\x80\x03" + - "\x80\x03\x80\x03\x81\x03\x81\x03\x81\x03\x81\x03\x82\x03\x82\x03\x82\x03" + - "\x82\x03\x83\x03\x83\x03\x83\x03\x83\x03\x84\x03\x84\x03\x84\x03\x84\x03" + - "\x85\x03\x85\x03\x85\x03\x85\x03\x86\x03\x86\x03\x86\x03\x86\x03\x87\x03" + - "\x87\x03\x87\x03\x87\x03\x88\x03\x88\x03\x88\x03\x88\x03\x89\x03\x89\x03" + - "\x89\x03\x89\x03\x89\x03\x8A\x03\x8A\x03\x8A\x03\x8A\x03\x8B\x03\x8B\x03" + - "\x8B\x03\x8B\x03\x8C\x03\x8C\x03\x8C\x03\x8C\x03\x8D\x03\x8D\x03\x8D\x03" + - "\x8D\x03\x8E\x03\x8E\x03\x8E\x03\x8E\x03\x8F\x03\x8F\x03\x8F\x03\x8F\x03" + - "\x90\x03\x90\x03\x90\x03\x90\x03\x90\x03\x91\x03\x91\x03\x91\x03\x91\x03" + - "\x91\x03\x92\x03\x92\x03\x92\x03\x92\x03\x92\x03\x92\x03\x92\x03\x92\x03" + - "\x92\x03\x92\x03\x93\x03\x93\x03\x93\x03\x93\x03\x94\x03\x94\x03\x94\x03" + - "\x94\x03\x95\x03\x95\x03\x95\x03\x95\x03\x96\x03\x96\x03\x96\x03\x96\x03" + - "\x96\x03\x97\x03\x97\x03\x98\x03\x98\x03\x98\x03\x98\x03\x98\x06\x98\u04B0" + - "\n\x98\r\x98\x0E\x98\u04B1\x03\x99\x03\x99\x03\x99\x03\x99\x03\x9A\x03" + - "\x9A\x03\x9A\x03\x9A\x03\x9B\x03\x9B\x03\x9B\x03\x9B\x03\x9C\x03\x9C\x03" + - "\x9D\x03\x9D\x03\x9E\x03\x9E\x03\x9F\x03\x9F\x03\xA0\x03\xA0\x03\xA1\x03" + - "\xA1\x03\xA2\x03\xA2\x03\xA3\x03\xA3\x03\xA4\x03\xA4\x03\xA5\x03\xA5\x03" + - "\xA6\x03\xA6\x03\xA7\x03\xA7\x03\xA8\x03\xA8\x03\xA9\x03\xA9\x03\xAA\x03" + - "\xAA\x03\xAB\x03\xAB\x03\xAC\x03\xAC\x03\xAD\x03\xAD\x03\xAE\x03\xAE\x03" + - "\xAF\x03\xAF\x03\xB0\x03\xB0\x03\xB1\x03\xB1\x03\xB2\x03\xB2\x03\xB3\x03" + - "\xB3\x03\xB4\x03\xB4\x03\xB5\x03\xB5\x04\u022D\u0288\x02\x02\xB6\r\x02" + - "\x03\x0F\x02\x04\x11\x02\x05\x13\x02\x06\x15\x02\x07\x17\x02\b\x19\x02" + - "\t\x1B\x02\n\x1D\x02\v\x1F\x02\f!\x02\r#\x02\x0E%\x02\x0F\'\x02\x10)\x02" + - "\x11+\x02\x12-\x02\x13/\x02\x141\x02\x153\x02\x165\x02\x177\x02\x189\x02" + - "\x02;\x02\x02=\x02\x19?\x02\x1AA\x02\x1BC\x02\x1CE\x02\x02G\x02\x02I\x02" + - "\x02K\x02\x02M\x02\x02O\x02\x02Q\x02\x02S\x02\x02U\x02\x02W\x02\x02Y\x02" + - "\x1D[\x02\x1E]\x02\x1F_\x02 a\x02!c\x02\"e\x02#g\x02$i\x02%k\x02&m\x02" + - "\'o\x02(q\x02)s\x02*u\x02+w\x02,y\x02-{\x02.}\x02/\x7F\x020\x81\x021\x83" + - "\x022\x85\x023\x87\x024\x89\x025\x8B\x026\x8D\x027\x8F\x028\x91\x029\x93" + - "\x02:\x95\x02;\x97\x02<\x99\x02=\x9B\x02>\x9D\x02?\x9F\x02@\xA1\x02A\xA3" + - "\x02B\xA5\x02C\xA7\x02D\xA9\x02E\xAB\x02F\xAD\x02G\xAF\x02H\xB1\x02\x02" + - "\xB3\x02\x02\xB5\x02\x02\xB7\x02\x02\xB9\x02\x02\xBB\x02I\xBD\x02\x02" + - "\xBF\x02J\xC1\x02\x02\xC3\x02K\xC5\x02L\xC7\x02M\xC9\x02\x02\xCB\x02\x02" + - "\xCD\x02\x02\xCF\x02\x02\xD1\x02N\xD3\x02\x02\xD5\x02\x02\xD7\x02O\xD9" + - "\x02P\xDB\x02Q\xDD\x02\x02\xDF\x02\x02\xE1\x02\x02\xE3\x02\x02\xE5\x02" + - "R\xE7\x02\x02\xE9\x02\x02\xEB\x02S\xED\x02T\xEF\x02U\xF1\x02\x02\xF3\x02" + - "\x02\xF5\x02V\xF7\x02W\xF9\x02\x02\xFB\x02X\xFD\x02\x02\xFF\x02\x02\u0101" + - "\x02Y\u0103\x02Z\u0105\x02[\u0107\x02\x02\u0109\x02\x02\u010B\x02\x02" + - "\u010D\x02\x02\u010F\x02\x02\u0111\x02\x02\u0113\x02\x02\u0115\x02\\\u0117" + - "\x02]\u0119\x02^\u011B\x02\x02\u011D\x02\x02\u011F\x02\x02\u0121\x02\x02" + - "\u0123\x02_\u0125\x02`\u0127\x02a\u0129\x02\x02\u012B\x02b\u012D\x02c" + - "\u012F\x02d\u0131\x02e\u0133\x02f\u0135\x02\x02\u0137\x02g\u0139\x02h" + - "\u013B\x02i\u013D\x02j\u013F\x02k\u0141\x02\x02\u0143\x02\x02\u0145\x02" + - "\x02\u0147\x02\x02\u0149\x02\x02\u014B\x02\x02\u014D\x02\x02\u014F\x02" + - "\x02\u0151\x02\x02\u0153\x02\x02\u0155\x02\x02\u0157\x02\x02\u0159\x02" + - "\x02\u015B\x02\x02\u015D\x02\x02\u015F\x02\x02\u0161\x02\x02\u0163\x02" + - "\x02\u0165\x02\x02\u0167\x02\x02\u0169\x02\x02\u016B\x02\x02\u016D\x02" + - "\x02\u016F\x02\x02\u0171\x02\x02\u0173\x02\x02\r\x02\x03\x04\x05\x06\x07" + - "\b\t\n\v\f(\b\x02\v\f\x0F\x0F\"\"11]]__\x04\x02\f\f\x0F\x0F\x05\x02\v" + - "\f\x0F\x0F\"\"\x03\x022;\x04\x02C\\c|\x07\x02$$^^ppttvv\x06\x02\f\f\x0F" + - "\x0F$$^^\x04\x02GGgg\x04\x02--//\x03\x02bb\f\x02\v\f\x0F\x0F\"\"..11?" + - "?]]__bb~~\x04\x02,,11\r\x02\v\f\x0F\x0F\"\"$%..11<<>>@A^^~~\x04\x02CC" + - "cc\x04\x02DDdd\x04\x02EEee\x04\x02FFff\x04\x02HHhh\x04\x02IIii\x04\x02" + - "JJjj\x04\x02KKkk\x04\x02LLll\x04\x02MMmm\x04\x02NNnn\x04\x02OOoo\x04\x02" + - "PPpp\x04\x02QQqq\x04\x02RRrr\x04\x02SSss\x04\x02TTtt\x04\x02UUuu\x04\x02" + - "VVvv\x04\x02WWww\x04\x02XXxx\x04\x02YYyy\x04\x02ZZzz\x04\x02[[{{\x04\x02" + - "\\\\||\x02\u04F4\x02\r\x03\x02\x02\x02\x02\x0F\x03\x02\x02\x02\x02\x11" + - "\x03\x02\x02\x02\x02\x13\x03\x02\x02\x02\x02\x15\x03\x02\x02\x02\x02\x17" + - "\x03\x02\x02\x02\x02\x19\x03\x02\x02\x02\x02\x1B\x03\x02\x02\x02\x02\x1D" + - "\x03\x02\x02\x02\x02\x1F\x03\x02\x02\x02\x02!\x03\x02\x02\x02\x02#\x03" + - "\x02\x02\x02\x02%\x03\x02\x02\x02\x02\'\x03\x02\x02\x02\x02)\x03\x02\x02" + - "\x02\x02+\x03\x02\x02\x02\x02-\x03\x02\x02\x02\x02/\x03\x02\x02\x02\x02" + - "1\x03\x02\x02\x02\x023\x03\x02\x02\x02\x025\x03\x02\x02\x02\x027\x03\x02" + - "\x02\x02\x039\x03\x02\x02\x02\x03;\x03\x02\x02\x02\x03=\x03\x02\x02\x02" + - "\x03?\x03\x02\x02\x02\x03A\x03\x02\x02\x02\x04C\x03\x02\x02\x02\x04Y\x03" + - "\x02\x02\x02\x04[\x03\x02\x02\x02\x04]\x03\x02\x02\x02\x04_\x03\x02\x02" + - "\x02\x04a\x03\x02\x02\x02\x04c\x03\x02\x02\x02\x04e\x03\x02\x02\x02\x04" + - "g\x03\x02\x02\x02\x04i\x03\x02\x02\x02\x04k\x03\x02\x02\x02\x04m\x03\x02" + - "\x02\x02\x04o\x03\x02\x02\x02\x04q\x03\x02\x02\x02\x04s\x03\x02\x02\x02" + - "\x04u\x03\x02\x02\x02\x04w\x03\x02\x02\x02\x04y\x03\x02\x02\x02\x04{\x03" + - "\x02\x02\x02\x04}\x03\x02\x02\x02\x04\x7F\x03\x02\x02\x02\x04\x81\x03" + - "\x02\x02\x02\x04\x83\x03\x02\x02\x02\x04\x85\x03\x02\x02\x02\x04\x87\x03" + - "\x02\x02\x02\x04\x89\x03\x02\x02\x02\x04\x8B\x03\x02\x02\x02\x04\x8D\x03" + - "\x02\x02\x02\x04\x8F\x03\x02\x02\x02\x04\x91\x03\x02\x02\x02\x04\x93\x03" + - "\x02\x02\x02\x04\x95\x03\x02\x02\x02\x04\x97\x03\x02\x02\x02\x04\x99\x03" + - "\x02\x02\x02\x04\x9B\x03\x02\x02\x02\x04\x9D\x03\x02\x02\x02\x04\x9F\x03" + - "\x02\x02\x02\x04\xA1\x03\x02\x02\x02\x04\xA3\x03\x02\x02\x02\x04\xA5\x03" + - "\x02\x02\x02\x04\xA7\x03\x02\x02\x02\x04\xA9\x03\x02\x02\x02\x04\xAB\x03" + - "\x02\x02\x02\x04\xAD\x03\x02\x02\x02\x04\xAF\x03\x02\x02\x02\x05\xB1\x03" + - "\x02\x02\x02\x05\xB3\x03\x02\x02\x02\x05\xB5\x03\x02\x02\x02\x05\xB7\x03" + - "\x02\x02\x02\x05\xB9\x03\x02\x02\x02\x05\xBB\x03\x02\x02\x02\x05\xBF\x03" + - "\x02\x02\x02\x05\xC1\x03\x02\x02\x02\x05\xC3\x03\x02\x02\x02\x05\xC5\x03" + - "\x02\x02\x02\x05\xC7\x03\x02\x02\x02\x06\xC9\x03\x02\x02\x02\x06\xCB\x03" + - "\x02\x02\x02\x06\xCD\x03\x02\x02\x02\x06\xD1\x03\x02\x02\x02\x06\xD3\x03" + - "\x02\x02\x02\x06\xD5\x03\x02\x02\x02\x06\xD7\x03\x02\x02\x02\x06\xD9\x03" + - "\x02\x02\x02\x06\xDB\x03\x02\x02\x02\x07\xDD\x03\x02\x02\x02\x07\xDF\x03" + - "\x02\x02\x02\x07\xE1\x03\x02\x02\x02\x07\xE3\x03\x02\x02\x02\x07\xE5\x03" + - "\x02\x02\x02\x07\xE7\x03\x02\x02\x02\x07\xE9\x03\x02\x02\x02\x07\xEB\x03" + - "\x02\x02\x02\x07\xED\x03\x02\x02\x02\x07\xEF\x03\x02\x02\x02\b\xF1\x03" + - "\x02\x02\x02\b\xF3\x03\x02\x02\x02\b\xF5\x03\x02\x02\x02\b\xF7\x03\x02" + - "\x02\x02\b\xFB\x03\x02\x02\x02\b\xFD\x03\x02\x02\x02\b\xFF\x03\x02\x02" + - "\x02\b\u0101\x03\x02\x02\x02\b\u0103\x03\x02\x02\x02\b\u0105\x03\x02\x02" + - "\x02\t\u0107\x03\x02\x02\x02\t\u0109\x03\x02\x02\x02\t\u010B\x03\x02\x02" + - "\x02\t\u010D\x03\x02\x02\x02\t\u010F\x03\x02\x02\x02\t\u0111\x03\x02\x02" + - "\x02\t\u0113\x03\x02\x02\x02\t\u0115\x03\x02\x02\x02\t\u0117\x03\x02\x02" + - "\x02\t\u0119\x03\x02\x02\x02\n\u011B\x03\x02\x02\x02\n\u011D\x03\x02\x02" + - "\x02\n\u011F\x03\x02\x02\x02\n\u0121\x03\x02\x02\x02\n\u0123\x03\x02\x02" + - "\x02\n\u0125\x03\x02\x02\x02\n\u0127\x03\x02\x02\x02\v\u0129\x03\x02\x02" + - "\x02\v\u012B\x03\x02\x02\x02\v\u012D\x03\x02\x02\x02\v\u012F\x03\x02\x02" + - "\x02\v\u0131\x03\x02\x02\x02\v\u0133\x03\x02\x02\x02\f\u0135\x03\x02\x02" + - "\x02\f\u0137\x03\x02\x02\x02\f\u0139\x03\x02\x02\x02\f\u013B\x03\x02\x02" + - "\x02\f\u013D\x03\x02\x02\x02\f\u013F\x03\x02\x02\x02\r\u0175\x03\x02\x02" + - "\x02\x0F\u017F\x03\x02\x02\x02\x11\u0186\x03\x02\x02\x02\x13\u018F\x03" + - "\x02\x02\x02\x15\u0196\x03\x02\x02\x02\x17\u01A0\x03\x02\x02\x02\x19\u01A7" + - "\x03\x02\x02\x02\x1B\u01AE\x03\x02\x02\x02\x1D\u01BC\x03\x02\x02\x02\x1F" + - "\u01C3\x03\x02\x02\x02!\u01CB\x03\x02\x02\x02#\u01D7\x03\x02\x02\x02%" + - "\u01E1\x03\x02\x02\x02\'\u01EA\x03\x02\x02\x02)\u01F0\x03\x02\x02\x02" + - "+\u01F7\x03\x02\x02\x02-\u01FE\x03\x02\x02\x02/\u0206\x03\x02\x02\x02" + - "1\u020F\x03\x02\x02\x023\u0215\x03\x02\x02\x025\u0226\x03\x02\x02\x02" + - "7\u0236\x03\x02\x02\x029\u023C\x03\x02\x02\x02;\u0241\x03\x02\x02\x02" + - "=\u0246\x03\x02\x02\x02?\u024A\x03\x02\x02\x02A\u024E\x03\x02\x02\x02" + - "C\u0252\x03\x02\x02\x02E\u0256\x03\x02\x02\x02G\u0258\x03\x02\x02\x02" + - "I\u025A\x03\x02\x02\x02K\u025D\x03\x02\x02\x02M\u025F\x03\x02\x02\x02" + - "O\u0268\x03\x02\x02\x02Q\u026A\x03\x02\x02\x02S\u026F\x03\x02\x02\x02" + - "U\u0271\x03\x02\x02\x02W\u0276\x03\x02\x02\x02Y\u0295\x03\x02\x02\x02" + - "[\u0298\x03\x02\x02\x02]\u02C6\x03\x02\x02\x02_\u02C8\x03\x02\x02\x02" + - "a\u02CB\x03\x02\x02\x02c\u02CF\x03\x02\x02\x02e\u02D3\x03\x02\x02\x02" + - "g\u02D5\x03\x02\x02\x02i\u02D7\x03\x02\x02\x02k\u02DC\x03\x02\x02\x02" + - "m\u02DE\x03\x02\x02\x02o\u02E4\x03\x02\x02\x02q\u02EA\x03\x02\x02\x02" + - "s\u02EF\x03\x02\x02\x02u\u02F1\x03\x02\x02\x02w\u02F4\x03\x02\x02\x02" + - "y\u02F7\x03\x02\x02\x02{\u02FC\x03\x02\x02\x02}\u0300\x03\x02\x02\x02" + - "\x7F\u0305\x03\x02\x02\x02\x81\u030B\x03\x02\x02\x02\x83\u030E\x03\x02" + - "\x02\x02\x85\u0310\x03\x02\x02\x02\x87\u0316\x03\x02\x02\x02\x89\u0318" + - "\x03\x02\x02\x02\x8B\u031D\x03\x02\x02\x02\x8D\u0320\x03\x02\x02\x02\x8F" + - "\u0323\x03\x02\x02\x02\x91\u0326\x03\x02\x02\x02\x93\u0328\x03\x02\x02" + - "\x02\x95\u032B\x03\x02\x02\x02\x97\u032D\x03\x02\x02\x02\x99\u0330\x03" + - "\x02\x02\x02\x9B\u0332\x03\x02\x02\x02\x9D\u0334\x03\x02\x02\x02\x9F\u0336" + - "\x03\x02\x02\x02\xA1\u0338\x03\x02\x02\x02\xA3\u033A\x03\x02\x02\x02\xA5" + - "\u033F\x03\x02\x02\x02\xA7\u0354\x03\x02\x02\x02\xA9\u0356\x03\x02\x02" + - "\x02\xAB\u035E\x03\x02\x02\x02\xAD\u0362\x03\x02\x02\x02\xAF\u0366\x03" + - "\x02\x02\x02\xB1\u036A\x03\x02\x02\x02\xB3\u036F\x03\x02\x02\x02\xB5\u0373" + - "\x03\x02\x02\x02\xB7\u0377\x03\x02\x02\x02\xB9\u037B\x03\x02\x02\x02\xBB" + - "\u037F\x03\x02\x02\x02\xBD\u038B\x03\x02\x02\x02\xBF\u038E\x03\x02\x02" + - "\x02\xC1\u0392\x03\x02\x02\x02\xC3\u0396\x03\x02\x02\x02\xC5\u039A\x03" + - "\x02\x02\x02\xC7\u039E\x03\x02\x02\x02\xC9\u03A2\x03\x02\x02\x02\xCB\u03A7" + - "\x03\x02\x02\x02\xCD\u03AB\x03\x02\x02\x02\xCF\u03B3\x03\x02\x02\x02\xD1" + - "\u03C8\x03\x02\x02\x02\xD3\u03CA\x03\x02\x02\x02\xD5\u03CE\x03\x02\x02" + - "\x02\xD7\u03D2\x03\x02\x02\x02\xD9\u03D6\x03\x02\x02\x02\xDB\u03DA\x03" + - "\x02\x02\x02\xDD\u03DE\x03\x02\x02\x02\xDF\u03E3\x03\x02\x02\x02\xE1\u03E7" + - "\x03\x02\x02\x02\xE3\u03EB\x03\x02\x02\x02\xE5\u03EF\x03\x02\x02\x02\xE7" + - "\u03F2\x03\x02\x02\x02\xE9\u03F6\x03\x02\x02\x02\xEB\u03FA\x03\x02\x02" + - "\x02\xED\u03FE\x03\x02\x02\x02\xEF\u0402\x03\x02\x02\x02\xF1\u0406\x03" + - "\x02\x02\x02\xF3\u040B\x03\x02\x02\x02\xF5\u0410\x03\x02\x02\x02\xF7\u0415" + - "\x03\x02\x02\x02\xF9\u041C\x03"; + "\t\xB4\x03\x02\x03\x02\x03\x02\x03\x02\x03\x02\x03\x02\x03\x02\x03\x02" + + "\x03\x02\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03" + + "\x03\x04\x03\x04\x03\x04\x03\x04\x03\x04\x03\x04\x03\x04\x03\x04\x03\x04" + + "\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x06\x03\x06" + + "\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x07" + + "\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\b\x03\b\x03\b\x03" + + "\b\x03\b\x03\b\x03\b\x03\t\x03\t\x03\t\x03\t\x03\t\x03\t\x03\t\x03\t\x03" + + "\t\x03\t\x03\t\x03\t\x03\t\x03\t\x03\n\x03\n\x03\n\x03\n\x03\n\x03\n\x03" + + "\n\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\f\x03\f\x03\f\x03" + + "\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x03\r\x03\r\x03\r\x03" + + "\r\x03\r\x03\r\x03\r\x03\r\x03\r\x03\x0E\x03\x0E\x03\x0E\x03\x0E\x03\x0E" + + "\x03\x0E\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x10" + + "\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10\x03\x10\x03\x11\x03\x11\x03\x11" + + "\x03\x11\x03\x11\x03\x11\x03\x11\x03\x11\x03\x12\x03\x12\x03\x12\x03\x12" + + "\x03\x12\x03\x12\x03\x12\x03\x12\x03\x13\x06\x13\u0204\n\x13\r\x13\x0E" + + "\x13\u0205\x03\x13\x03\x13\x03\x14\x03\x14\x03\x14\x03\x14\x07\x14\u020E" + + "\n\x14\f\x14\x0E\x14\u0211\v\x14\x03\x14\x05\x14\u0214\n\x14\x03\x14\x05" + + "\x14\u0217\n\x14\x03\x14\x03\x14\x03\x15\x03\x15\x03\x15\x03\x15\x03\x15" + + "\x07\x15\u0220\n\x15\f\x15\x0E\x15\u0223\v\x15\x03\x15\x03\x15\x03\x15" + + "\x03\x15\x03\x15\x03\x16\x06\x16\u022B\n\x16\r\x16\x0E\x16\u022C\x03\x16" + + "\x03\x16\x03\x17\x03\x17\x03\x17\x03\x17\x03\x17\x03\x18\x03\x18\x03\x18" + + "\x03\x18\x03\x18\x03\x19\x03\x19\x03\x19\x03\x19\x03\x1A\x03\x1A\x03\x1A" + + "\x03\x1A\x03\x1B\x03\x1B\x03\x1B\x03\x1B\x03\x1C\x03\x1C\x03\x1C\x03\x1C" + + "\x03\x1D\x03\x1D\x03\x1E\x03\x1E\x03\x1F\x03\x1F\x03\x1F\x03 \x03 \x03" + + "!\x03!\x05!\u0256\n!\x03!\x06!\u0259\n!\r!\x0E!\u025A\x03\"\x03\"\x03" + + "#\x03#\x03$\x03$\x03$\x05$\u0264\n$\x03%\x03%\x03&\x03&\x03&\x05&\u026B" + + "\n&\x03\'\x03\'\x03\'\x07\'\u0270\n\'\f\'\x0E\'\u0273\v\'\x03\'\x03\'" + + "\x03\'\x03\'\x03\'\x03\'\x07\'\u027B\n\'\f\'\x0E\'\u027E\v\'\x03\'\x03" + + "\'\x03\'\x03\'\x03\'\x05\'\u0285\n\'\x03\'\x05\'\u0288\n\'\x05\'\u028A" + + "\n\'\x03(\x06(\u028D\n(\r(\x0E(\u028E\x03)\x06)\u0292\n)\r)\x0E)\u0293" + + "\x03)\x03)\x07)\u0298\n)\f)\x0E)\u029B\v)\x03)\x03)\x06)\u029F\n)\r)\x0E" + + ")\u02A0\x03)\x06)\u02A4\n)\r)\x0E)\u02A5\x03)\x03)\x07)\u02AA\n)\f)\x0E" + + ")\u02AD\v)\x05)\u02AF\n)\x03)\x03)\x03)\x03)\x06)\u02B5\n)\r)\x0E)\u02B6" + + "\x03)\x03)\x05)\u02BB\n)\x03*\x03*\x03*\x03+\x03+\x03+\x03+\x03,\x03," + + "\x03,\x03,\x03-\x03-\x03.\x03.\x03/\x03/\x03/\x03/\x03/\x030\x030\x03" + + "1\x031\x031\x031\x031\x031\x032\x032\x032\x032\x032\x032\x033\x033\x03" + + "3\x033\x033\x034\x034\x035\x035\x035\x036\x036\x036\x037\x037\x037\x03" + + "7\x037\x038\x038\x038\x038\x039\x039\x039\x039\x039\x03:\x03:\x03:\x03" + + ":\x03:\x03:\x03;\x03;\x03;\x03<\x03<\x03=\x03=\x03=\x03=\x03=\x03=\x03" + + ">\x03>\x03?\x03?\x03?\x03?\x03?\x03@\x03@\x03@\x03A\x03A\x03A\x03B\x03" + + "B\x03B\x03C\x03C\x03D\x03D\x03D\x03E\x03E\x03F\x03F\x03F\x03G\x03G\x03" + + "H\x03H\x03I\x03I\x03J\x03J\x03K\x03K\x03L\x03L\x03L\x03L\x03L\x03M\x03" + + "M\x03M\x03M\x03M\x03N\x03N\x07N\u033B\nN\fN\x0EN\u033E\vN\x03N\x03N\x05" + + "N\u0342\nN\x03N\x06N\u0345\nN\rN\x0EN\u0346\x05N\u0349\nN\x03O\x03O\x06" + + "O\u034D\nO\rO\x0EO\u034E\x03O\x03O\x03P\x03P\x03P\x03P\x03Q\x03Q\x03Q" + + "\x03Q\x03R\x03R\x03R\x03R\x03S\x03S\x03S\x03S\x03S\x03T\x03T\x03T\x03" + + "T\x03U\x03U\x03U\x03U\x03V\x03V\x03V\x03V\x03W\x03W\x03W\x03W\x03X\x03" + + "X\x03X\x03X\x03X\x03X\x03X\x03X\x03X\x03Y\x03Y\x03Y\x05Y\u0380\nY\x03" + + "Z\x06Z\u0383\nZ\rZ\x0EZ\u0384\x03[\x03[\x03[\x03[\x03\\\x03\\\x03\\\x03" + + "\\\x03]\x03]\x03]\x03]\x03^\x03^\x03^\x03^\x03_\x03_\x03_\x03_\x03_\x03" + + "`\x03`\x03`\x03`\x03a\x03a\x03a\x03a\x03b\x03b\x03b\x03b\x05b\u03A8\n" + + "b\x03c\x03c\x05c\u03AC\nc\x03c\x07c\u03AF\nc\fc\x0Ec\u03B2\vc\x03c\x03" + + "c\x05c\u03B6\nc\x03c\x06c\u03B9\nc\rc\x0Ec\u03BA\x05c\u03BD\nc\x03d\x03" + + "d\x03d\x03d\x03e\x03e\x03e\x03e\x03f\x03f\x03f\x03f\x03g\x03g\x03g\x03" + + "g\x03h\x03h\x03h\x03h\x03i\x03i\x03i\x03i\x03i\x03j\x03j\x03j\x03j\x03" + + "k\x03k\x03k\x03k\x03l\x03l\x03l\x03l\x03m\x03m\x03m\x03n\x03n\x03n\x03" + + "n\x03o\x03o\x03o\x03o\x03p\x03p\x03p\x03p\x03q\x03q\x03q\x03q\x03r\x03" + + "r\x03r\x03r\x03s\x03s\x03s\x03s\x03s\x03t\x03t\x03t\x03t\x03t\x03u\x03" + + "u\x03u\x03u\x03u\x03v\x03v\x03v\x03v\x03v\x03v\x03v\x03w\x03w\x03x\x06" + + "x\u0414\nx\rx\x0Ex\u0415\x03x\x03x\x05x\u041A\nx\x03x\x06x\u041D\nx\r" + + "x\x0Ex\u041E\x03y\x03y\x03y\x03y\x03z\x03z\x03z\x03z\x03{\x03{\x03{\x03" + + "{\x03|\x03|\x03|\x03|\x03}\x03}\x03}\x03}\x03~\x03~\x03~\x03~\x03~\x03" + + "~\x03\x7F\x03\x7F\x03\x7F\x03\x7F\x03\x80\x03\x80\x03\x80\x03\x80\x03" + + "\x81\x03\x81\x03\x81\x03\x81\x03\x82\x03\x82\x03\x82\x03\x82\x03\x83\x03" + + "\x83\x03\x83\x03\x83\x03\x84\x03\x84\x03\x84\x03\x84\x03\x85\x03\x85\x03" + + "\x85\x03\x85\x03\x86\x03\x86\x03\x86\x03\x86\x03\x87\x03\x87\x03\x87\x03" + + "\x87\x03\x88\x03\x88\x03\x88\x03\x88\x03\x88\x03\x89\x03\x89\x03\x89\x03" + + "\x89\x03\x8A\x03\x8A\x03\x8A\x03\x8A\x03\x8B\x03\x8B\x03\x8B\x03\x8B\x03" + + "\x8C\x03\x8C\x03\x8C\x03\x8C\x03\x8D\x03\x8D\x03\x8D\x03\x8D\x03\x8E\x03" + + "\x8E\x03\x8E\x03\x8E\x03\x8F\x03\x8F\x03\x8F\x03\x8F\x03\x8F\x03\x90\x03" + + "\x90\x03\x90\x03\x90\x03\x90\x03\x91\x03\x91\x03\x91\x03\x91\x03\x91\x03" + + "\x91\x03\x91\x03\x91\x03\x91\x03\x91\x03\x92\x03\x92\x03\x92\x03\x92\x03" + + "\x93\x03\x93\x03\x93\x03\x93\x03\x94\x03\x94\x03\x94\x03\x94\x03\x95\x03" + + "\x95\x03\x95\x03\x95\x03\x95\x03\x96\x03\x96\x03\x97\x03\x97\x03\x97\x03" + + "\x97\x03\x97\x06\x97\u04A8\n\x97\r\x97\x0E\x97\u04A9\x03\x98\x03\x98\x03" + + "\x98\x03\x98\x03\x99\x03\x99\x03\x99\x03\x99\x03\x9A\x03\x9A\x03\x9A\x03" + + "\x9A\x03\x9B\x03\x9B\x03\x9C\x03\x9C\x03\x9D\x03\x9D\x03\x9E\x03\x9E\x03" + + "\x9F\x03\x9F\x03\xA0\x03\xA0\x03\xA1\x03\xA1\x03\xA2\x03\xA2\x03\xA3\x03" + + "\xA3\x03\xA4\x03\xA4\x03\xA5\x03\xA5\x03\xA6\x03\xA6\x03\xA7\x03\xA7\x03" + + "\xA8\x03\xA8\x03\xA9\x03\xA9\x03\xAA\x03\xAA\x03\xAB\x03\xAB\x03\xAC\x03" + + "\xAC\x03\xAD\x03\xAD\x03\xAE\x03\xAE\x03\xAF\x03\xAF\x03\xB0\x03\xB0\x03" + + "\xB1\x03\xB1\x03\xB2\x03\xB2\x03\xB3\x03\xB3\x03\xB4\x03\xB4\x04\u0221" + + "\u027C\x02\x02\xB5\r\x02\x03\x0F\x02\x04\x11\x02\x05\x13\x02\x06\x15\x02" + + "\x07\x17\x02\b\x19\x02\t\x1B\x02\n\x1D\x02\v\x1F\x02\f!\x02\r#\x02\x0E" + + "%\x02\x0F\'\x02\x10)\x02\x11+\x02\x12-\x02\x13/\x02\x141\x02\x153\x02" + + "\x165\x02\x177\x02\x029\x02\x02;\x02\x18=\x02\x19?\x02\x1AA\x02\x1BC\x02" + + "\x02E\x02\x02G\x02\x02I\x02\x02K\x02\x02M\x02\x02O\x02\x02Q\x02\x02S\x02" + + "\x02U\x02\x02W\x02\x1CY\x02\x1D[\x02\x1E]\x02\x1F_\x02 a\x02!c\x02\"e" + + "\x02#g\x02$i\x02%k\x02&m\x02\'o\x02(q\x02)s\x02*u\x02+w\x02,y\x02-{\x02" + + ".}\x02/\x7F\x020\x81\x021\x83\x022\x85\x023\x87\x024\x89\x025\x8B\x02" + + "6\x8D\x027\x8F\x028\x91\x029\x93\x02:\x95\x02;\x97\x02<\x99\x02=\x9B\x02" + + ">\x9D\x02?\x9F\x02@\xA1\x02A\xA3\x02B\xA5\x02C\xA7\x02D\xA9\x02E\xAB\x02" + + "F\xAD\x02G\xAF\x02\x02\xB1\x02\x02\xB3\x02\x02\xB5\x02\x02\xB7\x02\x02" + + "\xB9\x02H\xBB\x02\x02\xBD\x02I\xBF\x02\x02\xC1\x02J\xC3\x02K\xC5\x02L" + + "\xC7\x02\x02\xC9\x02\x02\xCB\x02\x02\xCD\x02\x02\xCF\x02M\xD1\x02\x02" + + "\xD3\x02\x02\xD5\x02N\xD7\x02O\xD9\x02P\xDB\x02\x02\xDD\x02\x02\xDF\x02" + + "\x02\xE1\x02\x02\xE3\x02Q\xE5\x02\x02\xE7\x02\x02\xE9\x02R\xEB\x02S\xED" + + "\x02T\xEF\x02\x02\xF1\x02\x02\xF3\x02U\xF5\x02V\xF7\x02\x02\xF9\x02W\xFB" + + "\x02\x02\xFD\x02\x02\xFF\x02X\u0101\x02Y\u0103\x02Z\u0105\x02\x02\u0107" + + "\x02\x02\u0109\x02\x02\u010B\x02\x02\u010D\x02\x02\u010F\x02\x02\u0111" + + "\x02\x02\u0113\x02[\u0115\x02\\\u0117\x02]\u0119\x02\x02\u011B\x02\x02" + + "\u011D\x02\x02\u011F\x02\x02\u0121\x02^\u0123\x02_\u0125\x02`\u0127\x02" + + "\x02\u0129\x02a\u012B\x02b\u012D\x02c\u012F\x02d\u0131\x02e\u0133\x02" + + "\x02\u0135\x02f\u0137\x02g\u0139\x02h\u013B\x02i\u013D\x02j\u013F\x02" + + "\x02\u0141\x02\x02\u0143\x02\x02\u0145\x02\x02\u0147\x02\x02\u0149\x02" + + "\x02\u014B\x02\x02\u014D\x02\x02\u014F\x02\x02\u0151\x02\x02\u0153\x02" + + "\x02\u0155\x02\x02\u0157\x02\x02\u0159\x02\x02\u015B\x02\x02\u015D\x02" + + "\x02\u015F\x02\x02\u0161\x02\x02\u0163\x02\x02\u0165\x02\x02\u0167\x02" + + "\x02\u0169\x02\x02\u016B\x02\x02\u016D\x02\x02\u016F\x02\x02\u0171\x02" + + "\x02\r\x02\x03\x04\x05\x06\x07\b\t\n\v\f(\b\x02\v\f\x0F\x0F\"\"11]]__" + + "\x04\x02\f\f\x0F\x0F\x05\x02\v\f\x0F\x0F\"\"\x03\x022;\x04\x02C\\c|\x07" + + "\x02$$^^ppttvv\x06\x02\f\f\x0F\x0F$$^^\x04\x02GGgg\x04\x02--//\x03\x02" + + "bb\f\x02\v\f\x0F\x0F\"\"..11??]]__bb~~\x04\x02,,11\r\x02\v\f\x0F\x0F\"" + + "\"$%..11<<>>@A^^~~\x04\x02CCcc\x04\x02DDdd\x04\x02EEee\x04\x02FFff\x04" + + "\x02HHhh\x04\x02IIii\x04\x02JJjj\x04\x02KKkk\x04\x02LLll\x04\x02MMmm\x04" + + "\x02NNnn\x04\x02OOoo\x04\x02PPpp\x04\x02QQqq\x04\x02RRrr\x04\x02SSss\x04" + + "\x02TTtt\x04\x02UUuu\x04\x02VVvv\x04\x02WWww\x04\x02XXxx\x04\x02YYyy\x04" + + "\x02ZZzz\x04\x02[[{{\x04\x02\\\\||\x02\u04ED\x02\r\x03\x02\x02\x02\x02" + + "\x0F\x03\x02\x02\x02\x02\x11\x03\x02\x02\x02\x02\x13\x03\x02\x02\x02\x02" + + "\x15\x03\x02\x02\x02\x02\x17\x03\x02\x02\x02\x02\x19\x03\x02\x02\x02\x02" + + "\x1B\x03\x02\x02\x02\x02\x1D\x03\x02\x02\x02\x02\x1F\x03\x02\x02\x02\x02" + + "!\x03\x02\x02\x02\x02#\x03\x02\x02\x02\x02%\x03\x02\x02\x02\x02\'\x03" + + "\x02\x02\x02\x02)\x03\x02\x02\x02\x02+\x03\x02\x02\x02\x02-\x03\x02\x02" + + "\x02\x02/\x03\x02\x02\x02\x021\x03\x02\x02\x02\x023\x03\x02\x02\x02\x02" + + "5\x03\x02\x02\x02\x037\x03\x02\x02\x02\x039\x03\x02\x02\x02\x03;\x03\x02" + + "\x02\x02\x03=\x03\x02\x02\x02\x03?\x03\x02\x02\x02\x04A\x03\x02\x02\x02" + + "\x04W\x03\x02\x02\x02\x04Y\x03\x02\x02\x02\x04[\x03\x02\x02\x02\x04]\x03" + + "\x02\x02\x02\x04_\x03\x02\x02\x02\x04a\x03\x02\x02\x02\x04c\x03\x02\x02" + + "\x02\x04e\x03\x02\x02\x02\x04g\x03\x02\x02\x02\x04i\x03\x02\x02\x02\x04" + + "k\x03\x02\x02\x02\x04m\x03\x02\x02\x02\x04o\x03\x02\x02\x02\x04q\x03\x02" + + "\x02\x02\x04s\x03\x02\x02\x02\x04u\x03\x02\x02\x02\x04w\x03\x02\x02\x02" + + "\x04y\x03\x02\x02\x02\x04{\x03\x02\x02\x02\x04}\x03\x02\x02\x02\x04\x7F" + + "\x03\x02\x02\x02\x04\x81\x03\x02\x02\x02\x04\x83\x03\x02\x02\x02\x04\x85" + + "\x03\x02\x02\x02\x04\x87\x03\x02\x02\x02\x04\x89\x03\x02\x02\x02\x04\x8B" + + "\x03\x02\x02\x02\x04\x8D\x03\x02\x02\x02\x04\x8F\x03\x02\x02\x02\x04\x91" + + "\x03\x02\x02\x02\x04\x93\x03\x02\x02\x02\x04\x95\x03\x02\x02\x02\x04\x97" + + "\x03\x02\x02\x02\x04\x99\x03\x02\x02\x02\x04\x9B\x03\x02\x02\x02\x04\x9D" + + "\x03\x02\x02\x02\x04\x9F\x03\x02\x02\x02\x04\xA1\x03\x02\x02\x02\x04\xA3" + + "\x03\x02\x02\x02\x04\xA5\x03\x02\x02\x02\x04\xA7\x03\x02\x02\x02\x04\xA9" + + "\x03\x02\x02\x02\x04\xAB\x03\x02\x02\x02\x04\xAD\x03\x02\x02\x02\x05\xAF" + + "\x03\x02\x02\x02\x05\xB1\x03\x02\x02\x02\x05\xB3\x03\x02\x02\x02\x05\xB5" + + "\x03\x02\x02\x02\x05\xB7\x03\x02\x02\x02\x05\xB9\x03\x02\x02\x02\x05\xBD" + + "\x03\x02\x02\x02\x05\xBF\x03\x02\x02\x02\x05\xC1\x03\x02\x02\x02\x05\xC3" + + "\x03\x02\x02\x02\x05\xC5\x03\x02\x02\x02\x06\xC7\x03\x02\x02\x02\x06\xC9" + + "\x03\x02\x02\x02\x06\xCB\x03\x02\x02\x02\x06\xCF\x03\x02\x02\x02\x06\xD1" + + "\x03\x02\x02\x02\x06\xD3\x03\x02\x02\x02\x06\xD5\x03\x02\x02\x02\x06\xD7" + + "\x03\x02\x02\x02\x06\xD9\x03\x02\x02\x02\x07\xDB\x03\x02\x02\x02\x07\xDD" + + "\x03\x02\x02\x02\x07\xDF\x03\x02\x02\x02\x07\xE1\x03\x02\x02\x02\x07\xE3" + + "\x03\x02\x02\x02\x07\xE5\x03\x02\x02\x02\x07\xE7\x03\x02\x02\x02\x07\xE9" + + "\x03\x02\x02\x02\x07\xEB\x03\x02\x02\x02\x07\xED\x03\x02\x02\x02\b\xEF" + + "\x03\x02\x02\x02\b\xF1\x03\x02\x02\x02\b\xF3\x03\x02\x02\x02\b\xF5\x03" + + "\x02\x02\x02\b\xF9\x03\x02\x02\x02\b\xFB\x03\x02\x02\x02\b\xFD\x03\x02" + + "\x02\x02\b\xFF\x03\x02\x02\x02\b\u0101\x03\x02\x02\x02\b\u0103\x03\x02" + + "\x02\x02\t\u0105\x03\x02\x02\x02\t\u0107\x03\x02\x02\x02\t\u0109\x03\x02" + + "\x02\x02\t\u010B\x03\x02\x02\x02\t\u010D\x03\x02\x02\x02\t\u010F\x03\x02" + + "\x02\x02\t\u0111\x03\x02\x02\x02\t\u0113\x03\x02\x02\x02\t\u0115\x03\x02" + + "\x02\x02\t\u0117\x03\x02\x02\x02\n\u0119\x03\x02\x02\x02\n\u011B\x03\x02" + + "\x02\x02\n\u011D\x03\x02\x02\x02\n\u011F\x03\x02\x02\x02\n\u0121\x03\x02" + + "\x02\x02\n\u0123\x03\x02\x02\x02\n\u0125\x03\x02\x02\x02\v\u0127\x03\x02" + + "\x02\x02\v\u0129\x03\x02\x02\x02\v\u012B\x03\x02\x02\x02\v\u012D\x03\x02" + + "\x02\x02\v\u012F\x03\x02\x02\x02\v\u0131\x03\x02\x02\x02\f\u0133\x03\x02" + + "\x02\x02\f\u0135\x03\x02\x02\x02\f\u0137\x03\x02\x02\x02\f\u0139\x03\x02" + + "\x02\x02\f\u013B\x03\x02\x02\x02\f\u013D\x03\x02\x02\x02\r\u0173\x03\x02" + + "\x02\x02\x0F\u017D\x03\x02\x02\x02\x11\u0184\x03\x02\x02\x02\x13\u018D" + + "\x03\x02\x02\x02\x15\u0194\x03\x02\x02\x02\x17\u019E\x03\x02\x02\x02\x19" + + "\u01A5\x03\x02\x02\x02\x1B\u01AC\x03\x02\x02\x02\x1D\u01BA\x03\x02\x02" + + "\x02\x1F\u01C1\x03\x02\x02\x02!\u01C9\x03\x02\x02\x02#\u01D5\x03\x02\x02" + + "\x02%\u01DE\x03\x02\x02\x02\'\u01E4\x03\x02\x02\x02)\u01EB\x03\x02\x02" + + "\x02+\u01F2\x03\x02\x02\x02-\u01FA\x03\x02\x02\x02/\u0203\x03\x02\x02" + + "\x021\u0209\x03\x02\x02\x023\u021A\x03\x02\x02\x025\u022A\x03\x02\x02" + + "\x027\u0230\x03\x02\x02\x029\u0235\x03\x02\x02\x02;\u023A\x03\x02\x02" + + "\x02=\u023E\x03\x02\x02\x02?\u0242\x03\x02\x02\x02A\u0246\x03\x02\x02" + + "\x02C\u024A\x03\x02\x02\x02E\u024C\x03\x02\x02\x02G\u024E\x03\x02\x02" + + "\x02I\u0251\x03\x02\x02\x02K\u0253\x03\x02\x02\x02M\u025C\x03\x02\x02" + + "\x02O\u025E\x03\x02\x02\x02Q\u0263\x03\x02\x02\x02S\u0265\x03\x02\x02" + + "\x02U\u026A\x03\x02\x02\x02W\u0289\x03\x02\x02\x02Y\u028C\x03\x02\x02" + + "\x02[\u02BA\x03\x02\x02\x02]\u02BC\x03\x02\x02\x02_\u02BF\x03\x02\x02" + + "\x02a\u02C3\x03\x02\x02\x02c\u02C7\x03\x02\x02\x02e\u02C9\x03\x02\x02" + + "\x02g\u02CB\x03\x02\x02\x02i\u02D0\x03\x02\x02\x02k\u02D2\x03\x02\x02" + + "\x02m\u02D8\x03\x02\x02\x02o\u02DE\x03\x02\x02\x02q\u02E3\x03\x02\x02" + + "\x02s\u02E5\x03\x02\x02\x02u\u02E8\x03\x02\x02\x02w\u02EB\x03\x02\x02" + + "\x02y\u02F0\x03\x02\x02\x02{\u02F4\x03\x02\x02\x02}\u02F9\x03\x02\x02" + + "\x02\x7F\u02FF\x03\x02\x02\x02\x81\u0302\x03\x02\x02\x02\x83\u0304\x03" + + "\x02\x02\x02\x85\u030A\x03\x02\x02\x02\x87\u030C\x03\x02\x02\x02\x89\u0311" + + "\x03\x02\x02\x02\x8B\u0314\x03\x02\x02\x02\x8D\u0317\x03\x02\x02\x02\x8F" + + "\u031A\x03\x02\x02\x02\x91\u031C\x03\x02\x02\x02\x93\u031F\x03\x02\x02" + + "\x02\x95\u0321\x03\x02\x02\x02\x97\u0324\x03\x02\x02\x02\x99\u0326\x03" + + "\x02\x02\x02\x9B\u0328\x03\x02\x02\x02\x9D\u032A\x03\x02\x02\x02\x9F\u032C" + + "\x03\x02\x02\x02\xA1\u032E\x03\x02\x02\x02\xA3\u0333\x03\x02\x02\x02\xA5" + + "\u0348\x03\x02\x02\x02\xA7\u034A\x03\x02\x02\x02\xA9\u0352\x03\x02\x02" + + "\x02\xAB\u0356\x03\x02\x02\x02\xAD\u035A\x03\x02\x02\x02\xAF\u035E\x03" + + "\x02\x02\x02\xB1\u0363\x03\x02\x02\x02\xB3\u0367\x03\x02\x02\x02\xB5\u036B" + + "\x03\x02\x02\x02\xB7\u036F\x03\x02\x02\x02\xB9\u0373\x03\x02\x02\x02\xBB" + + "\u037F\x03\x02\x02\x02\xBD\u0382\x03\x02\x02\x02\xBF\u0386\x03\x02\x02" + + "\x02\xC1\u038A\x03\x02\x02\x02\xC3\u038E\x03\x02\x02\x02\xC5\u0392\x03" + + "\x02\x02\x02\xC7\u0396\x03\x02\x02\x02\xC9\u039B\x03\x02\x02\x02\xCB\u039F" + + "\x03\x02\x02\x02\xCD\u03A7\x03\x02\x02\x02\xCF\u03BC\x03\x02\x02\x02\xD1" + + "\u03BE\x03\x02\x02\x02\xD3\u03C2\x03\x02\x02\x02\xD5\u03C6\x03\x02\x02" + + "\x02\xD7\u03CA\x03\x02\x02\x02\xD9\u03CE\x03\x02\x02\x02\xDB\u03D2\x03" + + "\x02\x02\x02\xDD\u03D7\x03\x02\x02\x02\xDF\u03DB\x03\x02\x02\x02\xE1\u03DF" + + "\x03\x02\x02\x02\xE3\u03E3\x03\x02\x02\x02\xE5\u03E6\x03\x02\x02\x02\xE7" + + "\u03EA\x03\x02\x02\x02\xE9\u03EE\x03\x02\x02\x02\xEB\u03F2\x03\x02\x02" + + "\x02\xED\u03F6\x03\x02\x02\x02\xEF\u03FA\x03\x02\x02\x02\xF1\u03FF\x03" + + "\x02\x02\x02\xF3\u0404\x03\x02\x02\x02\xF5\u0409\x03\x02\x02\x02\xF7\u0410" + + "\x03\x02\x02\x02\xF9\u0419\x03\x02\x02\x02\xFB\u0420\x03\x02\x02\x02\xFD" + + "\u0424\x03\x02\x02\x02\xFF\u0428\x03\x02\x02\x02\u0101\u042C"; private static readonly _serializedATNSegment1: string = - "\x02\x02\x02\xFB\u0420\x03\x02\x02\x02\xFD\u0428\x03\x02\x02\x02\xFF\u042C" + - "\x03\x02\x02\x02\u0101\u0430\x03\x02\x02\x02\u0103\u0434\x03\x02\x02\x02" + - "\u0105\u0438\x03\x02\x02\x02\u0107\u043C\x03\x02\x02\x02\u0109\u0442\x03" + - "\x02\x02\x02\u010B\u0446\x03\x02\x02\x02\u010D\u044A\x03\x02\x02\x02\u010F" + - "\u044E\x03\x02\x02\x02\u0111\u0452\x03\x02\x02\x02\u0113\u0456\x03\x02" + - "\x02\x02\u0115\u045A\x03\x02\x02\x02\u0117\u045E\x03\x02\x02\x02\u0119" + - "\u0462\x03\x02\x02\x02\u011B\u0466\x03\x02\x02\x02\u011D\u046B\x03\x02" + - "\x02\x02\u011F\u046F\x03\x02\x02\x02\u0121\u0473\x03\x02\x02\x02\u0123" + - "\u0477\x03\x02\x02\x02\u0125\u047B\x03\x02\x02\x02\u0127\u047F\x03\x02" + - "\x02\x02\u0129\u0483\x03\x02\x02\x02\u012B\u0488\x03\x02\x02\x02\u012D" + - "\u048D\x03\x02\x02\x02\u012F\u0497\x03\x02\x02\x02\u0131\u049B\x03\x02" + - "\x02\x02\u0133\u049F\x03\x02\x02\x02\u0135\u04A3\x03\x02\x02\x02\u0137" + - "\u04A8\x03\x02\x02\x02\u0139\u04AF\x03\x02\x02\x02\u013B\u04B3\x03\x02" + - "\x02\x02\u013D\u04B7\x03\x02\x02\x02\u013F\u04BB\x03\x02\x02\x02\u0141" + - "\u04BF\x03\x02\x02\x02\u0143\u04C1\x03\x02\x02\x02\u0145\u04C3\x03\x02" + - "\x02\x02\u0147\u04C5\x03\x02\x02\x02\u0149\u04C7\x03\x02\x02\x02\u014B" + - "\u04C9\x03\x02\x02\x02\u014D\u04CB\x03\x02\x02\x02\u014F\u04CD\x03\x02" + - "\x02\x02\u0151\u04CF\x03\x02\x02\x02\u0153\u04D1\x03\x02\x02\x02\u0155" + - "\u04D3\x03\x02\x02\x02\u0157\u04D5\x03\x02\x02\x02\u0159\u04D7\x03\x02" + - "\x02\x02\u015B\u04D9\x03\x02\x02\x02\u015D\u04DB\x03\x02\x02\x02\u015F" + - "\u04DD\x03\x02\x02\x02\u0161\u04DF\x03\x02\x02\x02\u0163\u04E1\x03\x02" + - "\x02\x02\u0165\u04E3\x03\x02\x02\x02\u0167\u04E5\x03\x02\x02\x02\u0169" + - "\u04E7\x03\x02\x02\x02\u016B\u04E9\x03\x02\x02\x02\u016D\u04EB\x03\x02" + - "\x02\x02\u016F\u04ED\x03\x02\x02\x02\u0171\u04EF\x03\x02\x02\x02\u0173" + - "\u04F1\x03\x02\x02\x02\u0175\u0176\x05\u0147\x9F\x02\u0176\u0177\x05\u0151" + - "\xA4\x02\u0177\u0178\x05\u0165\xAE\x02\u0178\u0179\x05\u0165\xAE\x02\u0179" + - "\u017A\x05\u0149\xA0\x02\u017A\u017B\x05\u0145\x9E\x02\u017B\u017C\x05" + - "\u0167\xAF\x02\u017C\u017D\x03\x02\x02\x02\u017D\u017E\b\x02\x02\x02\u017E" + - "\x0E\x03\x02\x02\x02\u017F\u0180\x05\u0147\x9F\x02\u0180\u0181\x05\u0163" + - "\xAD\x02\u0181\u0182\x05\u015D\xAA\x02\u0182\u0183\x05\u015F\xAB\x02\u0183" + - "\u0184\x03\x02\x02\x02\u0184\u0185\b\x03\x03\x02\u0185\x10\x03\x02\x02" + - "\x02\u0186\u0187\x05\u0149\xA0\x02\u0187\u0188\x05\u015B\xA9\x02\u0188" + - "\u0189\x05\u0163\xAD\x02\u0189\u018A\x05\u0151\xA4\x02\u018A\u018B\x05" + - "\u0145\x9E\x02\u018B\u018C\x05\u014F\xA3\x02\u018C\u018D\x03\x02\x02\x02" + - "\u018D\u018E\b\x04\x04\x02\u018E\x12\x03\x02\x02\x02\u018F\u0190\x05\u0149" + - "\xA0\x02\u0190\u0191\x05\u016B\xB1\x02\u0191\u0192\x05\u0141\x9C\x02\u0192" + - "\u0193\x05\u0157\xA7\x02\u0193\u0194\x03\x02\x02\x02\u0194\u0195\b\x05" + - "\x02\x02\u0195\x14\x03\x02\x02\x02\u0196\u0197\x05\u0149\xA0\x02\u0197" + - "\u0198\x05\u016F\xB3\x02\u0198\u0199\x05\u015F\xAB\x02\u0199\u019A\x05" + - "\u0157\xA7\x02\u019A\u019B\x05\u0141\x9C\x02\u019B\u019C\x05\u0151\xA4" + - "\x02\u019C\u019D\x05\u015B\xA9\x02\u019D\u019E\x03\x02\x02\x02\u019E\u019F" + - "\b\x06\x05\x02\u019F\x16\x03\x02\x02\x02\u01A0\u01A1\x05\u014B\xA1\x02" + - "\u01A1\u01A2\x05\u0163\xAD\x02\u01A2\u01A3\x05\u015D\xAA\x02\u01A3\u01A4" + - "\x05\u0159\xA8\x02\u01A4\u01A5\x03\x02\x02\x02\u01A5\u01A6\b\x07\x06\x02" + - "\u01A6\x18\x03\x02\x02\x02\u01A7\u01A8\x05\u014D\xA2\x02\u01A8\u01A9\x05" + - "\u0163\xAD\x02\u01A9\u01AA\x05\u015D\xAA\x02\u01AA\u01AB\x05\u0155\xA6" + - "\x02\u01AB\u01AC\x03\x02\x02\x02\u01AC\u01AD\b\b\x02\x02\u01AD\x1A\x03" + - "\x02\x02\x02\u01AE\u01AF\x05\u0151\xA4\x02\u01AF\u01B0\x05\u015B\xA9\x02" + - "\u01B0\u01B1\x05\u0157\xA7\x02\u01B1\u01B2\x05\u0151\xA4\x02\u01B2\u01B3" + - "\x05\u015B\xA9\x02\u01B3\u01B4\x05\u0149\xA0\x02\u01B4\u01B5\x05\u0165" + - "\xAE\x02\u01B5\u01B6\x05\u0167\xAF\x02\u01B6\u01B7\x05\u0141\x9C\x02\u01B7" + - "\u01B8\x05\u0167\xAF\x02\u01B8\u01B9\x05\u0165\xAE\x02\u01B9\u01BA\x03" + - "\x02\x02\x02\u01BA\u01BB\b\t\x02\x02\u01BB\x1C\x03\x02\x02\x02\u01BC\u01BD" + - "\x05\u0155\xA6\x02\u01BD\u01BE\x05\u0149\xA0\x02\u01BE\u01BF\x05\u0149" + - "\xA0\x02\u01BF\u01C0\x05\u015F\xAB\x02\u01C0\u01C1\x03\x02\x02\x02\u01C1" + - "\u01C2\b\n\x03\x02\u01C2\x1E\x03\x02\x02\x02\u01C3\u01C4\x05\u0157\xA7" + - "\x02\u01C4\u01C5\x05\u0151\xA4\x02\u01C5\u01C6\x05\u0159\xA8\x02\u01C6" + - "\u01C7\x05\u0151\xA4\x02\u01C7\u01C8\x05\u0167\xAF\x02\u01C8\u01C9\x03" + - "\x02\x02\x02\u01C9\u01CA\b\v\x02\x02\u01CA \x03\x02\x02\x02\u01CB\u01CC" + - "\x05\u0159\xA8\x02\u01CC\u01CD\x05\u016B\xB1\x02\u01CD\u01CE\x05U&\x02" + - "\u01CE\u01CF\x05\u0149\xA0\x02\u01CF\u01D0\x05\u016F\xB3\x02\u01D0\u01D1" + - "\x05\u015F\xAB\x02\u01D1\u01D2\x05\u0141\x9C\x02\u01D2\u01D3\x05\u015B" + - "\xA9\x02\u01D3\u01D4\x05\u0147\x9F\x02\u01D4\u01D5\x03\x02\x02\x02\u01D5" + - "\u01D6\b\f\x07\x02\u01D6\"\x03\x02\x02\x02\u01D7\u01D8\x05\u015F\xAB\x02" + - "\u01D8\u01D9\x05\u0163\xAD\x02\u01D9\u01DA\x05\u015D\xAA\x02\u01DA\u01DB" + - "\x05\u0153\xA5\x02\u01DB\u01DC\x05\u0149\xA0\x02\u01DC\u01DD\x05\u0145" + - "\x9E\x02\u01DD\u01DE\x05\u0167\xAF\x02\u01DE\u01DF\x03\x02\x02\x02\u01DF" + - "\u01E0\b\r\x03\x02\u01E0$\x03\x02\x02\x02\u01E1\u01E2\x05\u0163\xAD\x02" + - "\u01E2\u01E3\x05\u0149\xA0\x02\u01E3\u01E4\x05\u015B\xA9\x02\u01E4\u01E5" + - "\x05\u0141\x9C\x02\u01E5\u01E6\x05\u0159\xA8\x02\u01E6\u01E7\x05\u0149" + - "\xA0\x02\u01E7\u01E8\x03\x02\x02\x02\u01E8\u01E9\b\x0E\b\x02\u01E9&\x03" + - "\x02\x02\x02\u01EA\u01EB\x05\u0163\xAD\x02\u01EB\u01EC\x05\u015D\xAA\x02" + - "\u01EC\u01ED\x05\u016D\xB2\x02\u01ED\u01EE\x03\x02\x02\x02\u01EE\u01EF" + - "\b\x0F\x02\x02\u01EF(\x03\x02\x02\x02\u01F0\u01F1\x05\u0165\xAE\x02\u01F1" + - "\u01F2\x05\u014F\xA3\x02\u01F2\u01F3\x05\u015D\xAA\x02\u01F3\u01F4\x05" + - "\u016D\xB2\x02\u01F4\u01F5\x03\x02\x02\x02\u01F5\u01F6\b\x10\t\x02\u01F6" + - "*\x03\x02\x02\x02\u01F7\u01F8\x05\u0165\xAE\x02\u01F8\u01F9\x05\u015D" + - "\xAA\x02\u01F9\u01FA\x05\u0163\xAD\x02\u01FA\u01FB\x05\u0167\xAF\x02\u01FB" + - "\u01FC\x03\x02\x02\x02\u01FC\u01FD\b\x11\x02\x02\u01FD,\x03\x02\x02\x02" + - "\u01FE\u01FF\x05\u0165\xAE\x02\u01FF\u0200\x05\u0167\xAF\x02\u0200\u0201" + - "\x05\u0141\x9C\x02\u0201\u0202\x05\u0167\xAF\x02\u0202\u0203\x05\u0165" + - "\xAE\x02\u0203\u0204\x03\x02\x02\x02\u0204\u0205\b\x12\x02\x02\u0205." + - "\x03\x02\x02\x02\u0206\u0207\x05\u016D\xB2\x02\u0207\u0208\x05\u014F\xA3" + - "\x02\u0208\u0209\x05\u0149\xA0\x02\u0209\u020A\x05\u0163\xAD\x02\u020A" + - "\u020B\x05\u0149\xA0\x02\u020B\u020C\x03\x02\x02\x02\u020C\u020D\b\x13" + - "\x02\x02\u020D0\x03\x02\x02\x02\u020E\u0210\n\x02\x02\x02\u020F\u020E" + - "\x03\x02\x02\x02\u0210\u0211\x03\x02\x02\x02\u0211\u020F\x03\x02\x02\x02" + - "\u0211\u0212\x03\x02\x02\x02\u0212\u0213\x03\x02\x02\x02\u0213\u0214\b" + - "\x14\x02\x02\u02142\x03\x02\x02\x02\u0215\u0216\x071\x02\x02\u0216\u0217" + - "\x071\x02\x02\u0217\u021B\x03\x02\x02\x02\u0218\u021A\n\x03\x02\x02\u0219" + - "\u0218\x03\x02\x02\x02\u021A\u021D\x03\x02\x02\x02\u021B\u0219\x03\x02" + - "\x02\x02\u021B\u021C\x03\x02\x02\x02\u021C\u021F\x03\x02\x02\x02\u021D" + - "\u021B\x03\x02\x02\x02\u021E\u0220\x07\x0F\x02\x02\u021F\u021E\x03\x02" + - "\x02\x02\u021F\u0220\x03\x02\x02\x02\u0220\u0222\x03\x02\x02\x02\u0221" + - "\u0223\x07\f\x02\x02\u0222\u0221\x03\x02\x02\x02\u0222\u0223\x03\x02\x02" + - "\x02\u0223\u0224\x03\x02\x02\x02\u0224\u0225\b\x15\n\x02\u02254\x03\x02" + - "\x02\x02\u0226\u0227\x071\x02\x02\u0227\u0228\x07,\x02\x02\u0228\u022D" + - "\x03\x02\x02\x02\u0229\u022C\x055\x16\x02\u022A\u022C\v\x02\x02\x02\u022B" + - "\u0229\x03\x02\x02\x02\u022B\u022A\x03\x02\x02\x02\u022C\u022F\x03\x02" + - "\x02\x02\u022D\u022E\x03\x02\x02\x02\u022D\u022B\x03\x02\x02\x02\u022E" + - "\u0230\x03\x02\x02\x02\u022F\u022D\x03\x02\x02\x02\u0230\u0231\x07,\x02" + - "\x02\u0231\u0232\x071\x02\x02\u0232\u0233\x03\x02\x02\x02\u0233\u0234" + - "\b\x16\n\x02\u02346\x03\x02\x02\x02\u0235\u0237\t\x04\x02\x02\u0236\u0235" + - "\x03\x02\x02\x02\u0237\u0238\x03\x02\x02\x02\u0238\u0236\x03\x02\x02\x02" + - "\u0238\u0239\x03\x02\x02\x02\u0239\u023A\x03\x02\x02\x02\u023A\u023B\b" + - "\x17\n\x02\u023B8\x03\x02\x02\x02\u023C\u023D\x05\xA3M\x02\u023D\u023E" + - "\x03\x02\x02\x02\u023E\u023F\b\x18\v\x02\u023F\u0240\b\x18\f\x02\u0240" + - ":\x03\x02\x02\x02\u0241\u0242\x05C\x1D\x02\u0242\u0243\x03\x02\x02\x02" + - "\u0243\u0244\b\x19\r\x02\u0244\u0245\b\x19\x0E\x02\u0245<\x03\x02\x02" + - "\x02\u0246\u0247\x057\x17\x02\u0247\u0248\x03\x02\x02\x02\u0248\u0249" + - "\b\x1A\n\x02\u0249>\x03\x02\x02\x02\u024A\u024B\x053\x15\x02\u024B\u024C" + - "\x03\x02\x02\x02\u024C\u024D\b\x1B\n\x02\u024D@\x03\x02\x02\x02\u024E" + - "\u024F\x055\x16\x02\u024F\u0250\x03\x02\x02\x02\u0250\u0251\b\x1C\n\x02" + - "\u0251B\x03\x02\x02\x02\u0252\u0253\x07~\x02\x02\u0253\u0254\x03\x02\x02" + - "\x02\u0254\u0255\b\x1D\x0E\x02\u0255D\x03\x02\x02\x02\u0256\u0257\t\x05" + - "\x02\x02\u0257F\x03\x02\x02\x02\u0258\u0259\t\x06\x02\x02\u0259H\x03\x02" + - "\x02\x02\u025A\u025B\x07^\x02\x02\u025B\u025C\t\x07\x02\x02\u025CJ\x03" + - "\x02\x02\x02\u025D\u025E\n\b\x02\x02\u025EL\x03\x02\x02\x02\u025F\u0261" + - "\t\t\x02\x02\u0260\u0262\t\n\x02\x02\u0261\u0260\x03\x02\x02\x02\u0261" + - "\u0262\x03\x02\x02\x02\u0262\u0264\x03\x02\x02\x02\u0263\u0265\x05E\x1E" + - "\x02\u0264\u0263\x03\x02\x02\x02\u0265\u0266\x03\x02\x02\x02\u0266\u0264" + - "\x03\x02\x02\x02\u0266\u0267\x03\x02\x02\x02\u0267N\x03\x02\x02\x02\u0268" + - "\u0269\x07B\x02\x02\u0269P\x03\x02\x02\x02\u026A\u026B\x07b\x02\x02\u026B" + - "R\x03\x02\x02\x02\u026C\u0270\n\v\x02\x02\u026D\u026E\x07b\x02\x02\u026E" + - "\u0270\x07b\x02\x02\u026F\u026C\x03\x02\x02\x02\u026F\u026D\x03\x02\x02" + - "\x02\u0270T\x03\x02\x02\x02\u0271\u0272\x07a\x02\x02\u0272V\x03\x02\x02" + - "\x02\u0273\u0277\x05G\x1F\x02\u0274\u0277\x05E\x1E\x02\u0275\u0277\x05" + - "U&\x02\u0276\u0273\x03\x02\x02\x02\u0276\u0274\x03\x02\x02\x02\u0276\u0275" + - "\x03\x02\x02\x02\u0277X\x03\x02\x02\x02\u0278\u027D\x07$\x02\x02\u0279" + - "\u027C\x05I \x02\u027A\u027C\x05K!\x02\u027B\u0279\x03\x02\x02\x02\u027B" + - "\u027A\x03\x02\x02\x02\u027C\u027F\x03\x02\x02\x02\u027D\u027B\x03\x02" + - "\x02\x02\u027D\u027E\x03\x02\x02\x02\u027E\u0280\x03\x02\x02\x02\u027F" + - "\u027D\x03\x02\x02\x02\u0280\u0296\x07$\x02\x02\u0281\u0282\x07$\x02\x02" + - "\u0282\u0283\x07$\x02\x02\u0283\u0284\x07$\x02\x02\u0284\u0288\x03\x02" + - "\x02\x02\u0285\u0287\n\x03\x02\x02\u0286\u0285\x03\x02\x02\x02\u0287\u028A" + - "\x03\x02\x02\x02\u0288\u0289\x03\x02\x02\x02\u0288\u0286\x03\x02\x02\x02" + - "\u0289\u028B\x03\x02\x02\x02\u028A\u0288\x03\x02\x02\x02\u028B\u028C\x07" + - "$\x02\x02\u028C\u028D\x07$\x02\x02\u028D\u028E\x07$\x02\x02\u028E\u0290" + - "\x03\x02\x02\x02\u028F\u0291\x07$\x02\x02\u0290\u028F\x03\x02\x02\x02" + - "\u0290\u0291\x03\x02\x02\x02\u0291\u0293\x03\x02\x02\x02\u0292\u0294\x07" + - "$\x02\x02\u0293\u0292\x03\x02\x02\x02\u0293\u0294\x03\x02\x02\x02\u0294" + - "\u0296\x03\x02\x02\x02\u0295\u0278\x03\x02\x02\x02\u0295\u0281\x03\x02" + - "\x02\x02\u0296Z\x03\x02\x02\x02\u0297\u0299\x05E\x1E\x02\u0298\u0297\x03" + - "\x02\x02\x02\u0299\u029A\x03\x02\x02\x02\u029A\u0298\x03\x02\x02\x02\u029A" + - "\u029B\x03\x02\x02\x02\u029B\\\x03\x02\x02\x02\u029C\u029E\x05E\x1E\x02" + - "\u029D\u029C\x03\x02\x02\x02\u029E\u029F\x03\x02\x02\x02\u029F\u029D\x03" + - "\x02\x02\x02\u029F\u02A0\x03\x02\x02\x02\u02A0\u02A1\x03\x02\x02\x02\u02A1" + - "\u02A5\x05k1\x02\u02A2\u02A4\x05E\x1E\x02\u02A3\u02A2\x03\x02\x02\x02" + - "\u02A4\u02A7\x03\x02\x02\x02\u02A5\u02A3\x03\x02\x02\x02\u02A5\u02A6\x03" + - "\x02\x02\x02\u02A6\u02C7\x03\x02\x02\x02\u02A7\u02A5\x03\x02\x02\x02\u02A8" + - "\u02AA\x05k1\x02\u02A9\u02AB\x05E\x1E\x02\u02AA\u02A9\x03\x02\x02\x02" + - "\u02AB\u02AC\x03\x02\x02\x02\u02AC\u02AA\x03\x02\x02\x02\u02AC\u02AD\x03" + - "\x02\x02\x02\u02AD\u02C7\x03\x02\x02\x02\u02AE\u02B0\x05E\x1E\x02\u02AF" + - "\u02AE\x03\x02\x02\x02\u02B0\u02B1\x03\x02\x02\x02\u02B1\u02AF\x03\x02" + - "\x02\x02\u02B1\u02B2\x03\x02\x02\x02\u02B2\u02BA\x03\x02\x02\x02\u02B3" + - "\u02B7\x05k1\x02\u02B4\u02B6\x05E\x1E\x02\u02B5\u02B4\x03\x02\x02\x02" + - "\u02B6\u02B9\x03\x02\x02\x02\u02B7\u02B5\x03\x02\x02\x02\u02B7\u02B8\x03" + - "\x02\x02\x02\u02B8\u02BB\x03\x02\x02\x02\u02B9\u02B7\x03\x02\x02\x02\u02BA" + - "\u02B3\x03\x02\x02\x02\u02BA\u02BB\x03\x02\x02\x02\u02BB\u02BC\x03\x02" + - "\x02\x02\u02BC\u02BD\x05M\"\x02\u02BD\u02C7\x03\x02\x02\x02\u02BE\u02C0" + - "\x05k1\x02\u02BF\u02C1\x05E\x1E\x02\u02C0\u02BF\x03\x02\x02\x02\u02C1" + - "\u02C2\x03\x02\x02\x02\u02C2\u02C0\x03\x02\x02\x02\u02C2\u02C3\x03\x02" + - "\x02\x02\u02C3\u02C4\x03\x02\x02\x02\u02C4\u02C5\x05M\"\x02\u02C5\u02C7" + - "\x03\x02\x02\x02\u02C6\u029D\x03\x02\x02\x02\u02C6\u02A8\x03\x02\x02\x02" + - "\u02C6\u02AF\x03\x02\x02\x02\u02C6\u02BE\x03\x02\x02\x02\u02C7^\x03\x02" + - "\x02\x02\u02C8\u02C9\x05\u0143\x9D\x02\u02C9\u02CA\x05\u0171\xB4\x02\u02CA" + - "`\x03\x02\x02\x02\u02CB\u02CC\x05\u0141\x9C\x02\u02CC\u02CD\x05\u015B" + - "\xA9\x02\u02CD\u02CE\x05\u0147\x9F\x02\u02CEb\x03\x02\x02\x02\u02CF\u02D0" + - "\x05\u0141\x9C\x02\u02D0\u02D1\x05\u0165\xAE\x02\u02D1\u02D2\x05\u0145" + - "\x9E\x02\u02D2d\x03\x02\x02\x02\u02D3\u02D4\x07?\x02\x02\u02D4f\x03\x02" + - "\x02\x02\u02D5\u02D6\x07.\x02\x02\u02D6h\x03\x02\x02\x02\u02D7\u02D8\x05" + - "\u0147\x9F\x02\u02D8\u02D9\x05\u0149\xA0\x02\u02D9\u02DA\x05\u0165\xAE" + - "\x02\u02DA\u02DB\x05\u0145\x9E\x02\u02DBj\x03\x02\x02\x02\u02DC\u02DD" + - "\x070\x02\x02\u02DDl\x03\x02\x02\x02\u02DE\u02DF\x05\u014B\xA1\x02\u02DF" + - "\u02E0\x05\u0141\x9C\x02\u02E0\u02E1\x05\u0157\xA7\x02\u02E1\u02E2\x05" + - "\u0165\xAE\x02\u02E2\u02E3\x05\u0149\xA0\x02\u02E3n\x03\x02\x02\x02\u02E4" + - "\u02E5\x05\u014B\xA1\x02\u02E5\u02E6\x05\u0151\xA4\x02\u02E6\u02E7\x05" + - "\u0163\xAD\x02\u02E7\u02E8\x05\u0165\xAE\x02\u02E8\u02E9\x05\u0167\xAF" + - "\x02\u02E9p\x03\x02\x02\x02\u02EA\u02EB\x05\u0157\xA7\x02\u02EB\u02EC" + - "\x05\u0141\x9C\x02\u02EC\u02ED\x05\u0165\xAE\x02\u02ED\u02EE\x05\u0167" + - "\xAF\x02\u02EEr\x03\x02\x02\x02\u02EF\u02F0\x07*\x02\x02\u02F0t\x03\x02" + - "\x02\x02\u02F1\u02F2\x05\u0151\xA4\x02\u02F2\u02F3\x05\u015B\xA9\x02\u02F3" + - "v\x03\x02\x02\x02\u02F4\u02F5\x05\u0151\xA4\x02\u02F5\u02F6\x05\u0165" + - "\xAE\x02\u02F6x\x03\x02\x02\x02\u02F7\u02F8\x05\u0157\xA7\x02\u02F8\u02F9" + - "\x05\u0151\xA4\x02\u02F9\u02FA\x05\u0155\xA6\x02\u02FA\u02FB\x05\u0149" + - "\xA0\x02\u02FBz\x03\x02\x02\x02\u02FC\u02FD\x05\u015B\xA9\x02\u02FD\u02FE" + - "\x05\u015D\xAA\x02\u02FE\u02FF\x05\u0167\xAF\x02\u02FF|\x03\x02\x02\x02" + - "\u0300\u0301\x05\u015B\xA9\x02\u0301\u0302\x05\u0169\xB0\x02\u0302\u0303" + - "\x05\u0157\xA7\x02\u0303\u0304\x05\u0157\xA7\x02\u0304~\x03\x02\x02\x02" + - "\u0305\u0306\x05\u015B\xA9\x02\u0306\u0307\x05\u0169\xB0\x02\u0307\u0308" + - "\x05\u0157\xA7\x02\u0308\u0309\x05\u0157\xA7\x02\u0309\u030A\x05\u0165" + - "\xAE\x02\u030A\x80\x03\x02\x02\x02\u030B\u030C\x05\u015D\xAA\x02\u030C" + - "\u030D\x05\u0163\xAD\x02\u030D\x82\x03\x02\x02\x02\u030E\u030F\x07A\x02" + - "\x02\u030F\x84\x03\x02\x02\x02\u0310\u0311\x05\u0163\xAD\x02\u0311\u0312" + - "\x05\u0157\xA7\x02\u0312\u0313\x05\u0151\xA4\x02\u0313\u0314\x05\u0155" + - "\xA6\x02\u0314\u0315\x05\u0149\xA0\x02\u0315\x86\x03\x02\x02\x02\u0316" + - "\u0317\x07+\x02\x02\u0317\x88\x03\x02\x02\x02\u0318\u0319\x05\u0167\xAF" + - "\x02\u0319\u031A\x05\u0163\xAD\x02\u031A\u031B\x05\u0169\xB0\x02\u031B" + - "\u031C\x05\u0149\xA0\x02\u031C\x8A\x03\x02\x02\x02\u031D\u031E\x07?\x02" + - "\x02\u031E\u031F\x07?\x02\x02\u031F\x8C\x03\x02\x02\x02\u0320\u0321\x07" + - "?\x02\x02\u0321\u0322\x07\x80\x02\x02\u0322\x8E\x03\x02\x02\x02\u0323" + - "\u0324\x07#\x02\x02\u0324\u0325\x07?\x02\x02\u0325\x90\x03\x02\x02\x02" + - "\u0326\u0327\x07>\x02\x02\u0327\x92\x03\x02\x02\x02\u0328\u0329\x07>\x02" + - "\x02\u0329\u032A\x07?\x02\x02\u032A\x94\x03\x02\x02\x02\u032B\u032C\x07" + - "@\x02\x02\u032C\x96\x03\x02\x02\x02\u032D\u032E\x07@\x02\x02\u032E\u032F" + - "\x07?\x02\x02\u032F\x98\x03\x02\x02\x02\u0330\u0331\x07-\x02\x02\u0331" + - "\x9A\x03\x02\x02\x02\u0332\u0333\x07/\x02\x02\u0333\x9C\x03\x02\x02\x02" + - "\u0334\u0335\x07,\x02\x02\u0335\x9E\x03\x02\x02\x02\u0336\u0337\x071\x02" + - "\x02\u0337\xA0\x03\x02\x02\x02\u0338\u0339\x07\'\x02\x02\u0339\xA2\x03" + - "\x02\x02\x02\u033A\u033B\x07]\x02\x02\u033B\u033C\x03\x02\x02\x02\u033C" + - "\u033D\bM\x02\x02\u033D\u033E\bM\x02\x02\u033E\xA4\x03\x02\x02\x02\u033F" + - "\u0340\x07_\x02\x02\u0340\u0341\x03\x02\x02\x02\u0341\u0342\bN\x0E\x02" + - "\u0342\u0343\bN\x0E\x02\u0343\xA6\x03\x02\x02\x02\u0344\u0348\x05G\x1F" + - "\x02\u0345\u0347\x05W\'\x02\u0346\u0345\x03\x02\x02\x02\u0347\u034A\x03" + - "\x02\x02\x02\u0348\u0346\x03\x02\x02\x02\u0348\u0349\x03\x02\x02\x02\u0349" + - "\u0355\x03\x02\x02\x02\u034A\u0348\x03\x02\x02\x02\u034B\u034E\x05U&\x02" + - "\u034C\u034E\x05O#\x02\u034D\u034B\x03\x02\x02\x02\u034D\u034C\x03\x02" + - "\x02\x02\u034E\u0350\x03\x02\x02\x02\u034F\u0351\x05W\'\x02\u0350\u034F" + - "\x03\x02\x02\x02\u0351\u0352\x03\x02\x02\x02\u0352\u0350\x03\x02\x02\x02" + - "\u0352\u0353\x03\x02\x02\x02\u0353\u0355\x03\x02\x02\x02\u0354\u0344\x03" + - "\x02\x02\x02\u0354\u034D\x03\x02\x02\x02\u0355\xA8\x03\x02\x02\x02\u0356" + - "\u0358\x05Q$\x02\u0357\u0359\x05S%\x02\u0358\u0357\x03\x02\x02\x02\u0359" + - "\u035A\x03\x02\x02\x02\u035A\u0358\x03\x02\x02\x02\u035A\u035B\x03\x02" + - "\x02\x02\u035B\u035C\x03\x02\x02\x02\u035C\u035D\x05Q$\x02\u035D\xAA\x03" + - "\x02\x02\x02\u035E\u035F\x053\x15\x02\u035F\u0360\x03\x02\x02\x02\u0360" + - "\u0361\bQ\n\x02\u0361\xAC\x03\x02\x02\x02\u0362\u0363\x055\x16\x02\u0363" + - "\u0364\x03\x02\x02\x02\u0364\u0365\bR\n\x02\u0365\xAE\x03\x02\x02\x02" + - "\u0366\u0367\x057\x17\x02\u0367\u0368\x03\x02\x02\x02\u0368\u0369\bS\n" + - "\x02\u0369\xB0\x03\x02\x02\x02\u036A\u036B\x05C\x1D\x02\u036B\u036C\x03" + - "\x02\x02\x02\u036C\u036D\bT\r\x02\u036D\u036E\bT\x0E\x02\u036E\xB2\x03" + - "\x02\x02\x02\u036F\u0370\x05\xA3M\x02\u0370\u0371\x03\x02\x02\x02\u0371" + - "\u0372\bU\v\x02\u0372\xB4\x03\x02\x02\x02\u0373\u0374\x05\xA5N\x02\u0374" + - "\u0375\x03\x02\x02\x02\u0375\u0376\bV\x0F\x02\u0376\xB6\x03\x02\x02\x02" + - "\u0377\u0378\x05g/\x02\u0378\u0379\x03\x02\x02\x02\u0379\u037A\bW\x10" + - "\x02\u037A\xB8\x03\x02\x02\x02\u037B\u037C\x05e.\x02\u037C\u037D\x03\x02" + - "\x02\x02\u037D\u037E\bX\x11\x02\u037E\xBA\x03\x02\x02\x02\u037F\u0380" + - "\x05\u0159\xA8\x02\u0380\u0381\x05\u0149\xA0\x02\u0381\u0382\x05\u0167" + - "\xAF\x02\u0382\u0383\x05\u0141\x9C\x02\u0383\u0384\x05\u0147\x9F\x02\u0384" + - "\u0385\x05\u0141\x9C\x02\u0385\u0386\x05\u0167\xAF\x02\u0386\u0387\x05" + - "\u0141\x9C\x02\u0387\xBC\x03\x02\x02\x02\u0388\u038C\n\f\x02\x02\u0389" + - "\u038A\x071\x02\x02\u038A\u038C\n\r\x02\x02\u038B\u0388\x03\x02\x02\x02" + - "\u038B\u0389\x03\x02\x02\x02\u038C\xBE\x03\x02\x02\x02\u038D\u038F\x05" + - "\xBDZ\x02\u038E\u038D\x03\x02\x02\x02\u038F\u0390\x03\x02\x02\x02\u0390" + - "\u038E\x03\x02\x02\x02\u0390\u0391\x03\x02\x02\x02\u0391\xC0\x03\x02\x02" + - "\x02\u0392\u0393\x05\xA9P\x02\u0393\u0394\x03\x02\x02\x02\u0394\u0395" + - "\b\\\x12\x02\u0395\xC2\x03\x02\x02\x02\u0396\u0397\x053\x15\x02\u0397" + - "\u0398\x03\x02\x02\x02\u0398\u0399\b]\n\x02\u0399\xC4\x03\x02\x02\x02" + - "\u039A\u039B\x055\x16\x02\u039B\u039C\x03\x02\x02\x02\u039C\u039D\b^\n" + - "\x02\u039D\xC6\x03\x02\x02\x02\u039E\u039F\x057\x17\x02\u039F\u03A0\x03" + - "\x02\x02\x02\u03A0\u03A1\b_\n\x02\u03A1\xC8\x03\x02\x02\x02\u03A2\u03A3" + - "\x05C\x1D\x02\u03A3\u03A4\x03\x02\x02\x02\u03A4\u03A5\b`\r\x02\u03A5\u03A6" + - "\b`\x0E\x02\u03A6\xCA\x03\x02\x02\x02\u03A7\u03A8\x05k1\x02\u03A8\u03A9" + - "\x03\x02\x02\x02\u03A9\u03AA\ba\x13\x02\u03AA\xCC\x03\x02\x02\x02\u03AB" + - "\u03AC\x05g/\x02\u03AC\u03AD\x03\x02\x02\x02\u03AD\u03AE\bb\x10\x02\u03AE" + - "\xCE\x03\x02\x02\x02\u03AF\u03B4\x05G\x1F\x02\u03B0\u03B4\x05E\x1E\x02" + - "\u03B1\u03B4\x05U&\x02\u03B2\u03B4\x05\x9DJ\x02\u03B3\u03AF\x03\x02\x02" + - "\x02\u03B3\u03B0\x03\x02\x02\x02\u03B3\u03B1\x03\x02\x02\x02\u03B3\u03B2" + - "\x03\x02\x02\x02\u03B4\xD0\x03\x02\x02\x02\u03B5\u03B8\x05G\x1F\x02\u03B6" + - "\u03B8\x05\x9DJ\x02\u03B7\u03B5\x03\x02\x02\x02\u03B7\u03B6\x03\x02\x02" + - "\x02\u03B8\u03BC\x03\x02\x02\x02\u03B9\u03BB\x05\xCFc\x02\u03BA\u03B9" + - "\x03\x02\x02\x02\u03BB\u03BE\x03\x02\x02\x02\u03BC\u03BA\x03\x02\x02\x02" + - "\u03BC\u03BD\x03\x02\x02\x02\u03BD\u03C9\x03\x02\x02\x02\u03BE\u03BC\x03" + - "\x02\x02\x02\u03BF\u03C2\x05U&\x02\u03C0\u03C2\x05O#\x02\u03C1\u03BF\x03" + - "\x02\x02\x02\u03C1\u03C0\x03\x02\x02\x02\u03C2\u03C4\x03\x02\x02\x02\u03C3" + - "\u03C5\x05\xCFc\x02\u03C4\u03C3\x03\x02\x02\x02\u03C5\u03C6\x03\x02\x02" + - "\x02\u03C6\u03C4\x03\x02\x02\x02\u03C6\u03C7\x03\x02\x02\x02\u03C7\u03C9" + - "\x03\x02\x02\x02\u03C8\u03B7\x03\x02\x02\x02\u03C8\u03C1\x03\x02\x02\x02" + - "\u03C9\xD2\x03\x02\x02\x02\u03CA\u03CB\x05\xD1d\x02\u03CB\u03CC\x03\x02" + - "\x02\x02\u03CC\u03CD\be\x14\x02\u03CD\xD4\x03\x02\x02\x02\u03CE\u03CF" + - "\x05\xA9P\x02\u03CF\u03D0\x03\x02\x02\x02\u03D0\u03D1\bf\x12\x02\u03D1" + - "\xD6\x03\x02\x02\x02\u03D2\u03D3\x053\x15\x02\u03D3\u03D4\x03\x02\x02" + - "\x02\u03D4\u03D5\bg\n\x02\u03D5\xD8\x03\x02\x02\x02\u03D6\u03D7\x055\x16" + - "\x02\u03D7\u03D8\x03\x02\x02\x02\u03D8\u03D9\bh\n\x02\u03D9\xDA\x03\x02" + - "\x02\x02\u03DA\u03DB\x057\x17\x02\u03DB\u03DC\x03\x02\x02\x02\u03DC\u03DD" + - "\bi\n\x02\u03DD\xDC\x03\x02\x02\x02\u03DE\u03DF\x05C\x1D\x02\u03DF\u03E0" + - "\x03\x02\x02\x02\u03E0\u03E1\bj\r\x02\u03E1\u03E2\bj\x0E\x02\u03E2\xDE" + - "\x03\x02\x02\x02\u03E3\u03E4\x05e.\x02\u03E4\u03E5\x03\x02\x02\x02\u03E5" + - "\u03E6\bk\x11\x02\u03E6\xE0\x03\x02\x02\x02\u03E7\u03E8\x05g/\x02\u03E8" + - "\u03E9\x03\x02\x02\x02\u03E9\u03EA\bl\x10\x02\u03EA\xE2\x03\x02\x02\x02" + - "\u03EB\u03EC\x05k1\x02\u03EC\u03ED\x03\x02\x02\x02\u03ED\u03EE\bm\x13" + - "\x02\u03EE\xE4\x03\x02\x02\x02\u03EF\u03F0\x05\u0141\x9C\x02\u03F0\u03F1" + - "\x05\u0165\xAE\x02\u03F1\xE6\x03\x02\x02\x02\u03F2\u03F3\x05\xA9P\x02" + - "\u03F3\u03F4\x03\x02\x02\x02\u03F4\u03F5\bo\x12\x02\u03F5\xE8\x03\x02" + - "\x02\x02\u03F6\u03F7\x05\xD1d\x02\u03F7\u03F8\x03\x02\x02\x02\u03F8\u03F9" + - "\bp\x14\x02\u03F9\xEA\x03\x02\x02\x02\u03FA\u03FB\x053\x15\x02\u03FB\u03FC" + - "\x03\x02\x02\x02\u03FC\u03FD\bq\n\x02\u03FD\xEC\x03\x02\x02\x02\u03FE" + - "\u03FF\x055\x16\x02\u03FF\u0400\x03\x02\x02\x02\u0400\u0401\br\n\x02\u0401" + - "\xEE\x03\x02\x02\x02\u0402\u0403\x057\x17\x02\u0403\u0404\x03\x02\x02" + - "\x02\u0404\u0405\bs\n\x02\u0405\xF0\x03\x02\x02\x02\u0406\u0407\x05C\x1D" + - "\x02\u0407\u0408\x03\x02\x02\x02\u0408\u0409\bt\r\x02\u0409\u040A\bt\x0E" + - "\x02\u040A\xF2\x03\x02\x02\x02\u040B\u040C\x05\xA3M\x02\u040C\u040D\x03" + - "\x02\x02\x02\u040D\u040E\bu\v\x02\u040E\u040F\bu\x15\x02\u040F\xF4\x03" + - "\x02\x02\x02\u0410\u0411\x05\u015D\xAA\x02\u0411\u0412\x05\u015B\xA9\x02" + - "\u0412\u0413\x03\x02\x02\x02\u0413\u0414\bv\x16\x02\u0414\xF6\x03\x02" + - "\x02\x02\u0415\u0416\x05\u016D\xB2\x02\u0416\u0417\x05\u0151\xA4\x02\u0417" + - "\u0418\x05\u0167\xAF\x02\u0418\u0419\x05\u014F\xA3\x02\u0419\u041A\x03" + - "\x02\x02\x02\u041A\u041B\bw\x16\x02\u041B\xF8\x03\x02\x02\x02\u041C\u041D" + - "\n\x0E\x02\x02\u041D\xFA\x03\x02\x02\x02\u041E\u0421\x05G\x1F\x02\u041F" + - "\u0421\x05E\x1E\x02\u0420\u041E\x03\x02\x02\x02\u0420\u041F\x03\x02\x02" + - "\x02\u0421\u0425\x03\x02\x02\x02\u0422\u0424\x05\xF9x\x02\u0423\u0422" + - "\x03\x02\x02\x02\u0424\u0427\x03\x02\x02\x02\u0425\u0423\x03\x02\x02\x02" + - "\u0425\u0426\x03\x02\x02\x02\u0426\xFC\x03\x02\x02\x02\u0427\u0425\x03" + - "\x02\x02\x02\u0428\u0429\x05\xA9P\x02\u0429\u042A\x03\x02\x02\x02\u042A" + - "\u042B\bz\x12\x02\u042B\xFE\x03\x02\x02\x02\u042C\u042D\x05\xFBy\x02\u042D" + - "\u042E\x03\x02\x02\x02\u042E\u042F\b{\x17\x02\u042F\u0100\x03\x02\x02" + - "\x02\u0430\u0431\x053\x15\x02\u0431\u0432\x03\x02\x02\x02\u0432\u0433" + - "\b|\n\x02\u0433\u0102\x03\x02\x02\x02\u0434\u0435\x055\x16\x02\u0435\u0436" + - "\x03\x02\x02\x02\u0436\u0437\b}\n\x02\u0437\u0104\x03\x02\x02\x02\u0438" + - "\u0439\x057\x17\x02\u0439\u043A\x03\x02\x02\x02\u043A\u043B\b~\n\x02\u043B" + - "\u0106\x03\x02\x02\x02\u043C\u043D\x05C\x1D\x02\u043D\u043E\x03\x02\x02" + - "\x02\u043E\u043F\b\x7F\r\x02\u043F\u0440\b\x7F\x0E\x02\u0440\u0441\b\x7F" + - "\x0E\x02\u0441\u0108\x03\x02\x02\x02\u0442\u0443\x05e.\x02\u0443\u0444" + - "\x03\x02\x02\x02\u0444\u0445\b\x80\x11\x02\u0445\u010A\x03\x02\x02\x02" + - "\u0446\u0447\x05g/\x02\u0447\u0448\x03\x02\x02\x02\u0448\u0449\b\x81\x10" + - "\x02\u0449\u010C\x03\x02\x02\x02\u044A\u044B\x05k1"; + "\x03\x02\x02\x02\u0103\u0430\x03\x02\x02\x02\u0105\u0434\x03\x02\x02\x02" + + "\u0107\u043A\x03\x02\x02\x02\u0109\u043E\x03\x02\x02\x02\u010B\u0442\x03" + + "\x02\x02\x02\u010D\u0446\x03\x02\x02\x02\u010F\u044A\x03\x02\x02\x02\u0111" + + "\u044E\x03\x02\x02\x02\u0113\u0452\x03\x02\x02\x02\u0115\u0456\x03\x02" + + "\x02\x02\u0117\u045A\x03\x02\x02\x02\u0119\u045E\x03\x02\x02\x02\u011B" + + "\u0463\x03\x02\x02\x02\u011D\u0467\x03\x02\x02\x02\u011F\u046B\x03\x02" + + "\x02\x02\u0121\u046F\x03\x02\x02\x02\u0123\u0473\x03\x02\x02\x02\u0125" + + "\u0477\x03\x02\x02\x02\u0127\u047B\x03\x02\x02\x02\u0129\u0480\x03\x02" + + "\x02\x02\u012B\u0485\x03\x02\x02\x02\u012D\u048F\x03\x02\x02\x02\u012F" + + "\u0493\x03\x02\x02\x02\u0131\u0497\x03\x02\x02\x02\u0133\u049B\x03\x02" + + "\x02\x02\u0135\u04A0\x03\x02\x02\x02\u0137\u04A7\x03\x02\x02\x02\u0139" + + "\u04AB\x03\x02\x02\x02\u013B\u04AF\x03\x02\x02\x02\u013D\u04B3\x03\x02" + + "\x02\x02\u013F\u04B7\x03\x02\x02\x02\u0141\u04B9\x03\x02\x02\x02\u0143" + + "\u04BB\x03\x02\x02\x02\u0145\u04BD\x03\x02\x02\x02\u0147\u04BF\x03\x02" + + "\x02\x02\u0149\u04C1\x03\x02\x02\x02\u014B\u04C3\x03\x02\x02\x02\u014D" + + "\u04C5\x03\x02\x02\x02\u014F\u04C7\x03\x02\x02\x02\u0151\u04C9\x03\x02" + + "\x02\x02\u0153\u04CB\x03\x02\x02\x02\u0155\u04CD\x03\x02\x02\x02\u0157" + + "\u04CF\x03\x02\x02\x02\u0159\u04D1\x03\x02\x02\x02\u015B\u04D3\x03\x02" + + "\x02\x02\u015D\u04D5\x03\x02\x02\x02\u015F\u04D7\x03\x02\x02\x02\u0161" + + "\u04D9\x03\x02\x02\x02\u0163\u04DB\x03\x02\x02\x02\u0165\u04DD\x03\x02" + + "\x02\x02\u0167\u04DF\x03\x02\x02\x02\u0169\u04E1\x03\x02\x02\x02\u016B" + + "\u04E3\x03\x02\x02\x02\u016D\u04E5\x03\x02\x02\x02\u016F\u04E7\x03\x02" + + "\x02\x02\u0171\u04E9\x03\x02\x02\x02\u0173\u0174\x05\u0145\x9E\x02\u0174" + + "\u0175\x05\u014F\xA3\x02\u0175\u0176\x05\u0163\xAD\x02\u0176\u0177\x05" + + "\u0163\xAD\x02\u0177\u0178\x05\u0147\x9F\x02\u0178\u0179\x05\u0143\x9D" + + "\x02\u0179\u017A\x05\u0165\xAE\x02\u017A\u017B\x03\x02\x02\x02\u017B\u017C" + + "\b\x02\x02\x02\u017C\x0E\x03\x02\x02\x02\u017D\u017E\x05\u0145\x9E\x02" + + "\u017E\u017F\x05\u0161\xAC\x02\u017F\u0180\x05\u015B\xA9\x02\u0180\u0181" + + "\x05\u015D\xAA\x02\u0181\u0182\x03\x02\x02\x02\u0182\u0183\b\x03\x03\x02" + + "\u0183\x10\x03\x02\x02\x02\u0184\u0185\x05\u0147\x9F\x02\u0185\u0186\x05" + + "\u0159\xA8\x02\u0186\u0187\x05\u0161\xAC\x02\u0187\u0188\x05\u014F\xA3" + + "\x02\u0188\u0189\x05\u0143\x9D\x02\u0189\u018A\x05\u014D\xA2\x02\u018A" + + "\u018B\x03\x02\x02\x02\u018B\u018C\b\x04\x04\x02\u018C\x12\x03\x02\x02" + + "\x02\u018D\u018E\x05\u0147\x9F\x02\u018E\u018F\x05\u0169\xB0\x02\u018F" + + "\u0190\x05\u013F\x9B\x02\u0190\u0191\x05\u0155\xA6\x02\u0191\u0192\x03" + + "\x02\x02\x02\u0192\u0193\b\x05\x02\x02\u0193\x14\x03\x02\x02\x02\u0194" + + "\u0195\x05\u0147\x9F\x02\u0195\u0196\x05\u016D\xB2\x02\u0196\u0197\x05" + + "\u015D\xAA\x02\u0197\u0198\x05\u0155\xA6\x02\u0198\u0199\x05\u013F\x9B" + + "\x02\u0199\u019A\x05\u014F\xA3\x02\u019A\u019B\x05\u0159\xA8\x02\u019B" + + "\u019C\x03\x02\x02\x02\u019C\u019D\b\x06\x05\x02\u019D\x16\x03\x02\x02" + + "\x02\u019E\u019F\x05\u0149\xA0\x02\u019F\u01A0\x05\u0161\xAC\x02\u01A0" + + "\u01A1\x05\u015B\xA9\x02\u01A1\u01A2\x05\u0157\xA7\x02\u01A2\u01A3\x03" + + "\x02\x02\x02\u01A3\u01A4\b\x07\x06\x02\u01A4\x18\x03\x02\x02\x02\u01A5" + + "\u01A6\x05\u014B\xA1\x02\u01A6\u01A7\x05\u0161\xAC\x02\u01A7\u01A8\x05" + + "\u015B\xA9\x02\u01A8\u01A9\x05\u0153\xA5\x02\u01A9\u01AA\x03\x02\x02\x02" + + "\u01AA\u01AB\b\b\x02\x02\u01AB\x1A\x03\x02\x02\x02\u01AC\u01AD\x05\u014F" + + "\xA3\x02\u01AD\u01AE\x05\u0159\xA8\x02\u01AE\u01AF\x05\u0155\xA6\x02\u01AF" + + "\u01B0\x05\u014F\xA3\x02\u01B0\u01B1\x05\u0159\xA8\x02\u01B1\u01B2\x05" + + "\u0147\x9F\x02\u01B2\u01B3\x05\u0163\xAD\x02\u01B3\u01B4\x05\u0165\xAE" + + "\x02\u01B4\u01B5\x05\u013F\x9B\x02\u01B5\u01B6\x05\u0165\xAE\x02\u01B6" + + "\u01B7\x05\u0163\xAD\x02\u01B7\u01B8\x03\x02\x02\x02\u01B8\u01B9\b\t\x02" + + "\x02\u01B9\x1C\x03\x02\x02\x02\u01BA\u01BB\x05\u0153\xA5\x02\u01BB\u01BC" + + "\x05\u0147\x9F\x02\u01BC\u01BD\x05\u0147\x9F\x02\u01BD\u01BE\x05\u015D" + + "\xAA\x02\u01BE\u01BF\x03\x02\x02\x02\u01BF\u01C0\b\n\x03\x02\u01C0\x1E" + + "\x03\x02\x02\x02\u01C1\u01C2\x05\u0155\xA6\x02\u01C2\u01C3\x05\u014F\xA3" + + "\x02\u01C3\u01C4\x05\u0157\xA7\x02\u01C4\u01C5\x05\u014F\xA3\x02\u01C5" + + "\u01C6\x05\u0165\xAE\x02\u01C6\u01C7\x03\x02\x02\x02\u01C7\u01C8\b\v\x02" + + "\x02\u01C8 \x03\x02\x02\x02\u01C9\u01CA\x05\u0157\xA7\x02\u01CA\u01CB" + + "\x05\u0169\xB0\x02\u01CB\u01CC\x05S%\x02\u01CC\u01CD\x05\u0147\x9F\x02" + + "\u01CD\u01CE\x05\u016D\xB2\x02\u01CE\u01CF\x05\u015D\xAA\x02\u01CF\u01D0" + + "\x05\u013F\x9B\x02\u01D0\u01D1\x05\u0159\xA8\x02\u01D1\u01D2\x05\u0145" + + "\x9E\x02\u01D2\u01D3\x03\x02\x02\x02\u01D3\u01D4\b\f\x07\x02\u01D4\"\x03" + + "\x02\x02\x02\u01D5\u01D6\x05\u0161\xAC\x02\u01D6\u01D7\x05\u0147\x9F\x02" + + "\u01D7\u01D8\x05\u0159\xA8\x02\u01D8\u01D9\x05\u013F\x9B\x02\u01D9\u01DA" + + "\x05\u0157\xA7\x02\u01DA\u01DB\x05\u0147\x9F\x02\u01DB\u01DC\x03\x02\x02" + + "\x02\u01DC\u01DD\b\r\b\x02\u01DD$\x03\x02\x02\x02\u01DE\u01DF\x05\u0161" + + "\xAC\x02\u01DF\u01E0\x05\u015B\xA9\x02\u01E0\u01E1\x05\u016B\xB1\x02\u01E1" + + "\u01E2\x03\x02\x02\x02\u01E2\u01E3\b\x0E\x02\x02\u01E3&\x03\x02\x02\x02" + + "\u01E4\u01E5\x05\u0163\xAD\x02\u01E5\u01E6\x05\u014D\xA2\x02\u01E6\u01E7" + + "\x05\u015B\xA9\x02\u01E7\u01E8\x05\u016B\xB1\x02\u01E8\u01E9\x03\x02\x02" + + "\x02\u01E9\u01EA\b\x0F\t\x02\u01EA(\x03\x02\x02\x02\u01EB\u01EC\x05\u0163" + + "\xAD\x02\u01EC\u01ED\x05\u015B\xA9\x02\u01ED\u01EE\x05\u0161\xAC\x02\u01EE" + + "\u01EF\x05\u0165\xAE\x02\u01EF\u01F0\x03\x02\x02\x02\u01F0\u01F1\b\x10" + + "\x02\x02\u01F1*\x03\x02\x02\x02\u01F2\u01F3\x05\u0163\xAD\x02\u01F3\u01F4" + + "\x05\u0165\xAE\x02\u01F4\u01F5\x05\u013F\x9B\x02\u01F5\u01F6\x05\u0165" + + "\xAE\x02\u01F6\u01F7\x05\u0163\xAD\x02\u01F7\u01F8\x03\x02\x02\x02\u01F8" + + "\u01F9\b\x11\x02\x02\u01F9,\x03\x02\x02\x02\u01FA\u01FB\x05\u016B\xB1" + + "\x02\u01FB\u01FC\x05\u014D\xA2\x02\u01FC\u01FD\x05\u0147\x9F\x02\u01FD" + + "\u01FE\x05\u0161\xAC\x02\u01FE\u01FF\x05\u0147\x9F\x02\u01FF\u0200\x03" + + "\x02\x02\x02\u0200\u0201\b\x12\x02\x02\u0201.\x03\x02\x02\x02\u0202\u0204" + + "\n\x02\x02\x02\u0203\u0202\x03\x02\x02\x02\u0204\u0205\x03\x02\x02\x02" + + "\u0205\u0203\x03\x02\x02\x02\u0205\u0206\x03\x02\x02\x02\u0206\u0207\x03" + + "\x02\x02\x02\u0207\u0208\b\x13\x02\x02\u02080\x03\x02\x02\x02\u0209\u020A" + + "\x071\x02\x02\u020A\u020B\x071\x02\x02\u020B\u020F\x03\x02\x02\x02\u020C" + + "\u020E\n\x03\x02\x02\u020D\u020C\x03\x02\x02\x02\u020E\u0211\x03\x02\x02" + + "\x02\u020F\u020D\x03\x02\x02\x02\u020F\u0210\x03\x02\x02\x02\u0210\u0213" + + "\x03\x02\x02\x02\u0211\u020F\x03\x02\x02\x02\u0212\u0214\x07\x0F\x02\x02" + + "\u0213\u0212\x03\x02\x02\x02\u0213\u0214\x03\x02\x02\x02\u0214\u0216\x03" + + "\x02\x02\x02\u0215\u0217\x07\f\x02\x02\u0216\u0215\x03\x02\x02\x02\u0216" + + "\u0217\x03\x02\x02\x02\u0217\u0218\x03\x02\x02\x02\u0218\u0219\b\x14\n" + + "\x02\u02192\x03\x02\x02\x02\u021A\u021B\x071\x02\x02\u021B\u021C\x07," + + "\x02\x02\u021C\u0221\x03\x02\x02\x02\u021D\u0220\x053\x15\x02\u021E\u0220" + + "\v\x02\x02\x02\u021F\u021D\x03\x02\x02\x02\u021F\u021E\x03\x02\x02\x02" + + "\u0220\u0223\x03\x02\x02\x02\u0221\u0222\x03\x02\x02\x02\u0221\u021F\x03" + + "\x02\x02\x02\u0222\u0224\x03\x02\x02\x02\u0223\u0221\x03\x02\x02\x02\u0224" + + "\u0225\x07,\x02\x02\u0225\u0226\x071\x02\x02\u0226\u0227\x03\x02\x02\x02" + + "\u0227\u0228\b\x15\n\x02\u02284\x03\x02\x02\x02\u0229\u022B\t\x04\x02" + + "\x02\u022A\u0229\x03\x02\x02\x02\u022B\u022C\x03\x02\x02\x02\u022C\u022A" + + "\x03\x02\x02\x02\u022C\u022D\x03\x02\x02\x02\u022D\u022E\x03\x02\x02\x02" + + "\u022E\u022F\b\x16\n\x02\u022F6\x03\x02\x02\x02\u0230\u0231\x05\xA1L\x02" + + "\u0231\u0232\x03\x02\x02\x02\u0232\u0233\b\x17\v\x02\u0233\u0234\b\x17" + + "\f\x02\u02348\x03\x02\x02\x02\u0235\u0236\x05A\x1C\x02\u0236\u0237\x03" + + "\x02\x02\x02\u0237\u0238\b\x18\r\x02\u0238\u0239\b\x18\x0E\x02\u0239:" + + "\x03\x02\x02\x02\u023A\u023B\x055\x16\x02\u023B\u023C\x03\x02\x02\x02" + + "\u023C\u023D\b\x19\n\x02\u023D<\x03\x02\x02\x02\u023E\u023F\x051\x14\x02" + + "\u023F\u0240\x03\x02\x02\x02\u0240\u0241\b\x1A\n\x02\u0241>\x03\x02\x02" + + "\x02\u0242\u0243\x053\x15\x02\u0243\u0244\x03\x02\x02\x02\u0244\u0245" + + "\b\x1B\n\x02\u0245@\x03\x02\x02\x02\u0246\u0247\x07~\x02\x02\u0247\u0248" + + "\x03\x02\x02\x02\u0248\u0249\b\x1C\x0E\x02\u0249B\x03\x02\x02\x02\u024A" + + "\u024B\t\x05\x02\x02\u024BD\x03\x02\x02\x02\u024C\u024D\t\x06\x02\x02" + + "\u024DF\x03\x02\x02\x02\u024E\u024F\x07^\x02\x02\u024F\u0250\t\x07\x02" + + "\x02\u0250H\x03\x02\x02\x02\u0251\u0252\n\b\x02\x02\u0252J\x03\x02\x02" + + "\x02\u0253\u0255\t\t\x02\x02\u0254\u0256\t\n\x02\x02\u0255\u0254\x03\x02" + + "\x02\x02\u0255\u0256\x03\x02\x02\x02\u0256\u0258\x03\x02\x02\x02\u0257" + + "\u0259\x05C\x1D\x02\u0258\u0257\x03\x02\x02\x02\u0259\u025A\x03\x02\x02" + + "\x02\u025A\u0258\x03\x02\x02\x02\u025A\u025B\x03\x02\x02\x02\u025BL\x03" + + "\x02\x02\x02\u025C\u025D\x07B\x02\x02\u025DN\x03\x02\x02\x02\u025E\u025F" + + "\x07b\x02\x02\u025FP\x03\x02\x02\x02\u0260\u0264\n\v\x02\x02\u0261\u0262" + + "\x07b\x02\x02\u0262\u0264\x07b\x02\x02\u0263\u0260\x03\x02\x02\x02\u0263" + + "\u0261\x03\x02\x02\x02\u0264R\x03\x02\x02\x02\u0265\u0266\x07a\x02\x02" + + "\u0266T\x03\x02\x02\x02\u0267\u026B\x05E\x1E\x02\u0268\u026B\x05C\x1D" + + "\x02\u0269\u026B\x05S%\x02\u026A\u0267\x03\x02\x02\x02\u026A\u0268\x03" + + "\x02\x02\x02\u026A\u0269\x03\x02\x02\x02\u026BV\x03\x02\x02\x02\u026C" + + "\u0271\x07$\x02\x02\u026D\u0270\x05G\x1F\x02\u026E\u0270\x05I \x02\u026F" + + "\u026D\x03\x02\x02\x02\u026F\u026E\x03\x02\x02\x02\u0270\u0273\x03\x02" + + "\x02\x02\u0271\u026F\x03\x02\x02\x02\u0271\u0272\x03\x02\x02\x02\u0272" + + "\u0274\x03\x02\x02\x02\u0273\u0271\x03\x02\x02\x02\u0274\u028A\x07$\x02" + + "\x02\u0275\u0276\x07$\x02\x02\u0276\u0277\x07$\x02\x02\u0277\u0278\x07" + + "$\x02\x02\u0278\u027C\x03\x02\x02\x02\u0279\u027B\n\x03\x02\x02\u027A" + + "\u0279\x03\x02\x02\x02\u027B\u027E\x03\x02\x02\x02\u027C\u027D\x03\x02" + + "\x02\x02\u027C\u027A\x03\x02\x02\x02\u027D\u027F\x03\x02\x02\x02\u027E" + + "\u027C\x03\x02\x02\x02\u027F\u0280\x07$\x02\x02\u0280\u0281\x07$\x02\x02" + + "\u0281\u0282\x07$\x02\x02\u0282\u0284\x03\x02\x02\x02\u0283\u0285\x07" + + "$\x02\x02\u0284\u0283\x03\x02\x02\x02\u0284\u0285\x03\x02\x02\x02\u0285" + + "\u0287\x03\x02\x02\x02\u0286\u0288\x07$\x02\x02\u0287\u0286\x03\x02\x02" + + "\x02\u0287\u0288\x03\x02\x02\x02\u0288\u028A\x03\x02\x02\x02\u0289\u026C" + + "\x03\x02\x02\x02\u0289\u0275\x03\x02\x02\x02\u028AX\x03\x02\x02\x02\u028B" + + "\u028D\x05C\x1D\x02\u028C\u028B\x03\x02\x02\x02\u028D\u028E\x03\x02\x02" + + "\x02\u028E\u028C\x03\x02\x02\x02\u028E\u028F\x03\x02\x02\x02\u028FZ\x03" + + "\x02\x02\x02\u0290\u0292\x05C\x1D\x02\u0291\u0290\x03\x02\x02\x02\u0292" + + "\u0293\x03\x02\x02\x02\u0293\u0291\x03\x02\x02\x02\u0293\u0294\x03\x02" + + "\x02\x02\u0294\u0295\x03\x02\x02\x02\u0295\u0299\x05i0\x02\u0296\u0298" + + "\x05C\x1D\x02\u0297\u0296\x03\x02\x02\x02\u0298\u029B\x03\x02\x02\x02" + + "\u0299\u0297\x03\x02\x02\x02\u0299\u029A\x03\x02\x02\x02\u029A\u02BB\x03" + + "\x02\x02\x02\u029B\u0299\x03\x02\x02\x02\u029C\u029E\x05i0\x02\u029D\u029F" + + "\x05C\x1D\x02\u029E\u029D\x03\x02\x02\x02\u029F\u02A0\x03\x02\x02\x02" + + "\u02A0\u029E\x03\x02\x02\x02\u02A0\u02A1\x03\x02\x02\x02\u02A1\u02BB\x03" + + "\x02\x02\x02\u02A2\u02A4\x05C\x1D\x02\u02A3\u02A2\x03\x02\x02\x02\u02A4" + + "\u02A5\x03\x02\x02\x02\u02A5\u02A3\x03\x02\x02\x02\u02A5\u02A6\x03\x02" + + "\x02\x02\u02A6\u02AE\x03\x02\x02\x02\u02A7\u02AB\x05i0\x02\u02A8\u02AA" + + "\x05C\x1D\x02\u02A9\u02A8\x03\x02\x02\x02\u02AA\u02AD\x03\x02\x02\x02" + + "\u02AB\u02A9\x03\x02\x02\x02\u02AB\u02AC\x03\x02\x02\x02\u02AC\u02AF\x03" + + "\x02\x02\x02\u02AD\u02AB\x03\x02\x02\x02\u02AE\u02A7\x03\x02\x02\x02\u02AE" + + "\u02AF\x03\x02\x02\x02\u02AF\u02B0\x03\x02\x02\x02\u02B0\u02B1\x05K!\x02" + + "\u02B1\u02BB\x03\x02\x02\x02\u02B2\u02B4\x05i0\x02\u02B3\u02B5\x05C\x1D" + + "\x02\u02B4\u02B3\x03\x02\x02\x02\u02B5\u02B6\x03\x02\x02\x02\u02B6\u02B4" + + "\x03\x02\x02\x02\u02B6\u02B7\x03\x02\x02\x02\u02B7\u02B8\x03\x02\x02\x02" + + "\u02B8\u02B9\x05K!\x02\u02B9\u02BB\x03\x02\x02\x02\u02BA\u0291\x03\x02" + + "\x02\x02\u02BA\u029C\x03\x02\x02\x02\u02BA\u02A3\x03\x02\x02\x02\u02BA" + + "\u02B2\x03\x02\x02\x02\u02BB\\\x03\x02\x02\x02\u02BC\u02BD\x05\u0141\x9C" + + "\x02\u02BD\u02BE\x05\u016F\xB3\x02\u02BE^\x03\x02\x02\x02\u02BF\u02C0" + + "\x05\u013F\x9B\x02\u02C0\u02C1\x05\u0159\xA8\x02\u02C1\u02C2\x05\u0145" + + "\x9E\x02\u02C2`\x03\x02\x02\x02\u02C3\u02C4\x05\u013F\x9B\x02\u02C4\u02C5" + + "\x05\u0163\xAD\x02\u02C5\u02C6\x05\u0143\x9D\x02\u02C6b\x03\x02\x02\x02" + + "\u02C7\u02C8\x07?\x02\x02\u02C8d\x03\x02\x02\x02\u02C9\u02CA\x07.\x02" + + "\x02\u02CAf\x03\x02\x02\x02\u02CB\u02CC\x05\u0145\x9E\x02\u02CC\u02CD" + + "\x05\u0147\x9F\x02\u02CD\u02CE\x05\u0163\xAD\x02\u02CE\u02CF\x05\u0143" + + "\x9D\x02\u02CFh\x03\x02\x02\x02\u02D0\u02D1\x070\x02\x02\u02D1j\x03\x02" + + "\x02\x02\u02D2\u02D3\x05\u0149\xA0\x02\u02D3\u02D4\x05\u013F\x9B\x02\u02D4" + + "\u02D5\x05\u0155\xA6\x02\u02D5\u02D6\x05\u0163\xAD\x02\u02D6\u02D7\x05" + + "\u0147\x9F\x02\u02D7l\x03\x02\x02\x02\u02D8\u02D9\x05\u0149\xA0\x02\u02D9" + + "\u02DA\x05\u014F\xA3\x02\u02DA\u02DB\x05\u0161\xAC\x02\u02DB\u02DC\x05" + + "\u0163\xAD\x02\u02DC\u02DD\x05\u0165\xAE\x02\u02DDn\x03\x02\x02\x02\u02DE" + + "\u02DF\x05\u0155\xA6\x02\u02DF\u02E0\x05\u013F\x9B\x02\u02E0\u02E1\x05" + + "\u0163\xAD\x02\u02E1\u02E2\x05\u0165\xAE\x02\u02E2p\x03\x02\x02\x02\u02E3" + + "\u02E4\x07*\x02\x02\u02E4r\x03\x02\x02\x02\u02E5\u02E6\x05\u014F\xA3\x02" + + "\u02E6\u02E7\x05\u0159\xA8\x02\u02E7t\x03\x02\x02\x02\u02E8\u02E9\x05" + + "\u014F\xA3\x02\u02E9\u02EA\x05\u0163\xAD\x02\u02EAv\x03\x02\x02\x02\u02EB" + + "\u02EC\x05\u0155\xA6\x02\u02EC\u02ED\x05\u014F\xA3\x02\u02ED\u02EE\x05" + + "\u0153\xA5\x02\u02EE\u02EF\x05\u0147\x9F\x02\u02EFx\x03\x02\x02\x02\u02F0" + + "\u02F1\x05\u0159\xA8\x02\u02F1\u02F2\x05\u015B\xA9\x02\u02F2\u02F3\x05" + + "\u0165\xAE\x02\u02F3z\x03\x02\x02\x02\u02F4\u02F5\x05\u0159\xA8\x02\u02F5" + + "\u02F6\x05\u0167\xAF\x02\u02F6\u02F7\x05\u0155\xA6\x02\u02F7\u02F8\x05" + + "\u0155\xA6\x02\u02F8|\x03\x02\x02\x02\u02F9\u02FA\x05\u0159\xA8\x02\u02FA" + + "\u02FB\x05\u0167\xAF\x02\u02FB\u02FC\x05\u0155\xA6\x02\u02FC\u02FD\x05" + + "\u0155\xA6\x02\u02FD\u02FE\x05\u0163\xAD\x02\u02FE~\x03\x02\x02\x02\u02FF" + + "\u0300\x05\u015B\xA9\x02\u0300\u0301\x05\u0161\xAC\x02\u0301\x80\x03\x02" + + "\x02\x02\u0302\u0303\x07A\x02\x02\u0303\x82\x03\x02\x02\x02\u0304\u0305" + + "\x05\u0161\xAC\x02\u0305\u0306\x05\u0155\xA6\x02\u0306\u0307\x05\u014F" + + "\xA3\x02\u0307\u0308\x05\u0153\xA5\x02\u0308\u0309\x05\u0147\x9F\x02\u0309" + + "\x84\x03\x02\x02\x02\u030A\u030B\x07+\x02\x02\u030B\x86\x03\x02\x02\x02" + + "\u030C\u030D\x05\u0165\xAE\x02\u030D\u030E\x05\u0161\xAC\x02\u030E\u030F" + + "\x05\u0167\xAF\x02\u030F\u0310\x05\u0147\x9F\x02\u0310\x88\x03\x02\x02" + + "\x02\u0311\u0312\x07?\x02\x02\u0312\u0313\x07?\x02\x02\u0313\x8A\x03\x02" + + "\x02\x02\u0314\u0315\x07?\x02\x02\u0315\u0316\x07\x80\x02\x02\u0316\x8C" + + "\x03\x02\x02\x02\u0317\u0318\x07#\x02\x02\u0318\u0319\x07?\x02\x02\u0319" + + "\x8E\x03\x02\x02\x02\u031A\u031B\x07>\x02\x02\u031B\x90\x03\x02\x02\x02" + + "\u031C\u031D\x07>\x02\x02\u031D\u031E\x07?\x02\x02\u031E\x92\x03\x02\x02" + + "\x02\u031F\u0320\x07@\x02\x02\u0320\x94\x03\x02\x02\x02\u0321\u0322\x07" + + "@\x02\x02\u0322\u0323\x07?\x02\x02\u0323\x96\x03\x02\x02\x02\u0324\u0325" + + "\x07-\x02\x02\u0325\x98\x03\x02\x02\x02\u0326\u0327\x07/\x02\x02\u0327" + + "\x9A\x03\x02\x02\x02\u0328\u0329\x07,\x02\x02\u0329\x9C\x03\x02\x02\x02" + + "\u032A\u032B\x071\x02\x02\u032B\x9E\x03\x02\x02\x02\u032C\u032D\x07\'" + + "\x02\x02\u032D\xA0\x03\x02\x02\x02\u032E\u032F\x07]\x02\x02\u032F\u0330" + + "\x03\x02\x02\x02\u0330\u0331\bL\x02\x02\u0331\u0332\bL\x02\x02\u0332\xA2" + + "\x03\x02\x02\x02\u0333\u0334\x07_\x02\x02\u0334\u0335\x03\x02\x02\x02" + + "\u0335\u0336\bM\x0E\x02\u0336\u0337\bM\x0E\x02\u0337\xA4\x03\x02\x02\x02" + + "\u0338\u033C\x05E\x1E\x02\u0339\u033B\x05U&\x02\u033A\u0339\x03\x02\x02" + + "\x02\u033B\u033E\x03\x02\x02\x02\u033C\u033A\x03\x02\x02\x02\u033C\u033D" + + "\x03\x02\x02\x02\u033D\u0349\x03\x02\x02\x02\u033E\u033C\x03\x02\x02\x02" + + "\u033F\u0342\x05S%\x02\u0340\u0342\x05M\"\x02\u0341\u033F\x03\x02\x02" + + "\x02\u0341\u0340\x03\x02\x02\x02\u0342\u0344\x03\x02\x02\x02\u0343\u0345" + + "\x05U&\x02\u0344\u0343\x03\x02\x02\x02\u0345\u0346\x03\x02\x02\x02\u0346" + + "\u0344\x03\x02\x02\x02\u0346\u0347\x03\x02\x02\x02\u0347\u0349\x03\x02" + + "\x02\x02\u0348\u0338\x03\x02\x02\x02\u0348\u0341\x03\x02\x02\x02\u0349" + + "\xA6\x03\x02\x02\x02\u034A\u034C\x05O#\x02\u034B\u034D\x05Q$\x02\u034C" + + "\u034B\x03\x02\x02\x02\u034D\u034E\x03\x02\x02\x02\u034E\u034C\x03\x02" + + "\x02\x02\u034E\u034F\x03\x02\x02\x02\u034F\u0350\x03\x02\x02\x02\u0350" + + "\u0351\x05O#\x02\u0351\xA8\x03\x02\x02\x02\u0352\u0353\x051\x14\x02\u0353" + + "\u0354\x03\x02\x02\x02\u0354\u0355\bP\n\x02\u0355\xAA\x03\x02\x02\x02" + + "\u0356\u0357\x053\x15\x02\u0357\u0358\x03\x02\x02\x02\u0358\u0359\bQ\n" + + "\x02\u0359\xAC\x03\x02\x02\x02\u035A\u035B\x055\x16\x02\u035B\u035C\x03" + + "\x02\x02\x02\u035C\u035D\bR\n\x02\u035D\xAE\x03\x02\x02\x02\u035E\u035F" + + "\x05A\x1C\x02\u035F\u0360\x03\x02\x02\x02\u0360\u0361\bS\r\x02\u0361\u0362" + + "\bS\x0E\x02\u0362\xB0\x03\x02\x02\x02\u0363\u0364\x05\xA1L\x02\u0364\u0365" + + "\x03\x02\x02\x02\u0365\u0366\bT\v\x02\u0366\xB2\x03\x02\x02\x02\u0367" + + "\u0368\x05\xA3M\x02\u0368\u0369\x03\x02\x02\x02\u0369\u036A\bU\x0F\x02" + + "\u036A\xB4\x03\x02\x02\x02\u036B\u036C\x05e.\x02\u036C\u036D\x03\x02\x02" + + "\x02\u036D\u036E\bV\x10\x02\u036E\xB6\x03\x02\x02\x02\u036F\u0370\x05" + + "c-\x02\u0370\u0371\x03\x02\x02\x02\u0371\u0372\bW\x11\x02\u0372\xB8\x03" + + "\x02\x02\x02\u0373\u0374\x05\u0157\xA7\x02\u0374\u0375\x05\u0147\x9F\x02" + + "\u0375\u0376\x05\u0165\xAE\x02\u0376\u0377\x05\u013F\x9B\x02\u0377\u0378" + + "\x05\u0145\x9E\x02\u0378\u0379\x05\u013F\x9B\x02\u0379\u037A\x05\u0165" + + "\xAE\x02\u037A\u037B\x05\u013F\x9B\x02\u037B\xBA\x03\x02\x02\x02\u037C" + + "\u0380\n\f\x02\x02\u037D\u037E\x071\x02\x02\u037E\u0380\n\r\x02\x02\u037F" + + "\u037C\x03\x02\x02\x02\u037F\u037D\x03\x02\x02\x02\u0380\xBC\x03\x02\x02" + + "\x02\u0381\u0383\x05\xBBY\x02\u0382\u0381\x03\x02\x02\x02\u0383\u0384" + + "\x03\x02\x02\x02\u0384\u0382\x03\x02\x02\x02\u0384\u0385\x03\x02\x02\x02" + + "\u0385\xBE\x03\x02\x02\x02\u0386\u0387\x05\xA7O\x02\u0387\u0388\x03\x02" + + "\x02\x02\u0388\u0389\b[\x12\x02\u0389\xC0\x03\x02\x02\x02\u038A\u038B" + + "\x051\x14\x02\u038B\u038C\x03\x02\x02\x02\u038C\u038D\b\\\n\x02\u038D" + + "\xC2\x03\x02\x02\x02\u038E\u038F\x053\x15\x02\u038F\u0390\x03\x02\x02" + + "\x02\u0390\u0391\b]\n\x02\u0391\xC4\x03\x02\x02\x02\u0392\u0393\x055\x16" + + "\x02\u0393\u0394\x03\x02\x02\x02\u0394\u0395\b^\n\x02\u0395\xC6\x03\x02" + + "\x02\x02\u0396\u0397\x05A\x1C\x02\u0397\u0398\x03\x02\x02\x02\u0398\u0399" + + "\b_\r\x02\u0399\u039A\b_\x0E\x02\u039A\xC8\x03\x02\x02\x02\u039B\u039C" + + "\x05i0\x02\u039C\u039D\x03\x02\x02\x02\u039D\u039E\b`\x13\x02\u039E\xCA" + + "\x03\x02\x02\x02\u039F\u03A0\x05e.\x02\u03A0\u03A1\x03\x02\x02\x02\u03A1" + + "\u03A2\ba\x10\x02\u03A2\xCC\x03\x02\x02\x02\u03A3\u03A8\x05E\x1E\x02\u03A4" + + "\u03A8\x05C\x1D\x02\u03A5\u03A8\x05S%\x02\u03A6\u03A8\x05\x9BI\x02\u03A7" + + "\u03A3\x03\x02\x02\x02\u03A7\u03A4\x03\x02\x02\x02\u03A7\u03A5\x03\x02" + + "\x02\x02\u03A7\u03A6\x03\x02\x02\x02\u03A8\xCE\x03\x02\x02\x02\u03A9\u03AC" + + "\x05E\x1E\x02\u03AA\u03AC\x05\x9BI\x02\u03AB\u03A9\x03\x02\x02\x02\u03AB" + + "\u03AA\x03\x02\x02\x02\u03AC\u03B0\x03\x02\x02\x02\u03AD\u03AF\x05\xCD" + + "b\x02\u03AE\u03AD\x03\x02\x02\x02\u03AF\u03B2\x03\x02\x02\x02\u03B0\u03AE" + + "\x03\x02\x02\x02\u03B0\u03B1\x03\x02\x02\x02\u03B1\u03BD\x03\x02\x02\x02" + + "\u03B2\u03B0\x03\x02\x02\x02\u03B3\u03B6\x05S%\x02\u03B4\u03B6\x05M\"" + + "\x02\u03B5\u03B3\x03\x02\x02\x02\u03B5\u03B4\x03\x02\x02\x02\u03B6\u03B8" + + "\x03\x02\x02\x02\u03B7\u03B9\x05\xCDb\x02\u03B8\u03B7\x03\x02\x02\x02" + + "\u03B9\u03BA\x03\x02\x02\x02\u03BA\u03B8\x03\x02\x02\x02\u03BA\u03BB\x03" + + "\x02\x02\x02\u03BB\u03BD\x03\x02\x02\x02\u03BC\u03AB\x03\x02\x02\x02\u03BC" + + "\u03B5\x03\x02\x02\x02\u03BD\xD0\x03\x02\x02\x02\u03BE\u03BF\x05\xCFc" + + "\x02\u03BF\u03C0\x03\x02\x02\x02\u03C0\u03C1\bd\x14\x02\u03C1\xD2\x03" + + "\x02\x02\x02\u03C2\u03C3\x05\xA7O\x02\u03C3\u03C4\x03\x02\x02\x02\u03C4" + + "\u03C5\be\x12\x02\u03C5\xD4\x03\x02\x02\x02\u03C6\u03C7\x051\x14\x02\u03C7" + + "\u03C8\x03\x02\x02\x02\u03C8\u03C9\bf\n\x02\u03C9\xD6\x03\x02\x02\x02" + + "\u03CA\u03CB\x053\x15\x02\u03CB\u03CC\x03\x02\x02\x02\u03CC\u03CD\bg\n" + + "\x02\u03CD\xD8\x03\x02\x02\x02\u03CE\u03CF\x055\x16\x02\u03CF\u03D0\x03" + + "\x02\x02\x02\u03D0\u03D1\bh\n\x02\u03D1\xDA\x03\x02\x02\x02\u03D2\u03D3" + + "\x05A\x1C\x02\u03D3\u03D4\x03\x02\x02\x02\u03D4\u03D5\bi\r\x02\u03D5\u03D6" + + "\bi\x0E\x02\u03D6\xDC\x03\x02\x02\x02\u03D7\u03D8\x05c-\x02\u03D8\u03D9" + + "\x03\x02\x02\x02\u03D9\u03DA\bj\x11\x02\u03DA\xDE\x03\x02\x02\x02\u03DB" + + "\u03DC\x05e.\x02\u03DC\u03DD\x03\x02\x02\x02\u03DD\u03DE\bk\x10\x02\u03DE" + + "\xE0\x03\x02\x02\x02\u03DF\u03E0\x05i0\x02\u03E0\u03E1\x03\x02\x02\x02" + + "\u03E1\u03E2\bl\x13\x02\u03E2\xE2\x03\x02\x02\x02\u03E3\u03E4\x05\u013F" + + "\x9B\x02\u03E4\u03E5\x05\u0163\xAD\x02\u03E5\xE4\x03\x02\x02\x02\u03E6" + + "\u03E7\x05\xA7O\x02\u03E7\u03E8\x03\x02\x02\x02\u03E8\u03E9\bn\x12\x02" + + "\u03E9\xE6\x03\x02\x02\x02\u03EA\u03EB\x05\xCFc\x02\u03EB\u03EC\x03\x02" + + "\x02\x02\u03EC\u03ED\bo\x14\x02\u03ED\xE8\x03\x02\x02\x02\u03EE\u03EF" + + "\x051\x14\x02\u03EF\u03F0\x03\x02\x02\x02\u03F0\u03F1\bp\n\x02\u03F1\xEA" + + "\x03\x02\x02\x02\u03F2\u03F3\x053\x15\x02\u03F3\u03F4\x03\x02\x02\x02" + + "\u03F4\u03F5\bq\n\x02\u03F5\xEC\x03\x02\x02\x02\u03F6\u03F7\x055\x16\x02" + + "\u03F7\u03F8\x03\x02\x02\x02\u03F8\u03F9\br\n\x02\u03F9\xEE\x03\x02\x02" + + "\x02\u03FA\u03FB\x05A\x1C\x02\u03FB\u03FC\x03\x02\x02\x02\u03FC\u03FD" + + "\bs\r\x02\u03FD\u03FE\bs\x0E\x02\u03FE\xF0\x03\x02\x02\x02\u03FF\u0400" + + "\x05\xA1L\x02\u0400\u0401\x03\x02\x02\x02\u0401\u0402\bt\v\x02\u0402\u0403" + + "\bt\x15\x02\u0403\xF2\x03\x02\x02\x02\u0404\u0405\x05\u015B\xA9\x02\u0405" + + "\u0406\x05\u0159\xA8\x02\u0406\u0407\x03\x02\x02\x02\u0407\u0408\bu\x16" + + "\x02\u0408\xF4\x03\x02\x02\x02\u0409\u040A\x05\u016B\xB1\x02\u040A\u040B" + + "\x05\u014F\xA3\x02\u040B\u040C\x05\u0165\xAE\x02\u040C\u040D\x05\u014D" + + "\xA2\x02\u040D\u040E\x03\x02\x02\x02\u040E\u040F\bv\x16\x02\u040F\xF6" + + "\x03\x02\x02\x02\u0410\u0411\n\x0E\x02\x02\u0411\xF8\x03\x02\x02\x02\u0412" + + "\u0414\x05\xF7w\x02\u0413\u0412\x03\x02\x02\x02\u0414\u0415\x03\x02\x02" + + "\x02\u0415\u0413\x03\x02\x02\x02\u0415\u0416\x03\x02\x02\x02\u0416\u0417" + + "\x03\x02\x02\x02\u0417\u0418\x05\u0135\x96\x02\u0418\u041A\x03\x02\x02" + + "\x02\u0419\u0413\x03\x02\x02\x02\u0419\u041A\x03\x02\x02\x02\u041A\u041C" + + "\x03\x02\x02\x02\u041B\u041D\x05\xF7w\x02\u041C\u041B\x03\x02\x02\x02" + + "\u041D\u041E\x03\x02\x02\x02\u041E\u041C\x03\x02\x02\x02\u041E\u041F\x03" + + "\x02\x02\x02\u041F\xFA\x03\x02\x02\x02\u0420\u0421\x05\xA7O\x02\u0421" + + "\u0422\x03\x02\x02\x02\u0422\u0423\by\x12\x02\u0423\xFC\x03\x02\x02\x02" + + "\u0424\u0425\x05\xF9x\x02\u0425\u0426\x03\x02\x02\x02\u0426\u0427\bz\x17" + + "\x02\u0427\xFE\x03\x02\x02\x02\u0428\u0429\x051\x14\x02\u0429\u042A\x03" + + "\x02\x02\x02\u042A\u042B\b{\n\x02\u042B\u0100\x03\x02\x02\x02\u042C\u042D" + + "\x053\x15\x02\u042D\u042E\x03\x02\x02\x02\u042E\u042F\b|\n\x02\u042F\u0102" + + "\x03\x02\x02\x02\u0430\u0431\x055\x16\x02\u0431\u0432\x03\x02\x02\x02" + + "\u0432\u0433\b}\n\x02\u0433\u0104\x03\x02\x02\x02\u0434\u0435\x05A\x1C" + + "\x02\u0435\u0436\x03\x02\x02\x02\u0436\u0437\b~\r\x02\u0437\u0438\b~\x0E" + + "\x02\u0438\u0439\b~\x0E\x02\u0439\u0106\x03\x02\x02\x02\u043A\u043B\x05" + + "c-\x02\u043B\u043C\x03\x02\x02\x02\u043C\u043D\b\x7F\x11\x02\u043D\u0108" + + "\x03\x02\x02\x02\u043E\u043F\x05e.\x02\u043F\u0440\x03\x02\x02\x02\u0440" + + "\u0441\b\x80\x10\x02\u0441\u010A\x03\x02\x02\x02\u0442\u0443\x05i0\x02" + + "\u0443\u0444\x03\x02\x02\x02\u0444\u0445\b\x81\x13\x02\u0445\u010C\x03" + + "\x02\x02\x02\u0446\u0447\x05\xF5v\x02\u0447\u0448\x03\x02\x02\x02\u0448" + + "\u0449\b\x82\x18\x02\u0449\u010E\x03\x02\x02\x02\u044A\u044B\x05\xCFc" + + "\x02\u044B\u044C\x03\x02\x02\x02\u044C\u044D\b\x83"; private static readonly _serializedATNSegment2: string = - "\x02\u044B\u044C\x03\x02\x02\x02\u044C\u044D\b\x82\x13\x02\u044D\u010E" + - "\x03\x02\x02\x02\u044E\u044F\x05\xF7w\x02\u044F\u0450\x03\x02\x02\x02" + - "\u0450\u0451\b\x83\x18\x02\u0451\u0110\x03\x02\x02\x02\u0452\u0453\x05" + - "\xD1d\x02\u0453\u0454\x03\x02\x02\x02\u0454\u0455\b\x84\x14\x02\u0455" + - "\u0112\x03\x02\x02\x02\u0456\u0457\x05\xA9P\x02\u0457\u0458\x03\x02\x02" + - "\x02\u0458\u0459\b\x85\x12\x02\u0459\u0114\x03\x02\x02\x02\u045A\u045B" + - "\x053\x15\x02\u045B\u045C\x03\x02\x02\x02\u045C\u045D\b\x86\n\x02\u045D" + - "\u0116\x03\x02\x02\x02\u045E\u045F\x055\x16\x02\u045F\u0460\x03\x02\x02" + - "\x02\u0460\u0461\b\x87\n\x02\u0461\u0118\x03\x02\x02\x02\u0462\u0463\x05" + - "7\x17\x02\u0463\u0464\x03\x02\x02\x02\u0464\u0465\b\x88\n\x02\u0465\u011A" + - "\x03\x02\x02\x02\u0466\u0467\x05C\x1D\x02\u0467\u0468\x03\x02\x02\x02" + - "\u0468\u0469\b\x89\r\x02\u0469\u046A\b\x89\x0E\x02\u046A\u011C\x03\x02" + - "\x02\x02\u046B\u046C\x05k1\x02\u046C\u046D\x03\x02\x02\x02\u046D\u046E" + - "\b\x8A\x13\x02\u046E\u011E\x03\x02\x02\x02\u046F\u0470\x05\xA9P\x02\u0470" + - "\u0471\x03\x02\x02\x02\u0471\u0472\b\x8B\x12\x02\u0472\u0120\x03\x02\x02" + - "\x02\u0473\u0474\x05\xA7O\x02\u0474\u0475\x03\x02\x02\x02\u0475\u0476" + - "\b\x8C\x19\x02\u0476\u0122\x03\x02\x02\x02\u0477\u0478\x053\x15\x02\u0478" + - "\u0479\x03\x02\x02\x02\u0479\u047A\b\x8D\n\x02\u047A\u0124\x03\x02\x02" + - "\x02\u047B\u047C\x055\x16\x02\u047C\u047D\x03\x02\x02\x02\u047D\u047E" + - "\b\x8E\n\x02\u047E\u0126\x03\x02\x02\x02\u047F\u0480\x057\x17\x02\u0480" + - "\u0481\x03\x02\x02\x02\u0481\u0482\b\x8F\n\x02\u0482\u0128\x03\x02\x02" + - "\x02\u0483\u0484\x05C\x1D\x02\u0484\u0485\x03\x02\x02\x02\u0485\u0486" + - "\b\x90\r\x02\u0486\u0487\b\x90\x0E\x02\u0487\u012A\x03\x02\x02\x02\u0488" + - "\u0489\x05\u0151\xA4\x02\u0489\u048A\x05\u015B\xA9\x02\u048A\u048B\x05" + - "\u014B\xA1\x02\u048B\u048C\x05\u015D\xAA\x02\u048C\u012C\x03\x02\x02\x02" + - "\u048D\u048E\x05\u014B\xA1\x02\u048E\u048F\x05\u0169\xB0\x02\u048F\u0490" + - "\x05\u015B\xA9\x02\u0490\u0491\x05\u0145\x9E\x02\u0491\u0492\x05\u0167" + - "\xAF\x02\u0492\u0493\x05\u0151\xA4\x02\u0493\u0494\x05\u015D\xAA\x02\u0494" + - "\u0495\x05\u015B\xA9\x02\u0495\u0496\x05\u0165\xAE\x02\u0496\u012E\x03" + - "\x02\x02\x02\u0497\u0498\x053\x15\x02\u0498\u0499\x03\x02\x02\x02\u0499" + - "\u049A\b\x93\n\x02\u049A\u0130\x03\x02\x02\x02\u049B\u049C\x055\x16\x02" + - "\u049C\u049D\x03\x02\x02\x02\u049D\u049E\b\x94\n\x02\u049E\u0132\x03\x02" + - "\x02\x02\u049F\u04A0\x057\x17\x02\u04A0\u04A1\x03\x02\x02\x02\u04A1\u04A2" + - "\b\x95\n\x02\u04A2\u0134\x03\x02\x02\x02\u04A3\u04A4\x05\xA5N\x02\u04A4" + - "\u04A5\x03\x02\x02\x02\u04A5\u04A6\b\x96\x0F\x02\u04A6\u04A7\b\x96\x0E" + - "\x02\u04A7\u0136\x03\x02\x02\x02\u04A8\u04A9\x07<\x02\x02\u04A9\u0138" + - "\x03\x02\x02\x02\u04AA\u04B0\x05O#\x02\u04AB\u04B0\x05E\x1E\x02\u04AC" + - "\u04B0\x05k1\x02\u04AD\u04B0\x05G\x1F\x02\u04AE\u04B0\x05U&\x02\u04AF" + - "\u04AA\x03\x02\x02\x02\u04AF\u04AB\x03\x02\x02\x02\u04AF\u04AC\x03\x02" + - "\x02\x02\u04AF\u04AD\x03\x02\x02\x02\u04AF\u04AE\x03\x02\x02\x02\u04B0" + - "\u04B1\x03\x02\x02\x02\u04B1\u04AF\x03\x02\x02\x02\u04B1\u04B2\x03\x02" + - "\x02\x02\u04B2\u013A\x03\x02\x02\x02\u04B3\u04B4\x053\x15\x02\u04B4\u04B5" + - "\x03\x02\x02\x02\u04B5\u04B6\b\x99\n\x02\u04B6\u013C\x03\x02\x02\x02\u04B7" + - "\u04B8\x055\x16\x02\u04B8\u04B9\x03\x02\x02\x02\u04B9\u04BA\b\x9A\n\x02" + - "\u04BA\u013E\x03\x02\x02\x02\u04BB\u04BC\x057\x17\x02\u04BC\u04BD\x03" + - "\x02\x02\x02\u04BD\u04BE\b\x9B\n\x02\u04BE\u0140\x03\x02\x02\x02\u04BF" + - "\u04C0\t\x0F\x02\x02\u04C0\u0142\x03\x02\x02\x02\u04C1\u04C2\t\x10\x02" + - "\x02\u04C2\u0144\x03\x02\x02\x02\u04C3\u04C4\t\x11\x02\x02\u04C4\u0146" + - "\x03\x02\x02\x02\u04C5\u04C6\t\x12\x02\x02\u04C6\u0148\x03\x02\x02\x02" + - "\u04C7\u04C8\t\t\x02\x02\u04C8\u014A\x03\x02\x02\x02\u04C9\u04CA\t\x13" + - "\x02\x02\u04CA\u014C\x03\x02\x02\x02\u04CB\u04CC\t\x14\x02\x02\u04CC\u014E" + - "\x03\x02\x02\x02\u04CD\u04CE\t\x15\x02\x02\u04CE\u0150\x03\x02\x02\x02" + - "\u04CF\u04D0\t\x16\x02\x02\u04D0\u0152\x03\x02\x02\x02\u04D1\u04D2\t\x17" + - "\x02\x02\u04D2\u0154\x03\x02\x02\x02\u04D3\u04D4\t\x18\x02\x02\u04D4\u0156" + - "\x03\x02\x02\x02\u04D5\u04D6\t\x19\x02\x02\u04D6\u0158\x03\x02\x02\x02" + - "\u04D7\u04D8\t\x1A\x02\x02\u04D8\u015A\x03\x02\x02\x02\u04D9\u04DA\t\x1B" + - "\x02\x02\u04DA\u015C\x03\x02\x02\x02\u04DB\u04DC\t\x1C\x02\x02\u04DC\u015E" + - "\x03\x02\x02\x02\u04DD\u04DE\t\x1D\x02\x02\u04DE\u0160\x03\x02\x02\x02" + - "\u04DF\u04E0\t\x1E\x02\x02\u04E0\u0162\x03\x02\x02\x02\u04E1\u04E2\t\x1F" + - "\x02\x02\u04E2\u0164\x03\x02\x02\x02\u04E3\u04E4\t \x02\x02\u04E4\u0166" + - "\x03\x02\x02\x02\u04E5\u04E6\t!\x02\x02\u04E6\u0168\x03\x02\x02\x02\u04E7" + - "\u04E8\t\"\x02\x02\u04E8\u016A\x03\x02\x02\x02\u04E9\u04EA\t#\x02\x02" + - "\u04EA\u016C\x03\x02\x02\x02\u04EB\u04EC\t$\x02\x02\u04EC\u016E\x03\x02" + - "\x02\x02\u04ED\u04EE\t%\x02\x02\u04EE\u0170\x03\x02\x02\x02\u04EF\u04F0" + - "\t&\x02\x02\u04F0\u0172\x03\x02\x02\x02\u04F1\u04F2\t\'\x02\x02\u04F2" + - "\u0174\x03\x02\x02\x028\x02\x03\x04\x05\x06\x07\b\t\n\v\f\u0211\u021B" + - "\u021F\u0222\u022B\u022D\u0238\u0261\u0266\u026F\u0276\u027B\u027D\u0288" + - "\u0290\u0293\u0295\u029A\u029F\u02A5\u02AC\u02B1\u02B7\u02BA\u02C2\u02C6" + - "\u0348\u034D\u0352\u0354\u035A\u038B\u0390\u03B3\u03B7\u03BC\u03C1\u03C6" + - "\u03C8\u0420\u0425\u04AF\u04B1\x1A\x07\x04\x02\x07\x06\x02\x07\b\x02\x07" + - "\x03\x02\x07\x05\x02\x07\n\x02\x07\x07\x02\x07\v\x02\x02\x03\x02\tB\x02" + - "\x07\x02\x02\t\x1C\x02\x06\x02\x02\tC\x02\t$\x02\t#\x02\tE\x02\t&\x02" + - "\tN\x02\x07\f\x02\x07\t\x02\tX\x02\tW\x02\tD\x02"; + "\x14\x02\u044D\u0110\x03\x02\x02\x02\u044E\u044F\x05\xA7O\x02\u044F\u0450" + + "\x03\x02\x02\x02\u0450\u0451\b\x84\x12\x02\u0451\u0112\x03\x02\x02\x02" + + "\u0452\u0453\x051\x14\x02\u0453\u0454\x03\x02\x02\x02\u0454\u0455\b\x85" + + "\n\x02\u0455\u0114\x03\x02\x02\x02\u0456\u0457\x053\x15\x02\u0457\u0458" + + "\x03\x02\x02\x02\u0458\u0459\b\x86\n\x02\u0459\u0116\x03\x02\x02\x02\u045A" + + "\u045B\x055\x16\x02\u045B\u045C\x03\x02\x02\x02\u045C\u045D\b\x87\n\x02" + + "\u045D\u0118\x03\x02\x02\x02\u045E\u045F\x05A\x1C\x02\u045F\u0460\x03" + + "\x02\x02\x02\u0460\u0461\b\x88\r\x02\u0461\u0462\b\x88\x0E\x02\u0462\u011A" + + "\x03\x02\x02\x02\u0463\u0464\x05i0\x02\u0464\u0465\x03\x02\x02\x02\u0465" + + "\u0466\b\x89\x13\x02\u0466\u011C\x03\x02\x02\x02\u0467\u0468\x05\xA7O" + + "\x02\u0468\u0469\x03\x02\x02\x02\u0469\u046A\b\x8A\x12\x02\u046A\u011E" + + "\x03\x02\x02\x02\u046B\u046C\x05\xA5N\x02\u046C\u046D\x03\x02\x02\x02" + + "\u046D\u046E\b\x8B\x19\x02\u046E\u0120\x03\x02\x02\x02\u046F\u0470\x05" + + "1\x14\x02\u0470\u0471\x03\x02\x02\x02\u0471\u0472\b\x8C\n\x02\u0472\u0122" + + "\x03\x02\x02\x02\u0473\u0474\x053\x15\x02\u0474\u0475\x03\x02\x02\x02" + + "\u0475\u0476\b\x8D\n\x02\u0476\u0124\x03\x02\x02\x02\u0477\u0478\x055" + + "\x16\x02\u0478\u0479\x03\x02\x02\x02\u0479\u047A\b\x8E\n\x02\u047A\u0126" + + "\x03\x02\x02\x02\u047B\u047C\x05A\x1C\x02\u047C\u047D\x03\x02\x02\x02" + + "\u047D\u047E\b\x8F\r\x02\u047E\u047F\b\x8F\x0E\x02\u047F\u0128\x03\x02" + + "\x02\x02\u0480\u0481\x05\u014F\xA3\x02\u0481\u0482\x05\u0159\xA8\x02\u0482" + + "\u0483\x05\u0149\xA0\x02\u0483\u0484\x05\u015B\xA9\x02\u0484\u012A\x03" + + "\x02\x02\x02\u0485\u0486\x05\u0149\xA0\x02\u0486\u0487\x05\u0167\xAF\x02" + + "\u0487\u0488\x05\u0159\xA8\x02\u0488\u0489\x05\u0143\x9D\x02\u0489\u048A" + + "\x05\u0165\xAE\x02\u048A\u048B\x05\u014F\xA3\x02\u048B\u048C\x05\u015B" + + "\xA9\x02\u048C\u048D\x05\u0159\xA8\x02\u048D\u048E\x05\u0163\xAD\x02\u048E" + + "\u012C\x03\x02\x02\x02\u048F\u0490\x051\x14\x02\u0490\u0491\x03\x02\x02" + + "\x02\u0491\u0492\b\x92\n\x02\u0492\u012E\x03\x02\x02\x02\u0493\u0494\x05" + + "3\x15\x02\u0494\u0495\x03\x02\x02\x02\u0495\u0496\b\x93\n\x02\u0496\u0130" + + "\x03\x02\x02\x02\u0497\u0498\x055\x16\x02\u0498\u0499\x03\x02\x02\x02" + + "\u0499\u049A\b\x94\n\x02\u049A\u0132\x03\x02\x02\x02\u049B\u049C\x05\xA3" + + "M\x02\u049C\u049D\x03\x02\x02\x02\u049D\u049E\b\x95\x0F\x02\u049E\u049F" + + "\b\x95\x0E\x02\u049F\u0134\x03\x02\x02\x02\u04A0\u04A1\x07<\x02\x02\u04A1" + + "\u0136\x03\x02\x02\x02\u04A2\u04A8\x05M\"\x02\u04A3\u04A8\x05C\x1D\x02" + + "\u04A4\u04A8\x05i0\x02\u04A5\u04A8\x05E\x1E\x02\u04A6\u04A8\x05S%\x02" + + "\u04A7\u04A2\x03\x02\x02\x02\u04A7\u04A3\x03\x02\x02\x02\u04A7\u04A4\x03" + + "\x02\x02\x02\u04A7\u04A5\x03\x02\x02\x02\u04A7\u04A6\x03\x02\x02\x02\u04A8" + + "\u04A9\x03\x02\x02\x02\u04A9\u04A7\x03\x02\x02\x02\u04A9\u04AA\x03\x02" + + "\x02\x02\u04AA\u0138\x03\x02\x02\x02\u04AB\u04AC\x051\x14\x02\u04AC\u04AD" + + "\x03\x02\x02\x02\u04AD\u04AE\b\x98\n\x02\u04AE\u013A\x03\x02\x02\x02\u04AF" + + "\u04B0\x053\x15\x02\u04B0\u04B1\x03\x02\x02\x02\u04B1\u04B2\b\x99\n\x02" + + "\u04B2\u013C\x03\x02\x02\x02\u04B3\u04B4\x055\x16\x02\u04B4\u04B5\x03" + + "\x02\x02\x02\u04B5\u04B6\b\x9A\n\x02\u04B6\u013E\x03\x02\x02\x02\u04B7" + + "\u04B8\t\x0F\x02\x02\u04B8\u0140\x03\x02\x02\x02\u04B9\u04BA\t\x10\x02" + + "\x02\u04BA\u0142\x03\x02\x02\x02\u04BB\u04BC\t\x11\x02\x02\u04BC\u0144" + + "\x03\x02\x02\x02\u04BD\u04BE\t\x12\x02\x02\u04BE\u0146\x03\x02\x02\x02" + + "\u04BF\u04C0\t\t\x02\x02\u04C0\u0148\x03\x02\x02\x02\u04C1\u04C2\t\x13" + + "\x02\x02\u04C2\u014A\x03\x02\x02\x02\u04C3\u04C4\t\x14\x02\x02\u04C4\u014C" + + "\x03\x02\x02\x02\u04C5\u04C6\t\x15\x02\x02\u04C6\u014E\x03\x02\x02\x02" + + "\u04C7\u04C8\t\x16\x02\x02\u04C8\u0150\x03\x02\x02\x02\u04C9\u04CA\t\x17" + + "\x02\x02\u04CA\u0152\x03\x02\x02\x02\u04CB\u04CC\t\x18\x02\x02\u04CC\u0154" + + "\x03\x02\x02\x02\u04CD\u04CE\t\x19\x02\x02\u04CE\u0156\x03\x02\x02\x02" + + "\u04CF\u04D0\t\x1A\x02\x02\u04D0\u0158\x03\x02\x02\x02\u04D1\u04D2\t\x1B" + + "\x02\x02\u04D2\u015A\x03\x02\x02\x02\u04D3\u04D4\t\x1C\x02\x02\u04D4\u015C" + + "\x03\x02\x02\x02\u04D5\u04D6\t\x1D\x02\x02\u04D6\u015E\x03\x02\x02\x02" + + "\u04D7\u04D8\t\x1E\x02\x02\u04D8\u0160\x03\x02\x02\x02\u04D9\u04DA\t\x1F" + + "\x02\x02\u04DA\u0162\x03\x02\x02\x02\u04DB\u04DC\t \x02\x02\u04DC\u0164" + + "\x03\x02\x02\x02\u04DD\u04DE\t!\x02\x02\u04DE\u0166\x03\x02\x02\x02\u04DF" + + "\u04E0\t\"\x02\x02\u04E0\u0168\x03\x02\x02\x02\u04E1\u04E2\t#\x02\x02" + + "\u04E2\u016A\x03\x02\x02\x02\u04E3\u04E4\t$\x02\x02\u04E4\u016C\x03\x02" + + "\x02\x02\u04E5\u04E6\t%\x02\x02\u04E6\u016E\x03\x02\x02\x02\u04E7\u04E8" + + "\t&\x02\x02\u04E8\u0170\x03\x02\x02\x02\u04E9\u04EA\t\'\x02\x02\u04EA" + + "\u0172\x03\x02\x02\x029\x02\x03\x04\x05\x06\x07\b\t\n\v\f\u0205\u020F" + + "\u0213\u0216\u021F\u0221\u022C\u0255\u025A\u0263\u026A\u026F\u0271\u027C" + + "\u0284\u0287\u0289\u028E\u0293\u0299\u02A0\u02A5\u02AB\u02AE\u02B6\u02BA" + + "\u033C\u0341\u0346\u0348\u034E\u037F\u0384\u03A7\u03AB\u03B0\u03B5\u03BA" + + "\u03BC\u0415\u0419\u041E\u04A7\u04A9\x1A\x07\x04\x02\x07\x06\x02\x07\b" + + "\x02\x07\x03\x02\x07\x05\x02\x07\n\x02\x07\x07\x02\x07\v\x02\x02\x03\x02" + + "\tA\x02\x07\x02\x02\t\x1B\x02\x06\x02\x02\tB\x02\t#\x02\t\"\x02\tD\x02" + + "\t%\x02\tM\x02\x07\f\x02\x07\t\x02\tW\x02\tV\x02\tC\x02"; public static readonly _serializedATN: string = Utils.join( [ esql_lexer._serializedATNSegment0, diff --git a/packages/kbn-monaco/src/esql/antlr/esql_parser.g4 b/packages/kbn-monaco/src/esql/antlr/esql_parser.g4 index 57e7097eb03f8..06b955f84cd90 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_parser.g4 +++ b/packages/kbn-monaco/src/esql/antlr/esql_parser.g4 @@ -102,7 +102,16 @@ fromCommand ; metadata - : OPENING_BRACKET METADATA fromIdentifier (COMMA fromIdentifier)* CLOSING_BRACKET + : metadataOption + | deprecated_metadata + ; + +metadataOption + : METADATA fromIdentifier (COMMA fromIdentifier)* + ; + +deprecated_metadata + : OPENING_BRACKET metadataOption CLOSING_BRACKET ; @@ -168,7 +177,6 @@ orderExpression keepCommand : KEEP qualifiedNamePattern (COMMA qualifiedNamePattern)* - | PROJECT qualifiedNamePattern (COMMA qualifiedNamePattern)* ; dropCommand @@ -242,13 +250,9 @@ showCommand ; enrichCommand - : ENRICH setting* policyName=ENRICH_POLICY_NAME (ON matchField=qualifiedNamePattern)? (WITH enrichWithClause (COMMA enrichWithClause)*)? + : ENRICH policyName=ENRICH_POLICY_NAME (ON matchField=qualifiedNamePattern)? (WITH enrichWithClause (COMMA enrichWithClause)*)? ; enrichWithClause : (newName=qualifiedNamePattern ASSIGN)? enrichField=qualifiedNamePattern ; - -setting - : OPENING_BRACKET name=SETTING COLON value=SETTING CLOSING_BRACKET - ; \ No newline at end of file diff --git a/packages/kbn-monaco/src/esql/antlr/esql_parser.interp b/packages/kbn-monaco/src/esql/antlr/esql_parser.interp index ed3cd7a8d9227..3f76dcbcff6a0 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_parser.interp +++ b/packages/kbn-monaco/src/esql/antlr/esql_parser.interp @@ -24,7 +24,6 @@ null null null null -null '|' null null @@ -119,7 +118,6 @@ INLINESTATS KEEP LIMIT MV_EXPAND -PROJECT RENAME ROW SHOW @@ -231,6 +229,8 @@ fields field fromCommand metadata +metadataOption +deprecated_metadata evalCommand statsCommand inlinestatsCommand @@ -263,8 +263,7 @@ subqueryExpression showCommand enrichCommand enrichWithClause -setting atn: -[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 3, 107, 525, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 4, 38, 9, 38, 4, 39, 9, 39, 4, 40, 9, 40, 4, 41, 9, 41, 4, 42, 9, 42, 4, 43, 9, 43, 4, 44, 9, 44, 4, 45, 9, 45, 4, 46, 9, 46, 4, 47, 9, 47, 4, 48, 9, 48, 4, 49, 9, 49, 4, 50, 9, 50, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 7, 3, 110, 10, 3, 12, 3, 14, 3, 113, 11, 3, 3, 4, 3, 4, 3, 4, 3, 4, 5, 4, 119, 10, 4, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 5, 5, 134, 10, 5, 3, 6, 3, 6, 3, 6, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 5, 7, 146, 10, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 7, 7, 153, 10, 7, 12, 7, 14, 7, 156, 11, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 5, 7, 163, 10, 7, 3, 7, 3, 7, 5, 7, 167, 10, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 7, 7, 175, 10, 7, 12, 7, 14, 7, 178, 11, 7, 3, 8, 3, 8, 5, 8, 182, 10, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 5, 8, 189, 10, 8, 3, 8, 3, 8, 3, 8, 5, 8, 194, 10, 8, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 5, 9, 201, 10, 9, 3, 10, 3, 10, 3, 10, 3, 10, 5, 10, 207, 10, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 7, 10, 215, 10, 10, 12, 10, 14, 10, 218, 11, 10, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 5, 11, 227, 10, 11, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 7, 12, 235, 10, 12, 12, 12, 14, 12, 238, 11, 12, 5, 12, 240, 10, 12, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 14, 3, 14, 3, 14, 7, 14, 250, 10, 14, 12, 14, 14, 14, 253, 11, 14, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 5, 15, 260, 10, 15, 3, 16, 3, 16, 3, 16, 3, 16, 7, 16, 266, 10, 16, 12, 16, 14, 16, 269, 11, 16, 3, 16, 5, 16, 272, 10, 16, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 7, 17, 279, 10, 17, 12, 17, 14, 17, 282, 11, 17, 3, 17, 3, 17, 3, 18, 3, 18, 3, 18, 3, 19, 3, 19, 5, 19, 291, 10, 19, 3, 19, 3, 19, 5, 19, 295, 10, 19, 3, 20, 3, 20, 3, 20, 3, 20, 5, 20, 301, 10, 20, 3, 21, 3, 21, 3, 22, 3, 22, 3, 22, 7, 22, 308, 10, 22, 12, 22, 14, 22, 311, 11, 22, 3, 23, 3, 23, 3, 23, 7, 23, 316, 10, 23, 12, 23, 14, 23, 319, 11, 23, 3, 24, 3, 24, 3, 25, 3, 25, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 7, 26, 338, 10, 26, 12, 26, 14, 26, 341, 11, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 7, 26, 349, 10, 26, 12, 26, 14, 26, 352, 11, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 7, 26, 360, 10, 26, 12, 26, 14, 26, 363, 11, 26, 3, 26, 3, 26, 5, 26, 367, 10, 26, 3, 27, 3, 27, 3, 27, 3, 28, 3, 28, 3, 28, 3, 28, 7, 28, 376, 10, 28, 12, 28, 14, 28, 379, 11, 28, 3, 29, 3, 29, 5, 29, 383, 10, 29, 3, 29, 3, 29, 5, 29, 387, 10, 29, 3, 30, 3, 30, 3, 30, 3, 30, 7, 30, 393, 10, 30, 12, 30, 14, 30, 396, 11, 30, 3, 30, 3, 30, 3, 30, 3, 30, 7, 30, 402, 10, 30, 12, 30, 14, 30, 405, 11, 30, 5, 30, 407, 10, 30, 3, 31, 3, 31, 3, 31, 3, 31, 7, 31, 413, 10, 31, 12, 31, 14, 31, 416, 11, 31, 3, 32, 3, 32, 3, 32, 3, 32, 7, 32, 422, 10, 32, 12, 32, 14, 32, 425, 11, 32, 3, 33, 3, 33, 3, 33, 3, 33, 3, 34, 3, 34, 3, 34, 3, 34, 5, 34, 435, 10, 34, 3, 35, 3, 35, 3, 35, 3, 35, 3, 36, 3, 36, 3, 36, 3, 37, 3, 37, 3, 37, 7, 37, 447, 10, 37, 12, 37, 14, 37, 450, 11, 37, 3, 38, 3, 38, 3, 38, 3, 38, 3, 39, 3, 39, 3, 40, 3, 40, 5, 40, 460, 10, 40, 3, 41, 5, 41, 463, 10, 41, 3, 41, 3, 41, 3, 42, 5, 42, 468, 10, 42, 3, 42, 3, 42, 3, 43, 3, 43, 3, 44, 3, 44, 3, 45, 3, 45, 3, 45, 3, 46, 3, 46, 3, 46, 3, 46, 3, 47, 3, 47, 3, 47, 3, 47, 5, 47, 487, 10, 47, 3, 48, 3, 48, 7, 48, 491, 10, 48, 12, 48, 14, 48, 494, 11, 48, 3, 48, 3, 48, 3, 48, 5, 48, 499, 10, 48, 3, 48, 3, 48, 3, 48, 3, 48, 7, 48, 505, 10, 48, 12, 48, 14, 48, 508, 11, 48, 5, 48, 510, 10, 48, 3, 49, 3, 49, 3, 49, 5, 49, 515, 10, 49, 3, 49, 3, 49, 3, 50, 3, 50, 3, 50, 3, 50, 3, 50, 3, 50, 3, 50, 2, 2, 5, 4, 12, 18, 51, 2, 2, 4, 2, 6, 2, 8, 2, 10, 2, 12, 2, 14, 2, 16, 2, 18, 2, 20, 2, 22, 2, 24, 2, 26, 2, 28, 2, 30, 2, 32, 2, 34, 2, 36, 2, 38, 2, 40, 2, 42, 2, 44, 2, 46, 2, 48, 2, 50, 2, 52, 2, 54, 2, 56, 2, 58, 2, 60, 2, 62, 2, 64, 2, 66, 2, 68, 2, 70, 2, 72, 2, 74, 2, 76, 2, 78, 2, 80, 2, 82, 2, 84, 2, 86, 2, 88, 2, 90, 2, 92, 2, 94, 2, 96, 2, 98, 2, 2, 11, 3, 2, 61, 62, 3, 2, 63, 65, 4, 2, 69, 69, 74, 74, 3, 2, 68, 69, 4, 2, 69, 69, 78, 78, 4, 2, 34, 34, 37, 37, 3, 2, 40, 41, 4, 2, 39, 39, 53, 53, 3, 2, 54, 60, 2, 554, 2, 100, 3, 2, 2, 2, 4, 103, 3, 2, 2, 2, 6, 118, 3, 2, 2, 2, 8, 133, 3, 2, 2, 2, 10, 135, 3, 2, 2, 2, 12, 166, 3, 2, 2, 2, 14, 193, 3, 2, 2, 2, 16, 200, 3, 2, 2, 2, 18, 206, 3, 2, 2, 2, 20, 226, 3, 2, 2, 2, 22, 228, 3, 2, 2, 2, 24, 243, 3, 2, 2, 2, 26, 246, 3, 2, 2, 2, 28, 259, 3, 2, 2, 2, 30, 261, 3, 2, 2, 2, 32, 273, 3, 2, 2, 2, 34, 285, 3, 2, 2, 2, 36, 288, 3, 2, 2, 2, 38, 296, 3, 2, 2, 2, 40, 302, 3, 2, 2, 2, 42, 304, 3, 2, 2, 2, 44, 312, 3, 2, 2, 2, 46, 320, 3, 2, 2, 2, 48, 322, 3, 2, 2, 2, 50, 366, 3, 2, 2, 2, 52, 368, 3, 2, 2, 2, 54, 371, 3, 2, 2, 2, 56, 380, 3, 2, 2, 2, 58, 406, 3, 2, 2, 2, 60, 408, 3, 2, 2, 2, 62, 417, 3, 2, 2, 2, 64, 426, 3, 2, 2, 2, 66, 430, 3, 2, 2, 2, 68, 436, 3, 2, 2, 2, 70, 440, 3, 2, 2, 2, 72, 443, 3, 2, 2, 2, 74, 451, 3, 2, 2, 2, 76, 455, 3, 2, 2, 2, 78, 459, 3, 2, 2, 2, 80, 462, 3, 2, 2, 2, 82, 467, 3, 2, 2, 2, 84, 471, 3, 2, 2, 2, 86, 473, 3, 2, 2, 2, 88, 475, 3, 2, 2, 2, 90, 478, 3, 2, 2, 2, 92, 486, 3, 2, 2, 2, 94, 488, 3, 2, 2, 2, 96, 514, 3, 2, 2, 2, 98, 518, 3, 2, 2, 2, 100, 101, 5, 4, 3, 2, 101, 102, 7, 2, 2, 3, 102, 3, 3, 2, 2, 2, 103, 104, 8, 3, 1, 2, 104, 105, 5, 6, 4, 2, 105, 111, 3, 2, 2, 2, 106, 107, 12, 3, 2, 2, 107, 108, 7, 28, 2, 2, 108, 110, 5, 8, 5, 2, 109, 106, 3, 2, 2, 2, 110, 113, 3, 2, 2, 2, 111, 109, 3, 2, 2, 2, 111, 112, 3, 2, 2, 2, 112, 5, 3, 2, 2, 2, 113, 111, 3, 2, 2, 2, 114, 119, 5, 88, 45, 2, 115, 119, 5, 30, 16, 2, 116, 119, 5, 24, 13, 2, 117, 119, 5, 92, 47, 2, 118, 114, 3, 2, 2, 2, 118, 115, 3, 2, 2, 2, 118, 116, 3, 2, 2, 2, 118, 117, 3, 2, 2, 2, 119, 7, 3, 2, 2, 2, 120, 134, 5, 34, 18, 2, 121, 134, 5, 38, 20, 2, 122, 134, 5, 52, 27, 2, 123, 134, 5, 58, 30, 2, 124, 134, 5, 54, 28, 2, 125, 134, 5, 36, 19, 2, 126, 134, 5, 10, 6, 2, 127, 134, 5, 60, 31, 2, 128, 134, 5, 62, 32, 2, 129, 134, 5, 66, 34, 2, 130, 134, 5, 68, 35, 2, 131, 134, 5, 94, 48, 2, 132, 134, 5, 70, 36, 2, 133, 120, 3, 2, 2, 2, 133, 121, 3, 2, 2, 2, 133, 122, 3, 2, 2, 2, 133, 123, 3, 2, 2, 2, 133, 124, 3, 2, 2, 2, 133, 125, 3, 2, 2, 2, 133, 126, 3, 2, 2, 2, 133, 127, 3, 2, 2, 2, 133, 128, 3, 2, 2, 2, 133, 129, 3, 2, 2, 2, 133, 130, 3, 2, 2, 2, 133, 131, 3, 2, 2, 2, 133, 132, 3, 2, 2, 2, 134, 9, 3, 2, 2, 2, 135, 136, 7, 20, 2, 2, 136, 137, 5, 12, 7, 2, 137, 11, 3, 2, 2, 2, 138, 139, 8, 7, 1, 2, 139, 140, 7, 46, 2, 2, 140, 167, 5, 12, 7, 9, 141, 167, 5, 16, 9, 2, 142, 167, 5, 14, 8, 2, 143, 145, 5, 16, 9, 2, 144, 146, 7, 46, 2, 2, 145, 144, 3, 2, 2, 2, 145, 146, 3, 2, 2, 2, 146, 147, 3, 2, 2, 2, 147, 148, 7, 43, 2, 2, 148, 149, 7, 42, 2, 2, 149, 154, 5, 16, 9, 2, 150, 151, 7, 36, 2, 2, 151, 153, 5, 16, 9, 2, 152, 150, 3, 2, 2, 2, 153, 156, 3, 2, 2, 2, 154, 152, 3, 2, 2, 2, 154, 155, 3, 2, 2, 2, 155, 157, 3, 2, 2, 2, 156, 154, 3, 2, 2, 2, 157, 158, 7, 52, 2, 2, 158, 167, 3, 2, 2, 2, 159, 160, 5, 16, 9, 2, 160, 162, 7, 44, 2, 2, 161, 163, 7, 46, 2, 2, 162, 161, 3, 2, 2, 2, 162, 163, 3, 2, 2, 2, 163, 164, 3, 2, 2, 2, 164, 165, 7, 47, 2, 2, 165, 167, 3, 2, 2, 2, 166, 138, 3, 2, 2, 2, 166, 141, 3, 2, 2, 2, 166, 142, 3, 2, 2, 2, 166, 143, 3, 2, 2, 2, 166, 159, 3, 2, 2, 2, 167, 176, 3, 2, 2, 2, 168, 169, 12, 6, 2, 2, 169, 170, 7, 33, 2, 2, 170, 175, 5, 12, 7, 7, 171, 172, 12, 5, 2, 2, 172, 173, 7, 49, 2, 2, 173, 175, 5, 12, 7, 6, 174, 168, 3, 2, 2, 2, 174, 171, 3, 2, 2, 2, 175, 178, 3, 2, 2, 2, 176, 174, 3, 2, 2, 2, 176, 177, 3, 2, 2, 2, 177, 13, 3, 2, 2, 2, 178, 176, 3, 2, 2, 2, 179, 181, 5, 16, 9, 2, 180, 182, 7, 46, 2, 2, 181, 180, 3, 2, 2, 2, 181, 182, 3, 2, 2, 2, 182, 183, 3, 2, 2, 2, 183, 184, 7, 45, 2, 2, 184, 185, 5, 84, 43, 2, 185, 194, 3, 2, 2, 2, 186, 188, 5, 16, 9, 2, 187, 189, 7, 46, 2, 2, 188, 187, 3, 2, 2, 2, 188, 189, 3, 2, 2, 2, 189, 190, 3, 2, 2, 2, 190, 191, 7, 51, 2, 2, 191, 192, 5, 84, 43, 2, 192, 194, 3, 2, 2, 2, 193, 179, 3, 2, 2, 2, 193, 186, 3, 2, 2, 2, 194, 15, 3, 2, 2, 2, 195, 201, 5, 18, 10, 2, 196, 197, 5, 18, 10, 2, 197, 198, 5, 86, 44, 2, 198, 199, 5, 18, 10, 2, 199, 201, 3, 2, 2, 2, 200, 195, 3, 2, 2, 2, 200, 196, 3, 2, 2, 2, 201, 17, 3, 2, 2, 2, 202, 203, 8, 10, 1, 2, 203, 207, 5, 20, 11, 2, 204, 205, 9, 2, 2, 2, 205, 207, 5, 18, 10, 5, 206, 202, 3, 2, 2, 2, 206, 204, 3, 2, 2, 2, 207, 216, 3, 2, 2, 2, 208, 209, 12, 4, 2, 2, 209, 210, 9, 3, 2, 2, 210, 215, 5, 18, 10, 5, 211, 212, 12, 3, 2, 2, 212, 213, 9, 2, 2, 2, 213, 215, 5, 18, 10, 4, 214, 208, 3, 2, 2, 2, 214, 211, 3, 2, 2, 2, 215, 218, 3, 2, 2, 2, 216, 214, 3, 2, 2, 2, 216, 217, 3, 2, 2, 2, 217, 19, 3, 2, 2, 2, 218, 216, 3, 2, 2, 2, 219, 227, 5, 50, 26, 2, 220, 227, 5, 42, 22, 2, 221, 227, 5, 22, 12, 2, 222, 223, 7, 42, 2, 2, 223, 224, 5, 12, 7, 2, 224, 225, 7, 52, 2, 2, 225, 227, 3, 2, 2, 2, 226, 219, 3, 2, 2, 2, 226, 220, 3, 2, 2, 2, 226, 221, 3, 2, 2, 2, 226, 222, 3, 2, 2, 2, 227, 21, 3, 2, 2, 2, 228, 229, 5, 46, 24, 2, 229, 239, 7, 42, 2, 2, 230, 240, 7, 63, 2, 2, 231, 236, 5, 12, 7, 2, 232, 233, 7, 36, 2, 2, 233, 235, 5, 12, 7, 2, 234, 232, 3, 2, 2, 2, 235, 238, 3, 2, 2, 2, 236, 234, 3, 2, 2, 2, 236, 237, 3, 2, 2, 2, 237, 240, 3, 2, 2, 2, 238, 236, 3, 2, 2, 2, 239, 230, 3, 2, 2, 2, 239, 231, 3, 2, 2, 2, 239, 240, 3, 2, 2, 2, 240, 241, 3, 2, 2, 2, 241, 242, 7, 52, 2, 2, 242, 23, 3, 2, 2, 2, 243, 244, 7, 16, 2, 2, 244, 245, 5, 26, 14, 2, 245, 25, 3, 2, 2, 2, 246, 251, 5, 28, 15, 2, 247, 248, 7, 36, 2, 2, 248, 250, 5, 28, 15, 2, 249, 247, 3, 2, 2, 2, 250, 253, 3, 2, 2, 2, 251, 249, 3, 2, 2, 2, 251, 252, 3, 2, 2, 2, 252, 27, 3, 2, 2, 2, 253, 251, 3, 2, 2, 2, 254, 260, 5, 12, 7, 2, 255, 256, 5, 42, 22, 2, 256, 257, 7, 35, 2, 2, 257, 258, 5, 12, 7, 2, 258, 260, 3, 2, 2, 2, 259, 254, 3, 2, 2, 2, 259, 255, 3, 2, 2, 2, 260, 29, 3, 2, 2, 2, 261, 262, 7, 8, 2, 2, 262, 267, 5, 40, 21, 2, 263, 264, 7, 36, 2, 2, 264, 266, 5, 40, 21, 2, 265, 263, 3, 2, 2, 2, 266, 269, 3, 2, 2, 2, 267, 265, 3, 2, 2, 2, 267, 268, 3, 2, 2, 2, 268, 271, 3, 2, 2, 2, 269, 267, 3, 2, 2, 2, 270, 272, 5, 32, 17, 2, 271, 270, 3, 2, 2, 2, 271, 272, 3, 2, 2, 2, 272, 31, 3, 2, 2, 2, 273, 274, 7, 66, 2, 2, 274, 275, 7, 73, 2, 2, 275, 280, 5, 40, 21, 2, 276, 277, 7, 36, 2, 2, 277, 279, 5, 40, 21, 2, 278, 276, 3, 2, 2, 2, 279, 282, 3, 2, 2, 2, 280, 278, 3, 2, 2, 2, 280, 281, 3, 2, 2, 2, 281, 283, 3, 2, 2, 2, 282, 280, 3, 2, 2, 2, 283, 284, 7, 67, 2, 2, 284, 33, 3, 2, 2, 2, 285, 286, 7, 6, 2, 2, 286, 287, 5, 26, 14, 2, 287, 35, 3, 2, 2, 2, 288, 290, 7, 19, 2, 2, 289, 291, 5, 26, 14, 2, 290, 289, 3, 2, 2, 2, 290, 291, 3, 2, 2, 2, 291, 294, 3, 2, 2, 2, 292, 293, 7, 32, 2, 2, 293, 295, 5, 26, 14, 2, 294, 292, 3, 2, 2, 2, 294, 295, 3, 2, 2, 2, 295, 37, 3, 2, 2, 2, 296, 297, 7, 10, 2, 2, 297, 300, 5, 26, 14, 2, 298, 299, 7, 32, 2, 2, 299, 301, 5, 26, 14, 2, 300, 298, 3, 2, 2, 2, 300, 301, 3, 2, 2, 2, 301, 39, 3, 2, 2, 2, 302, 303, 9, 4, 2, 2, 303, 41, 3, 2, 2, 2, 304, 309, 5, 46, 24, 2, 305, 306, 7, 38, 2, 2, 306, 308, 5, 46, 24, 2, 307, 305, 3, 2, 2, 2, 308, 311, 3, 2, 2, 2, 309, 307, 3, 2, 2, 2, 309, 310, 3, 2, 2, 2, 310, 43, 3, 2, 2, 2, 311, 309, 3, 2, 2, 2, 312, 317, 5, 48, 25, 2, 313, 314, 7, 38, 2, 2, 314, 316, 5, 48, 25, 2, 315, 313, 3, 2, 2, 2, 316, 319, 3, 2, 2, 2, 317, 315, 3, 2, 2, 2, 317, 318, 3, 2, 2, 2, 318, 45, 3, 2, 2, 2, 319, 317, 3, 2, 2, 2, 320, 321, 9, 5, 2, 2, 321, 47, 3, 2, 2, 2, 322, 323, 9, 6, 2, 2, 323, 49, 3, 2, 2, 2, 324, 367, 7, 47, 2, 2, 325, 326, 5, 82, 42, 2, 326, 327, 7, 68, 2, 2, 327, 367, 3, 2, 2, 2, 328, 367, 5, 80, 41, 2, 329, 367, 5, 82, 42, 2, 330, 367, 5, 76, 39, 2, 331, 367, 7, 50, 2, 2, 332, 367, 5, 84, 43, 2, 333, 334, 7, 66, 2, 2, 334, 339, 5, 78, 40, 2, 335, 336, 7, 36, 2, 2, 336, 338, 5, 78, 40, 2, 337, 335, 3, 2, 2, 2, 338, 341, 3, 2, 2, 2, 339, 337, 3, 2, 2, 2, 339, 340, 3, 2, 2, 2, 340, 342, 3, 2, 2, 2, 341, 339, 3, 2, 2, 2, 342, 343, 7, 67, 2, 2, 343, 367, 3, 2, 2, 2, 344, 345, 7, 66, 2, 2, 345, 350, 5, 76, 39, 2, 346, 347, 7, 36, 2, 2, 347, 349, 5, 76, 39, 2, 348, 346, 3, 2, 2, 2, 349, 352, 3, 2, 2, 2, 350, 348, 3, 2, 2, 2, 350, 351, 3, 2, 2, 2, 351, 353, 3, 2, 2, 2, 352, 350, 3, 2, 2, 2, 353, 354, 7, 67, 2, 2, 354, 367, 3, 2, 2, 2, 355, 356, 7, 66, 2, 2, 356, 361, 5, 84, 43, 2, 357, 358, 7, 36, 2, 2, 358, 360, 5, 84, 43, 2, 359, 357, 3, 2, 2, 2, 360, 363, 3, 2, 2, 2, 361, 359, 3, 2, 2, 2, 361, 362, 3, 2, 2, 2, 362, 364, 3, 2, 2, 2, 363, 361, 3, 2, 2, 2, 364, 365, 7, 67, 2, 2, 365, 367, 3, 2, 2, 2, 366, 324, 3, 2, 2, 2, 366, 325, 3, 2, 2, 2, 366, 328, 3, 2, 2, 2, 366, 329, 3, 2, 2, 2, 366, 330, 3, 2, 2, 2, 366, 331, 3, 2, 2, 2, 366, 332, 3, 2, 2, 2, 366, 333, 3, 2, 2, 2, 366, 344, 3, 2, 2, 2, 366, 355, 3, 2, 2, 2, 367, 51, 3, 2, 2, 2, 368, 369, 7, 12, 2, 2, 369, 370, 7, 30, 2, 2, 370, 53, 3, 2, 2, 2, 371, 372, 7, 18, 2, 2, 372, 377, 5, 56, 29, 2, 373, 374, 7, 36, 2, 2, 374, 376, 5, 56, 29, 2, 375, 373, 3, 2, 2, 2, 376, 379, 3, 2, 2, 2, 377, 375, 3, 2, 2, 2, 377, 378, 3, 2, 2, 2, 378, 55, 3, 2, 2, 2, 379, 377, 3, 2, 2, 2, 380, 382, 5, 12, 7, 2, 381, 383, 9, 7, 2, 2, 382, 381, 3, 2, 2, 2, 382, 383, 3, 2, 2, 2, 383, 386, 3, 2, 2, 2, 384, 385, 7, 48, 2, 2, 385, 387, 9, 8, 2, 2, 386, 384, 3, 2, 2, 2, 386, 387, 3, 2, 2, 2, 387, 57, 3, 2, 2, 2, 388, 389, 7, 11, 2, 2, 389, 394, 5, 44, 23, 2, 390, 391, 7, 36, 2, 2, 391, 393, 5, 44, 23, 2, 392, 390, 3, 2, 2, 2, 393, 396, 3, 2, 2, 2, 394, 392, 3, 2, 2, 2, 394, 395, 3, 2, 2, 2, 395, 407, 3, 2, 2, 2, 396, 394, 3, 2, 2, 2, 397, 398, 7, 14, 2, 2, 398, 403, 5, 44, 23, 2, 399, 400, 7, 36, 2, 2, 400, 402, 5, 44, 23, 2, 401, 399, 3, 2, 2, 2, 402, 405, 3, 2, 2, 2, 403, 401, 3, 2, 2, 2, 403, 404, 3, 2, 2, 2, 404, 407, 3, 2, 2, 2, 405, 403, 3, 2, 2, 2, 406, 388, 3, 2, 2, 2, 406, 397, 3, 2, 2, 2, 407, 59, 3, 2, 2, 2, 408, 409, 7, 4, 2, 2, 409, 414, 5, 44, 23, 2, 410, 411, 7, 36, 2, 2, 411, 413, 5, 44, 23, 2, 412, 410, 3, 2, 2, 2, 413, 416, 3, 2, 2, 2, 414, 412, 3, 2, 2, 2, 414, 415, 3, 2, 2, 2, 415, 61, 3, 2, 2, 2, 416, 414, 3, 2, 2, 2, 417, 418, 7, 15, 2, 2, 418, 423, 5, 64, 33, 2, 419, 420, 7, 36, 2, 2, 420, 422, 5, 64, 33, 2, 421, 419, 3, 2, 2, 2, 422, 425, 3, 2, 2, 2, 423, 421, 3, 2, 2, 2, 423, 424, 3, 2, 2, 2, 424, 63, 3, 2, 2, 2, 425, 423, 3, 2, 2, 2, 426, 427, 5, 44, 23, 2, 427, 428, 7, 82, 2, 2, 428, 429, 5, 44, 23, 2, 429, 65, 3, 2, 2, 2, 430, 431, 7, 3, 2, 2, 431, 432, 5, 20, 11, 2, 432, 434, 5, 84, 43, 2, 433, 435, 5, 72, 37, 2, 434, 433, 3, 2, 2, 2, 434, 435, 3, 2, 2, 2, 435, 67, 3, 2, 2, 2, 436, 437, 7, 9, 2, 2, 437, 438, 5, 20, 11, 2, 438, 439, 5, 84, 43, 2, 439, 69, 3, 2, 2, 2, 440, 441, 7, 13, 2, 2, 441, 442, 5, 42, 22, 2, 442, 71, 3, 2, 2, 2, 443, 448, 5, 74, 38, 2, 444, 445, 7, 36, 2, 2, 445, 447, 5, 74, 38, 2, 446, 444, 3, 2, 2, 2, 447, 450, 3, 2, 2, 2, 448, 446, 3, 2, 2, 2, 448, 449, 3, 2, 2, 2, 449, 73, 3, 2, 2, 2, 450, 448, 3, 2, 2, 2, 451, 452, 5, 46, 24, 2, 452, 453, 7, 35, 2, 2, 453, 454, 5, 50, 26, 2, 454, 75, 3, 2, 2, 2, 455, 456, 9, 9, 2, 2, 456, 77, 3, 2, 2, 2, 457, 460, 5, 80, 41, 2, 458, 460, 5, 82, 42, 2, 459, 457, 3, 2, 2, 2, 459, 458, 3, 2, 2, 2, 460, 79, 3, 2, 2, 2, 461, 463, 9, 2, 2, 2, 462, 461, 3, 2, 2, 2, 462, 463, 3, 2, 2, 2, 463, 464, 3, 2, 2, 2, 464, 465, 7, 31, 2, 2, 465, 81, 3, 2, 2, 2, 466, 468, 9, 2, 2, 2, 467, 466, 3, 2, 2, 2, 467, 468, 3, 2, 2, 2, 468, 469, 3, 2, 2, 2, 469, 470, 7, 30, 2, 2, 470, 83, 3, 2, 2, 2, 471, 472, 7, 29, 2, 2, 472, 85, 3, 2, 2, 2, 473, 474, 9, 10, 2, 2, 474, 87, 3, 2, 2, 2, 475, 476, 7, 7, 2, 2, 476, 477, 5, 90, 46, 2, 477, 89, 3, 2, 2, 2, 478, 479, 7, 66, 2, 2, 479, 480, 5, 4, 3, 2, 480, 481, 7, 67, 2, 2, 481, 91, 3, 2, 2, 2, 482, 483, 7, 17, 2, 2, 483, 487, 7, 98, 2, 2, 484, 485, 7, 17, 2, 2, 485, 487, 7, 99, 2, 2, 486, 482, 3, 2, 2, 2, 486, 484, 3, 2, 2, 2, 487, 93, 3, 2, 2, 2, 488, 492, 7, 5, 2, 2, 489, 491, 5, 98, 50, 2, 490, 489, 3, 2, 2, 2, 491, 494, 3, 2, 2, 2, 492, 490, 3, 2, 2, 2, 492, 493, 3, 2, 2, 2, 493, 495, 3, 2, 2, 2, 494, 492, 3, 2, 2, 2, 495, 498, 7, 88, 2, 2, 496, 497, 7, 86, 2, 2, 497, 499, 5, 44, 23, 2, 498, 496, 3, 2, 2, 2, 498, 499, 3, 2, 2, 2, 499, 509, 3, 2, 2, 2, 500, 501, 7, 87, 2, 2, 501, 506, 5, 96, 49, 2, 502, 503, 7, 36, 2, 2, 503, 505, 5, 96, 49, 2, 504, 502, 3, 2, 2, 2, 505, 508, 3, 2, 2, 2, 506, 504, 3, 2, 2, 2, 506, 507, 3, 2, 2, 2, 507, 510, 3, 2, 2, 2, 508, 506, 3, 2, 2, 2, 509, 500, 3, 2, 2, 2, 509, 510, 3, 2, 2, 2, 510, 95, 3, 2, 2, 2, 511, 512, 5, 44, 23, 2, 512, 513, 7, 35, 2, 2, 513, 515, 3, 2, 2, 2, 514, 511, 3, 2, 2, 2, 514, 515, 3, 2, 2, 2, 515, 516, 3, 2, 2, 2, 516, 517, 5, 44, 23, 2, 517, 97, 3, 2, 2, 2, 518, 519, 7, 66, 2, 2, 519, 520, 7, 104, 2, 2, 520, 521, 7, 103, 2, 2, 521, 522, 7, 104, 2, 2, 522, 523, 7, 67, 2, 2, 523, 99, 3, 2, 2, 2, 54, 111, 118, 133, 145, 154, 162, 166, 174, 176, 181, 188, 193, 200, 206, 214, 216, 226, 236, 239, 251, 259, 267, 271, 280, 290, 294, 300, 309, 317, 339, 350, 361, 366, 377, 382, 386, 394, 403, 406, 414, 423, 434, 448, 459, 462, 467, 486, 492, 498, 506, 509, 514] \ No newline at end of file +[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 3, 106, 509, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 4, 38, 9, 38, 4, 39, 9, 39, 4, 40, 9, 40, 4, 41, 9, 41, 4, 42, 9, 42, 4, 43, 9, 43, 4, 44, 9, 44, 4, 45, 9, 45, 4, 46, 9, 46, 4, 47, 9, 47, 4, 48, 9, 48, 4, 49, 9, 49, 4, 50, 9, 50, 4, 51, 9, 51, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 7, 3, 112, 10, 3, 12, 3, 14, 3, 115, 11, 3, 3, 4, 3, 4, 3, 4, 3, 4, 5, 4, 121, 10, 4, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5, 5, 5, 136, 10, 5, 3, 6, 3, 6, 3, 6, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 5, 7, 148, 10, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 7, 7, 155, 10, 7, 12, 7, 14, 7, 158, 11, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 5, 7, 165, 10, 7, 3, 7, 3, 7, 5, 7, 169, 10, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 7, 7, 177, 10, 7, 12, 7, 14, 7, 180, 11, 7, 3, 8, 3, 8, 5, 8, 184, 10, 8, 3, 8, 3, 8, 3, 8, 3, 8, 3, 8, 5, 8, 191, 10, 8, 3, 8, 3, 8, 3, 8, 5, 8, 196, 10, 8, 3, 9, 3, 9, 3, 9, 3, 9, 3, 9, 5, 9, 203, 10, 9, 3, 10, 3, 10, 3, 10, 3, 10, 5, 10, 209, 10, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 3, 10, 7, 10, 217, 10, 10, 12, 10, 14, 10, 220, 11, 10, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 5, 11, 229, 10, 11, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 3, 12, 7, 12, 237, 10, 12, 12, 12, 14, 12, 240, 11, 12, 5, 12, 242, 10, 12, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 14, 3, 14, 3, 14, 7, 14, 252, 10, 14, 12, 14, 14, 14, 255, 11, 14, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 5, 15, 262, 10, 15, 3, 16, 3, 16, 3, 16, 3, 16, 7, 16, 268, 10, 16, 12, 16, 14, 16, 271, 11, 16, 3, 16, 5, 16, 274, 10, 16, 3, 17, 3, 17, 5, 17, 278, 10, 17, 3, 18, 3, 18, 3, 18, 3, 18, 7, 18, 284, 10, 18, 12, 18, 14, 18, 287, 11, 18, 3, 19, 3, 19, 3, 19, 3, 19, 3, 20, 3, 20, 3, 20, 3, 21, 3, 21, 5, 21, 298, 10, 21, 3, 21, 3, 21, 5, 21, 302, 10, 21, 3, 22, 3, 22, 3, 22, 3, 22, 5, 22, 308, 10, 22, 3, 23, 3, 23, 3, 24, 3, 24, 3, 24, 7, 24, 315, 10, 24, 12, 24, 14, 24, 318, 11, 24, 3, 25, 3, 25, 3, 25, 7, 25, 323, 10, 25, 12, 25, 14, 25, 326, 11, 25, 3, 26, 3, 26, 3, 27, 3, 27, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 7, 28, 345, 10, 28, 12, 28, 14, 28, 348, 11, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 7, 28, 356, 10, 28, 12, 28, 14, 28, 359, 11, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 7, 28, 367, 10, 28, 12, 28, 14, 28, 370, 11, 28, 3, 28, 3, 28, 5, 28, 374, 10, 28, 3, 29, 3, 29, 3, 29, 3, 30, 3, 30, 3, 30, 3, 30, 7, 30, 383, 10, 30, 12, 30, 14, 30, 386, 11, 30, 3, 31, 3, 31, 5, 31, 390, 10, 31, 3, 31, 3, 31, 5, 31, 394, 10, 31, 3, 32, 3, 32, 3, 32, 3, 32, 7, 32, 400, 10, 32, 12, 32, 14, 32, 403, 11, 32, 3, 33, 3, 33, 3, 33, 3, 33, 7, 33, 409, 10, 33, 12, 33, 14, 33, 412, 11, 33, 3, 34, 3, 34, 3, 34, 3, 34, 7, 34, 418, 10, 34, 12, 34, 14, 34, 421, 11, 34, 3, 35, 3, 35, 3, 35, 3, 35, 3, 36, 3, 36, 3, 36, 3, 36, 5, 36, 431, 10, 36, 3, 37, 3, 37, 3, 37, 3, 37, 3, 38, 3, 38, 3, 38, 3, 39, 3, 39, 3, 39, 7, 39, 443, 10, 39, 12, 39, 14, 39, 446, 11, 39, 3, 40, 3, 40, 3, 40, 3, 40, 3, 41, 3, 41, 3, 42, 3, 42, 5, 42, 456, 10, 42, 3, 43, 5, 43, 459, 10, 43, 3, 43, 3, 43, 3, 44, 5, 44, 464, 10, 44, 3, 44, 3, 44, 3, 45, 3, 45, 3, 46, 3, 46, 3, 47, 3, 47, 3, 47, 3, 48, 3, 48, 3, 48, 3, 48, 3, 49, 3, 49, 3, 49, 3, 49, 5, 49, 483, 10, 49, 3, 50, 3, 50, 3, 50, 3, 50, 5, 50, 489, 10, 50, 3, 50, 3, 50, 3, 50, 3, 50, 7, 50, 495, 10, 50, 12, 50, 14, 50, 498, 11, 50, 5, 50, 500, 10, 50, 3, 51, 3, 51, 3, 51, 5, 51, 505, 10, 51, 3, 51, 3, 51, 3, 51, 2, 2, 5, 4, 12, 18, 52, 2, 2, 4, 2, 6, 2, 8, 2, 10, 2, 12, 2, 14, 2, 16, 2, 18, 2, 20, 2, 22, 2, 24, 2, 26, 2, 28, 2, 30, 2, 32, 2, 34, 2, 36, 2, 38, 2, 40, 2, 42, 2, 44, 2, 46, 2, 48, 2, 50, 2, 52, 2, 54, 2, 56, 2, 58, 2, 60, 2, 62, 2, 64, 2, 66, 2, 68, 2, 70, 2, 72, 2, 74, 2, 76, 2, 78, 2, 80, 2, 82, 2, 84, 2, 86, 2, 88, 2, 90, 2, 92, 2, 94, 2, 96, 2, 98, 2, 100, 2, 2, 11, 3, 2, 60, 61, 3, 2, 62, 64, 4, 2, 68, 68, 73, 73, 3, 2, 67, 68, 4, 2, 68, 68, 77, 77, 4, 2, 33, 33, 36, 36, 3, 2, 39, 40, 4, 2, 38, 38, 52, 52, 3, 2, 53, 59, 2, 535, 2, 102, 3, 2, 2, 2, 4, 105, 3, 2, 2, 2, 6, 120, 3, 2, 2, 2, 8, 135, 3, 2, 2, 2, 10, 137, 3, 2, 2, 2, 12, 168, 3, 2, 2, 2, 14, 195, 3, 2, 2, 2, 16, 202, 3, 2, 2, 2, 18, 208, 3, 2, 2, 2, 20, 228, 3, 2, 2, 2, 22, 230, 3, 2, 2, 2, 24, 245, 3, 2, 2, 2, 26, 248, 3, 2, 2, 2, 28, 261, 3, 2, 2, 2, 30, 263, 3, 2, 2, 2, 32, 277, 3, 2, 2, 2, 34, 279, 3, 2, 2, 2, 36, 288, 3, 2, 2, 2, 38, 292, 3, 2, 2, 2, 40, 295, 3, 2, 2, 2, 42, 303, 3, 2, 2, 2, 44, 309, 3, 2, 2, 2, 46, 311, 3, 2, 2, 2, 48, 319, 3, 2, 2, 2, 50, 327, 3, 2, 2, 2, 52, 329, 3, 2, 2, 2, 54, 373, 3, 2, 2, 2, 56, 375, 3, 2, 2, 2, 58, 378, 3, 2, 2, 2, 60, 387, 3, 2, 2, 2, 62, 395, 3, 2, 2, 2, 64, 404, 3, 2, 2, 2, 66, 413, 3, 2, 2, 2, 68, 422, 3, 2, 2, 2, 70, 426, 3, 2, 2, 2, 72, 432, 3, 2, 2, 2, 74, 436, 3, 2, 2, 2, 76, 439, 3, 2, 2, 2, 78, 447, 3, 2, 2, 2, 80, 451, 3, 2, 2, 2, 82, 455, 3, 2, 2, 2, 84, 458, 3, 2, 2, 2, 86, 463, 3, 2, 2, 2, 88, 467, 3, 2, 2, 2, 90, 469, 3, 2, 2, 2, 92, 471, 3, 2, 2, 2, 94, 474, 3, 2, 2, 2, 96, 482, 3, 2, 2, 2, 98, 484, 3, 2, 2, 2, 100, 504, 3, 2, 2, 2, 102, 103, 5, 4, 3, 2, 103, 104, 7, 2, 2, 3, 104, 3, 3, 2, 2, 2, 105, 106, 8, 3, 1, 2, 106, 107, 5, 6, 4, 2, 107, 113, 3, 2, 2, 2, 108, 109, 12, 3, 2, 2, 109, 110, 7, 27, 2, 2, 110, 112, 5, 8, 5, 2, 111, 108, 3, 2, 2, 2, 112, 115, 3, 2, 2, 2, 113, 111, 3, 2, 2, 2, 113, 114, 3, 2, 2, 2, 114, 5, 3, 2, 2, 2, 115, 113, 3, 2, 2, 2, 116, 121, 5, 92, 47, 2, 117, 121, 5, 30, 16, 2, 118, 121, 5, 24, 13, 2, 119, 121, 5, 96, 49, 2, 120, 116, 3, 2, 2, 2, 120, 117, 3, 2, 2, 2, 120, 118, 3, 2, 2, 2, 120, 119, 3, 2, 2, 2, 121, 7, 3, 2, 2, 2, 122, 136, 5, 38, 20, 2, 123, 136, 5, 42, 22, 2, 124, 136, 5, 56, 29, 2, 125, 136, 5, 62, 32, 2, 126, 136, 5, 58, 30, 2, 127, 136, 5, 40, 21, 2, 128, 136, 5, 10, 6, 2, 129, 136, 5, 64, 33, 2, 130, 136, 5, 66, 34, 2, 131, 136, 5, 70, 36, 2, 132, 136, 5, 72, 37, 2, 133, 136, 5, 98, 50, 2, 134, 136, 5, 74, 38, 2, 135, 122, 3, 2, 2, 2, 135, 123, 3, 2, 2, 2, 135, 124, 3, 2, 2, 2, 135, 125, 3, 2, 2, 2, 135, 126, 3, 2, 2, 2, 135, 127, 3, 2, 2, 2, 135, 128, 3, 2, 2, 2, 135, 129, 3, 2, 2, 2, 135, 130, 3, 2, 2, 2, 135, 131, 3, 2, 2, 2, 135, 132, 3, 2, 2, 2, 135, 133, 3, 2, 2, 2, 135, 134, 3, 2, 2, 2, 136, 9, 3, 2, 2, 2, 137, 138, 7, 19, 2, 2, 138, 139, 5, 12, 7, 2, 139, 11, 3, 2, 2, 2, 140, 141, 8, 7, 1, 2, 141, 142, 7, 45, 2, 2, 142, 169, 5, 12, 7, 9, 143, 169, 5, 16, 9, 2, 144, 169, 5, 14, 8, 2, 145, 147, 5, 16, 9, 2, 146, 148, 7, 45, 2, 2, 147, 146, 3, 2, 2, 2, 147, 148, 3, 2, 2, 2, 148, 149, 3, 2, 2, 2, 149, 150, 7, 42, 2, 2, 150, 151, 7, 41, 2, 2, 151, 156, 5, 16, 9, 2, 152, 153, 7, 35, 2, 2, 153, 155, 5, 16, 9, 2, 154, 152, 3, 2, 2, 2, 155, 158, 3, 2, 2, 2, 156, 154, 3, 2, 2, 2, 156, 157, 3, 2, 2, 2, 157, 159, 3, 2, 2, 2, 158, 156, 3, 2, 2, 2, 159, 160, 7, 51, 2, 2, 160, 169, 3, 2, 2, 2, 161, 162, 5, 16, 9, 2, 162, 164, 7, 43, 2, 2, 163, 165, 7, 45, 2, 2, 164, 163, 3, 2, 2, 2, 164, 165, 3, 2, 2, 2, 165, 166, 3, 2, 2, 2, 166, 167, 7, 46, 2, 2, 167, 169, 3, 2, 2, 2, 168, 140, 3, 2, 2, 2, 168, 143, 3, 2, 2, 2, 168, 144, 3, 2, 2, 2, 168, 145, 3, 2, 2, 2, 168, 161, 3, 2, 2, 2, 169, 178, 3, 2, 2, 2, 170, 171, 12, 6, 2, 2, 171, 172, 7, 32, 2, 2, 172, 177, 5, 12, 7, 7, 173, 174, 12, 5, 2, 2, 174, 175, 7, 48, 2, 2, 175, 177, 5, 12, 7, 6, 176, 170, 3, 2, 2, 2, 176, 173, 3, 2, 2, 2, 177, 180, 3, 2, 2, 2, 178, 176, 3, 2, 2, 2, 178, 179, 3, 2, 2, 2, 179, 13, 3, 2, 2, 2, 180, 178, 3, 2, 2, 2, 181, 183, 5, 16, 9, 2, 182, 184, 7, 45, 2, 2, 183, 182, 3, 2, 2, 2, 183, 184, 3, 2, 2, 2, 184, 185, 3, 2, 2, 2, 185, 186, 7, 44, 2, 2, 186, 187, 5, 88, 45, 2, 187, 196, 3, 2, 2, 2, 188, 190, 5, 16, 9, 2, 189, 191, 7, 45, 2, 2, 190, 189, 3, 2, 2, 2, 190, 191, 3, 2, 2, 2, 191, 192, 3, 2, 2, 2, 192, 193, 7, 50, 2, 2, 193, 194, 5, 88, 45, 2, 194, 196, 3, 2, 2, 2, 195, 181, 3, 2, 2, 2, 195, 188, 3, 2, 2, 2, 196, 15, 3, 2, 2, 2, 197, 203, 5, 18, 10, 2, 198, 199, 5, 18, 10, 2, 199, 200, 5, 90, 46, 2, 200, 201, 5, 18, 10, 2, 201, 203, 3, 2, 2, 2, 202, 197, 3, 2, 2, 2, 202, 198, 3, 2, 2, 2, 203, 17, 3, 2, 2, 2, 204, 205, 8, 10, 1, 2, 205, 209, 5, 20, 11, 2, 206, 207, 9, 2, 2, 2, 207, 209, 5, 18, 10, 5, 208, 204, 3, 2, 2, 2, 208, 206, 3, 2, 2, 2, 209, 218, 3, 2, 2, 2, 210, 211, 12, 4, 2, 2, 211, 212, 9, 3, 2, 2, 212, 217, 5, 18, 10, 5, 213, 214, 12, 3, 2, 2, 214, 215, 9, 2, 2, 2, 215, 217, 5, 18, 10, 4, 216, 210, 3, 2, 2, 2, 216, 213, 3, 2, 2, 2, 217, 220, 3, 2, 2, 2, 218, 216, 3, 2, 2, 2, 218, 219, 3, 2, 2, 2, 219, 19, 3, 2, 2, 2, 220, 218, 3, 2, 2, 2, 221, 229, 5, 54, 28, 2, 222, 229, 5, 46, 24, 2, 223, 229, 5, 22, 12, 2, 224, 225, 7, 41, 2, 2, 225, 226, 5, 12, 7, 2, 226, 227, 7, 51, 2, 2, 227, 229, 3, 2, 2, 2, 228, 221, 3, 2, 2, 2, 228, 222, 3, 2, 2, 2, 228, 223, 3, 2, 2, 2, 228, 224, 3, 2, 2, 2, 229, 21, 3, 2, 2, 2, 230, 231, 5, 50, 26, 2, 231, 241, 7, 41, 2, 2, 232, 242, 7, 62, 2, 2, 233, 238, 5, 12, 7, 2, 234, 235, 7, 35, 2, 2, 235, 237, 5, 12, 7, 2, 236, 234, 3, 2, 2, 2, 237, 240, 3, 2, 2, 2, 238, 236, 3, 2, 2, 2, 238, 239, 3, 2, 2, 2, 239, 242, 3, 2, 2, 2, 240, 238, 3, 2, 2, 2, 241, 232, 3, 2, 2, 2, 241, 233, 3, 2, 2, 2, 241, 242, 3, 2, 2, 2, 242, 243, 3, 2, 2, 2, 243, 244, 7, 51, 2, 2, 244, 23, 3, 2, 2, 2, 245, 246, 7, 15, 2, 2, 246, 247, 5, 26, 14, 2, 247, 25, 3, 2, 2, 2, 248, 253, 5, 28, 15, 2, 249, 250, 7, 35, 2, 2, 250, 252, 5, 28, 15, 2, 251, 249, 3, 2, 2, 2, 252, 255, 3, 2, 2, 2, 253, 251, 3, 2, 2, 2, 253, 254, 3, 2, 2, 2, 254, 27, 3, 2, 2, 2, 255, 253, 3, 2, 2, 2, 256, 262, 5, 12, 7, 2, 257, 258, 5, 46, 24, 2, 258, 259, 7, 34, 2, 2, 259, 260, 5, 12, 7, 2, 260, 262, 3, 2, 2, 2, 261, 256, 3, 2, 2, 2, 261, 257, 3, 2, 2, 2, 262, 29, 3, 2, 2, 2, 263, 264, 7, 8, 2, 2, 264, 269, 5, 44, 23, 2, 265, 266, 7, 35, 2, 2, 266, 268, 5, 44, 23, 2, 267, 265, 3, 2, 2, 2, 268, 271, 3, 2, 2, 2, 269, 267, 3, 2, 2, 2, 269, 270, 3, 2, 2, 2, 270, 273, 3, 2, 2, 2, 271, 269, 3, 2, 2, 2, 272, 274, 5, 32, 17, 2, 273, 272, 3, 2, 2, 2, 273, 274, 3, 2, 2, 2, 274, 31, 3, 2, 2, 2, 275, 278, 5, 34, 18, 2, 276, 278, 5, 36, 19, 2, 277, 275, 3, 2, 2, 2, 277, 276, 3, 2, 2, 2, 278, 33, 3, 2, 2, 2, 279, 280, 7, 72, 2, 2, 280, 285, 5, 44, 23, 2, 281, 282, 7, 35, 2, 2, 282, 284, 5, 44, 23, 2, 283, 281, 3, 2, 2, 2, 284, 287, 3, 2, 2, 2, 285, 283, 3, 2, 2, 2, 285, 286, 3, 2, 2, 2, 286, 35, 3, 2, 2, 2, 287, 285, 3, 2, 2, 2, 288, 289, 7, 65, 2, 2, 289, 290, 5, 34, 18, 2, 290, 291, 7, 66, 2, 2, 291, 37, 3, 2, 2, 2, 292, 293, 7, 6, 2, 2, 293, 294, 5, 26, 14, 2, 294, 39, 3, 2, 2, 2, 295, 297, 7, 18, 2, 2, 296, 298, 5, 26, 14, 2, 297, 296, 3, 2, 2, 2, 297, 298, 3, 2, 2, 2, 298, 301, 3, 2, 2, 2, 299, 300, 7, 31, 2, 2, 300, 302, 5, 26, 14, 2, 301, 299, 3, 2, 2, 2, 301, 302, 3, 2, 2, 2, 302, 41, 3, 2, 2, 2, 303, 304, 7, 10, 2, 2, 304, 307, 5, 26, 14, 2, 305, 306, 7, 31, 2, 2, 306, 308, 5, 26, 14, 2, 307, 305, 3, 2, 2, 2, 307, 308, 3, 2, 2, 2, 308, 43, 3, 2, 2, 2, 309, 310, 9, 4, 2, 2, 310, 45, 3, 2, 2, 2, 311, 316, 5, 50, 26, 2, 312, 313, 7, 37, 2, 2, 313, 315, 5, 50, 26, 2, 314, 312, 3, 2, 2, 2, 315, 318, 3, 2, 2, 2, 316, 314, 3, 2, 2, 2, 316, 317, 3, 2, 2, 2, 317, 47, 3, 2, 2, 2, 318, 316, 3, 2, 2, 2, 319, 324, 5, 52, 27, 2, 320, 321, 7, 37, 2, 2, 321, 323, 5, 52, 27, 2, 322, 320, 3, 2, 2, 2, 323, 326, 3, 2, 2, 2, 324, 322, 3, 2, 2, 2, 324, 325, 3, 2, 2, 2, 325, 49, 3, 2, 2, 2, 326, 324, 3, 2, 2, 2, 327, 328, 9, 5, 2, 2, 328, 51, 3, 2, 2, 2, 329, 330, 9, 6, 2, 2, 330, 53, 3, 2, 2, 2, 331, 374, 7, 46, 2, 2, 332, 333, 5, 86, 44, 2, 333, 334, 7, 67, 2, 2, 334, 374, 3, 2, 2, 2, 335, 374, 5, 84, 43, 2, 336, 374, 5, 86, 44, 2, 337, 374, 5, 80, 41, 2, 338, 374, 7, 49, 2, 2, 339, 374, 5, 88, 45, 2, 340, 341, 7, 65, 2, 2, 341, 346, 5, 82, 42, 2, 342, 343, 7, 35, 2, 2, 343, 345, 5, 82, 42, 2, 344, 342, 3, 2, 2, 2, 345, 348, 3, 2, 2, 2, 346, 344, 3, 2, 2, 2, 346, 347, 3, 2, 2, 2, 347, 349, 3, 2, 2, 2, 348, 346, 3, 2, 2, 2, 349, 350, 7, 66, 2, 2, 350, 374, 3, 2, 2, 2, 351, 352, 7, 65, 2, 2, 352, 357, 5, 80, 41, 2, 353, 354, 7, 35, 2, 2, 354, 356, 5, 80, 41, 2, 355, 353, 3, 2, 2, 2, 356, 359, 3, 2, 2, 2, 357, 355, 3, 2, 2, 2, 357, 358, 3, 2, 2, 2, 358, 360, 3, 2, 2, 2, 359, 357, 3, 2, 2, 2, 360, 361, 7, 66, 2, 2, 361, 374, 3, 2, 2, 2, 362, 363, 7, 65, 2, 2, 363, 368, 5, 88, 45, 2, 364, 365, 7, 35, 2, 2, 365, 367, 5, 88, 45, 2, 366, 364, 3, 2, 2, 2, 367, 370, 3, 2, 2, 2, 368, 366, 3, 2, 2, 2, 368, 369, 3, 2, 2, 2, 369, 371, 3, 2, 2, 2, 370, 368, 3, 2, 2, 2, 371, 372, 7, 66, 2, 2, 372, 374, 3, 2, 2, 2, 373, 331, 3, 2, 2, 2, 373, 332, 3, 2, 2, 2, 373, 335, 3, 2, 2, 2, 373, 336, 3, 2, 2, 2, 373, 337, 3, 2, 2, 2, 373, 338, 3, 2, 2, 2, 373, 339, 3, 2, 2, 2, 373, 340, 3, 2, 2, 2, 373, 351, 3, 2, 2, 2, 373, 362, 3, 2, 2, 2, 374, 55, 3, 2, 2, 2, 375, 376, 7, 12, 2, 2, 376, 377, 7, 29, 2, 2, 377, 57, 3, 2, 2, 2, 378, 379, 7, 17, 2, 2, 379, 384, 5, 60, 31, 2, 380, 381, 7, 35, 2, 2, 381, 383, 5, 60, 31, 2, 382, 380, 3, 2, 2, 2, 383, 386, 3, 2, 2, 2, 384, 382, 3, 2, 2, 2, 384, 385, 3, 2, 2, 2, 385, 59, 3, 2, 2, 2, 386, 384, 3, 2, 2, 2, 387, 389, 5, 12, 7, 2, 388, 390, 9, 7, 2, 2, 389, 388, 3, 2, 2, 2, 389, 390, 3, 2, 2, 2, 390, 393, 3, 2, 2, 2, 391, 392, 7, 47, 2, 2, 392, 394, 9, 8, 2, 2, 393, 391, 3, 2, 2, 2, 393, 394, 3, 2, 2, 2, 394, 61, 3, 2, 2, 2, 395, 396, 7, 11, 2, 2, 396, 401, 5, 48, 25, 2, 397, 398, 7, 35, 2, 2, 398, 400, 5, 48, 25, 2, 399, 397, 3, 2, 2, 2, 400, 403, 3, 2, 2, 2, 401, 399, 3, 2, 2, 2, 401, 402, 3, 2, 2, 2, 402, 63, 3, 2, 2, 2, 403, 401, 3, 2, 2, 2, 404, 405, 7, 4, 2, 2, 405, 410, 5, 48, 25, 2, 406, 407, 7, 35, 2, 2, 407, 409, 5, 48, 25, 2, 408, 406, 3, 2, 2, 2, 409, 412, 3, 2, 2, 2, 410, 408, 3, 2, 2, 2, 410, 411, 3, 2, 2, 2, 411, 65, 3, 2, 2, 2, 412, 410, 3, 2, 2, 2, 413, 414, 7, 14, 2, 2, 414, 419, 5, 68, 35, 2, 415, 416, 7, 35, 2, 2, 416, 418, 5, 68, 35, 2, 417, 415, 3, 2, 2, 2, 418, 421, 3, 2, 2, 2, 419, 417, 3, 2, 2, 2, 419, 420, 3, 2, 2, 2, 420, 67, 3, 2, 2, 2, 421, 419, 3, 2, 2, 2, 422, 423, 5, 48, 25, 2, 423, 424, 7, 81, 2, 2, 424, 425, 5, 48, 25, 2, 425, 69, 3, 2, 2, 2, 426, 427, 7, 3, 2, 2, 427, 428, 5, 20, 11, 2, 428, 430, 5, 88, 45, 2, 429, 431, 5, 76, 39, 2, 430, 429, 3, 2, 2, 2, 430, 431, 3, 2, 2, 2, 431, 71, 3, 2, 2, 2, 432, 433, 7, 9, 2, 2, 433, 434, 5, 20, 11, 2, 434, 435, 5, 88, 45, 2, 435, 73, 3, 2, 2, 2, 436, 437, 7, 13, 2, 2, 437, 438, 5, 46, 24, 2, 438, 75, 3, 2, 2, 2, 439, 444, 5, 78, 40, 2, 440, 441, 7, 35, 2, 2, 441, 443, 5, 78, 40, 2, 442, 440, 3, 2, 2, 2, 443, 446, 3, 2, 2, 2, 444, 442, 3, 2, 2, 2, 444, 445, 3, 2, 2, 2, 445, 77, 3, 2, 2, 2, 446, 444, 3, 2, 2, 2, 447, 448, 5, 50, 26, 2, 448, 449, 7, 34, 2, 2, 449, 450, 5, 54, 28, 2, 450, 79, 3, 2, 2, 2, 451, 452, 9, 9, 2, 2, 452, 81, 3, 2, 2, 2, 453, 456, 5, 84, 43, 2, 454, 456, 5, 86, 44, 2, 455, 453, 3, 2, 2, 2, 455, 454, 3, 2, 2, 2, 456, 83, 3, 2, 2, 2, 457, 459, 9, 2, 2, 2, 458, 457, 3, 2, 2, 2, 458, 459, 3, 2, 2, 2, 459, 460, 3, 2, 2, 2, 460, 461, 7, 30, 2, 2, 461, 85, 3, 2, 2, 2, 462, 464, 9, 2, 2, 2, 463, 462, 3, 2, 2, 2, 463, 464, 3, 2, 2, 2, 464, 465, 3, 2, 2, 2, 465, 466, 7, 29, 2, 2, 466, 87, 3, 2, 2, 2, 467, 468, 7, 28, 2, 2, 468, 89, 3, 2, 2, 2, 469, 470, 9, 10, 2, 2, 470, 91, 3, 2, 2, 2, 471, 472, 7, 7, 2, 2, 472, 473, 5, 94, 48, 2, 473, 93, 3, 2, 2, 2, 474, 475, 7, 65, 2, 2, 475, 476, 5, 4, 3, 2, 476, 477, 7, 66, 2, 2, 477, 95, 3, 2, 2, 2, 478, 479, 7, 16, 2, 2, 479, 483, 7, 97, 2, 2, 480, 481, 7, 16, 2, 2, 481, 483, 7, 98, 2, 2, 482, 478, 3, 2, 2, 2, 482, 480, 3, 2, 2, 2, 483, 97, 3, 2, 2, 2, 484, 485, 7, 5, 2, 2, 485, 488, 7, 87, 2, 2, 486, 487, 7, 85, 2, 2, 487, 489, 5, 48, 25, 2, 488, 486, 3, 2, 2, 2, 488, 489, 3, 2, 2, 2, 489, 499, 3, 2, 2, 2, 490, 491, 7, 86, 2, 2, 491, 496, 5, 100, 51, 2, 492, 493, 7, 35, 2, 2, 493, 495, 5, 100, 51, 2, 494, 492, 3, 2, 2, 2, 495, 498, 3, 2, 2, 2, 496, 494, 3, 2, 2, 2, 496, 497, 3, 2, 2, 2, 497, 500, 3, 2, 2, 2, 498, 496, 3, 2, 2, 2, 499, 490, 3, 2, 2, 2, 499, 500, 3, 2, 2, 2, 500, 99, 3, 2, 2, 2, 501, 502, 5, 48, 25, 2, 502, 503, 7, 34, 2, 2, 503, 505, 3, 2, 2, 2, 504, 501, 3, 2, 2, 2, 504, 505, 3, 2, 2, 2, 505, 506, 3, 2, 2, 2, 506, 507, 5, 48, 25, 2, 507, 101, 3, 2, 2, 2, 52, 113, 120, 135, 147, 156, 164, 168, 176, 178, 183, 190, 195, 202, 208, 216, 218, 228, 238, 241, 253, 261, 269, 273, 277, 285, 297, 301, 307, 316, 324, 346, 357, 368, 373, 384, 389, 393, 401, 410, 419, 430, 444, 455, 458, 463, 482, 488, 496, 499, 504] \ No newline at end of file diff --git a/packages/kbn-monaco/src/esql/antlr/esql_parser.tokens b/packages/kbn-monaco/src/esql/antlr/esql_parser.tokens index 4bdf0572a3b8a..1f49f7e26406b 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_parser.tokens +++ b/packages/kbn-monaco/src/esql/antlr/esql_parser.tokens @@ -9,118 +9,117 @@ INLINESTATS=8 KEEP=9 LIMIT=10 MV_EXPAND=11 -PROJECT=12 -RENAME=13 -ROW=14 -SHOW=15 -SORT=16 -STATS=17 -WHERE=18 -UNKNOWN_CMD=19 -LINE_COMMENT=20 -MULTILINE_COMMENT=21 -WS=22 -EXPLAIN_WS=23 -EXPLAIN_LINE_COMMENT=24 -EXPLAIN_MULTILINE_COMMENT=25 -PIPE=26 -STRING=27 -INTEGER_LITERAL=28 -DECIMAL_LITERAL=29 -BY=30 -AND=31 -ASC=32 -ASSIGN=33 -COMMA=34 -DESC=35 -DOT=36 -FALSE=37 -FIRST=38 -LAST=39 -LP=40 -IN=41 -IS=42 -LIKE=43 -NOT=44 -NULL=45 -NULLS=46 -OR=47 -PARAM=48 -RLIKE=49 -RP=50 -TRUE=51 -EQ=52 -CIEQ=53 -NEQ=54 -LT=55 -LTE=56 -GT=57 -GTE=58 -PLUS=59 -MINUS=60 -ASTERISK=61 -SLASH=62 -PERCENT=63 -OPENING_BRACKET=64 -CLOSING_BRACKET=65 -UNQUOTED_IDENTIFIER=66 -QUOTED_IDENTIFIER=67 -EXPR_LINE_COMMENT=68 -EXPR_MULTILINE_COMMENT=69 -EXPR_WS=70 -METADATA=71 -FROM_UNQUOTED_IDENTIFIER=72 -FROM_LINE_COMMENT=73 -FROM_MULTILINE_COMMENT=74 -FROM_WS=75 -UNQUOTED_ID_PATTERN=76 -PROJECT_LINE_COMMENT=77 -PROJECT_MULTILINE_COMMENT=78 -PROJECT_WS=79 -AS=80 -RENAME_LINE_COMMENT=81 -RENAME_MULTILINE_COMMENT=82 -RENAME_WS=83 -ON=84 -WITH=85 -ENRICH_POLICY_NAME=86 -ENRICH_LINE_COMMENT=87 -ENRICH_MULTILINE_COMMENT=88 -ENRICH_WS=89 -ENRICH_FIELD_LINE_COMMENT=90 -ENRICH_FIELD_MULTILINE_COMMENT=91 -ENRICH_FIELD_WS=92 -MVEXPAND_LINE_COMMENT=93 -MVEXPAND_MULTILINE_COMMENT=94 -MVEXPAND_WS=95 -INFO=96 -FUNCTIONS=97 -SHOW_LINE_COMMENT=98 -SHOW_MULTILINE_COMMENT=99 -SHOW_WS=100 -COLON=101 -SETTING=102 -SETTING_LINE_COMMENT=103 -SETTTING_MULTILINE_COMMENT=104 -SETTING_WS=105 -'|'=26 -'='=33 -','=34 -'.'=36 -'('=40 -'?'=48 -')'=50 -'=='=52 -'=~'=53 -'!='=54 -'<'=55 -'<='=56 -'>'=57 -'>='=58 -'+'=59 -'-'=60 -'*'=61 -'/'=62 -'%'=63 -']'=65 -':'=101 +RENAME=12 +ROW=13 +SHOW=14 +SORT=15 +STATS=16 +WHERE=17 +UNKNOWN_CMD=18 +LINE_COMMENT=19 +MULTILINE_COMMENT=20 +WS=21 +EXPLAIN_WS=22 +EXPLAIN_LINE_COMMENT=23 +EXPLAIN_MULTILINE_COMMENT=24 +PIPE=25 +STRING=26 +INTEGER_LITERAL=27 +DECIMAL_LITERAL=28 +BY=29 +AND=30 +ASC=31 +ASSIGN=32 +COMMA=33 +DESC=34 +DOT=35 +FALSE=36 +FIRST=37 +LAST=38 +LP=39 +IN=40 +IS=41 +LIKE=42 +NOT=43 +NULL=44 +NULLS=45 +OR=46 +PARAM=47 +RLIKE=48 +RP=49 +TRUE=50 +EQ=51 +CIEQ=52 +NEQ=53 +LT=54 +LTE=55 +GT=56 +GTE=57 +PLUS=58 +MINUS=59 +ASTERISK=60 +SLASH=61 +PERCENT=62 +OPENING_BRACKET=63 +CLOSING_BRACKET=64 +UNQUOTED_IDENTIFIER=65 +QUOTED_IDENTIFIER=66 +EXPR_LINE_COMMENT=67 +EXPR_MULTILINE_COMMENT=68 +EXPR_WS=69 +METADATA=70 +FROM_UNQUOTED_IDENTIFIER=71 +FROM_LINE_COMMENT=72 +FROM_MULTILINE_COMMENT=73 +FROM_WS=74 +UNQUOTED_ID_PATTERN=75 +PROJECT_LINE_COMMENT=76 +PROJECT_MULTILINE_COMMENT=77 +PROJECT_WS=78 +AS=79 +RENAME_LINE_COMMENT=80 +RENAME_MULTILINE_COMMENT=81 +RENAME_WS=82 +ON=83 +WITH=84 +ENRICH_POLICY_NAME=85 +ENRICH_LINE_COMMENT=86 +ENRICH_MULTILINE_COMMENT=87 +ENRICH_WS=88 +ENRICH_FIELD_LINE_COMMENT=89 +ENRICH_FIELD_MULTILINE_COMMENT=90 +ENRICH_FIELD_WS=91 +MVEXPAND_LINE_COMMENT=92 +MVEXPAND_MULTILINE_COMMENT=93 +MVEXPAND_WS=94 +INFO=95 +FUNCTIONS=96 +SHOW_LINE_COMMENT=97 +SHOW_MULTILINE_COMMENT=98 +SHOW_WS=99 +COLON=100 +SETTING=101 +SETTING_LINE_COMMENT=102 +SETTTING_MULTILINE_COMMENT=103 +SETTING_WS=104 +'|'=25 +'='=32 +','=33 +'.'=35 +'('=39 +'?'=47 +')'=49 +'=='=51 +'=~'=52 +'!='=53 +'<'=54 +'<='=55 +'>'=56 +'>='=57 +'+'=58 +'-'=59 +'*'=60 +'/'=61 +'%'=62 +']'=64 +':'=100 diff --git a/packages/kbn-monaco/src/esql/antlr/esql_parser.ts b/packages/kbn-monaco/src/esql/antlr/esql_parser.ts index 696d67ba90f00..28d31ef92064a 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_parser.ts +++ b/packages/kbn-monaco/src/esql/antlr/esql_parser.ts @@ -38,100 +38,99 @@ export class esql_parser extends Parser { public static readonly KEEP = 9; public static readonly LIMIT = 10; public static readonly MV_EXPAND = 11; - public static readonly PROJECT = 12; - public static readonly RENAME = 13; - public static readonly ROW = 14; - public static readonly SHOW = 15; - public static readonly SORT = 16; - public static readonly STATS = 17; - public static readonly WHERE = 18; - public static readonly UNKNOWN_CMD = 19; - public static readonly LINE_COMMENT = 20; - public static readonly MULTILINE_COMMENT = 21; - public static readonly WS = 22; - public static readonly EXPLAIN_WS = 23; - public static readonly EXPLAIN_LINE_COMMENT = 24; - public static readonly EXPLAIN_MULTILINE_COMMENT = 25; - public static readonly PIPE = 26; - public static readonly STRING = 27; - public static readonly INTEGER_LITERAL = 28; - public static readonly DECIMAL_LITERAL = 29; - public static readonly BY = 30; - public static readonly AND = 31; - public static readonly ASC = 32; - public static readonly ASSIGN = 33; - public static readonly COMMA = 34; - public static readonly DESC = 35; - public static readonly DOT = 36; - public static readonly FALSE = 37; - public static readonly FIRST = 38; - public static readonly LAST = 39; - public static readonly LP = 40; - public static readonly IN = 41; - public static readonly IS = 42; - public static readonly LIKE = 43; - public static readonly NOT = 44; - public static readonly NULL = 45; - public static readonly NULLS = 46; - public static readonly OR = 47; - public static readonly PARAM = 48; - public static readonly RLIKE = 49; - public static readonly RP = 50; - public static readonly TRUE = 51; - public static readonly EQ = 52; - public static readonly CIEQ = 53; - public static readonly NEQ = 54; - public static readonly LT = 55; - public static readonly LTE = 56; - public static readonly GT = 57; - public static readonly GTE = 58; - public static readonly PLUS = 59; - public static readonly MINUS = 60; - public static readonly ASTERISK = 61; - public static readonly SLASH = 62; - public static readonly PERCENT = 63; - public static readonly OPENING_BRACKET = 64; - public static readonly CLOSING_BRACKET = 65; - public static readonly UNQUOTED_IDENTIFIER = 66; - public static readonly QUOTED_IDENTIFIER = 67; - public static readonly EXPR_LINE_COMMENT = 68; - public static readonly EXPR_MULTILINE_COMMENT = 69; - public static readonly EXPR_WS = 70; - public static readonly METADATA = 71; - public static readonly FROM_UNQUOTED_IDENTIFIER = 72; - public static readonly FROM_LINE_COMMENT = 73; - public static readonly FROM_MULTILINE_COMMENT = 74; - public static readonly FROM_WS = 75; - public static readonly UNQUOTED_ID_PATTERN = 76; - public static readonly PROJECT_LINE_COMMENT = 77; - public static readonly PROJECT_MULTILINE_COMMENT = 78; - public static readonly PROJECT_WS = 79; - public static readonly AS = 80; - public static readonly RENAME_LINE_COMMENT = 81; - public static readonly RENAME_MULTILINE_COMMENT = 82; - public static readonly RENAME_WS = 83; - public static readonly ON = 84; - public static readonly WITH = 85; - public static readonly ENRICH_POLICY_NAME = 86; - public static readonly ENRICH_LINE_COMMENT = 87; - public static readonly ENRICH_MULTILINE_COMMENT = 88; - public static readonly ENRICH_WS = 89; - public static readonly ENRICH_FIELD_LINE_COMMENT = 90; - public static readonly ENRICH_FIELD_MULTILINE_COMMENT = 91; - public static readonly ENRICH_FIELD_WS = 92; - public static readonly MVEXPAND_LINE_COMMENT = 93; - public static readonly MVEXPAND_MULTILINE_COMMENT = 94; - public static readonly MVEXPAND_WS = 95; - public static readonly INFO = 96; - public static readonly FUNCTIONS = 97; - public static readonly SHOW_LINE_COMMENT = 98; - public static readonly SHOW_MULTILINE_COMMENT = 99; - public static readonly SHOW_WS = 100; - public static readonly COLON = 101; - public static readonly SETTING = 102; - public static readonly SETTING_LINE_COMMENT = 103; - public static readonly SETTTING_MULTILINE_COMMENT = 104; - public static readonly SETTING_WS = 105; + public static readonly RENAME = 12; + public static readonly ROW = 13; + public static readonly SHOW = 14; + public static readonly SORT = 15; + public static readonly STATS = 16; + public static readonly WHERE = 17; + public static readonly UNKNOWN_CMD = 18; + public static readonly LINE_COMMENT = 19; + public static readonly MULTILINE_COMMENT = 20; + public static readonly WS = 21; + public static readonly EXPLAIN_WS = 22; + public static readonly EXPLAIN_LINE_COMMENT = 23; + public static readonly EXPLAIN_MULTILINE_COMMENT = 24; + public static readonly PIPE = 25; + public static readonly STRING = 26; + public static readonly INTEGER_LITERAL = 27; + public static readonly DECIMAL_LITERAL = 28; + public static readonly BY = 29; + public static readonly AND = 30; + public static readonly ASC = 31; + public static readonly ASSIGN = 32; + public static readonly COMMA = 33; + public static readonly DESC = 34; + public static readonly DOT = 35; + public static readonly FALSE = 36; + public static readonly FIRST = 37; + public static readonly LAST = 38; + public static readonly LP = 39; + public static readonly IN = 40; + public static readonly IS = 41; + public static readonly LIKE = 42; + public static readonly NOT = 43; + public static readonly NULL = 44; + public static readonly NULLS = 45; + public static readonly OR = 46; + public static readonly PARAM = 47; + public static readonly RLIKE = 48; + public static readonly RP = 49; + public static readonly TRUE = 50; + public static readonly EQ = 51; + public static readonly CIEQ = 52; + public static readonly NEQ = 53; + public static readonly LT = 54; + public static readonly LTE = 55; + public static readonly GT = 56; + public static readonly GTE = 57; + public static readonly PLUS = 58; + public static readonly MINUS = 59; + public static readonly ASTERISK = 60; + public static readonly SLASH = 61; + public static readonly PERCENT = 62; + public static readonly OPENING_BRACKET = 63; + public static readonly CLOSING_BRACKET = 64; + public static readonly UNQUOTED_IDENTIFIER = 65; + public static readonly QUOTED_IDENTIFIER = 66; + public static readonly EXPR_LINE_COMMENT = 67; + public static readonly EXPR_MULTILINE_COMMENT = 68; + public static readonly EXPR_WS = 69; + public static readonly METADATA = 70; + public static readonly FROM_UNQUOTED_IDENTIFIER = 71; + public static readonly FROM_LINE_COMMENT = 72; + public static readonly FROM_MULTILINE_COMMENT = 73; + public static readonly FROM_WS = 74; + public static readonly UNQUOTED_ID_PATTERN = 75; + public static readonly PROJECT_LINE_COMMENT = 76; + public static readonly PROJECT_MULTILINE_COMMENT = 77; + public static readonly PROJECT_WS = 78; + public static readonly AS = 79; + public static readonly RENAME_LINE_COMMENT = 80; + public static readonly RENAME_MULTILINE_COMMENT = 81; + public static readonly RENAME_WS = 82; + public static readonly ON = 83; + public static readonly WITH = 84; + public static readonly ENRICH_POLICY_NAME = 85; + public static readonly ENRICH_LINE_COMMENT = 86; + public static readonly ENRICH_MULTILINE_COMMENT = 87; + public static readonly ENRICH_WS = 88; + public static readonly ENRICH_FIELD_LINE_COMMENT = 89; + public static readonly ENRICH_FIELD_MULTILINE_COMMENT = 90; + public static readonly ENRICH_FIELD_WS = 91; + public static readonly MVEXPAND_LINE_COMMENT = 92; + public static readonly MVEXPAND_MULTILINE_COMMENT = 93; + public static readonly MVEXPAND_WS = 94; + public static readonly INFO = 95; + public static readonly FUNCTIONS = 96; + public static readonly SHOW_LINE_COMMENT = 97; + public static readonly SHOW_MULTILINE_COMMENT = 98; + public static readonly SHOW_WS = 99; + public static readonly COLON = 100; + public static readonly SETTING = 101; + public static readonly SETTING_LINE_COMMENT = 102; + public static readonly SETTTING_MULTILINE_COMMENT = 103; + public static readonly SETTING_WS = 104; public static readonly RULE_singleStatement = 0; public static readonly RULE_query = 1; public static readonly RULE_sourceCommand = 2; @@ -148,61 +147,62 @@ export class esql_parser extends Parser { public static readonly RULE_field = 13; public static readonly RULE_fromCommand = 14; public static readonly RULE_metadata = 15; - public static readonly RULE_evalCommand = 16; - public static readonly RULE_statsCommand = 17; - public static readonly RULE_inlinestatsCommand = 18; - public static readonly RULE_fromIdentifier = 19; - public static readonly RULE_qualifiedName = 20; - public static readonly RULE_qualifiedNamePattern = 21; - public static readonly RULE_identifier = 22; - public static readonly RULE_identifierPattern = 23; - public static readonly RULE_constant = 24; - public static readonly RULE_limitCommand = 25; - public static readonly RULE_sortCommand = 26; - public static readonly RULE_orderExpression = 27; - public static readonly RULE_keepCommand = 28; - public static readonly RULE_dropCommand = 29; - public static readonly RULE_renameCommand = 30; - public static readonly RULE_renameClause = 31; - public static readonly RULE_dissectCommand = 32; - public static readonly RULE_grokCommand = 33; - public static readonly RULE_mvExpandCommand = 34; - public static readonly RULE_commandOptions = 35; - public static readonly RULE_commandOption = 36; - public static readonly RULE_booleanValue = 37; - public static readonly RULE_numericValue = 38; - public static readonly RULE_decimalValue = 39; - public static readonly RULE_integerValue = 40; - public static readonly RULE_string = 41; - public static readonly RULE_comparisonOperator = 42; - public static readonly RULE_explainCommand = 43; - public static readonly RULE_subqueryExpression = 44; - public static readonly RULE_showCommand = 45; - public static readonly RULE_enrichCommand = 46; - public static readonly RULE_enrichWithClause = 47; - public static readonly RULE_setting = 48; + public static readonly RULE_metadataOption = 16; + public static readonly RULE_deprecated_metadata = 17; + public static readonly RULE_evalCommand = 18; + public static readonly RULE_statsCommand = 19; + public static readonly RULE_inlinestatsCommand = 20; + public static readonly RULE_fromIdentifier = 21; + public static readonly RULE_qualifiedName = 22; + public static readonly RULE_qualifiedNamePattern = 23; + public static readonly RULE_identifier = 24; + public static readonly RULE_identifierPattern = 25; + public static readonly RULE_constant = 26; + public static readonly RULE_limitCommand = 27; + public static readonly RULE_sortCommand = 28; + public static readonly RULE_orderExpression = 29; + public static readonly RULE_keepCommand = 30; + public static readonly RULE_dropCommand = 31; + public static readonly RULE_renameCommand = 32; + public static readonly RULE_renameClause = 33; + public static readonly RULE_dissectCommand = 34; + public static readonly RULE_grokCommand = 35; + public static readonly RULE_mvExpandCommand = 36; + public static readonly RULE_commandOptions = 37; + public static readonly RULE_commandOption = 38; + public static readonly RULE_booleanValue = 39; + public static readonly RULE_numericValue = 40; + public static readonly RULE_decimalValue = 41; + public static readonly RULE_integerValue = 42; + public static readonly RULE_string = 43; + public static readonly RULE_comparisonOperator = 44; + public static readonly RULE_explainCommand = 45; + public static readonly RULE_subqueryExpression = 46; + public static readonly RULE_showCommand = 47; + public static readonly RULE_enrichCommand = 48; + public static readonly RULE_enrichWithClause = 49; // tslint:disable:no-trailing-whitespace public static readonly ruleNames: string[] = [ "singleStatement", "query", "sourceCommand", "processingCommand", "whereCommand", "booleanExpression", "regexBooleanExpression", "valueExpression", "operatorExpression", "primaryExpression", "functionExpression", "rowCommand", "fields", "field", - "fromCommand", "metadata", "evalCommand", "statsCommand", "inlinestatsCommand", - "fromIdentifier", "qualifiedName", "qualifiedNamePattern", "identifier", - "identifierPattern", "constant", "limitCommand", "sortCommand", "orderExpression", - "keepCommand", "dropCommand", "renameCommand", "renameClause", "dissectCommand", - "grokCommand", "mvExpandCommand", "commandOptions", "commandOption", "booleanValue", - "numericValue", "decimalValue", "integerValue", "string", "comparisonOperator", - "explainCommand", "subqueryExpression", "showCommand", "enrichCommand", - "enrichWithClause", "setting", + "fromCommand", "metadata", "metadataOption", "deprecated_metadata", "evalCommand", + "statsCommand", "inlinestatsCommand", "fromIdentifier", "qualifiedName", + "qualifiedNamePattern", "identifier", "identifierPattern", "constant", + "limitCommand", "sortCommand", "orderExpression", "keepCommand", "dropCommand", + "renameCommand", "renameClause", "dissectCommand", "grokCommand", "mvExpandCommand", + "commandOptions", "commandOption", "booleanValue", "numericValue", "decimalValue", + "integerValue", "string", "comparisonOperator", "explainCommand", "subqueryExpression", + "showCommand", "enrichCommand", "enrichWithClause", ]; private static readonly _LITERAL_NAMES: Array = [ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, - undefined, undefined, undefined, undefined, undefined, "'|'", undefined, - undefined, undefined, undefined, undefined, undefined, "'='", "','", undefined, - "'.'", undefined, undefined, undefined, "'('", undefined, undefined, undefined, + undefined, undefined, undefined, undefined, "'|'", undefined, undefined, + undefined, undefined, undefined, undefined, "'='", "','", undefined, "'.'", + undefined, undefined, undefined, "'('", undefined, undefined, undefined, undefined, undefined, undefined, undefined, "'?'", undefined, "')'", undefined, "'=='", "'=~'", "'!='", "'<'", "'<='", "'>'", "'>='", "'+'", "'-'", "'*'", "'/'", "'%'", undefined, "']'", undefined, undefined, undefined, undefined, @@ -214,8 +214,8 @@ export class esql_parser extends Parser { ]; private static readonly _SYMBOLIC_NAMES: Array = [ undefined, "DISSECT", "DROP", "ENRICH", "EVAL", "EXPLAIN", "FROM", "GROK", - "INLINESTATS", "KEEP", "LIMIT", "MV_EXPAND", "PROJECT", "RENAME", "ROW", - "SHOW", "SORT", "STATS", "WHERE", "UNKNOWN_CMD", "LINE_COMMENT", "MULTILINE_COMMENT", + "INLINESTATS", "KEEP", "LIMIT", "MV_EXPAND", "RENAME", "ROW", "SHOW", + "SORT", "STATS", "WHERE", "UNKNOWN_CMD", "LINE_COMMENT", "MULTILINE_COMMENT", "WS", "EXPLAIN_WS", "EXPLAIN_LINE_COMMENT", "EXPLAIN_MULTILINE_COMMENT", "PIPE", "STRING", "INTEGER_LITERAL", "DECIMAL_LITERAL", "BY", "AND", "ASC", "ASSIGN", "COMMA", "DESC", "DOT", "FALSE", "FIRST", "LAST", "LP", "IN", @@ -262,9 +262,9 @@ export class esql_parser extends Parser { try { this.enterOuterAlt(_localctx, 1); { - this.state = 98; + this.state = 100; this.query(0); - this.state = 99; + this.state = 101; this.match(esql_parser.EOF); } } @@ -306,11 +306,11 @@ export class esql_parser extends Parser { this._ctx = _localctx; _prevctx = _localctx; - this.state = 102; + this.state = 104; this.sourceCommand(); } this._ctx._stop = this._input.tryLT(-1); - this.state = 109; + this.state = 111; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 0, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -323,18 +323,18 @@ export class esql_parser extends Parser { { _localctx = new CompositeQueryContext(new QueryContext(_parentctx, _parentState)); this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_query); - this.state = 104; + this.state = 106; if (!(this.precpred(this._ctx, 1))) { throw new FailedPredicateException(this, "this.precpred(this._ctx, 1)"); } - this.state = 105; + this.state = 107; this.match(esql_parser.PIPE); - this.state = 106; + this.state = 108; this.processingCommand(); } } } - this.state = 111; + this.state = 113; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 0, this._ctx); } @@ -359,34 +359,34 @@ export class esql_parser extends Parser { let _localctx: SourceCommandContext = new SourceCommandContext(this._ctx, this.state); this.enterRule(_localctx, 4, esql_parser.RULE_sourceCommand); try { - this.state = 116; + this.state = 118; this._errHandler.sync(this); switch (this._input.LA(1)) { case esql_parser.EXPLAIN: this.enterOuterAlt(_localctx, 1); { - this.state = 112; + this.state = 114; this.explainCommand(); } break; case esql_parser.FROM: this.enterOuterAlt(_localctx, 2); { - this.state = 113; + this.state = 115; this.fromCommand(); } break; case esql_parser.ROW: this.enterOuterAlt(_localctx, 3); { - this.state = 114; + this.state = 116; this.rowCommand(); } break; case esql_parser.SHOW: this.enterOuterAlt(_localctx, 4); { - this.state = 115; + this.state = 117; this.showCommand(); } break; @@ -413,98 +413,97 @@ export class esql_parser extends Parser { let _localctx: ProcessingCommandContext = new ProcessingCommandContext(this._ctx, this.state); this.enterRule(_localctx, 6, esql_parser.RULE_processingCommand); try { - this.state = 131; + this.state = 133; this._errHandler.sync(this); switch (this._input.LA(1)) { case esql_parser.EVAL: this.enterOuterAlt(_localctx, 1); { - this.state = 118; + this.state = 120; this.evalCommand(); } break; case esql_parser.INLINESTATS: this.enterOuterAlt(_localctx, 2); { - this.state = 119; + this.state = 121; this.inlinestatsCommand(); } break; case esql_parser.LIMIT: this.enterOuterAlt(_localctx, 3); { - this.state = 120; + this.state = 122; this.limitCommand(); } break; case esql_parser.KEEP: - case esql_parser.PROJECT: this.enterOuterAlt(_localctx, 4); { - this.state = 121; + this.state = 123; this.keepCommand(); } break; case esql_parser.SORT: this.enterOuterAlt(_localctx, 5); { - this.state = 122; + this.state = 124; this.sortCommand(); } break; case esql_parser.STATS: this.enterOuterAlt(_localctx, 6); { - this.state = 123; + this.state = 125; this.statsCommand(); } break; case esql_parser.WHERE: this.enterOuterAlt(_localctx, 7); { - this.state = 124; + this.state = 126; this.whereCommand(); } break; case esql_parser.DROP: this.enterOuterAlt(_localctx, 8); { - this.state = 125; + this.state = 127; this.dropCommand(); } break; case esql_parser.RENAME: this.enterOuterAlt(_localctx, 9); { - this.state = 126; + this.state = 128; this.renameCommand(); } break; case esql_parser.DISSECT: this.enterOuterAlt(_localctx, 10); { - this.state = 127; + this.state = 129; this.dissectCommand(); } break; case esql_parser.GROK: this.enterOuterAlt(_localctx, 11); { - this.state = 128; + this.state = 130; this.grokCommand(); } break; case esql_parser.ENRICH: this.enterOuterAlt(_localctx, 12); { - this.state = 129; + this.state = 131; this.enrichCommand(); } break; case esql_parser.MV_EXPAND: this.enterOuterAlt(_localctx, 13); { - this.state = 130; + this.state = 132; this.mvExpandCommand(); } break; @@ -533,9 +532,9 @@ export class esql_parser extends Parser { try { this.enterOuterAlt(_localctx, 1); { - this.state = 133; + this.state = 135; this.match(esql_parser.WHERE); - this.state = 134; + this.state = 136; this.booleanExpression(0); } } @@ -573,7 +572,7 @@ export class esql_parser extends Parser { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 164; + this.state = 166; this._errHandler.sync(this); switch ( this.interpreter.adaptivePredict(this._input, 6, this._ctx) ) { case 1: @@ -582,9 +581,9 @@ export class esql_parser extends Parser { this._ctx = _localctx; _prevctx = _localctx; - this.state = 137; + this.state = 139; this.match(esql_parser.NOT); - this.state = 138; + this.state = 140; this.booleanExpression(7); } break; @@ -594,7 +593,7 @@ export class esql_parser extends Parser { _localctx = new BooleanDefaultContext(_localctx); this._ctx = _localctx; _prevctx = _localctx; - this.state = 139; + this.state = 141; this.valueExpression(); } break; @@ -604,7 +603,7 @@ export class esql_parser extends Parser { _localctx = new RegexExpressionContext(_localctx); this._ctx = _localctx; _prevctx = _localctx; - this.state = 140; + this.state = 142; this.regexBooleanExpression(); } break; @@ -614,41 +613,41 @@ export class esql_parser extends Parser { _localctx = new LogicalInContext(_localctx); this._ctx = _localctx; _prevctx = _localctx; - this.state = 141; - this.valueExpression(); this.state = 143; + this.valueExpression(); + this.state = 145; this._errHandler.sync(this); _la = this._input.LA(1); if (_la === esql_parser.NOT) { { - this.state = 142; + this.state = 144; this.match(esql_parser.NOT); } } - this.state = 145; + this.state = 147; this.match(esql_parser.IN); - this.state = 146; + this.state = 148; this.match(esql_parser.LP); - this.state = 147; + this.state = 149; this.valueExpression(); - this.state = 152; + this.state = 154; this._errHandler.sync(this); _la = this._input.LA(1); while (_la === esql_parser.COMMA) { { { - this.state = 148; + this.state = 150; this.match(esql_parser.COMMA); - this.state = 149; + this.state = 151; this.valueExpression(); } } - this.state = 154; + this.state = 156; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 155; + this.state = 157; this.match(esql_parser.RP); } break; @@ -658,27 +657,27 @@ export class esql_parser extends Parser { _localctx = new IsNullContext(_localctx); this._ctx = _localctx; _prevctx = _localctx; - this.state = 157; + this.state = 159; this.valueExpression(); - this.state = 158; - this.match(esql_parser.IS); this.state = 160; + this.match(esql_parser.IS); + this.state = 162; this._errHandler.sync(this); _la = this._input.LA(1); if (_la === esql_parser.NOT) { { - this.state = 159; + this.state = 161; this.match(esql_parser.NOT); } } - this.state = 162; + this.state = 164; this.match(esql_parser.NULL); } break; } this._ctx._stop = this._input.tryLT(-1); - this.state = 174; + this.state = 176; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 8, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -688,7 +687,7 @@ export class esql_parser extends Parser { } _prevctx = _localctx; { - this.state = 172; + this.state = 174; this._errHandler.sync(this); switch ( this.interpreter.adaptivePredict(this._input, 7, this._ctx) ) { case 1: @@ -696,13 +695,13 @@ export class esql_parser extends Parser { _localctx = new LogicalBinaryContext(new BooleanExpressionContext(_parentctx, _parentState)); (_localctx as LogicalBinaryContext)._left = _prevctx; this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_booleanExpression); - this.state = 166; + this.state = 168; if (!(this.precpred(this._ctx, 4))) { throw new FailedPredicateException(this, "this.precpred(this._ctx, 4)"); } - this.state = 167; + this.state = 169; (_localctx as LogicalBinaryContext)._operator = this.match(esql_parser.AND); - this.state = 168; + this.state = 170; (_localctx as LogicalBinaryContext)._right = this.booleanExpression(5); } break; @@ -712,20 +711,20 @@ export class esql_parser extends Parser { _localctx = new LogicalBinaryContext(new BooleanExpressionContext(_parentctx, _parentState)); (_localctx as LogicalBinaryContext)._left = _prevctx; this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_booleanExpression); - this.state = 169; + this.state = 171; if (!(this.precpred(this._ctx, 3))) { throw new FailedPredicateException(this, "this.precpred(this._ctx, 3)"); } - this.state = 170; + this.state = 172; (_localctx as LogicalBinaryContext)._operator = this.match(esql_parser.OR); - this.state = 171; + this.state = 173; (_localctx as LogicalBinaryContext)._right = this.booleanExpression(4); } break; } } } - this.state = 176; + this.state = 178; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 8, this._ctx); } @@ -751,27 +750,27 @@ export class esql_parser extends Parser { this.enterRule(_localctx, 12, esql_parser.RULE_regexBooleanExpression); let _la: number; try { - this.state = 191; + this.state = 193; this._errHandler.sync(this); switch ( this.interpreter.adaptivePredict(this._input, 11, this._ctx) ) { case 1: this.enterOuterAlt(_localctx, 1); { - this.state = 177; - this.valueExpression(); this.state = 179; + this.valueExpression(); + this.state = 181; this._errHandler.sync(this); _la = this._input.LA(1); if (_la === esql_parser.NOT) { { - this.state = 178; + this.state = 180; this.match(esql_parser.NOT); } } - this.state = 181; + this.state = 183; _localctx._kind = this.match(esql_parser.LIKE); - this.state = 182; + this.state = 184; _localctx._pattern = this.string(); } break; @@ -779,21 +778,21 @@ export class esql_parser extends Parser { case 2: this.enterOuterAlt(_localctx, 2); { - this.state = 184; - this.valueExpression(); this.state = 186; + this.valueExpression(); + this.state = 188; this._errHandler.sync(this); _la = this._input.LA(1); if (_la === esql_parser.NOT) { { - this.state = 185; + this.state = 187; this.match(esql_parser.NOT); } } - this.state = 188; + this.state = 190; _localctx._kind = this.match(esql_parser.RLIKE); - this.state = 189; + this.state = 191; _localctx._pattern = this.string(); } break; @@ -818,14 +817,14 @@ export class esql_parser extends Parser { let _localctx: ValueExpressionContext = new ValueExpressionContext(this._ctx, this.state); this.enterRule(_localctx, 14, esql_parser.RULE_valueExpression); try { - this.state = 198; + this.state = 200; this._errHandler.sync(this); switch ( this.interpreter.adaptivePredict(this._input, 12, this._ctx) ) { case 1: _localctx = new ValueExpressionDefaultContext(_localctx); this.enterOuterAlt(_localctx, 1); { - this.state = 193; + this.state = 195; this.operatorExpression(0); } break; @@ -834,11 +833,11 @@ export class esql_parser extends Parser { _localctx = new ComparisonContext(_localctx); this.enterOuterAlt(_localctx, 2); { - this.state = 194; + this.state = 196; (_localctx as ComparisonContext)._left = this.operatorExpression(0); - this.state = 195; + this.state = 197; this.comparisonOperator(); - this.state = 196; + this.state = 198; (_localctx as ComparisonContext)._right = this.operatorExpression(0); } break; @@ -878,7 +877,7 @@ export class esql_parser extends Parser { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 204; + this.state = 206; this._errHandler.sync(this); switch ( this.interpreter.adaptivePredict(this._input, 13, this._ctx) ) { case 1: @@ -887,7 +886,7 @@ export class esql_parser extends Parser { this._ctx = _localctx; _prevctx = _localctx; - this.state = 201; + this.state = 203; this.primaryExpression(); } break; @@ -897,7 +896,7 @@ export class esql_parser extends Parser { _localctx = new ArithmeticUnaryContext(_localctx); this._ctx = _localctx; _prevctx = _localctx; - this.state = 202; + this.state = 204; (_localctx as ArithmeticUnaryContext)._operator = this._input.LT(1); _la = this._input.LA(1); if (!(_la === esql_parser.PLUS || _la === esql_parser.MINUS)) { @@ -910,13 +909,13 @@ export class esql_parser extends Parser { this._errHandler.reportMatch(this); this.consume(); } - this.state = 203; + this.state = 205; this.operatorExpression(3); } break; } this._ctx._stop = this._input.tryLT(-1); - this.state = 214; + this.state = 216; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 15, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { @@ -926,7 +925,7 @@ export class esql_parser extends Parser { } _prevctx = _localctx; { - this.state = 212; + this.state = 214; this._errHandler.sync(this); switch ( this.interpreter.adaptivePredict(this._input, 14, this._ctx) ) { case 1: @@ -934,14 +933,14 @@ export class esql_parser extends Parser { _localctx = new ArithmeticBinaryContext(new OperatorExpressionContext(_parentctx, _parentState)); (_localctx as ArithmeticBinaryContext)._left = _prevctx; this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_operatorExpression); - this.state = 206; + this.state = 208; if (!(this.precpred(this._ctx, 2))) { throw new FailedPredicateException(this, "this.precpred(this._ctx, 2)"); } - this.state = 207; + this.state = 209; (_localctx as ArithmeticBinaryContext)._operator = this._input.LT(1); _la = this._input.LA(1); - if (!(((((_la - 61)) & ~0x1F) === 0 && ((1 << (_la - 61)) & ((1 << (esql_parser.ASTERISK - 61)) | (1 << (esql_parser.SLASH - 61)) | (1 << (esql_parser.PERCENT - 61)))) !== 0))) { + if (!(((((_la - 60)) & ~0x1F) === 0 && ((1 << (_la - 60)) & ((1 << (esql_parser.ASTERISK - 60)) | (1 << (esql_parser.SLASH - 60)) | (1 << (esql_parser.PERCENT - 60)))) !== 0))) { (_localctx as ArithmeticBinaryContext)._operator = this._errHandler.recoverInline(this); } else { if (this._input.LA(1) === Token.EOF) { @@ -951,7 +950,7 @@ export class esql_parser extends Parser { this._errHandler.reportMatch(this); this.consume(); } - this.state = 208; + this.state = 210; (_localctx as ArithmeticBinaryContext)._right = this.operatorExpression(3); } break; @@ -961,11 +960,11 @@ export class esql_parser extends Parser { _localctx = new ArithmeticBinaryContext(new OperatorExpressionContext(_parentctx, _parentState)); (_localctx as ArithmeticBinaryContext)._left = _prevctx; this.pushNewRecursionContext(_localctx, _startState, esql_parser.RULE_operatorExpression); - this.state = 209; + this.state = 211; if (!(this.precpred(this._ctx, 1))) { throw new FailedPredicateException(this, "this.precpred(this._ctx, 1)"); } - this.state = 210; + this.state = 212; (_localctx as ArithmeticBinaryContext)._operator = this._input.LT(1); _la = this._input.LA(1); if (!(_la === esql_parser.PLUS || _la === esql_parser.MINUS)) { @@ -978,14 +977,14 @@ export class esql_parser extends Parser { this._errHandler.reportMatch(this); this.consume(); } - this.state = 211; + this.state = 213; (_localctx as ArithmeticBinaryContext)._right = this.operatorExpression(2); } break; } } } - this.state = 216; + this.state = 218; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 15, this._ctx); } @@ -1010,14 +1009,14 @@ export class esql_parser extends Parser { let _localctx: PrimaryExpressionContext = new PrimaryExpressionContext(this._ctx, this.state); this.enterRule(_localctx, 18, esql_parser.RULE_primaryExpression); try { - this.state = 224; + this.state = 226; this._errHandler.sync(this); switch ( this.interpreter.adaptivePredict(this._input, 16, this._ctx) ) { case 1: _localctx = new ConstantDefaultContext(_localctx); this.enterOuterAlt(_localctx, 1); { - this.state = 217; + this.state = 219; this.constant(); } break; @@ -1026,7 +1025,7 @@ export class esql_parser extends Parser { _localctx = new DereferenceContext(_localctx); this.enterOuterAlt(_localctx, 2); { - this.state = 218; + this.state = 220; this.qualifiedName(); } break; @@ -1035,7 +1034,7 @@ export class esql_parser extends Parser { _localctx = new FunctionContext(_localctx); this.enterOuterAlt(_localctx, 3); { - this.state = 219; + this.state = 221; this.functionExpression(); } break; @@ -1044,11 +1043,11 @@ export class esql_parser extends Parser { _localctx = new ParenthesizedExpressionContext(_localctx); this.enterOuterAlt(_localctx, 4); { - this.state = 220; + this.state = 222; this.match(esql_parser.LP); - this.state = 221; + this.state = 223; this.booleanExpression(0); - this.state = 222; + this.state = 224; this.match(esql_parser.RP); } break; @@ -1076,16 +1075,16 @@ export class esql_parser extends Parser { try { this.enterOuterAlt(_localctx, 1); { - this.state = 226; + this.state = 228; this.identifier(); - this.state = 227; + this.state = 229; this.match(esql_parser.LP); - this.state = 237; + this.state = 239; this._errHandler.sync(this); switch (this._input.LA(1)) { case esql_parser.ASTERISK: { - this.state = 228; + this.state = 230; this.match(esql_parser.ASTERISK); } break; @@ -1105,21 +1104,21 @@ export class esql_parser extends Parser { case esql_parser.QUOTED_IDENTIFIER: { { - this.state = 229; + this.state = 231; this.booleanExpression(0); - this.state = 234; + this.state = 236; this._errHandler.sync(this); _la = this._input.LA(1); while (_la === esql_parser.COMMA) { { { - this.state = 230; + this.state = 232; this.match(esql_parser.COMMA); - this.state = 231; + this.state = 233; this.booleanExpression(0); } } - this.state = 236; + this.state = 238; this._errHandler.sync(this); _la = this._input.LA(1); } @@ -1131,7 +1130,7 @@ export class esql_parser extends Parser { default: break; } - this.state = 239; + this.state = 241; this.match(esql_parser.RP); } } @@ -1156,9 +1155,9 @@ export class esql_parser extends Parser { try { this.enterOuterAlt(_localctx, 1); { - this.state = 241; + this.state = 243; this.match(esql_parser.ROW); - this.state = 242; + this.state = 244; this.fields(); } } @@ -1184,23 +1183,23 @@ export class esql_parser extends Parser { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 244; + this.state = 246; this.field(); - this.state = 249; + this.state = 251; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 19, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 245; + this.state = 247; this.match(esql_parser.COMMA); - this.state = 246; + this.state = 248; this.field(); } } } - this.state = 251; + this.state = 253; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 19, this._ctx); } @@ -1225,13 +1224,13 @@ export class esql_parser extends Parser { let _localctx: FieldContext = new FieldContext(this._ctx, this.state); this.enterRule(_localctx, 26, esql_parser.RULE_field); try { - this.state = 257; + this.state = 259; this._errHandler.sync(this); switch ( this.interpreter.adaptivePredict(this._input, 20, this._ctx) ) { case 1: this.enterOuterAlt(_localctx, 1); { - this.state = 252; + this.state = 254; this.booleanExpression(0); } break; @@ -1239,11 +1238,11 @@ export class esql_parser extends Parser { case 2: this.enterOuterAlt(_localctx, 2); { - this.state = 253; + this.state = 255; this.qualifiedName(); - this.state = 254; + this.state = 256; this.match(esql_parser.ASSIGN); - this.state = 255; + this.state = 257; this.booleanExpression(0); } break; @@ -1271,34 +1270,34 @@ export class esql_parser extends Parser { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 259; + this.state = 261; this.match(esql_parser.FROM); - this.state = 260; + this.state = 262; this.fromIdentifier(); - this.state = 265; + this.state = 267; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 21, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 261; + this.state = 263; this.match(esql_parser.COMMA); - this.state = 262; + this.state = 264; this.fromIdentifier(); } } } - this.state = 267; + this.state = 269; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 21, this._ctx); } - this.state = 269; + this.state = 271; this._errHandler.sync(this); switch ( this.interpreter.adaptivePredict(this._input, 22, this._ctx) ) { case 1: { - this.state = 268; + this.state = 270; this.metadata(); } break; @@ -1323,33 +1322,100 @@ export class esql_parser extends Parser { public metadata(): MetadataContext { let _localctx: MetadataContext = new MetadataContext(this._ctx, this.state); this.enterRule(_localctx, 30, esql_parser.RULE_metadata); - let _la: number; try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 271; - this.match(esql_parser.OPENING_BRACKET); - this.state = 272; - this.match(esql_parser.METADATA); - this.state = 273; - this.fromIdentifier(); - this.state = 278; + this.state = 275; this._errHandler.sync(this); - _la = this._input.LA(1); - while (_la === esql_parser.COMMA) { + switch (this._input.LA(1)) { + case esql_parser.METADATA: + this.enterOuterAlt(_localctx, 1); { + this.state = 273; + this.metadataOption(); + } + break; + case esql_parser.OPENING_BRACKET: + this.enterOuterAlt(_localctx, 2); { this.state = 274; - this.match(esql_parser.COMMA); - this.state = 275; - this.fromIdentifier(); + this.deprecated_metadata(); } + break; + default: + throw new NoViableAltException(this); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public metadataOption(): MetadataOptionContext { + let _localctx: MetadataOptionContext = new MetadataOptionContext(this._ctx, this.state); + this.enterRule(_localctx, 32, esql_parser.RULE_metadataOption); + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 277; + this.match(esql_parser.METADATA); + this.state = 278; + this.fromIdentifier(); + this.state = 283; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 24, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 279; + this.match(esql_parser.COMMA); + this.state = 280; + this.fromIdentifier(); + } + } } - this.state = 280; + this.state = 285; this._errHandler.sync(this); - _la = this._input.LA(1); + _alt = this.interpreter.adaptivePredict(this._input, 24, this._ctx); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; } - this.state = 281; + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public deprecated_metadata(): Deprecated_metadataContext { + let _localctx: Deprecated_metadataContext = new Deprecated_metadataContext(this._ctx, this.state); + this.enterRule(_localctx, 34, esql_parser.RULE_deprecated_metadata); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 286; + this.match(esql_parser.OPENING_BRACKET); + this.state = 287; + this.metadataOption(); + this.state = 288; this.match(esql_parser.CLOSING_BRACKET); } } @@ -1370,13 +1436,13 @@ export class esql_parser extends Parser { // @RuleVersion(0) public evalCommand(): EvalCommandContext { let _localctx: EvalCommandContext = new EvalCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 32, esql_parser.RULE_evalCommand); + this.enterRule(_localctx, 36, esql_parser.RULE_evalCommand); try { this.enterOuterAlt(_localctx, 1); { - this.state = 283; + this.state = 290; this.match(esql_parser.EVAL); - this.state = 284; + this.state = 291; this.fields(); } } @@ -1397,30 +1463,30 @@ export class esql_parser extends Parser { // @RuleVersion(0) public statsCommand(): StatsCommandContext { let _localctx: StatsCommandContext = new StatsCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 34, esql_parser.RULE_statsCommand); + this.enterRule(_localctx, 38, esql_parser.RULE_statsCommand); try { this.enterOuterAlt(_localctx, 1); { - this.state = 286; + this.state = 293; this.match(esql_parser.STATS); - this.state = 288; + this.state = 295; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 24, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 25, this._ctx) ) { case 1: { - this.state = 287; + this.state = 294; _localctx._stats = this.fields(); } break; } - this.state = 292; + this.state = 299; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 25, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 26, this._ctx) ) { case 1: { - this.state = 290; + this.state = 297; this.match(esql_parser.BY); - this.state = 291; + this.state = 298; _localctx._grouping = this.fields(); } break; @@ -1444,22 +1510,22 @@ export class esql_parser extends Parser { // @RuleVersion(0) public inlinestatsCommand(): InlinestatsCommandContext { let _localctx: InlinestatsCommandContext = new InlinestatsCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 36, esql_parser.RULE_inlinestatsCommand); + this.enterRule(_localctx, 40, esql_parser.RULE_inlinestatsCommand); try { this.enterOuterAlt(_localctx, 1); { - this.state = 294; + this.state = 301; this.match(esql_parser.INLINESTATS); - this.state = 295; + this.state = 302; _localctx._stats = this.fields(); - this.state = 298; + this.state = 305; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 26, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 27, this._ctx) ) { case 1: { - this.state = 296; + this.state = 303; this.match(esql_parser.BY); - this.state = 297; + this.state = 304; _localctx._grouping = this.fields(); } break; @@ -1483,12 +1549,12 @@ export class esql_parser extends Parser { // @RuleVersion(0) public fromIdentifier(): FromIdentifierContext { let _localctx: FromIdentifierContext = new FromIdentifierContext(this._ctx, this.state); - this.enterRule(_localctx, 38, esql_parser.RULE_fromIdentifier); + this.enterRule(_localctx, 42, esql_parser.RULE_fromIdentifier); let _la: number; try { this.enterOuterAlt(_localctx, 1); { - this.state = 300; + this.state = 307; _la = this._input.LA(1); if (!(_la === esql_parser.QUOTED_IDENTIFIER || _la === esql_parser.FROM_UNQUOTED_IDENTIFIER)) { this._errHandler.recoverInline(this); @@ -1519,30 +1585,30 @@ export class esql_parser extends Parser { // @RuleVersion(0) public qualifiedName(): QualifiedNameContext { let _localctx: QualifiedNameContext = new QualifiedNameContext(this._ctx, this.state); - this.enterRule(_localctx, 40, esql_parser.RULE_qualifiedName); + this.enterRule(_localctx, 44, esql_parser.RULE_qualifiedName); try { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 302; + this.state = 309; this.identifier(); - this.state = 307; + this.state = 314; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 27, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 28, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 303; + this.state = 310; this.match(esql_parser.DOT); - this.state = 304; + this.state = 311; this.identifier(); } } } - this.state = 309; + this.state = 316; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 27, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 28, this._ctx); } } } @@ -1563,30 +1629,30 @@ export class esql_parser extends Parser { // @RuleVersion(0) public qualifiedNamePattern(): QualifiedNamePatternContext { let _localctx: QualifiedNamePatternContext = new QualifiedNamePatternContext(this._ctx, this.state); - this.enterRule(_localctx, 42, esql_parser.RULE_qualifiedNamePattern); + this.enterRule(_localctx, 46, esql_parser.RULE_qualifiedNamePattern); try { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 310; + this.state = 317; this.identifierPattern(); - this.state = 315; + this.state = 322; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 28, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 29, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 311; + this.state = 318; this.match(esql_parser.DOT); - this.state = 312; + this.state = 319; this.identifierPattern(); } } } - this.state = 317; + this.state = 324; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 28, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 29, this._ctx); } } } @@ -1607,12 +1673,12 @@ export class esql_parser extends Parser { // @RuleVersion(0) public identifier(): IdentifierContext { let _localctx: IdentifierContext = new IdentifierContext(this._ctx, this.state); - this.enterRule(_localctx, 44, esql_parser.RULE_identifier); + this.enterRule(_localctx, 48, esql_parser.RULE_identifier); let _la: number; try { this.enterOuterAlt(_localctx, 1); { - this.state = 318; + this.state = 325; _la = this._input.LA(1); if (!(_la === esql_parser.UNQUOTED_IDENTIFIER || _la === esql_parser.QUOTED_IDENTIFIER)) { this._errHandler.recoverInline(this); @@ -1643,12 +1709,12 @@ export class esql_parser extends Parser { // @RuleVersion(0) public identifierPattern(): IdentifierPatternContext { let _localctx: IdentifierPatternContext = new IdentifierPatternContext(this._ctx, this.state); - this.enterRule(_localctx, 46, esql_parser.RULE_identifierPattern); + this.enterRule(_localctx, 50, esql_parser.RULE_identifierPattern); let _la: number; try { this.enterOuterAlt(_localctx, 1); { - this.state = 320; + this.state = 327; _la = this._input.LA(1); if (!(_la === esql_parser.QUOTED_IDENTIFIER || _la === esql_parser.UNQUOTED_ID_PATTERN)) { this._errHandler.recoverInline(this); @@ -1679,17 +1745,17 @@ export class esql_parser extends Parser { // @RuleVersion(0) public constant(): ConstantContext { let _localctx: ConstantContext = new ConstantContext(this._ctx, this.state); - this.enterRule(_localctx, 48, esql_parser.RULE_constant); + this.enterRule(_localctx, 52, esql_parser.RULE_constant); let _la: number; try { - this.state = 364; + this.state = 371; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 32, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 33, this._ctx) ) { case 1: _localctx = new NullLiteralContext(_localctx); this.enterOuterAlt(_localctx, 1); { - this.state = 322; + this.state = 329; this.match(esql_parser.NULL); } break; @@ -1698,9 +1764,9 @@ export class esql_parser extends Parser { _localctx = new QualifiedIntegerLiteralContext(_localctx); this.enterOuterAlt(_localctx, 2); { - this.state = 323; + this.state = 330; this.integerValue(); - this.state = 324; + this.state = 331; this.match(esql_parser.UNQUOTED_IDENTIFIER); } break; @@ -1709,7 +1775,7 @@ export class esql_parser extends Parser { _localctx = new DecimalLiteralContext(_localctx); this.enterOuterAlt(_localctx, 3); { - this.state = 326; + this.state = 333; this.decimalValue(); } break; @@ -1718,7 +1784,7 @@ export class esql_parser extends Parser { _localctx = new IntegerLiteralContext(_localctx); this.enterOuterAlt(_localctx, 4); { - this.state = 327; + this.state = 334; this.integerValue(); } break; @@ -1727,7 +1793,7 @@ export class esql_parser extends Parser { _localctx = new BooleanLiteralContext(_localctx); this.enterOuterAlt(_localctx, 5); { - this.state = 328; + this.state = 335; this.booleanValue(); } break; @@ -1736,7 +1802,7 @@ export class esql_parser extends Parser { _localctx = new InputParamContext(_localctx); this.enterOuterAlt(_localctx, 6); { - this.state = 329; + this.state = 336; this.match(esql_parser.PARAM); } break; @@ -1745,7 +1811,7 @@ export class esql_parser extends Parser { _localctx = new StringLiteralContext(_localctx); this.enterOuterAlt(_localctx, 7); { - this.state = 330; + this.state = 337; this.string(); } break; @@ -1754,27 +1820,27 @@ export class esql_parser extends Parser { _localctx = new NumericArrayLiteralContext(_localctx); this.enterOuterAlt(_localctx, 8); { - this.state = 331; + this.state = 338; this.match(esql_parser.OPENING_BRACKET); - this.state = 332; + this.state = 339; this.numericValue(); - this.state = 337; + this.state = 344; this._errHandler.sync(this); _la = this._input.LA(1); while (_la === esql_parser.COMMA) { { { - this.state = 333; + this.state = 340; this.match(esql_parser.COMMA); - this.state = 334; + this.state = 341; this.numericValue(); } } - this.state = 339; + this.state = 346; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 340; + this.state = 347; this.match(esql_parser.CLOSING_BRACKET); } break; @@ -1783,27 +1849,27 @@ export class esql_parser extends Parser { _localctx = new BooleanArrayLiteralContext(_localctx); this.enterOuterAlt(_localctx, 9); { - this.state = 342; + this.state = 349; this.match(esql_parser.OPENING_BRACKET); - this.state = 343; + this.state = 350; this.booleanValue(); - this.state = 348; + this.state = 355; this._errHandler.sync(this); _la = this._input.LA(1); while (_la === esql_parser.COMMA) { { { - this.state = 344; + this.state = 351; this.match(esql_parser.COMMA); - this.state = 345; + this.state = 352; this.booleanValue(); } } - this.state = 350; + this.state = 357; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 351; + this.state = 358; this.match(esql_parser.CLOSING_BRACKET); } break; @@ -1812,27 +1878,27 @@ export class esql_parser extends Parser { _localctx = new StringArrayLiteralContext(_localctx); this.enterOuterAlt(_localctx, 10); { - this.state = 353; + this.state = 360; this.match(esql_parser.OPENING_BRACKET); - this.state = 354; + this.state = 361; this.string(); - this.state = 359; + this.state = 366; this._errHandler.sync(this); _la = this._input.LA(1); while (_la === esql_parser.COMMA) { { { - this.state = 355; + this.state = 362; this.match(esql_parser.COMMA); - this.state = 356; + this.state = 363; this.string(); } } - this.state = 361; + this.state = 368; this._errHandler.sync(this); _la = this._input.LA(1); } - this.state = 362; + this.state = 369; this.match(esql_parser.CLOSING_BRACKET); } break; @@ -1855,13 +1921,13 @@ export class esql_parser extends Parser { // @RuleVersion(0) public limitCommand(): LimitCommandContext { let _localctx: LimitCommandContext = new LimitCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 50, esql_parser.RULE_limitCommand); + this.enterRule(_localctx, 54, esql_parser.RULE_limitCommand); try { this.enterOuterAlt(_localctx, 1); { - this.state = 366; + this.state = 373; this.match(esql_parser.LIMIT); - this.state = 367; + this.state = 374; this.match(esql_parser.INTEGER_LITERAL); } } @@ -1882,32 +1948,32 @@ export class esql_parser extends Parser { // @RuleVersion(0) public sortCommand(): SortCommandContext { let _localctx: SortCommandContext = new SortCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 52, esql_parser.RULE_sortCommand); + this.enterRule(_localctx, 56, esql_parser.RULE_sortCommand); try { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 369; + this.state = 376; this.match(esql_parser.SORT); - this.state = 370; + this.state = 377; this.orderExpression(); - this.state = 375; + this.state = 382; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 33, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 34, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 371; + this.state = 378; this.match(esql_parser.COMMA); - this.state = 372; + this.state = 379; this.orderExpression(); } } } - this.state = 377; + this.state = 384; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 33, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 34, this._ctx); } } } @@ -1928,19 +1994,19 @@ export class esql_parser extends Parser { // @RuleVersion(0) public orderExpression(): OrderExpressionContext { let _localctx: OrderExpressionContext = new OrderExpressionContext(this._ctx, this.state); - this.enterRule(_localctx, 54, esql_parser.RULE_orderExpression); + this.enterRule(_localctx, 58, esql_parser.RULE_orderExpression); let _la: number; try { this.enterOuterAlt(_localctx, 1); { - this.state = 378; + this.state = 385; this.booleanExpression(0); - this.state = 380; + this.state = 387; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 34, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 35, this._ctx) ) { case 1: { - this.state = 379; + this.state = 386; _localctx._ordering = this._input.LT(1); _la = this._input.LA(1); if (!(_la === esql_parser.ASC || _la === esql_parser.DESC)) { @@ -1956,14 +2022,14 @@ export class esql_parser extends Parser { } break; } - this.state = 384; + this.state = 391; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 35, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 36, this._ctx) ) { case 1: { - this.state = 382; + this.state = 389; this.match(esql_parser.NULLS); - this.state = 383; + this.state = 390; _localctx._nullOrdering = this._input.LT(1); _la = this._input.LA(1); if (!(_la === esql_parser.FIRST || _la === esql_parser.LAST)) { @@ -1998,68 +2064,33 @@ export class esql_parser extends Parser { // @RuleVersion(0) public keepCommand(): KeepCommandContext { let _localctx: KeepCommandContext = new KeepCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 56, esql_parser.RULE_keepCommand); + this.enterRule(_localctx, 60, esql_parser.RULE_keepCommand); try { let _alt: number; - this.state = 404; + this.enterOuterAlt(_localctx, 1); + { + this.state = 393; + this.match(esql_parser.KEEP); + this.state = 394; + this.qualifiedNamePattern(); + this.state = 399; this._errHandler.sync(this); - switch (this._input.LA(1)) { - case esql_parser.KEEP: - this.enterOuterAlt(_localctx, 1); - { - this.state = 386; - this.match(esql_parser.KEEP); - this.state = 387; - this.qualifiedNamePattern(); - this.state = 392; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 36, this._ctx); - while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { - if (_alt === 1) { - { - { - this.state = 388; - this.match(esql_parser.COMMA); - this.state = 389; - this.qualifiedNamePattern(); - } - } + _alt = this.interpreter.adaptivePredict(this._input, 37, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 395; + this.match(esql_parser.COMMA); + this.state = 396; + this.qualifiedNamePattern(); + } } - this.state = 394; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 36, this._ctx); - } } - break; - case esql_parser.PROJECT: - this.enterOuterAlt(_localctx, 2); - { - this.state = 395; - this.match(esql_parser.PROJECT); - this.state = 396; - this.qualifiedNamePattern(); this.state = 401; this._errHandler.sync(this); _alt = this.interpreter.adaptivePredict(this._input, 37, this._ctx); - while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { - if (_alt === 1) { - { - { - this.state = 397; - this.match(esql_parser.COMMA); - this.state = 398; - this.qualifiedNamePattern(); - } - } - } - this.state = 403; - this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 37, this._ctx); - } - } - break; - default: - throw new NoViableAltException(this); + } } } catch (re) { @@ -2079,32 +2110,32 @@ export class esql_parser extends Parser { // @RuleVersion(0) public dropCommand(): DropCommandContext { let _localctx: DropCommandContext = new DropCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 58, esql_parser.RULE_dropCommand); + this.enterRule(_localctx, 62, esql_parser.RULE_dropCommand); try { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 406; + this.state = 402; this.match(esql_parser.DROP); - this.state = 407; + this.state = 403; this.qualifiedNamePattern(); - this.state = 412; + this.state = 408; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 39, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 38, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 408; + this.state = 404; this.match(esql_parser.COMMA); - this.state = 409; + this.state = 405; this.qualifiedNamePattern(); } } } - this.state = 414; + this.state = 410; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 39, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 38, this._ctx); } } } @@ -2125,32 +2156,32 @@ export class esql_parser extends Parser { // @RuleVersion(0) public renameCommand(): RenameCommandContext { let _localctx: RenameCommandContext = new RenameCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 60, esql_parser.RULE_renameCommand); + this.enterRule(_localctx, 64, esql_parser.RULE_renameCommand); try { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 415; + this.state = 411; this.match(esql_parser.RENAME); - this.state = 416; + this.state = 412; this.renameClause(); - this.state = 421; + this.state = 417; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 40, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 39, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 417; + this.state = 413; this.match(esql_parser.COMMA); - this.state = 418; + this.state = 414; this.renameClause(); } } } - this.state = 423; + this.state = 419; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 40, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 39, this._ctx); } } } @@ -2171,15 +2202,15 @@ export class esql_parser extends Parser { // @RuleVersion(0) public renameClause(): RenameClauseContext { let _localctx: RenameClauseContext = new RenameClauseContext(this._ctx, this.state); - this.enterRule(_localctx, 62, esql_parser.RULE_renameClause); + this.enterRule(_localctx, 66, esql_parser.RULE_renameClause); try { this.enterOuterAlt(_localctx, 1); { - this.state = 424; + this.state = 420; _localctx._oldName = this.qualifiedNamePattern(); - this.state = 425; + this.state = 421; this.match(esql_parser.AS); - this.state = 426; + this.state = 422; _localctx._newName = this.qualifiedNamePattern(); } } @@ -2200,22 +2231,22 @@ export class esql_parser extends Parser { // @RuleVersion(0) public dissectCommand(): DissectCommandContext { let _localctx: DissectCommandContext = new DissectCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 64, esql_parser.RULE_dissectCommand); + this.enterRule(_localctx, 68, esql_parser.RULE_dissectCommand); try { this.enterOuterAlt(_localctx, 1); { - this.state = 428; + this.state = 424; this.match(esql_parser.DISSECT); - this.state = 429; + this.state = 425; this.primaryExpression(); - this.state = 430; + this.state = 426; this.string(); - this.state = 432; + this.state = 428; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 41, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 40, this._ctx) ) { case 1: { - this.state = 431; + this.state = 427; this.commandOptions(); } break; @@ -2239,15 +2270,15 @@ export class esql_parser extends Parser { // @RuleVersion(0) public grokCommand(): GrokCommandContext { let _localctx: GrokCommandContext = new GrokCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 66, esql_parser.RULE_grokCommand); + this.enterRule(_localctx, 70, esql_parser.RULE_grokCommand); try { this.enterOuterAlt(_localctx, 1); { - this.state = 434; + this.state = 430; this.match(esql_parser.GROK); - this.state = 435; + this.state = 431; this.primaryExpression(); - this.state = 436; + this.state = 432; this.string(); } } @@ -2268,13 +2299,13 @@ export class esql_parser extends Parser { // @RuleVersion(0) public mvExpandCommand(): MvExpandCommandContext { let _localctx: MvExpandCommandContext = new MvExpandCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 68, esql_parser.RULE_mvExpandCommand); + this.enterRule(_localctx, 72, esql_parser.RULE_mvExpandCommand); try { this.enterOuterAlt(_localctx, 1); { - this.state = 438; + this.state = 434; this.match(esql_parser.MV_EXPAND); - this.state = 439; + this.state = 435; this.qualifiedName(); } } @@ -2295,30 +2326,30 @@ export class esql_parser extends Parser { // @RuleVersion(0) public commandOptions(): CommandOptionsContext { let _localctx: CommandOptionsContext = new CommandOptionsContext(this._ctx, this.state); - this.enterRule(_localctx, 70, esql_parser.RULE_commandOptions); + this.enterRule(_localctx, 74, esql_parser.RULE_commandOptions); try { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 441; + this.state = 437; this.commandOption(); - this.state = 446; + this.state = 442; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 42, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 41, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 442; + this.state = 438; this.match(esql_parser.COMMA); - this.state = 443; + this.state = 439; this.commandOption(); } } } - this.state = 448; + this.state = 444; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 42, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 41, this._ctx); } } } @@ -2339,15 +2370,15 @@ export class esql_parser extends Parser { // @RuleVersion(0) public commandOption(): CommandOptionContext { let _localctx: CommandOptionContext = new CommandOptionContext(this._ctx, this.state); - this.enterRule(_localctx, 72, esql_parser.RULE_commandOption); + this.enterRule(_localctx, 76, esql_parser.RULE_commandOption); try { this.enterOuterAlt(_localctx, 1); { - this.state = 449; + this.state = 445; this.identifier(); - this.state = 450; + this.state = 446; this.match(esql_parser.ASSIGN); - this.state = 451; + this.state = 447; this.constant(); } } @@ -2368,12 +2399,12 @@ export class esql_parser extends Parser { // @RuleVersion(0) public booleanValue(): BooleanValueContext { let _localctx: BooleanValueContext = new BooleanValueContext(this._ctx, this.state); - this.enterRule(_localctx, 74, esql_parser.RULE_booleanValue); + this.enterRule(_localctx, 78, esql_parser.RULE_booleanValue); let _la: number; try { this.enterOuterAlt(_localctx, 1); { - this.state = 453; + this.state = 449; _la = this._input.LA(1); if (!(_la === esql_parser.FALSE || _la === esql_parser.TRUE)) { this._errHandler.recoverInline(this); @@ -2404,15 +2435,15 @@ export class esql_parser extends Parser { // @RuleVersion(0) public numericValue(): NumericValueContext { let _localctx: NumericValueContext = new NumericValueContext(this._ctx, this.state); - this.enterRule(_localctx, 76, esql_parser.RULE_numericValue); + this.enterRule(_localctx, 80, esql_parser.RULE_numericValue); try { - this.state = 457; + this.state = 453; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 43, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 42, this._ctx) ) { case 1: this.enterOuterAlt(_localctx, 1); { - this.state = 455; + this.state = 451; this.decimalValue(); } break; @@ -2420,7 +2451,7 @@ export class esql_parser extends Parser { case 2: this.enterOuterAlt(_localctx, 2); { - this.state = 456; + this.state = 452; this.integerValue(); } break; @@ -2443,17 +2474,17 @@ export class esql_parser extends Parser { // @RuleVersion(0) public decimalValue(): DecimalValueContext { let _localctx: DecimalValueContext = new DecimalValueContext(this._ctx, this.state); - this.enterRule(_localctx, 78, esql_parser.RULE_decimalValue); + this.enterRule(_localctx, 82, esql_parser.RULE_decimalValue); let _la: number; try { this.enterOuterAlt(_localctx, 1); { - this.state = 460; + this.state = 456; this._errHandler.sync(this); _la = this._input.LA(1); if (_la === esql_parser.PLUS || _la === esql_parser.MINUS) { { - this.state = 459; + this.state = 455; _la = this._input.LA(1); if (!(_la === esql_parser.PLUS || _la === esql_parser.MINUS)) { this._errHandler.recoverInline(this); @@ -2468,7 +2499,7 @@ export class esql_parser extends Parser { } } - this.state = 462; + this.state = 458; this.match(esql_parser.DECIMAL_LITERAL); } } @@ -2489,17 +2520,17 @@ export class esql_parser extends Parser { // @RuleVersion(0) public integerValue(): IntegerValueContext { let _localctx: IntegerValueContext = new IntegerValueContext(this._ctx, this.state); - this.enterRule(_localctx, 80, esql_parser.RULE_integerValue); + this.enterRule(_localctx, 84, esql_parser.RULE_integerValue); let _la: number; try { this.enterOuterAlt(_localctx, 1); { - this.state = 465; + this.state = 461; this._errHandler.sync(this); _la = this._input.LA(1); if (_la === esql_parser.PLUS || _la === esql_parser.MINUS) { { - this.state = 464; + this.state = 460; _la = this._input.LA(1); if (!(_la === esql_parser.PLUS || _la === esql_parser.MINUS)) { this._errHandler.recoverInline(this); @@ -2514,7 +2545,7 @@ export class esql_parser extends Parser { } } - this.state = 467; + this.state = 463; this.match(esql_parser.INTEGER_LITERAL); } } @@ -2535,11 +2566,11 @@ export class esql_parser extends Parser { // @RuleVersion(0) public string(): StringContext { let _localctx: StringContext = new StringContext(this._ctx, this.state); - this.enterRule(_localctx, 82, esql_parser.RULE_string); + this.enterRule(_localctx, 86, esql_parser.RULE_string); try { this.enterOuterAlt(_localctx, 1); { - this.state = 469; + this.state = 465; this.match(esql_parser.STRING); } } @@ -2560,14 +2591,14 @@ export class esql_parser extends Parser { // @RuleVersion(0) public comparisonOperator(): ComparisonOperatorContext { let _localctx: ComparisonOperatorContext = new ComparisonOperatorContext(this._ctx, this.state); - this.enterRule(_localctx, 84, esql_parser.RULE_comparisonOperator); + this.enterRule(_localctx, 88, esql_parser.RULE_comparisonOperator); let _la: number; try { this.enterOuterAlt(_localctx, 1); { - this.state = 471; + this.state = 467; _la = this._input.LA(1); - if (!(((((_la - 52)) & ~0x1F) === 0 && ((1 << (_la - 52)) & ((1 << (esql_parser.EQ - 52)) | (1 << (esql_parser.CIEQ - 52)) | (1 << (esql_parser.NEQ - 52)) | (1 << (esql_parser.LT - 52)) | (1 << (esql_parser.LTE - 52)) | (1 << (esql_parser.GT - 52)) | (1 << (esql_parser.GTE - 52)))) !== 0))) { + if (!(((((_la - 51)) & ~0x1F) === 0 && ((1 << (_la - 51)) & ((1 << (esql_parser.EQ - 51)) | (1 << (esql_parser.CIEQ - 51)) | (1 << (esql_parser.NEQ - 51)) | (1 << (esql_parser.LT - 51)) | (1 << (esql_parser.LTE - 51)) | (1 << (esql_parser.GT - 51)) | (1 << (esql_parser.GTE - 51)))) !== 0))) { this._errHandler.recoverInline(this); } else { if (this._input.LA(1) === Token.EOF) { @@ -2596,13 +2627,13 @@ export class esql_parser extends Parser { // @RuleVersion(0) public explainCommand(): ExplainCommandContext { let _localctx: ExplainCommandContext = new ExplainCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 86, esql_parser.RULE_explainCommand); + this.enterRule(_localctx, 90, esql_parser.RULE_explainCommand); try { this.enterOuterAlt(_localctx, 1); { - this.state = 473; + this.state = 469; this.match(esql_parser.EXPLAIN); - this.state = 474; + this.state = 470; this.subqueryExpression(); } } @@ -2623,15 +2654,15 @@ export class esql_parser extends Parser { // @RuleVersion(0) public subqueryExpression(): SubqueryExpressionContext { let _localctx: SubqueryExpressionContext = new SubqueryExpressionContext(this._ctx, this.state); - this.enterRule(_localctx, 88, esql_parser.RULE_subqueryExpression); + this.enterRule(_localctx, 92, esql_parser.RULE_subqueryExpression); try { this.enterOuterAlt(_localctx, 1); { - this.state = 476; + this.state = 472; this.match(esql_parser.OPENING_BRACKET); - this.state = 477; + this.state = 473; this.query(0); - this.state = 478; + this.state = 474; this.match(esql_parser.CLOSING_BRACKET); } } @@ -2652,18 +2683,18 @@ export class esql_parser extends Parser { // @RuleVersion(0) public showCommand(): ShowCommandContext { let _localctx: ShowCommandContext = new ShowCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 90, esql_parser.RULE_showCommand); + this.enterRule(_localctx, 94, esql_parser.RULE_showCommand); try { - this.state = 484; + this.state = 480; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 46, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 45, this._ctx) ) { case 1: _localctx = new ShowInfoContext(_localctx); this.enterOuterAlt(_localctx, 1); { - this.state = 480; + this.state = 476; this.match(esql_parser.SHOW); - this.state = 481; + this.state = 477; this.match(esql_parser.INFO); } break; @@ -2672,9 +2703,9 @@ export class esql_parser extends Parser { _localctx = new ShowFunctionsContext(_localctx); this.enterOuterAlt(_localctx, 2); { - this.state = 482; + this.state = 478; this.match(esql_parser.SHOW); - this.state = 483; + this.state = 479; this.match(esql_parser.FUNCTIONS); } break; @@ -2697,68 +2728,53 @@ export class esql_parser extends Parser { // @RuleVersion(0) public enrichCommand(): EnrichCommandContext { let _localctx: EnrichCommandContext = new EnrichCommandContext(this._ctx, this.state); - this.enterRule(_localctx, 92, esql_parser.RULE_enrichCommand); - let _la: number; + this.enterRule(_localctx, 96, esql_parser.RULE_enrichCommand); try { let _alt: number; this.enterOuterAlt(_localctx, 1); { - this.state = 486; + this.state = 482; this.match(esql_parser.ENRICH); - this.state = 490; - this._errHandler.sync(this); - _la = this._input.LA(1); - while (_la === esql_parser.OPENING_BRACKET) { - { - { - this.state = 487; - this.setting(); - } - } - this.state = 492; - this._errHandler.sync(this); - _la = this._input.LA(1); - } - this.state = 493; + this.state = 483; _localctx._policyName = this.match(esql_parser.ENRICH_POLICY_NAME); - this.state = 496; + this.state = 486; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 48, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 46, this._ctx) ) { case 1: { - this.state = 494; + this.state = 484; this.match(esql_parser.ON); - this.state = 495; + this.state = 485; _localctx._matchField = this.qualifiedNamePattern(); } break; } - this.state = 507; + this.state = 497; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 50, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 48, this._ctx) ) { case 1: { - this.state = 498; + this.state = 488; this.match(esql_parser.WITH); - this.state = 499; + this.state = 489; this.enrichWithClause(); - this.state = 504; + this.state = 494; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 49, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 47, this._ctx); while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { if (_alt === 1) { { { - this.state = 500; + this.state = 490; this.match(esql_parser.COMMA); - this.state = 501; + this.state = 491; this.enrichWithClause(); } } } - this.state = 506; + this.state = 496; this._errHandler.sync(this); - _alt = this.interpreter.adaptivePredict(this._input, 49, this._ctx); + _alt = this.interpreter.adaptivePredict(this._input, 47, this._ctx); } } break; @@ -2782,23 +2798,23 @@ export class esql_parser extends Parser { // @RuleVersion(0) public enrichWithClause(): EnrichWithClauseContext { let _localctx: EnrichWithClauseContext = new EnrichWithClauseContext(this._ctx, this.state); - this.enterRule(_localctx, 94, esql_parser.RULE_enrichWithClause); + this.enterRule(_localctx, 98, esql_parser.RULE_enrichWithClause); try { this.enterOuterAlt(_localctx, 1); { - this.state = 512; + this.state = 502; this._errHandler.sync(this); - switch ( this.interpreter.adaptivePredict(this._input, 51, this._ctx) ) { + switch ( this.interpreter.adaptivePredict(this._input, 49, this._ctx) ) { case 1: { - this.state = 509; + this.state = 499; _localctx._newName = this.qualifiedNamePattern(); - this.state = 510; + this.state = 500; this.match(esql_parser.ASSIGN); } break; } - this.state = 514; + this.state = 504; _localctx._enrichField = this.qualifiedNamePattern(); } } @@ -2816,39 +2832,6 @@ export class esql_parser extends Parser { } return _localctx; } - // @RuleVersion(0) - public setting(): SettingContext { - let _localctx: SettingContext = new SettingContext(this._ctx, this.state); - this.enterRule(_localctx, 96, esql_parser.RULE_setting); - try { - this.enterOuterAlt(_localctx, 1); - { - this.state = 516; - this.match(esql_parser.OPENING_BRACKET); - this.state = 517; - _localctx._name = this.match(esql_parser.SETTING); - this.state = 518; - this.match(esql_parser.COLON); - this.state = 519; - _localctx._value = this.match(esql_parser.SETTING); - this.state = 520; - this.match(esql_parser.CLOSING_BRACKET); - } - } - catch (re) { - if (re instanceof RecognitionException) { - _localctx.exception = re; - this._errHandler.reportError(this, re); - this._errHandler.recover(this, re); - } else { - throw re; - } - } - finally { - this.exitRule(); - } - return _localctx; - } public sempred(_localctx: RuleContext, ruleIndex: number, predIndex: number): boolean { switch (ruleIndex) { @@ -2892,7 +2875,7 @@ export class esql_parser extends Parser { } public static readonly _serializedATN: string = - "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x03k\u020D\x04\x02" + + "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x03j\u01FD\x04\x02" + "\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06\x04\x07" + "\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f\t\f\x04\r\t\r\x04" + "\x0E\t\x0E\x04\x0F\t\x0F\x04\x10\t\x10\x04\x11\t\x11\x04\x12\t\x12\x04" + @@ -2900,251 +2883,242 @@ export class esql_parser extends Parser { "\x18\t\x18\x04\x19\t\x19\x04\x1A\t\x1A\x04\x1B\t\x1B\x04\x1C\t\x1C\x04" + "\x1D\t\x1D\x04\x1E\t\x1E\x04\x1F\t\x1F\x04 \t \x04!\t!\x04\"\t\"\x04#" + "\t#\x04$\t$\x04%\t%\x04&\t&\x04\'\t\'\x04(\t(\x04)\t)\x04*\t*\x04+\t+" + - "\x04,\t,\x04-\t-\x04.\t.\x04/\t/\x040\t0\x041\t1\x042\t2\x03\x02\x03\x02" + - "\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x07\x03n\n\x03" + - "\f\x03\x0E\x03q\v\x03\x03\x04\x03\x04\x03\x04\x03\x04\x05\x04w\n\x04\x03" + - "\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03" + - "\x05\x03\x05\x03\x05\x03\x05\x05\x05\x86\n\x05\x03\x06\x03\x06\x03\x06" + - "\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x05\x07\x92\n" + - "\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x07\x07\x99\n\x07\f\x07\x0E" + - "\x07\x9C\v\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x05\x07\xA3\n\x07" + - "\x03\x07\x03\x07\x05\x07\xA7\n\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03" + - "\x07\x03\x07\x07\x07\xAF\n\x07\f\x07\x0E\x07\xB2\v\x07\x03\b\x03\b\x05" + - "\b\xB6\n\b\x03\b\x03\b\x03\b\x03\b\x03\b\x05\b\xBD\n\b\x03\b\x03\b\x03" + - "\b\x05\b\xC2\n\b\x03\t\x03\t\x03\t\x03\t\x03\t\x05\t\xC9\n\t\x03\n\x03" + - "\n\x03\n\x03\n\x05\n\xCF\n\n\x03\n\x03\n\x03\n\x03\n\x03\n\x03\n\x07\n" + - "\xD7\n\n\f\n\x0E\n\xDA\v\n\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x05" + - "\v\xE3\n\v\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x07\f\xEB\n\f\f\f\x0E\f" + - "\xEE\v\f\x05\f\xF0\n\f\x03\f\x03\f\x03\r\x03\r\x03\r\x03\x0E\x03\x0E\x03" + - "\x0E\x07\x0E\xFA\n\x0E\f\x0E\x0E\x0E\xFD\v\x0E\x03\x0F\x03\x0F\x03\x0F" + - "\x03\x0F\x03\x0F\x05\x0F\u0104\n\x0F\x03\x10\x03\x10\x03\x10\x03\x10\x07" + - "\x10\u010A\n\x10\f\x10\x0E\x10\u010D\v\x10\x03\x10\x05\x10\u0110\n\x10" + - "\x03\x11\x03\x11\x03\x11\x03\x11\x03\x11\x07\x11\u0117\n\x11\f\x11\x0E" + - "\x11\u011A\v\x11\x03\x11\x03\x11\x03\x12\x03\x12\x03\x12\x03\x13\x03\x13" + - "\x05\x13\u0123\n\x13\x03\x13\x03\x13\x05\x13\u0127\n\x13\x03\x14\x03\x14" + - "\x03\x14\x03\x14\x05\x14\u012D\n\x14\x03\x15\x03\x15\x03\x16\x03\x16\x03" + - "\x16\x07\x16\u0134\n\x16\f\x16\x0E\x16\u0137\v\x16\x03\x17\x03\x17\x03" + - "\x17\x07\x17\u013C\n\x17\f\x17\x0E\x17\u013F\v\x17\x03\x18\x03\x18\x03" + - "\x19\x03\x19\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03" + - "\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x07\x1A\u0152\n\x1A\f\x1A" + - "\x0E\x1A\u0155\v\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x07" + - "\x1A\u015D\n\x1A\f\x1A\x0E\x1A\u0160\v\x1A\x03\x1A\x03\x1A\x03\x1A\x03" + - "\x1A\x03\x1A\x03\x1A\x07\x1A\u0168\n\x1A\f\x1A\x0E\x1A\u016B\v\x1A\x03" + - "\x1A\x03\x1A\x05\x1A\u016F\n\x1A\x03\x1B\x03\x1B\x03\x1B\x03\x1C\x03\x1C" + - "\x03\x1C\x03\x1C\x07\x1C\u0178\n\x1C\f\x1C\x0E\x1C\u017B\v\x1C\x03\x1D" + - "\x03\x1D\x05\x1D\u017F\n\x1D\x03\x1D\x03\x1D\x05\x1D\u0183\n\x1D\x03\x1E" + - "\x03\x1E\x03\x1E\x03\x1E\x07\x1E\u0189\n\x1E\f\x1E\x0E\x1E\u018C\v\x1E" + - "\x03\x1E\x03\x1E\x03\x1E\x03\x1E\x07\x1E\u0192\n\x1E\f\x1E\x0E\x1E\u0195" + - "\v\x1E\x05\x1E\u0197\n\x1E\x03\x1F\x03\x1F\x03\x1F\x03\x1F\x07\x1F\u019D" + - "\n\x1F\f\x1F\x0E\x1F\u01A0\v\x1F\x03 \x03 \x03 \x03 \x07 \u01A6\n \f " + - "\x0E \u01A9\v \x03!\x03!\x03!\x03!\x03\"\x03\"\x03\"\x03\"\x05\"\u01B3" + - "\n\"\x03#\x03#\x03#\x03#\x03$\x03$\x03$\x03%\x03%\x03%\x07%\u01BF\n%\f" + - "%\x0E%\u01C2\v%\x03&\x03&\x03&\x03&\x03\'\x03\'\x03(\x03(\x05(\u01CC\n" + - "(\x03)\x05)\u01CF\n)\x03)\x03)\x03*\x05*\u01D4\n*\x03*\x03*\x03+\x03+" + - "\x03,\x03,\x03-\x03-\x03-\x03.\x03.\x03.\x03.\x03/\x03/\x03/\x03/\x05" + - "/\u01E7\n/\x030\x030\x070\u01EB\n0\f0\x0E0\u01EE\v0\x030\x030\x030\x05" + - "0\u01F3\n0\x030\x030\x030\x030\x070\u01F9\n0\f0\x0E0\u01FC\v0\x050\u01FE" + - "\n0\x031\x031\x031\x051\u0203\n1\x031\x031\x032\x032\x032\x032\x032\x03" + - "2\x032\x02\x02\x05\x04\f\x123\x02\x02\x04\x02\x06\x02\b\x02\n\x02\f\x02" + - "\x0E\x02\x10\x02\x12\x02\x14\x02\x16\x02\x18\x02\x1A\x02\x1C\x02\x1E\x02" + - " \x02\"\x02$\x02&\x02(\x02*\x02,\x02.\x020\x022\x024\x026\x028\x02:\x02" + - "<\x02>\x02@\x02B\x02D\x02F\x02H\x02J\x02L\x02N\x02P\x02R\x02T\x02V\x02" + - "X\x02Z\x02\\\x02^\x02`\x02b\x02\x02\v\x03\x02=>\x03\x02?A\x04\x02EEJJ" + - "\x03\x02DE\x04\x02EENN\x04\x02\"\"%%\x03\x02()\x04\x02\'\'55\x03\x026" + - "<\x02\u022A\x02d\x03\x02\x02\x02\x04g\x03\x02\x02\x02\x06v\x03\x02\x02" + - "\x02\b\x85\x03\x02\x02\x02\n\x87\x03\x02\x02\x02\f\xA6\x03\x02\x02\x02" + - "\x0E\xC1\x03\x02\x02\x02\x10\xC8\x03\x02\x02\x02\x12\xCE\x03\x02\x02\x02" + - "\x14\xE2\x03\x02\x02\x02\x16\xE4\x03\x02\x02\x02\x18\xF3\x03\x02\x02\x02" + - "\x1A\xF6\x03\x02\x02\x02\x1C\u0103\x03\x02\x02\x02\x1E\u0105\x03\x02\x02" + - "\x02 \u0111\x03\x02\x02\x02\"\u011D\x03\x02\x02\x02$\u0120\x03\x02\x02" + - "\x02&\u0128\x03\x02\x02\x02(\u012E\x03\x02\x02\x02*\u0130\x03\x02\x02" + - "\x02,\u0138\x03\x02\x02\x02.\u0140\x03\x02\x02\x020\u0142\x03\x02\x02" + - "\x022\u016E\x03\x02\x02\x024\u0170\x03\x02\x02\x026\u0173\x03\x02\x02" + - "\x028\u017C\x03\x02\x02\x02:\u0196\x03\x02\x02\x02<\u0198\x03\x02\x02" + - "\x02>\u01A1\x03\x02\x02\x02@\u01AA\x03\x02\x02\x02B\u01AE\x03\x02\x02" + - "\x02D\u01B4\x03\x02\x02\x02F\u01B8\x03\x02\x02\x02H\u01BB\x03\x02\x02" + - "\x02J\u01C3\x03\x02\x02\x02L\u01C7\x03\x02\x02\x02N\u01CB\x03\x02\x02" + - "\x02P\u01CE\x03\x02\x02\x02R\u01D3\x03\x02\x02\x02T\u01D7\x03\x02\x02" + - "\x02V\u01D9\x03\x02\x02\x02X\u01DB\x03\x02\x02\x02Z\u01DE\x03\x02\x02" + - "\x02\\\u01E6\x03\x02\x02\x02^\u01E8\x03\x02\x02\x02`\u0202\x03\x02\x02" + - "\x02b\u0206\x03\x02\x02\x02de\x05\x04\x03\x02ef\x07\x02\x02\x03f\x03\x03" + - "\x02\x02\x02gh\b\x03\x01\x02hi\x05\x06\x04\x02io\x03\x02\x02\x02jk\f\x03" + - "\x02\x02kl\x07\x1C\x02\x02ln\x05\b\x05\x02mj\x03\x02\x02\x02nq\x03\x02" + - "\x02\x02om\x03\x02\x02\x02op\x03\x02\x02\x02p\x05\x03\x02\x02\x02qo\x03" + - "\x02\x02\x02rw\x05X-\x02sw\x05\x1E\x10\x02tw\x05\x18\r\x02uw\x05\\/\x02" + - "vr\x03\x02\x02\x02vs\x03\x02\x02\x02vt\x03\x02\x02\x02vu\x03\x02\x02\x02" + - "w\x07\x03\x02\x02\x02x\x86\x05\"\x12\x02y\x86\x05&\x14\x02z\x86\x054\x1B" + - "\x02{\x86\x05:\x1E\x02|\x86\x056\x1C\x02}\x86\x05$\x13\x02~\x86\x05\n" + - "\x06\x02\x7F\x86\x05<\x1F\x02\x80\x86\x05> \x02\x81\x86\x05B\"\x02\x82" + - "\x86\x05D#\x02\x83\x86\x05^0\x02\x84\x86\x05F$\x02\x85x\x03\x02\x02\x02" + - "\x85y\x03\x02\x02\x02\x85z\x03\x02\x02\x02\x85{\x03\x02\x02\x02\x85|\x03" + - "\x02\x02\x02\x85}\x03\x02\x02\x02\x85~\x03\x02\x02\x02\x85\x7F\x03\x02" + - "\x02\x02\x85\x80\x03\x02\x02\x02\x85\x81\x03\x02\x02\x02\x85\x82\x03\x02" + - "\x02\x02\x85\x83\x03\x02\x02\x02\x85\x84\x03\x02\x02\x02\x86\t\x03\x02" + - "\x02\x02\x87\x88\x07\x14\x02\x02\x88\x89\x05\f\x07\x02\x89\v\x03\x02\x02" + - "\x02\x8A\x8B\b\x07\x01\x02\x8B\x8C\x07.\x02\x02\x8C\xA7\x05\f\x07\t\x8D" + - "\xA7\x05\x10\t\x02\x8E\xA7\x05\x0E\b\x02\x8F\x91\x05\x10\t\x02\x90\x92" + - "\x07.\x02\x02\x91\x90\x03\x02\x02\x02\x91\x92\x03\x02\x02\x02\x92\x93" + - "\x03\x02\x02\x02\x93\x94\x07+\x02\x02\x94\x95\x07*\x02\x02\x95\x9A\x05" + - "\x10\t\x02\x96\x97\x07$\x02\x02\x97\x99\x05\x10\t\x02\x98\x96\x03\x02" + - "\x02\x02\x99\x9C\x03\x02\x02\x02\x9A\x98\x03\x02\x02\x02\x9A\x9B\x03\x02" + - "\x02\x02\x9B\x9D\x03\x02\x02\x02\x9C\x9A\x03\x02\x02\x02\x9D\x9E\x074" + - "\x02\x02\x9E\xA7\x03\x02\x02\x02\x9F\xA0\x05\x10\t\x02\xA0\xA2\x07,\x02" + - "\x02\xA1\xA3\x07.\x02\x02\xA2\xA1\x03\x02\x02\x02\xA2\xA3\x03\x02\x02" + - "\x02\xA3\xA4\x03\x02\x02\x02\xA4\xA5\x07/\x02\x02\xA5\xA7\x03\x02\x02" + - "\x02\xA6\x8A\x03\x02\x02\x02\xA6\x8D\x03\x02\x02\x02\xA6\x8E\x03\x02\x02" + - "\x02\xA6\x8F\x03\x02\x02\x02\xA6\x9F\x03\x02\x02\x02\xA7\xB0\x03\x02\x02" + - "\x02\xA8\xA9\f\x06\x02\x02\xA9\xAA\x07!\x02\x02\xAA\xAF\x05\f\x07\x07" + - "\xAB\xAC\f\x05\x02\x02\xAC\xAD\x071\x02\x02\xAD\xAF\x05\f\x07\x06\xAE" + - "\xA8\x03\x02\x02\x02\xAE\xAB\x03\x02\x02\x02\xAF\xB2\x03\x02\x02\x02\xB0" + - "\xAE\x03\x02\x02\x02\xB0\xB1\x03\x02\x02\x02\xB1\r\x03\x02\x02\x02\xB2" + - "\xB0\x03\x02\x02\x02\xB3\xB5\x05\x10\t\x02\xB4\xB6\x07.\x02\x02\xB5\xB4" + - "\x03\x02\x02\x02\xB5\xB6\x03\x02\x02\x02\xB6\xB7\x03\x02\x02\x02\xB7\xB8" + - "\x07-\x02\x02\xB8\xB9\x05T+\x02\xB9\xC2\x03\x02\x02\x02\xBA\xBC\x05\x10" + - "\t\x02\xBB\xBD\x07.\x02\x02\xBC\xBB\x03\x02\x02\x02\xBC\xBD\x03\x02\x02" + - "\x02\xBD\xBE\x03\x02\x02\x02\xBE\xBF\x073\x02\x02\xBF\xC0\x05T+\x02\xC0" + - "\xC2\x03\x02\x02\x02\xC1\xB3\x03\x02\x02\x02\xC1\xBA\x03\x02\x02\x02\xC2" + - "\x0F\x03\x02\x02\x02\xC3\xC9\x05\x12\n\x02\xC4\xC5\x05\x12\n\x02\xC5\xC6" + - "\x05V,\x02\xC6\xC7\x05\x12\n\x02\xC7\xC9\x03\x02\x02\x02\xC8\xC3\x03\x02" + - "\x02\x02\xC8\xC4\x03\x02\x02\x02\xC9\x11\x03\x02\x02\x02\xCA\xCB\b\n\x01" + - "\x02\xCB\xCF\x05\x14\v\x02\xCC\xCD\t\x02\x02\x02\xCD\xCF\x05\x12\n\x05" + - "\xCE\xCA\x03\x02\x02\x02\xCE\xCC\x03\x02\x02\x02\xCF\xD8\x03\x02\x02\x02" + - "\xD0\xD1\f\x04\x02\x02\xD1\xD2\t\x03\x02\x02\xD2\xD7\x05\x12\n\x05\xD3" + - "\xD4\f\x03\x02\x02\xD4\xD5\t\x02\x02\x02\xD5\xD7\x05\x12\n\x04\xD6\xD0" + - "\x03\x02\x02\x02\xD6\xD3\x03\x02\x02\x02\xD7\xDA\x03\x02\x02\x02\xD8\xD6" + - "\x03\x02\x02\x02\xD8\xD9\x03\x02\x02\x02\xD9\x13\x03\x02\x02\x02\xDA\xD8" + - "\x03\x02\x02\x02\xDB\xE3\x052\x1A\x02\xDC\xE3\x05*\x16\x02\xDD\xE3\x05" + - "\x16\f\x02\xDE\xDF\x07*\x02\x02\xDF\xE0\x05\f\x07\x02\xE0\xE1\x074\x02" + - "\x02\xE1\xE3\x03\x02\x02\x02\xE2\xDB\x03\x02\x02\x02\xE2\xDC\x03\x02\x02" + - "\x02\xE2\xDD\x03\x02\x02\x02\xE2\xDE\x03\x02\x02\x02\xE3\x15\x03\x02\x02" + - "\x02\xE4\xE5\x05.\x18\x02\xE5\xEF\x07*\x02\x02\xE6\xF0\x07?\x02\x02\xE7" + - "\xEC\x05\f\x07\x02\xE8\xE9\x07$\x02\x02\xE9\xEB\x05\f\x07\x02\xEA\xE8" + - "\x03\x02\x02\x02\xEB\xEE\x03\x02\x02\x02\xEC\xEA\x03\x02\x02\x02\xEC\xED" + - "\x03\x02\x02\x02\xED\xF0\x03\x02\x02\x02\xEE\xEC\x03\x02\x02\x02\xEF\xE6" + - "\x03\x02\x02\x02\xEF\xE7\x03\x02\x02\x02\xEF\xF0\x03\x02\x02\x02\xF0\xF1" + - "\x03\x02\x02\x02\xF1\xF2\x074\x02\x02\xF2\x17\x03\x02\x02\x02\xF3\xF4" + - "\x07\x10\x02\x02\xF4\xF5\x05\x1A\x0E\x02\xF5\x19\x03\x02\x02\x02\xF6\xFB" + - "\x05\x1C\x0F\x02\xF7\xF8\x07$\x02\x02\xF8\xFA\x05\x1C\x0F\x02\xF9\xF7" + - "\x03\x02\x02\x02\xFA\xFD\x03\x02\x02\x02\xFB\xF9\x03\x02\x02\x02\xFB\xFC" + - "\x03\x02\x02\x02\xFC\x1B\x03\x02\x02\x02\xFD\xFB\x03\x02\x02\x02\xFE\u0104" + - "\x05\f\x07\x02\xFF\u0100\x05*\x16\x02\u0100\u0101\x07#\x02\x02\u0101\u0102" + - "\x05\f\x07\x02\u0102\u0104\x03\x02\x02\x02\u0103\xFE\x03\x02\x02\x02\u0103" + - "\xFF\x03\x02\x02\x02\u0104\x1D\x03\x02\x02\x02\u0105\u0106\x07\b\x02\x02" + - "\u0106\u010B\x05(\x15\x02\u0107\u0108\x07$\x02\x02\u0108\u010A\x05(\x15" + - "\x02\u0109\u0107\x03\x02\x02\x02\u010A\u010D\x03\x02\x02\x02\u010B\u0109" + - "\x03\x02\x02\x02\u010B\u010C\x03\x02\x02\x02\u010C\u010F\x03\x02\x02\x02" + - "\u010D\u010B\x03\x02\x02\x02\u010E\u0110\x05 \x11\x02\u010F\u010E\x03" + - "\x02\x02\x02\u010F\u0110\x03\x02\x02\x02\u0110\x1F\x03\x02\x02\x02\u0111" + - "\u0112\x07B\x02\x02\u0112\u0113\x07I\x02\x02\u0113\u0118\x05(\x15\x02" + - "\u0114\u0115\x07$\x02\x02\u0115\u0117\x05(\x15\x02\u0116\u0114\x03\x02" + - "\x02\x02\u0117\u011A\x03\x02\x02\x02\u0118\u0116\x03\x02\x02\x02\u0118" + - "\u0119\x03\x02\x02\x02\u0119\u011B\x03\x02\x02\x02\u011A\u0118\x03\x02" + - "\x02\x02\u011B\u011C\x07C\x02\x02\u011C!\x03\x02\x02\x02\u011D\u011E\x07" + - "\x06\x02\x02\u011E\u011F\x05\x1A\x0E\x02\u011F#\x03\x02\x02\x02\u0120" + - "\u0122\x07\x13\x02\x02\u0121\u0123\x05\x1A\x0E\x02\u0122\u0121\x03\x02" + - "\x02\x02\u0122\u0123\x03\x02\x02\x02\u0123\u0126\x03\x02\x02\x02\u0124" + - "\u0125\x07 \x02\x02\u0125\u0127\x05\x1A\x0E\x02\u0126\u0124\x03\x02\x02" + - "\x02\u0126\u0127\x03\x02\x02\x02\u0127%\x03\x02\x02\x02\u0128\u0129\x07" + - "\n\x02\x02\u0129\u012C\x05\x1A\x0E\x02\u012A\u012B\x07 \x02\x02\u012B" + - "\u012D\x05\x1A\x0E\x02\u012C\u012A\x03\x02\x02\x02\u012C\u012D\x03\x02" + - "\x02\x02\u012D\'\x03\x02\x02\x02\u012E\u012F\t\x04\x02\x02\u012F)\x03" + - "\x02\x02\x02\u0130\u0135\x05.\x18\x02\u0131\u0132\x07&\x02\x02\u0132\u0134" + - "\x05.\x18\x02\u0133\u0131\x03\x02\x02\x02\u0134\u0137\x03\x02\x02\x02" + - "\u0135\u0133\x03\x02\x02\x02\u0135\u0136\x03\x02\x02\x02\u0136+\x03\x02" + - "\x02\x02\u0137\u0135\x03\x02\x02\x02\u0138\u013D\x050\x19\x02\u0139\u013A" + - "\x07&\x02\x02\u013A\u013C\x050\x19\x02\u013B\u0139\x03\x02\x02\x02\u013C" + - "\u013F\x03\x02\x02\x02\u013D\u013B\x03\x02\x02\x02\u013D\u013E\x03\x02" + - "\x02\x02\u013E-\x03\x02\x02\x02\u013F\u013D\x03\x02\x02\x02\u0140\u0141" + - "\t\x05\x02\x02\u0141/\x03\x02\x02\x02\u0142\u0143\t\x06\x02\x02\u0143" + - "1\x03\x02\x02\x02\u0144\u016F\x07/\x02\x02\u0145\u0146\x05R*\x02\u0146" + - "\u0147\x07D\x02\x02\u0147\u016F\x03\x02\x02\x02\u0148\u016F\x05P)\x02" + - "\u0149\u016F\x05R*\x02\u014A\u016F\x05L\'\x02\u014B\u016F\x072\x02\x02" + - "\u014C\u016F\x05T+\x02\u014D\u014E\x07B\x02\x02\u014E\u0153\x05N(\x02" + - "\u014F\u0150\x07$\x02\x02\u0150\u0152\x05N(\x02\u0151\u014F\x03\x02\x02" + - "\x02\u0152\u0155\x03\x02\x02\x02\u0153\u0151\x03\x02\x02\x02\u0153\u0154" + - "\x03\x02\x02\x02\u0154\u0156\x03\x02\x02\x02\u0155\u0153\x03\x02\x02\x02" + - "\u0156\u0157\x07C\x02\x02\u0157\u016F\x03\x02\x02\x02\u0158\u0159\x07" + - "B\x02\x02\u0159\u015E\x05L\'\x02\u015A\u015B\x07$\x02\x02\u015B\u015D" + - "\x05L\'\x02\u015C\u015A\x03\x02\x02\x02\u015D\u0160\x03\x02\x02\x02\u015E" + - "\u015C\x03\x02\x02\x02\u015E\u015F\x03\x02\x02\x02\u015F\u0161\x03\x02" + - "\x02\x02\u0160\u015E\x03\x02\x02\x02\u0161\u0162\x07C\x02\x02\u0162\u016F" + - "\x03\x02\x02\x02\u0163\u0164\x07B\x02\x02\u0164\u0169\x05T+\x02\u0165" + - "\u0166\x07$\x02\x02\u0166\u0168\x05T+\x02\u0167\u0165\x03\x02\x02\x02" + - "\u0168\u016B\x03\x02\x02\x02\u0169\u0167\x03\x02\x02\x02\u0169\u016A\x03" + - "\x02\x02\x02\u016A\u016C\x03\x02\x02\x02\u016B\u0169\x03\x02\x02\x02\u016C" + - "\u016D\x07C\x02\x02\u016D\u016F\x03\x02\x02\x02\u016E\u0144\x03\x02\x02" + - "\x02\u016E\u0145\x03\x02\x02\x02\u016E\u0148\x03\x02\x02\x02\u016E\u0149" + - "\x03\x02\x02\x02\u016E\u014A\x03\x02\x02\x02\u016E\u014B\x03\x02\x02\x02" + - "\u016E\u014C\x03\x02\x02\x02\u016E\u014D\x03\x02\x02\x02\u016E\u0158\x03" + - "\x02\x02\x02\u016E\u0163\x03\x02\x02\x02\u016F3\x03\x02\x02\x02\u0170" + - "\u0171\x07\f\x02\x02\u0171\u0172\x07\x1E\x02\x02\u01725\x03\x02\x02\x02" + - "\u0173\u0174\x07\x12\x02\x02\u0174\u0179\x058\x1D\x02\u0175\u0176\x07" + - "$\x02\x02\u0176\u0178\x058\x1D\x02\u0177\u0175\x03\x02\x02\x02\u0178\u017B" + - "\x03\x02\x02\x02\u0179\u0177\x03\x02\x02\x02\u0179\u017A\x03\x02\x02\x02" + - "\u017A7\x03\x02\x02\x02\u017B\u0179\x03\x02\x02\x02\u017C\u017E\x05\f" + - "\x07\x02\u017D\u017F\t\x07\x02\x02\u017E\u017D\x03\x02\x02\x02\u017E\u017F" + - "\x03\x02\x02\x02\u017F\u0182\x03\x02\x02\x02\u0180\u0181\x070\x02\x02" + - "\u0181\u0183\t\b\x02\x02\u0182\u0180\x03\x02\x02\x02\u0182\u0183\x03\x02" + - "\x02\x02\u01839\x03\x02\x02\x02\u0184\u0185\x07\v\x02\x02\u0185\u018A" + - "\x05,\x17\x02\u0186\u0187\x07$\x02\x02\u0187\u0189\x05,\x17\x02\u0188" + - "\u0186\x03\x02\x02\x02\u0189\u018C\x03\x02\x02\x02\u018A\u0188\x03\x02" + - "\x02\x02\u018A\u018B\x03\x02\x02\x02\u018B\u0197\x03\x02\x02\x02\u018C" + - "\u018A\x03\x02\x02\x02\u018D\u018E\x07\x0E\x02\x02\u018E\u0193\x05,\x17" + - "\x02\u018F\u0190\x07$\x02\x02\u0190\u0192\x05,\x17\x02\u0191\u018F\x03" + - "\x02\x02\x02\u0192\u0195\x03\x02\x02\x02\u0193\u0191\x03\x02\x02\x02\u0193" + - "\u0194\x03\x02\x02\x02\u0194\u0197\x03\x02\x02\x02\u0195\u0193\x03\x02" + - "\x02\x02\u0196\u0184\x03\x02\x02\x02\u0196\u018D\x03\x02\x02\x02\u0197" + - ";\x03\x02\x02\x02\u0198\u0199\x07\x04\x02\x02\u0199\u019E\x05,\x17\x02" + - "\u019A\u019B\x07$\x02\x02\u019B\u019D\x05,\x17\x02\u019C\u019A\x03\x02" + - "\x02\x02\u019D\u01A0\x03\x02\x02\x02\u019E\u019C\x03\x02\x02\x02\u019E" + - "\u019F\x03\x02\x02\x02\u019F=\x03\x02\x02\x02\u01A0\u019E\x03\x02\x02" + - "\x02\u01A1\u01A2\x07\x0F\x02\x02\u01A2\u01A7\x05@!\x02\u01A3\u01A4\x07" + - "$\x02\x02\u01A4\u01A6\x05@!\x02\u01A5\u01A3\x03\x02\x02\x02\u01A6\u01A9" + - "\x03\x02\x02\x02\u01A7\u01A5\x03\x02\x02\x02\u01A7\u01A8\x03\x02\x02\x02" + - "\u01A8?\x03\x02\x02\x02\u01A9\u01A7\x03\x02\x02\x02\u01AA\u01AB\x05,\x17" + - "\x02\u01AB\u01AC\x07R\x02\x02\u01AC\u01AD\x05,\x17\x02\u01ADA\x03\x02" + - "\x02\x02\u01AE\u01AF\x07\x03\x02\x02\u01AF\u01B0\x05\x14\v\x02\u01B0\u01B2" + - "\x05T+\x02\u01B1\u01B3\x05H%\x02\u01B2\u01B1\x03\x02\x02\x02\u01B2\u01B3" + - "\x03\x02\x02\x02\u01B3C\x03\x02\x02\x02\u01B4\u01B5\x07\t\x02\x02\u01B5" + - "\u01B6\x05\x14\v\x02\u01B6\u01B7\x05T+\x02\u01B7E\x03\x02\x02\x02\u01B8" + - "\u01B9\x07\r\x02\x02\u01B9\u01BA\x05*\x16\x02\u01BAG\x03\x02\x02\x02\u01BB" + - "\u01C0\x05J&\x02\u01BC\u01BD\x07$\x02\x02\u01BD\u01BF\x05J&\x02\u01BE" + - "\u01BC\x03\x02\x02\x02\u01BF\u01C2\x03\x02\x02\x02\u01C0\u01BE\x03\x02" + - "\x02\x02\u01C0\u01C1\x03\x02\x02\x02\u01C1I\x03\x02\x02\x02\u01C2\u01C0" + - "\x03\x02\x02\x02\u01C3\u01C4\x05.\x18\x02\u01C4\u01C5\x07#\x02\x02\u01C5" + - "\u01C6\x052\x1A\x02\u01C6K\x03\x02\x02\x02\u01C7\u01C8\t\t\x02\x02\u01C8" + - "M\x03\x02\x02\x02\u01C9\u01CC\x05P)\x02\u01CA\u01CC\x05R*\x02\u01CB\u01C9" + - "\x03\x02\x02\x02\u01CB\u01CA\x03\x02\x02\x02\u01CCO\x03\x02\x02\x02\u01CD" + - "\u01CF\t\x02\x02\x02\u01CE\u01CD\x03\x02\x02\x02\u01CE\u01CF\x03\x02\x02" + - "\x02\u01CF\u01D0\x03\x02\x02\x02\u01D0\u01D1\x07\x1F\x02\x02\u01D1Q\x03" + - "\x02\x02\x02\u01D2\u01D4\t\x02\x02\x02\u01D3\u01D2\x03\x02\x02\x02\u01D3" + - "\u01D4\x03\x02\x02\x02\u01D4\u01D5\x03\x02\x02\x02\u01D5\u01D6\x07\x1E" + - "\x02\x02\u01D6S\x03\x02\x02\x02\u01D7\u01D8\x07\x1D\x02\x02\u01D8U\x03" + - "\x02\x02\x02\u01D9\u01DA\t\n\x02\x02\u01DAW\x03\x02\x02\x02\u01DB\u01DC" + - "\x07\x07\x02\x02\u01DC\u01DD\x05Z.\x02\u01DDY\x03\x02\x02\x02\u01DE\u01DF" + - "\x07B\x02\x02\u01DF\u01E0\x05\x04\x03\x02\u01E0\u01E1\x07C\x02\x02\u01E1" + - "[\x03\x02\x02\x02\u01E2\u01E3\x07\x11\x02\x02\u01E3\u01E7\x07b\x02\x02" + - "\u01E4\u01E5\x07\x11\x02\x02\u01E5\u01E7\x07c\x02\x02\u01E6\u01E2\x03" + - "\x02\x02\x02\u01E6\u01E4\x03\x02\x02\x02\u01E7]\x03\x02\x02\x02\u01E8" + - "\u01EC\x07\x05\x02\x02\u01E9\u01EB\x05b2\x02\u01EA\u01E9\x03\x02\x02\x02" + - "\u01EB\u01EE\x03\x02\x02\x02\u01EC\u01EA\x03\x02\x02\x02\u01EC\u01ED\x03" + - "\x02\x02\x02\u01ED\u01EF\x03\x02\x02\x02\u01EE\u01EC\x03\x02\x02\x02\u01EF" + - "\u01F2\x07X\x02\x02\u01F0\u01F1\x07V\x02\x02\u01F1\u01F3\x05,\x17\x02" + - "\u01F2\u01F0\x03\x02\x02\x02\u01F2\u01F3\x03\x02\x02\x02\u01F3\u01FD\x03" + - "\x02\x02\x02\u01F4\u01F5\x07W\x02\x02\u01F5\u01FA\x05`1\x02\u01F6\u01F7" + - "\x07$\x02\x02\u01F7\u01F9\x05`1\x02\u01F8\u01F6\x03\x02\x02\x02\u01F9" + - "\u01FC\x03\x02\x02\x02\u01FA\u01F8\x03\x02\x02\x02\u01FA\u01FB\x03\x02" + - "\x02\x02\u01FB\u01FE\x03\x02\x02\x02\u01FC\u01FA\x03\x02\x02\x02\u01FD" + - "\u01F4\x03\x02\x02\x02\u01FD\u01FE\x03\x02\x02\x02\u01FE_\x03\x02\x02" + - "\x02\u01FF\u0200\x05,\x17\x02\u0200\u0201\x07#\x02\x02\u0201\u0203\x03" + - "\x02\x02\x02\u0202\u01FF\x03\x02\x02\x02\u0202\u0203\x03\x02\x02\x02\u0203" + - "\u0204\x03\x02\x02\x02\u0204\u0205\x05,\x17\x02\u0205a\x03\x02\x02\x02" + - "\u0206\u0207\x07B\x02\x02\u0207\u0208\x07h\x02\x02\u0208\u0209\x07g\x02" + - "\x02\u0209\u020A\x07h\x02\x02\u020A\u020B\x07C\x02\x02\u020Bc\x03\x02" + - "\x02\x026ov\x85\x91\x9A\xA2\xA6\xAE\xB0\xB5\xBC\xC1\xC8\xCE\xD6\xD8\xE2" + - "\xEC\xEF\xFB\u0103\u010B\u010F\u0118\u0122\u0126\u012C\u0135\u013D\u0153" + - "\u015E\u0169\u016E\u0179\u017E\u0182\u018A\u0193\u0196\u019E\u01A7\u01B2" + - "\u01C0\u01CB\u01CE\u01D3\u01E6\u01EC\u01F2\u01FA\u01FD\u0202"; + "\x04,\t,\x04-\t-\x04.\t.\x04/\t/\x040\t0\x041\t1\x042\t2\x043\t3\x03\x02" + + "\x03\x02\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x07\x03" + + "p\n\x03\f\x03\x0E\x03s\v\x03\x03\x04\x03\x04\x03\x04\x03\x04\x05\x04y" + + "\n\x04\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05" + + "\x03\x05\x03\x05\x03\x05\x03\x05\x03\x05\x05\x05\x88\n\x05\x03\x06\x03" + + "\x06\x03\x06\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x05" + + "\x07\x94\n\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x07\x07\x9B\n\x07" + + "\f\x07\x0E\x07\x9E\v\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x05\x07" + + "\xA5\n\x07\x03\x07\x03\x07\x05\x07\xA9\n\x07\x03\x07\x03\x07\x03\x07\x03" + + "\x07\x03\x07\x03\x07\x07\x07\xB1\n\x07\f\x07\x0E\x07\xB4\v\x07\x03\b\x03" + + "\b\x05\b\xB8\n\b\x03\b\x03\b\x03\b\x03\b\x03\b\x05\b\xBF\n\b\x03\b\x03" + + "\b\x03\b\x05\b\xC4\n\b\x03\t\x03\t\x03\t\x03\t\x03\t\x05\t\xCB\n\t\x03" + + "\n\x03\n\x03\n\x03\n\x05\n\xD1\n\n\x03\n\x03\n\x03\n\x03\n\x03\n\x03\n" + + "\x07\n\xD9\n\n\f\n\x0E\n\xDC\v\n\x03\v\x03\v\x03\v\x03\v\x03\v\x03\v\x03" + + "\v\x05\v\xE5\n\v\x03\f\x03\f\x03\f\x03\f\x03\f\x03\f\x07\f\xED\n\f\f\f" + + "\x0E\f\xF0\v\f\x05\f\xF2\n\f\x03\f\x03\f\x03\r\x03\r\x03\r\x03\x0E\x03" + + "\x0E\x03\x0E\x07\x0E\xFC\n\x0E\f\x0E\x0E\x0E\xFF\v\x0E\x03\x0F\x03\x0F" + + "\x03\x0F\x03\x0F\x03\x0F\x05\x0F\u0106\n\x0F\x03\x10\x03\x10\x03\x10\x03" + + "\x10\x07\x10\u010C\n\x10\f\x10\x0E\x10\u010F\v\x10\x03\x10\x05\x10\u0112" + + "\n\x10\x03\x11\x03\x11\x05\x11\u0116\n\x11\x03\x12\x03\x12\x03\x12\x03" + + "\x12\x07\x12\u011C\n\x12\f\x12\x0E\x12\u011F\v\x12\x03\x13\x03\x13\x03" + + "\x13\x03\x13\x03\x14\x03\x14\x03\x14\x03\x15\x03\x15\x05\x15\u012A\n\x15" + + "\x03\x15\x03\x15\x05\x15\u012E\n\x15\x03\x16\x03\x16\x03\x16\x03\x16\x05" + + "\x16\u0134\n\x16\x03\x17\x03\x17\x03\x18\x03\x18\x03\x18\x07\x18\u013B" + + "\n\x18\f\x18\x0E\x18\u013E\v\x18\x03\x19\x03\x19\x03\x19\x07\x19\u0143" + + "\n\x19\f\x19\x0E\x19\u0146\v\x19\x03\x1A\x03\x1A\x03\x1B\x03\x1B\x03\x1C" + + "\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C" + + "\x03\x1C\x03\x1C\x03\x1C\x07\x1C\u0159\n\x1C\f\x1C\x0E\x1C\u015C\v\x1C" + + "\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x07\x1C\u0164\n\x1C\f" + + "\x1C\x0E\x1C\u0167\v\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C" + + "\x07\x1C\u016F\n\x1C\f\x1C\x0E\x1C\u0172\v\x1C\x03\x1C\x03\x1C\x05\x1C" + + "\u0176\n\x1C\x03\x1D\x03\x1D\x03\x1D\x03\x1E\x03\x1E\x03\x1E\x03\x1E\x07" + + "\x1E\u017F\n\x1E\f\x1E\x0E\x1E\u0182\v\x1E\x03\x1F\x03\x1F\x05\x1F\u0186" + + "\n\x1F\x03\x1F\x03\x1F\x05\x1F\u018A\n\x1F\x03 \x03 \x03 \x03 \x07 \u0190" + + "\n \f \x0E \u0193\v \x03!\x03!\x03!\x03!\x07!\u0199\n!\f!\x0E!\u019C\v" + + "!\x03\"\x03\"\x03\"\x03\"\x07\"\u01A2\n\"\f\"\x0E\"\u01A5\v\"\x03#\x03" + + "#\x03#\x03#\x03$\x03$\x03$\x03$\x05$\u01AF\n$\x03%\x03%\x03%\x03%\x03" + + "&\x03&\x03&\x03\'\x03\'\x03\'\x07\'\u01BB\n\'\f\'\x0E\'\u01BE\v\'\x03" + + "(\x03(\x03(\x03(\x03)\x03)\x03*\x03*\x05*\u01C8\n*\x03+\x05+\u01CB\n+" + + "\x03+\x03+\x03,\x05,\u01D0\n,\x03,\x03,\x03-\x03-\x03.\x03.\x03/\x03/" + + "\x03/\x030\x030\x030\x030\x031\x031\x031\x031\x051\u01E3\n1\x032\x032" + + "\x032\x032\x052\u01E9\n2\x032\x032\x032\x032\x072\u01EF\n2\f2\x0E2\u01F2" + + "\v2\x052\u01F4\n2\x033\x033\x033\x053\u01F9\n3\x033\x033\x033\x02\x02" + + "\x05\x04\f\x124\x02\x02\x04\x02\x06\x02\b\x02\n\x02\f\x02\x0E\x02\x10" + + "\x02\x12\x02\x14\x02\x16\x02\x18\x02\x1A\x02\x1C\x02\x1E\x02 \x02\"\x02" + + "$\x02&\x02(\x02*\x02,\x02.\x020\x022\x024\x026\x028\x02:\x02<\x02>\x02" + + "@\x02B\x02D\x02F\x02H\x02J\x02L\x02N\x02P\x02R\x02T\x02V\x02X\x02Z\x02" + + "\\\x02^\x02`\x02b\x02d\x02\x02\v\x03\x02<=\x03\x02>@\x04\x02DDII\x03\x02" + + "CD\x04\x02DDMM\x04\x02!!$$\x03\x02\'(\x04\x02&&44\x03\x025;\x02\u0217" + + "\x02f\x03\x02\x02\x02\x04i\x03\x02\x02\x02\x06x\x03\x02\x02\x02\b\x87" + + "\x03\x02\x02\x02\n\x89\x03\x02\x02\x02\f\xA8\x03\x02\x02\x02\x0E\xC3\x03" + + "\x02\x02\x02\x10\xCA\x03\x02\x02\x02\x12\xD0\x03\x02\x02\x02\x14\xE4\x03" + + "\x02\x02\x02\x16\xE6\x03\x02\x02\x02\x18\xF5\x03\x02\x02\x02\x1A\xF8\x03" + + "\x02\x02\x02\x1C\u0105\x03\x02\x02\x02\x1E\u0107\x03\x02\x02\x02 \u0115" + + "\x03\x02\x02\x02\"\u0117\x03\x02\x02\x02$\u0120\x03\x02\x02\x02&\u0124" + + "\x03\x02\x02\x02(\u0127\x03\x02\x02\x02*\u012F\x03\x02\x02\x02,\u0135" + + "\x03\x02\x02\x02.\u0137\x03\x02\x02\x020\u013F\x03\x02\x02\x022\u0147" + + "\x03\x02\x02\x024\u0149\x03\x02\x02\x026\u0175\x03\x02\x02\x028\u0177" + + "\x03\x02\x02\x02:\u017A\x03\x02\x02\x02<\u0183\x03\x02\x02\x02>\u018B" + + "\x03\x02\x02\x02@\u0194\x03\x02\x02\x02B\u019D\x03\x02\x02\x02D\u01A6" + + "\x03\x02\x02\x02F\u01AA\x03\x02\x02\x02H\u01B0\x03\x02\x02\x02J\u01B4" + + "\x03\x02\x02\x02L\u01B7\x03\x02\x02\x02N\u01BF\x03\x02\x02\x02P\u01C3" + + "\x03\x02\x02\x02R\u01C7\x03\x02\x02\x02T\u01CA\x03\x02\x02\x02V\u01CF" + + "\x03\x02\x02\x02X\u01D3\x03\x02\x02\x02Z\u01D5\x03\x02\x02\x02\\\u01D7" + + "\x03\x02\x02\x02^\u01DA\x03\x02\x02\x02`\u01E2\x03\x02\x02\x02b\u01E4" + + "\x03\x02\x02\x02d\u01F8\x03\x02\x02\x02fg\x05\x04\x03\x02gh\x07\x02\x02" + + "\x03h\x03\x03\x02\x02\x02ij\b\x03\x01\x02jk\x05\x06\x04\x02kq\x03\x02" + + "\x02\x02lm\f\x03\x02\x02mn\x07\x1B\x02\x02np\x05\b\x05\x02ol\x03\x02\x02" + + "\x02ps\x03\x02\x02\x02qo\x03\x02\x02\x02qr\x03\x02\x02\x02r\x05\x03\x02" + + "\x02\x02sq\x03\x02\x02\x02ty\x05\\/\x02uy\x05\x1E\x10\x02vy\x05\x18\r" + + "\x02wy\x05`1\x02xt\x03\x02\x02\x02xu\x03\x02\x02\x02xv\x03\x02\x02\x02" + + "xw\x03\x02\x02\x02y\x07\x03\x02\x02\x02z\x88\x05&\x14\x02{\x88\x05*\x16" + + "\x02|\x88\x058\x1D\x02}\x88\x05> \x02~\x88\x05:\x1E\x02\x7F\x88\x05(\x15" + + "\x02\x80\x88\x05\n\x06\x02\x81\x88\x05@!\x02\x82\x88\x05B\"\x02\x83\x88" + + "\x05F$\x02\x84\x88\x05H%\x02\x85\x88\x05b2\x02\x86\x88\x05J&\x02\x87z" + + "\x03\x02\x02\x02\x87{\x03\x02\x02\x02\x87|\x03\x02\x02\x02\x87}\x03\x02" + + "\x02\x02\x87~\x03\x02\x02\x02\x87\x7F\x03\x02\x02\x02\x87\x80\x03\x02" + + "\x02\x02\x87\x81\x03\x02\x02\x02\x87\x82\x03\x02\x02\x02\x87\x83\x03\x02" + + "\x02\x02\x87\x84\x03\x02\x02\x02\x87\x85\x03\x02\x02\x02\x87\x86\x03\x02" + + "\x02\x02\x88\t\x03\x02\x02\x02\x89\x8A\x07\x13\x02\x02\x8A\x8B\x05\f\x07" + + "\x02\x8B\v\x03\x02\x02\x02\x8C\x8D\b\x07\x01\x02\x8D\x8E\x07-\x02\x02" + + "\x8E\xA9\x05\f\x07\t\x8F\xA9\x05\x10\t\x02\x90\xA9\x05\x0E\b\x02\x91\x93" + + "\x05\x10\t\x02\x92\x94\x07-\x02\x02\x93\x92\x03\x02\x02\x02\x93\x94\x03" + + "\x02\x02\x02\x94\x95\x03\x02\x02\x02\x95\x96\x07*\x02\x02\x96\x97\x07" + + ")\x02\x02\x97\x9C\x05\x10\t\x02\x98\x99\x07#\x02\x02\x99\x9B\x05\x10\t" + + "\x02\x9A\x98\x03\x02\x02\x02\x9B\x9E\x03\x02\x02\x02\x9C\x9A\x03\x02\x02" + + "\x02\x9C\x9D\x03\x02\x02\x02\x9D\x9F\x03\x02\x02\x02\x9E\x9C\x03\x02\x02" + + "\x02\x9F\xA0\x073\x02\x02\xA0\xA9\x03\x02\x02\x02\xA1\xA2\x05\x10\t\x02" + + "\xA2\xA4\x07+\x02\x02\xA3\xA5\x07-\x02\x02\xA4\xA3\x03\x02\x02\x02\xA4" + + "\xA5\x03\x02\x02\x02\xA5\xA6\x03\x02\x02\x02\xA6\xA7\x07.\x02\x02\xA7" + + "\xA9\x03\x02\x02\x02\xA8\x8C\x03\x02\x02\x02\xA8\x8F\x03\x02\x02\x02\xA8" + + "\x90\x03\x02\x02\x02\xA8\x91\x03\x02\x02\x02\xA8\xA1\x03\x02\x02\x02\xA9" + + "\xB2\x03\x02\x02\x02\xAA\xAB\f\x06\x02\x02\xAB\xAC\x07 \x02\x02\xAC\xB1" + + "\x05\f\x07\x07\xAD\xAE\f\x05\x02\x02\xAE\xAF\x070\x02\x02\xAF\xB1\x05" + + "\f\x07\x06\xB0\xAA\x03\x02\x02\x02\xB0\xAD\x03\x02\x02\x02\xB1\xB4\x03" + + "\x02\x02\x02\xB2\xB0\x03\x02\x02\x02\xB2\xB3\x03\x02\x02\x02\xB3\r\x03" + + "\x02\x02\x02\xB4\xB2\x03\x02\x02\x02\xB5\xB7\x05\x10\t\x02\xB6\xB8\x07" + + "-\x02\x02\xB7\xB6\x03\x02\x02\x02\xB7\xB8\x03\x02\x02\x02\xB8\xB9\x03" + + "\x02\x02\x02\xB9\xBA\x07,\x02\x02\xBA\xBB\x05X-\x02\xBB\xC4\x03\x02\x02" + + "\x02\xBC\xBE\x05\x10\t\x02\xBD\xBF\x07-\x02\x02\xBE\xBD\x03\x02\x02\x02" + + "\xBE\xBF\x03\x02\x02\x02\xBF\xC0\x03\x02\x02\x02\xC0\xC1\x072\x02\x02" + + "\xC1\xC2\x05X-\x02\xC2\xC4\x03\x02\x02\x02\xC3\xB5\x03\x02\x02\x02\xC3" + + "\xBC\x03\x02\x02\x02\xC4\x0F\x03\x02\x02\x02\xC5\xCB\x05\x12\n\x02\xC6" + + "\xC7\x05\x12\n\x02\xC7\xC8\x05Z.\x02\xC8\xC9\x05\x12\n\x02\xC9\xCB\x03" + + "\x02\x02\x02\xCA\xC5\x03\x02\x02\x02\xCA\xC6\x03\x02\x02\x02\xCB\x11\x03" + + "\x02\x02\x02\xCC\xCD\b\n\x01\x02\xCD\xD1\x05\x14\v\x02\xCE\xCF\t\x02\x02" + + "\x02\xCF\xD1\x05\x12\n\x05\xD0\xCC\x03\x02\x02\x02\xD0\xCE\x03\x02\x02" + + "\x02\xD1\xDA\x03\x02\x02\x02\xD2\xD3\f\x04\x02\x02\xD3\xD4\t\x03\x02\x02" + + "\xD4\xD9\x05\x12\n\x05\xD5\xD6\f\x03\x02\x02\xD6\xD7\t\x02\x02\x02\xD7" + + "\xD9\x05\x12\n\x04\xD8\xD2\x03\x02\x02\x02\xD8\xD5\x03\x02\x02\x02\xD9" + + "\xDC\x03\x02\x02\x02\xDA\xD8\x03\x02\x02\x02\xDA\xDB\x03\x02\x02\x02\xDB" + + "\x13\x03\x02\x02\x02\xDC\xDA\x03\x02\x02\x02\xDD\xE5\x056\x1C\x02\xDE" + + "\xE5\x05.\x18\x02\xDF\xE5\x05\x16\f\x02\xE0\xE1\x07)\x02\x02\xE1\xE2\x05" + + "\f\x07\x02\xE2\xE3\x073\x02\x02\xE3\xE5\x03\x02\x02\x02\xE4\xDD\x03\x02" + + "\x02\x02\xE4\xDE\x03\x02\x02\x02\xE4\xDF\x03\x02\x02\x02\xE4\xE0\x03\x02" + + "\x02\x02\xE5\x15\x03\x02\x02\x02\xE6\xE7\x052\x1A\x02\xE7\xF1\x07)\x02" + + "\x02\xE8\xF2\x07>\x02\x02\xE9\xEE\x05\f\x07\x02\xEA\xEB\x07#\x02\x02\xEB" + + "\xED\x05\f\x07\x02\xEC\xEA\x03\x02\x02\x02\xED\xF0\x03\x02\x02\x02\xEE" + + "\xEC\x03\x02\x02\x02\xEE\xEF\x03\x02\x02\x02\xEF\xF2\x03\x02\x02\x02\xF0" + + "\xEE\x03\x02\x02\x02\xF1\xE8\x03\x02\x02\x02\xF1\xE9\x03\x02\x02\x02\xF1" + + "\xF2\x03\x02\x02\x02\xF2\xF3\x03\x02\x02\x02\xF3\xF4\x073\x02\x02\xF4" + + "\x17\x03\x02\x02\x02\xF5\xF6\x07\x0F\x02\x02\xF6\xF7\x05\x1A\x0E\x02\xF7" + + "\x19\x03\x02\x02\x02\xF8\xFD\x05\x1C\x0F\x02\xF9\xFA\x07#\x02\x02\xFA" + + "\xFC\x05\x1C\x0F\x02\xFB\xF9\x03\x02\x02\x02\xFC\xFF\x03\x02\x02\x02\xFD" + + "\xFB\x03\x02\x02\x02\xFD\xFE\x03\x02\x02\x02\xFE\x1B\x03\x02\x02\x02\xFF" + + "\xFD\x03\x02\x02\x02\u0100\u0106\x05\f\x07\x02\u0101\u0102\x05.\x18\x02" + + "\u0102\u0103\x07\"\x02\x02\u0103\u0104\x05\f\x07\x02\u0104\u0106\x03\x02" + + "\x02\x02\u0105\u0100\x03\x02\x02\x02\u0105\u0101\x03\x02\x02\x02\u0106" + + "\x1D\x03\x02\x02\x02\u0107\u0108\x07\b\x02\x02\u0108\u010D\x05,\x17\x02" + + "\u0109\u010A\x07#\x02\x02\u010A\u010C\x05,\x17\x02\u010B\u0109\x03\x02" + + "\x02\x02\u010C\u010F\x03\x02\x02\x02\u010D\u010B\x03\x02\x02\x02\u010D" + + "\u010E\x03\x02\x02\x02\u010E\u0111\x03\x02\x02\x02\u010F\u010D\x03\x02" + + "\x02\x02\u0110\u0112\x05 \x11\x02\u0111\u0110\x03\x02\x02\x02\u0111\u0112" + + "\x03\x02\x02\x02\u0112\x1F\x03\x02\x02\x02\u0113\u0116\x05\"\x12\x02\u0114" + + "\u0116\x05$\x13\x02\u0115\u0113\x03\x02\x02\x02\u0115\u0114\x03\x02\x02" + + "\x02\u0116!\x03\x02\x02\x02\u0117\u0118\x07H\x02\x02\u0118\u011D\x05," + + "\x17\x02\u0119\u011A\x07#\x02\x02\u011A\u011C\x05,\x17\x02\u011B\u0119" + + "\x03\x02\x02\x02\u011C\u011F\x03\x02\x02\x02\u011D\u011B\x03\x02\x02\x02" + + "\u011D\u011E\x03\x02\x02\x02\u011E#\x03\x02\x02\x02\u011F\u011D\x03\x02" + + "\x02\x02\u0120\u0121\x07A\x02\x02\u0121\u0122\x05\"\x12\x02\u0122\u0123" + + "\x07B\x02\x02\u0123%\x03\x02\x02\x02\u0124\u0125\x07\x06\x02\x02\u0125" + + "\u0126\x05\x1A\x0E\x02\u0126\'\x03\x02\x02\x02\u0127\u0129\x07\x12\x02" + + "\x02\u0128\u012A\x05\x1A\x0E\x02\u0129\u0128\x03\x02\x02\x02\u0129\u012A" + + "\x03\x02\x02\x02\u012A\u012D\x03\x02\x02\x02\u012B\u012C\x07\x1F\x02\x02" + + "\u012C\u012E\x05\x1A\x0E\x02\u012D\u012B\x03\x02\x02\x02\u012D\u012E\x03" + + "\x02\x02\x02\u012E)\x03\x02\x02\x02\u012F\u0130\x07\n\x02\x02\u0130\u0133" + + "\x05\x1A\x0E\x02\u0131\u0132\x07\x1F\x02\x02\u0132\u0134\x05\x1A\x0E\x02" + + "\u0133\u0131\x03\x02\x02\x02\u0133\u0134\x03\x02\x02\x02\u0134+\x03\x02" + + "\x02\x02\u0135\u0136\t\x04\x02\x02\u0136-\x03\x02\x02\x02\u0137\u013C" + + "\x052\x1A\x02\u0138\u0139\x07%\x02\x02\u0139\u013B\x052\x1A\x02\u013A" + + "\u0138\x03\x02\x02\x02\u013B\u013E\x03\x02\x02\x02\u013C\u013A\x03\x02" + + "\x02\x02\u013C\u013D\x03\x02\x02\x02\u013D/\x03\x02\x02\x02\u013E\u013C" + + "\x03\x02\x02\x02\u013F\u0144\x054\x1B\x02\u0140\u0141\x07%\x02\x02\u0141" + + "\u0143\x054\x1B\x02\u0142\u0140\x03\x02\x02\x02\u0143\u0146\x03\x02\x02" + + "\x02\u0144\u0142\x03\x02\x02\x02\u0144\u0145\x03\x02\x02\x02\u01451\x03" + + "\x02\x02\x02\u0146\u0144\x03\x02\x02\x02\u0147\u0148\t\x05\x02\x02\u0148" + + "3\x03\x02\x02\x02\u0149\u014A\t\x06\x02\x02\u014A5\x03\x02\x02\x02\u014B" + + "\u0176\x07.\x02\x02\u014C\u014D\x05V,\x02\u014D\u014E\x07C\x02\x02\u014E" + + "\u0176\x03\x02\x02\x02\u014F\u0176\x05T+\x02\u0150\u0176\x05V,\x02\u0151" + + "\u0176\x05P)\x02\u0152\u0176\x071\x02\x02\u0153\u0176\x05X-\x02\u0154" + + "\u0155\x07A\x02\x02\u0155\u015A\x05R*\x02\u0156\u0157\x07#\x02\x02\u0157" + + "\u0159\x05R*\x02\u0158\u0156\x03\x02\x02\x02\u0159\u015C\x03\x02\x02\x02" + + "\u015A\u0158\x03\x02\x02\x02\u015A\u015B\x03\x02\x02\x02\u015B\u015D\x03" + + "\x02\x02\x02\u015C\u015A\x03\x02\x02\x02\u015D\u015E\x07B\x02\x02\u015E" + + "\u0176\x03\x02\x02\x02\u015F\u0160\x07A\x02\x02\u0160\u0165\x05P)\x02" + + "\u0161\u0162\x07#\x02\x02\u0162\u0164\x05P)\x02\u0163\u0161\x03\x02\x02" + + "\x02\u0164\u0167\x03\x02\x02\x02\u0165\u0163\x03\x02\x02\x02\u0165\u0166" + + "\x03\x02\x02\x02\u0166\u0168\x03\x02\x02\x02\u0167\u0165\x03\x02\x02\x02" + + "\u0168\u0169\x07B\x02\x02\u0169\u0176\x03\x02\x02\x02\u016A\u016B\x07" + + "A\x02\x02\u016B\u0170\x05X-\x02\u016C\u016D\x07#\x02\x02\u016D\u016F\x05" + + "X-\x02\u016E\u016C\x03\x02\x02\x02\u016F\u0172\x03\x02\x02\x02\u0170\u016E" + + "\x03\x02\x02\x02\u0170\u0171\x03\x02\x02\x02\u0171\u0173\x03\x02\x02\x02" + + "\u0172\u0170\x03\x02\x02\x02\u0173\u0174\x07B\x02\x02\u0174\u0176\x03" + + "\x02\x02\x02\u0175\u014B\x03\x02\x02\x02\u0175\u014C\x03\x02\x02\x02\u0175" + + "\u014F\x03\x02\x02\x02\u0175\u0150\x03\x02\x02\x02\u0175\u0151\x03\x02" + + "\x02\x02\u0175\u0152\x03\x02\x02\x02\u0175\u0153\x03\x02\x02\x02\u0175" + + "\u0154\x03\x02\x02\x02\u0175\u015F\x03\x02\x02\x02\u0175\u016A\x03\x02" + + "\x02\x02\u01767\x03\x02\x02\x02\u0177\u0178\x07\f\x02\x02\u0178\u0179" + + "\x07\x1D\x02\x02\u01799\x03\x02\x02\x02\u017A\u017B\x07\x11\x02\x02\u017B" + + "\u0180\x05<\x1F\x02\u017C\u017D\x07#\x02\x02\u017D\u017F\x05<\x1F\x02" + + "\u017E\u017C\x03\x02\x02\x02\u017F\u0182\x03\x02\x02\x02\u0180\u017E\x03" + + "\x02\x02\x02\u0180\u0181\x03\x02\x02\x02\u0181;\x03\x02\x02\x02\u0182" + + "\u0180\x03\x02\x02\x02\u0183\u0185\x05\f\x07\x02\u0184\u0186\t\x07\x02" + + "\x02\u0185\u0184\x03\x02\x02\x02\u0185\u0186\x03\x02\x02\x02\u0186\u0189" + + "\x03\x02\x02\x02\u0187\u0188\x07/\x02\x02\u0188\u018A\t\b\x02\x02\u0189" + + "\u0187\x03\x02\x02\x02\u0189\u018A\x03\x02\x02\x02\u018A=\x03\x02\x02" + + "\x02\u018B\u018C\x07\v\x02\x02\u018C\u0191\x050\x19\x02\u018D\u018E\x07" + + "#\x02\x02\u018E\u0190\x050\x19\x02\u018F\u018D\x03\x02\x02\x02\u0190\u0193" + + "\x03\x02\x02\x02\u0191\u018F\x03\x02\x02\x02\u0191\u0192\x03\x02\x02\x02" + + "\u0192?\x03\x02\x02\x02\u0193\u0191\x03\x02\x02\x02\u0194\u0195\x07\x04" + + "\x02\x02\u0195\u019A\x050\x19\x02\u0196\u0197\x07#\x02\x02\u0197\u0199" + + "\x050\x19\x02\u0198\u0196\x03\x02\x02\x02\u0199\u019C\x03\x02\x02\x02" + + "\u019A\u0198\x03\x02\x02\x02\u019A\u019B\x03\x02\x02\x02\u019BA\x03\x02" + + "\x02\x02\u019C\u019A\x03\x02\x02\x02\u019D\u019E\x07\x0E\x02\x02\u019E" + + "\u01A3\x05D#\x02\u019F\u01A0\x07#\x02\x02\u01A0\u01A2\x05D#\x02\u01A1" + + "\u019F\x03\x02\x02\x02\u01A2\u01A5\x03\x02\x02\x02\u01A3\u01A1\x03\x02" + + "\x02\x02\u01A3\u01A4\x03\x02\x02\x02\u01A4C\x03\x02\x02\x02\u01A5\u01A3" + + "\x03\x02\x02\x02\u01A6\u01A7\x050\x19\x02\u01A7\u01A8\x07Q\x02\x02\u01A8" + + "\u01A9\x050\x19\x02\u01A9E\x03\x02\x02\x02\u01AA\u01AB\x07\x03\x02\x02" + + "\u01AB\u01AC\x05\x14\v\x02\u01AC\u01AE\x05X-\x02\u01AD\u01AF\x05L\'\x02" + + "\u01AE\u01AD\x03\x02\x02\x02\u01AE\u01AF\x03\x02\x02\x02\u01AFG\x03\x02" + + "\x02\x02\u01B0\u01B1\x07\t\x02\x02\u01B1\u01B2\x05\x14\v\x02\u01B2\u01B3" + + "\x05X-\x02\u01B3I\x03\x02\x02\x02\u01B4\u01B5\x07\r\x02\x02\u01B5\u01B6" + + "\x05.\x18\x02\u01B6K\x03\x02\x02\x02\u01B7\u01BC\x05N(\x02\u01B8\u01B9" + + "\x07#\x02\x02\u01B9\u01BB\x05N(\x02\u01BA\u01B8\x03\x02\x02\x02\u01BB" + + "\u01BE\x03\x02\x02\x02\u01BC\u01BA\x03\x02\x02\x02\u01BC\u01BD\x03\x02" + + "\x02\x02\u01BDM\x03\x02\x02\x02\u01BE\u01BC\x03\x02\x02\x02\u01BF\u01C0" + + "\x052\x1A\x02\u01C0\u01C1\x07\"\x02\x02\u01C1\u01C2\x056\x1C\x02\u01C2" + + "O\x03\x02\x02\x02\u01C3\u01C4\t\t\x02\x02\u01C4Q\x03\x02\x02\x02\u01C5" + + "\u01C8\x05T+\x02\u01C6\u01C8\x05V,\x02\u01C7\u01C5\x03\x02\x02\x02\u01C7" + + "\u01C6\x03\x02\x02\x02\u01C8S\x03\x02\x02\x02\u01C9\u01CB\t\x02\x02\x02" + + "\u01CA\u01C9\x03\x02\x02\x02\u01CA\u01CB\x03\x02\x02\x02\u01CB\u01CC\x03" + + "\x02\x02\x02\u01CC\u01CD\x07\x1E\x02\x02\u01CDU\x03\x02\x02\x02\u01CE" + + "\u01D0\t\x02\x02\x02\u01CF\u01CE\x03\x02\x02\x02\u01CF\u01D0\x03\x02\x02" + + "\x02\u01D0\u01D1\x03\x02\x02\x02\u01D1\u01D2\x07\x1D\x02\x02\u01D2W\x03" + + "\x02\x02\x02\u01D3\u01D4\x07\x1C\x02\x02\u01D4Y\x03\x02\x02\x02\u01D5" + + "\u01D6\t\n\x02\x02\u01D6[\x03\x02\x02\x02\u01D7\u01D8\x07\x07\x02\x02" + + "\u01D8\u01D9\x05^0\x02\u01D9]\x03\x02\x02\x02\u01DA\u01DB\x07A\x02\x02" + + "\u01DB\u01DC\x05\x04\x03\x02\u01DC\u01DD\x07B\x02\x02\u01DD_\x03\x02\x02" + + "\x02\u01DE\u01DF\x07\x10\x02\x02\u01DF\u01E3\x07a\x02\x02\u01E0\u01E1" + + "\x07\x10\x02\x02\u01E1\u01E3\x07b\x02\x02\u01E2\u01DE\x03\x02\x02\x02" + + "\u01E2\u01E0\x03\x02\x02\x02\u01E3a\x03\x02\x02\x02\u01E4\u01E5\x07\x05" + + "\x02\x02\u01E5\u01E8\x07W\x02\x02\u01E6\u01E7\x07U\x02\x02\u01E7\u01E9" + + "\x050\x19\x02\u01E8\u01E6\x03\x02\x02\x02\u01E8\u01E9\x03\x02\x02\x02" + + "\u01E9\u01F3\x03\x02\x02\x02\u01EA\u01EB\x07V\x02\x02\u01EB\u01F0\x05" + + "d3\x02\u01EC\u01ED\x07#\x02\x02\u01ED\u01EF\x05d3\x02\u01EE\u01EC\x03" + + "\x02\x02\x02\u01EF\u01F2\x03\x02\x02\x02\u01F0\u01EE\x03\x02\x02\x02\u01F0" + + "\u01F1\x03\x02\x02\x02\u01F1\u01F4\x03\x02\x02\x02\u01F2\u01F0\x03\x02" + + "\x02\x02\u01F3\u01EA\x03\x02\x02\x02\u01F3\u01F4\x03\x02\x02\x02\u01F4" + + "c\x03\x02\x02\x02\u01F5\u01F6\x050\x19\x02\u01F6\u01F7\x07\"\x02\x02\u01F7" + + "\u01F9\x03\x02\x02\x02\u01F8\u01F5\x03\x02\x02\x02\u01F8\u01F9\x03\x02" + + "\x02\x02\u01F9\u01FA\x03\x02\x02\x02\u01FA\u01FB\x050\x19\x02\u01FBe\x03" + + "\x02\x02\x024qx\x87\x93\x9C\xA4\xA8\xB0\xB2\xB7\xBE\xC3\xCA\xD0\xD8\xDA" + + "\xE4\xEE\xF1\xFD\u0105\u010D\u0111\u0115\u011D\u0129\u012D\u0133\u013C" + + "\u0144\u015A\u0165\u0170\u0175\u0180\u0185\u0189\u0191\u019A\u01A3\u01AE" + + "\u01BC\u01C7\u01CA\u01CF\u01E2\u01E8\u01F0\u01F3\u01F8"; public static __ATN: ATN; public static get _ATN(): ATN { if (!esql_parser.__ATN) { @@ -3997,7 +3971,33 @@ export class FromCommandContext extends ParserRuleContext { export class MetadataContext extends ParserRuleContext { - public OPENING_BRACKET(): TerminalNode { return this.getToken(esql_parser.OPENING_BRACKET, 0); } + public metadataOption(): MetadataOptionContext | undefined { + return this.tryGetRuleContext(0, MetadataOptionContext); + } + public deprecated_metadata(): Deprecated_metadataContext | undefined { + return this.tryGetRuleContext(0, Deprecated_metadataContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_metadata; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterMetadata) { + listener.enterMetadata(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitMetadata) { + listener.exitMetadata(this); + } + } +} + + +export class MetadataOptionContext extends ParserRuleContext { public METADATA(): TerminalNode { return this.getToken(esql_parser.METADATA, 0); } public fromIdentifier(): FromIdentifierContext[]; public fromIdentifier(i: number): FromIdentifierContext; @@ -4008,7 +4008,6 @@ export class MetadataContext extends ParserRuleContext { return this.getRuleContext(i, FromIdentifierContext); } } - public CLOSING_BRACKET(): TerminalNode { return this.getToken(esql_parser.CLOSING_BRACKET, 0); } public COMMA(): TerminalNode[]; public COMMA(i: number): TerminalNode; public COMMA(i?: number): TerminalNode | TerminalNode[] { @@ -4022,17 +4021,43 @@ export class MetadataContext extends ParserRuleContext { super(parent, invokingState); } // @Override - public get ruleIndex(): number { return esql_parser.RULE_metadata; } + public get ruleIndex(): number { return esql_parser.RULE_metadataOption; } // @Override public enterRule(listener: esql_parserListener): void { - if (listener.enterMetadata) { - listener.enterMetadata(this); + if (listener.enterMetadataOption) { + listener.enterMetadataOption(this); } } // @Override public exitRule(listener: esql_parserListener): void { - if (listener.exitMetadata) { - listener.exitMetadata(this); + if (listener.exitMetadataOption) { + listener.exitMetadataOption(this); + } + } +} + + +export class Deprecated_metadataContext extends ParserRuleContext { + public OPENING_BRACKET(): TerminalNode { return this.getToken(esql_parser.OPENING_BRACKET, 0); } + public metadataOption(): MetadataOptionContext { + return this.getRuleContext(0, MetadataOptionContext); + } + public CLOSING_BRACKET(): TerminalNode { return this.getToken(esql_parser.CLOSING_BRACKET, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return esql_parser.RULE_deprecated_metadata; } + // @Override + public enterRule(listener: esql_parserListener): void { + if (listener.enterDeprecated_metadata) { + listener.enterDeprecated_metadata(this); + } + } + // @Override + public exitRule(listener: esql_parserListener): void { + if (listener.exitDeprecated_metadata) { + listener.exitDeprecated_metadata(this); } } } @@ -4643,7 +4668,7 @@ export class OrderExpressionContext extends ParserRuleContext { export class KeepCommandContext extends ParserRuleContext { - public KEEP(): TerminalNode | undefined { return this.tryGetToken(esql_parser.KEEP, 0); } + public KEEP(): TerminalNode { return this.getToken(esql_parser.KEEP, 0); } public qualifiedNamePattern(): QualifiedNamePatternContext[]; public qualifiedNamePattern(i: number): QualifiedNamePatternContext; public qualifiedNamePattern(i?: number): QualifiedNamePatternContext | QualifiedNamePatternContext[] { @@ -4662,7 +4687,6 @@ export class KeepCommandContext extends ParserRuleContext { return this.getToken(esql_parser.COMMA, i); } } - public PROJECT(): TerminalNode | undefined { return this.tryGetToken(esql_parser.PROJECT, 0); } constructor(parent: ParserRuleContext | undefined, invokingState: number) { super(parent, invokingState); } @@ -5203,15 +5227,6 @@ export class EnrichCommandContext extends ParserRuleContext { public _matchField: QualifiedNamePatternContext; public ENRICH(): TerminalNode { return this.getToken(esql_parser.ENRICH, 0); } public ENRICH_POLICY_NAME(): TerminalNode { return this.getToken(esql_parser.ENRICH_POLICY_NAME, 0); } - public setting(): SettingContext[]; - public setting(i: number): SettingContext; - public setting(i?: number): SettingContext | SettingContext[] { - if (i === undefined) { - return this.getRuleContexts(SettingContext); - } else { - return this.getRuleContext(i, SettingContext); - } - } public ON(): TerminalNode | undefined { return this.tryGetToken(esql_parser.ON, 0); } public WITH(): TerminalNode | undefined { return this.tryGetToken(esql_parser.WITH, 0); } public enrichWithClause(): EnrichWithClauseContext[]; @@ -5288,38 +5303,3 @@ export class EnrichWithClauseContext extends ParserRuleContext { } -export class SettingContext extends ParserRuleContext { - public _name: Token; - public _value: Token; - public OPENING_BRACKET(): TerminalNode { return this.getToken(esql_parser.OPENING_BRACKET, 0); } - public COLON(): TerminalNode { return this.getToken(esql_parser.COLON, 0); } - public CLOSING_BRACKET(): TerminalNode { return this.getToken(esql_parser.CLOSING_BRACKET, 0); } - public SETTING(): TerminalNode[]; - public SETTING(i: number): TerminalNode; - public SETTING(i?: number): TerminalNode | TerminalNode[] { - if (i === undefined) { - return this.getTokens(esql_parser.SETTING); - } else { - return this.getToken(esql_parser.SETTING, i); - } - } - constructor(parent: ParserRuleContext | undefined, invokingState: number) { - super(parent, invokingState); - } - // @Override - public get ruleIndex(): number { return esql_parser.RULE_setting; } - // @Override - public enterRule(listener: esql_parserListener): void { - if (listener.enterSetting) { - listener.enterSetting(this); - } - } - // @Override - public exitRule(listener: esql_parserListener): void { - if (listener.exitSetting) { - listener.exitSetting(this); - } - } -} - - diff --git a/packages/kbn-monaco/src/esql/antlr/esql_parser_listener.ts b/packages/kbn-monaco/src/esql/antlr/esql_parser_listener.ts index 31c0d1ec27beb..233518b74555e 100644 --- a/packages/kbn-monaco/src/esql/antlr/esql_parser_listener.ts +++ b/packages/kbn-monaco/src/esql/antlr/esql_parser_listener.ts @@ -49,6 +49,8 @@ import { FieldsContext } from "./esql_parser"; import { FieldContext } from "./esql_parser"; import { FromCommandContext } from "./esql_parser"; import { MetadataContext } from "./esql_parser"; +import { MetadataOptionContext } from "./esql_parser"; +import { Deprecated_metadataContext } from "./esql_parser"; import { EvalCommandContext } from "./esql_parser"; import { StatsCommandContext } from "./esql_parser"; import { InlinestatsCommandContext } from "./esql_parser"; @@ -81,7 +83,6 @@ import { SubqueryExpressionContext } from "./esql_parser"; import { ShowCommandContext } from "./esql_parser"; import { EnrichCommandContext } from "./esql_parser"; import { EnrichWithClauseContext } from "./esql_parser"; -import { SettingContext } from "./esql_parser"; /** @@ -642,6 +643,28 @@ export interface esql_parserListener extends ParseTreeListener { */ exitMetadata?: (ctx: MetadataContext) => void; + /** + * Enter a parse tree produced by `esql_parser.metadataOption`. + * @param ctx the parse tree + */ + enterMetadataOption?: (ctx: MetadataOptionContext) => void; + /** + * Exit a parse tree produced by `esql_parser.metadataOption`. + * @param ctx the parse tree + */ + exitMetadataOption?: (ctx: MetadataOptionContext) => void; + + /** + * Enter a parse tree produced by `esql_parser.deprecated_metadata`. + * @param ctx the parse tree + */ + enterDeprecated_metadata?: (ctx: Deprecated_metadataContext) => void; + /** + * Exit a parse tree produced by `esql_parser.deprecated_metadata`. + * @param ctx the parse tree + */ + exitDeprecated_metadata?: (ctx: Deprecated_metadataContext) => void; + /** * Enter a parse tree produced by `esql_parser.evalCommand`. * @param ctx the parse tree @@ -993,16 +1016,5 @@ export interface esql_parserListener extends ParseTreeListener { * @param ctx the parse tree */ exitEnrichWithClause?: (ctx: EnrichWithClauseContext) => void; - - /** - * Enter a parse tree produced by `esql_parser.setting`. - * @param ctx the parse tree - */ - enterSetting?: (ctx: SettingContext) => void; - /** - * Exit a parse tree produced by `esql_parser.setting`. - * @param ctx the parse tree - */ - exitSetting?: (ctx: SettingContext) => void; } diff --git a/packages/kbn-monaco/src/esql/lib/ast/ast_factory.ts b/packages/kbn-monaco/src/esql/lib/ast/ast_factory.ts index 724e1d5bb0f78..328dfd51bc9bc 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/ast_factory.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/ast_factory.ts @@ -43,7 +43,6 @@ import { getPolicyName, getMatchField, getEnrichClauses, - getPolicySettings, } from './ast_walker'; import type { ESQLAst } from './types'; @@ -117,10 +116,12 @@ export class AstListener implements ESQLParserListener { this.ast.push(commandAst); commandAst.args.push(...collectAllSourceIdentifiers(ctx)); const metadataContext = ctx.metadata(); - if (metadataContext) { - const option = createOption(metadataContext.METADATA().text.toLowerCase(), metadataContext); + const metadataContent = + metadataContext?.deprecated_metadata()?.metadataOption() || metadataContext?.metadataOption(); + if (metadataContent) { + const option = createOption(metadataContent.METADATA().text.toLowerCase(), metadataContent); commandAst.args.push(option); - option.args.push(...collectAllColumnIdentifiers(metadataContext)); + option.args.push(...collectAllColumnIdentifiers(metadataContent)); } } @@ -251,11 +252,6 @@ export class AstListener implements ESQLParserListener { exitEnrichCommand(ctx: EnrichCommandContext) { const command = createCommand('enrich', ctx); this.ast.push(command); - command.args.push( - ...getPolicySettings(ctx), - ...getPolicyName(ctx), - ...getMatchField(ctx), - ...getEnrichClauses(ctx) - ); + command.args.push(...getPolicyName(ctx), ...getMatchField(ctx), ...getEnrichClauses(ctx)); } } diff --git a/packages/kbn-monaco/src/esql/lib/ast/ast_helpers.ts b/packages/kbn-monaco/src/esql/lib/ast/ast_helpers.ts index 73fd877f307bd..508fa7c371100 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/ast_helpers.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/ast_helpers.ts @@ -19,7 +19,6 @@ import type { DecimalValueContext, IntegerValueContext, QualifiedIntegerLiteralContext, - SettingContext, } from '../../antlr/esql_parser'; import { getPosition } from './ast_position_utils'; import type { @@ -198,15 +197,15 @@ export function computeLocationExtends(fn: ESQLFunction) { /* SCRIPT_MARKER_START */ function getQuotedText(ctx: ParserRuleContext) { - return [67 /* esql_parser.QUOTED_IDENTIFIER */] + return [66 /* esql_parser.QUOTED_IDENTIFIER */] .map((keyCode) => ctx.tryGetToken(keyCode, 0)) .filter(nonNullable)[0]; } function getUnquotedText(ctx: ParserRuleContext) { return [ - 66 /* esql_parser.UNQUOTED_IDENTIFIER */, 72 /* esql_parser.FROM_UNQUOTED_IDENTIFIER */, - 76 /* esql_parser.UNQUOTED_ID_PATTERN */, + 65 /* esql_parser.UNQUOTED_IDENTIFIER */, 71 /* esql_parser.FROM_UNQUOTED_IDENTIFIER */, + 75 /* esql_parser.UNQUOTED_ID_PATTERN */, ] .map((keyCode) => ctx.tryGetToken(keyCode, 0)) .filter(nonNullable)[0]; @@ -231,16 +230,13 @@ export function wrapIdentifierAsArray(identifierCtx return Array.isArray(identifierCtx) ? identifierCtx : [identifierCtx]; } -export function createSettingTuple(ctx: SettingContext): ESQLCommandMode { +export function createSetting(policyName: Token, mode: string): ESQLCommandMode { return { type: 'mode', - name: ctx._name?.text || '', - text: ctx.text!, - location: getPosition(ctx.start, ctx.stop), - incomplete: - (ctx._name?.text ? isMissingText(ctx._name.text) : true) || - (ctx._value?.text ? isMissingText(ctx._value.text) : true), - args: [], + name: mode.replace('_', '').toLowerCase(), + text: mode, + location: getPosition(policyName, { stopIndex: policyName.startIndex + mode.length - 1 }), // unfortunately this is the only location we have + incomplete: false, }; } @@ -248,13 +244,16 @@ export function createSettingTuple(ctx: SettingContext): ESQLCommandMode { * In https://github.com/elastic/elasticsearch/pull/103949 the ENRICH policy name * changed from rule to token type so we need to handle this specifically */ -export function createPolicy(token: Token): ESQLSource { +export function createPolicy(token: Token, policy: string): ESQLSource { return { type: 'source', - name: token.text!, - text: token.text!, + name: policy, + text: policy, sourceType: 'policy', - location: getPosition(token), + location: getPosition({ + startIndex: token.stopIndex - policy.length + 1, + stopIndex: token.stopIndex, + }), // take into account ccq modes incomplete: false, }; } diff --git a/packages/kbn-monaco/src/esql/lib/ast/ast_position_utils.ts b/packages/kbn-monaco/src/esql/lib/ast/ast_position_utils.ts index 73745b12f4908..24d3a806ace01 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/ast_position_utils.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/ast_position_utils.ts @@ -8,7 +8,10 @@ import type { Token } from 'antlr4ts'; -export function getPosition(token: Token | undefined, lastToken?: Token | undefined) { +export function getPosition( + token: Pick | undefined, + lastToken?: Pick | undefined +) { if (!token || token.startIndex < 0) { return { min: 0, max: 0 }; } diff --git a/packages/kbn-monaco/src/esql/lib/ast/ast_walker.ts b/packages/kbn-monaco/src/esql/lib/ast/ast_walker.ts index 00a84dd3d3214..fd33f33af976d 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/ast_walker.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/ast_walker.ts @@ -37,7 +37,7 @@ import { LogicalBinaryContext, LogicalInContext, LogicalNotContext, - MetadataContext, + MetadataOptionContext, MvExpandCommandContext, NullLiteralContext, NumericArrayLiteralContext, @@ -74,9 +74,8 @@ import { createColumnStar, wrapIdentifierAsArray, createPolicy, - createSettingTuple, - createLiteralString, isMissingText, + createSetting, } from './ast_helpers'; import { getPosition } from './ast_position_utils'; import type { @@ -92,9 +91,9 @@ export function collectAllSourceIdentifiers(ctx: FromCommandContext): ESQLAstIte } function extractIdentifiers( - ctx: KeepCommandContext | DropCommandContext | MvExpandCommandContext | MetadataContext + ctx: KeepCommandContext | DropCommandContext | MvExpandCommandContext | MetadataOptionContext ) { - if (ctx instanceof MetadataContext) { + if (ctx instanceof MetadataOptionContext) { return wrapIdentifierAsArray(ctx.fromIdentifier()); } if (ctx instanceof MvExpandCommandContext) { @@ -114,32 +113,22 @@ function makeColumnsOutOfIdentifiers(identifiers: ParserRuleContext[]) { } export function collectAllColumnIdentifiers( - ctx: KeepCommandContext | DropCommandContext | MvExpandCommandContext | MetadataContext + ctx: KeepCommandContext | DropCommandContext | MvExpandCommandContext | MetadataOptionContext ): ESQLAstItem[] { const identifiers = extractIdentifiers(ctx); return makeColumnsOutOfIdentifiers(identifiers); } export function getPolicyName(ctx: EnrichCommandContext) { - if (!ctx._policyName || (ctx._policyName.text && / 1) { + const [setting, policyName] = policyComponents; + return [createSetting(ctx._policyName, setting), createPolicy(ctx._policyName, policyName)]; } - return ctx.setting().map((setting) => { - const node = createSettingTuple(setting); - if (setting._name?.text && setting._value?.text) { - node.args.push(createLiteralString(setting._value)!); - return node; - } - // incomplete setting - return node; - }); + return [createPolicy(ctx._policyName, policyComponents[0])]; } export function getMatchField(ctx: EnrichCommandContext) { 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 55caf36f04c59..4190552d048e2 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 @@ -18,6 +18,7 @@ import { statsAggregationFunctionDefinitions } from '../definitions/aggs'; import { chronoLiterals, timeLiterals } from '../definitions/literals'; import { commandDefinitions } from '../definitions/commands'; import { TRIGGER_SUGGESTION_COMMAND } from './factories'; +import { camelCase } from 'lodash'; const triggerCharacters = [',', '(', '=', ' ']; @@ -332,11 +333,13 @@ describe('autocomplete', () => { ); testSuggestions('from ', suggestedIndexes); testSuggestions('from a,', suggestedIndexes); - testSuggestions('from a, b ', ['[metadata $0 ]', '|', ',']); + testSuggestions('from a, b ', ['metadata $0', '|', ',']); testSuggestions('from *,', suggestedIndexes); testSuggestions('from index', suggestedIndexes, 6 /* index index in from */); testSuggestions('from a, b [metadata ]', ['_index', '_score'], 20); + testSuggestions('from a, b metadata ', ['_index', '_score'], 19); testSuggestions('from a, b [metadata _index, ]', ['_score'], 27); + testSuggestions('from a, b metadata _index, ', ['_score'], 26); }); describe('show', () => { @@ -542,11 +545,11 @@ describe('autocomplete', () => { describe('rename', () => { testSuggestions('from a | rename ', getFieldNamesByType('any')); - testSuggestions('from a | rename stringField ', ['as']); + testSuggestions('from a | rename stringField ', ['as $0']); testSuggestions('from a | rename stringField as ', ['var0']); }); - for (const command of ['keep', 'drop', 'project']) { + for (const command of ['keep', 'drop']) { describe(command, () => { testSuggestions(`from a | ${command} `, getFieldNamesByType('any')); testSuggestions( @@ -560,40 +563,52 @@ describe('autocomplete', () => { const allAggFunctions = getFunctionSignaturesByReturnType('stats', 'any', { agg: true, }); - testSuggestions('from a | stats ', ['var0 =', ...allAggFunctions]); + const allEvaFunctions = getFunctionSignaturesByReturnType('stats', 'any', { + evalMath: true, + }); + testSuggestions('from a | stats ', ['var0 =', ...allAggFunctions, ...allEvaFunctions]); testSuggestions('from a | stats a ', ['= $0']); - testSuggestions('from a | stats a=', [...allAggFunctions]); + testSuggestions('from a | stats a=', [...allAggFunctions, ...allEvaFunctions]); testSuggestions('from a | stats a=max(b) by ', [ ...getFieldNamesByType('any'), - ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + ...allEvaFunctions, 'var0 =', ]); testSuggestions('from a | stats a=max(b) BY ', [ ...getFieldNamesByType('any'), - ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + ...allEvaFunctions, 'var0 =', ]); testSuggestions('from a | stats a=c by d ', ['|', ',']); testSuggestions('from a | stats a=c by d, ', [ ...getFieldNamesByType('any'), - ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + ...allEvaFunctions, 'var0 =', ]); - testSuggestions('from a | stats a=max(b), ', ['var0 =', ...allAggFunctions]); + testSuggestions('from a | stats a=max(b), ', [ + 'var0 =', + ...allAggFunctions, + ...allEvaFunctions, + ]); testSuggestions('from a | stats a=min()', getFieldNamesByType('number'), '('); - testSuggestions('from a | stats a=min(b) ', ['by', '|', ',']); + testSuggestions('from a | stats a=min(b) ', ['by $0', '|', ',']); testSuggestions('from a | stats a=min(b) by ', [ ...getFieldNamesByType('any'), - ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + ...allEvaFunctions, 'var0 =', ]); - testSuggestions('from a | stats a=min(b),', ['var0 =', ...allAggFunctions]); - testSuggestions('from a | stats var0=min(b),var1=c,', ['var2 =', ...allAggFunctions]); + testSuggestions('from a | stats a=min(b),', ['var0 =', ...allAggFunctions, ...allEvaFunctions]); + testSuggestions('from a | stats var0=min(b),var1=c,', [ + 'var2 =', + ...allAggFunctions, + ...allEvaFunctions, + ]); testSuggestions('from a | stats a=min(b), b=max()', getFieldNamesByType('number')); // @TODO: remove last 2 suggestions if possible testSuggestions('from a | eval var0=round(b), var1=round(c) | stats ', [ 'var2 =', ...allAggFunctions, + ...allEvaFunctions, 'var0', 'var1', ]); @@ -601,7 +616,7 @@ describe('autocomplete', () => { // smoke testing with suggestions not at the end of the string testSuggestions( 'from a | stats a = min(b) | sort b', - ['by', '|', ','], + ['by $0', '|', ','], 27 /* " " after min(b) */ ); testSuggestions( @@ -634,29 +649,25 @@ describe('autocomplete', () => { describe('enrich', () => { const modes = ['any', 'coordinator', 'remote']; + const policyNames = policies.map(({ name, suggestedAs }) => suggestedAs || name); for (const prevCommand of [ '', - '| enrich other-policy ', - '| enrich other-policy on b ', - '| enrich other-policy with c ', + // '| enrich other-policy ', + // '| enrich other-policy on b ', + // '| enrich other-policy with c ', ]) { + testSuggestions(`from a ${prevCommand}| enrich `, policyNames); testSuggestions( - `from a ${prevCommand}| enrich `, - policies.map(({ name, suggestedAs }) => suggestedAs || name) - ); - testSuggestions( - `from a ${prevCommand}| enrich [`, - modes.map((mode) => `ccq.mode:${mode}`), - '[' + `from a ${prevCommand}| enrich _`, + modes.map((mode) => `_${mode}:$0`), + '_' ); - // Not suggesting duplicate setting - testSuggestions(`from a ${prevCommand}| enrich [ccq.mode:any] [`, [], '['); - testSuggestions(`from a ${prevCommand}| enrich [ccq.mode:`, modes, ':'); - testSuggestions( - `from a ${prevCommand}| enrich [ccq.mode:any] `, - policies.map(({ name, suggestedAs }) => suggestedAs || name) - ); - testSuggestions(`from a ${prevCommand}| enrich policy `, ['on', 'with', '|']); + for (const mode of modes) { + testSuggestions(`from a ${prevCommand}| enrich _${mode}:`, policyNames, ':'); + testSuggestions(`from a ${prevCommand}| enrich _${mode.toUpperCase()}:`, policyNames, ':'); + testSuggestions(`from a ${prevCommand}| enrich _${camelCase(mode)}:`, policyNames, ':'); + } + testSuggestions(`from a ${prevCommand}| enrich policy `, ['on $0', 'with $0', '|']); testSuggestions(`from a ${prevCommand}| enrich policy on `, [ 'stringField', 'numberField', @@ -666,7 +677,7 @@ describe('autocomplete', () => { 'any#Char$Field', 'kubernetes.something.something', ]); - testSuggestions(`from a ${prevCommand}| enrich policy on b `, ['with', '|', ',']); + testSuggestions(`from a ${prevCommand}| enrich policy on b `, ['with $0', '|', ',']); testSuggestions(`from a ${prevCommand}| enrich policy on b with `, [ 'var0 =', ...getPolicyFields('policy'), @@ -1083,5 +1094,12 @@ describe('autocomplete', () => { suggestions.every(({ command }) => command === TRIGGER_SUGGESTION_COMMAND) ).toBeTruthy(); }); + it('should trigger further suggestions after enrich mode', async () => { + const suggestions = await getSuggestionsFor('from a | enrich _any:'); + // test that all commands will retrigger suggestions + expect( + suggestions.every(({ command }) => command === TRIGGER_SUGGESTION_COMMAND) + ).toBeTruthy(); + }); }); }); 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 2f3e997478050..a947ab4d6d7c6 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.ts @@ -14,7 +14,6 @@ import { columnExists, getColumnHit, getCommandDefinition, - getCommandMode, getCommandOption, getFunctionDefinition, getLastCharFromTrimmed, @@ -40,7 +39,6 @@ import type { AstProviderFn, ESQLAstItem, ESQLCommand, - ESQLCommandMode, ESQLCommandOption, ESQLFunction, ESQLSingleAstItem, @@ -70,7 +68,6 @@ import { buildVariablesDefinitions, buildOptionDefinition, buildSettingDefinitions, - buildSettingValueDefinitions, } from './factories'; import { EDITOR_MARKER } from '../shared/constants'; import { getAstContext, removeMarkerArgFromArgsList } from '../shared/context'; @@ -166,9 +163,12 @@ export async function suggest( const unclosedBrackets = unclosedRoundBrackets + unclosedSquaredBrackets; // if it's a comma by the user or a forced trigger by a function argument suggestion // add a marker to make the expression still valid + const charThatNeedMarkers = [',', ':']; if ( - context.triggerCharacter === ',' || - (context.triggerKind === 0 && unclosedRoundBrackets === 0) || + (context.triggerCharacter && charThatNeedMarkers.includes(context.triggerCharacter)) || + (context.triggerKind === 0 && + unclosedRoundBrackets === 0 && + getLastCharFromTrimmed(innerText) !== '_') || (context.triggerCharacter === ' ' && (isMathFunction(innerText, offset) || isComma(innerText[offset - 2]))) ) { @@ -225,18 +225,14 @@ export async function suggest( ); } if (astContext.type === 'setting') { - // need this wrap/unwrap thing to make TS happy - const { setting, ...rest } = astContext; - if (setting && isSettingItem(setting)) { - return getSettingArgsSuggestions( - innerText, - ast, - { setting, ...rest }, - getFieldsByType, - getFieldsMap, - getPolicyMetadata - ); - } + return getSettingArgsSuggestions( + innerText, + ast, + astContext, + getFieldsByType, + getFieldsMap, + getPolicyMetadata + ); } if (astContext.type === 'option') { // need this wrap/unwrap thing to make TS happy @@ -1213,10 +1209,8 @@ async function getSettingArgsSuggestions( { command, node, - setting, }: { command: ESQLCommand; - setting: ESQLCommandMode; node: ESQLSingleAstItem | undefined; }, getFieldsByType: GetFieldsByTypeFn, @@ -1224,25 +1218,15 @@ async function getSettingArgsSuggestions( getPolicyMetadata: GetPolicyMetadataFn ) { const suggestions = []; - const existingSettingArgs = new Set( - command.args - .filter((item) => isSettingItem(item) && !item.incomplete) - .map((item) => (isSettingItem(item) ? item.name : undefined)) - ); - const settingDef = - setting.name && setting.incomplete - ? getCommandMode(setting.name) - : getCommandDefinition(command.name).modes.find(({ name }) => !existingSettingArgs.has(name)); + const settingDefs = getCommandDefinition(command.name).modes; - if (settingDef) { + if (settingDefs.length) { const lastChar = getLastCharFromTrimmed(innerText); - if (lastChar === '[') { - // COMMAND [ - suggestions.push(...buildSettingDefinitions(settingDef)); - } else if (lastChar === ':') { - // COMMAND [setting: - suggestions.push(...buildSettingValueDefinitions(settingDef)); + const matchingSettingDefs = settingDefs.filter(({ prefix }) => lastChar === prefix); + if (matchingSettingDefs.length) { + // COMMAND _ + suggestions.push(...matchingSettingDefs.flatMap(buildSettingDefinitions)); } } return suggestions; 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 7fb94f38b962b..85f097a566443 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/factories.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/factories.ts @@ -217,12 +217,8 @@ export const buildOptionDefinition = ( detail: option.description, sortText: 'D', }; - if (option.wrapped) { - completeItem.insertText = `${option.wrapped[0]}${option.name} $0 ${option.wrapped[1]}`; - completeItem.insertTextRules = 4; // monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; - } - if (isAssignType) { - completeItem.insertText = `${option.name} = $0`; + if (isAssignType || option.signature.params.length) { + completeItem.insertText = isAssignType ? `${option.name} = $0` : `${option.name} $0`; completeItem.insertTextRules = 4; // monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; completeItem.command = TRIGGER_SUGGESTION_COMMAND; } @@ -233,38 +229,15 @@ export const buildSettingDefinitions = ( setting: CommandModeDefinition ): AutocompleteCommandDefinition[] => { // for now there's just a single setting with one argument - return setting.signature.params.flatMap(({ values, valueDescriptions }) => { - return values!.map((value, i) => { - const completeItem: AutocompleteCommandDefinition = { - label: `${setting.name}:${value}`, - insertText: `${setting.name}:${value}`, - kind: 21, - detail: valueDescriptions - ? `${setting.description} - ${valueDescriptions[i]}` - : setting.description, - sortText: 'D', - }; - return completeItem; - }); - }); -}; - -export const buildSettingValueDefinitions = ( - setting: CommandModeDefinition -): AutocompleteCommandDefinition[] => { - // for now there's just a single setting with one argument - return setting.signature.params.flatMap(({ values, valueDescriptions }) => { - return values!.map((value, i) => { - const completeItem: AutocompleteCommandDefinition = { - label: value, - insertText: value, - kind: 21, - detail: valueDescriptions ? valueDescriptions[i] : setting.description, - sortText: 'D', - }; - return completeItem; - }); - }); + return setting.values.map(({ name, description }) => ({ + label: `${setting.prefix || ''}${name}`, + insertText: `${setting.prefix || ''}${name}:$0`, + insertTextRules: 4, // monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + kind: 21, + detail: description ? `${setting.description} - ${description}` : setting.description, + sortText: 'D', + command: TRIGGER_SUGGESTION_COMMAND, + })); }; export const buildNoPoliciesAvailableDefinition = (): AutocompleteCommandDefinition => ({ diff --git a/packages/kbn-monaco/src/esql/lib/ast/code_actions/actions.test.ts b/packages/kbn-monaco/src/esql/lib/ast/code_actions/actions.test.ts index 4a69bb30a9806..4e82def27cb55 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/code_actions/actions.test.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/code_actions/actions.test.ts @@ -130,7 +130,7 @@ function testQuickFixesFn( }); } -type TestArgs = [string, string[], { equalityCheck?: 'include' | 'equal' }]; +type TestArgs = [string, string[], { equalityCheck?: 'include' | 'equal' }?]; // Make only and skip work with our custom wrapper const testQuickFixes = Object.assign(testQuickFixesFn, { @@ -184,9 +184,14 @@ describe('quick fixes logic', () => { ]); describe('metafields spellchecks', () => { - testQuickFixes(`FROM index [metadata _i_ndex]`, ['_index']); - testQuickFixes(`FROM index [metadata _id, _i_ndex]`, ['_index']); - testQuickFixes(`FROM index [METADATA _id, _i_ndex]`, ['_index']); + for (const isWrapped of [true, false]) { + function setWrapping(text: string) { + return isWrapped ? `[${text}]` : text; + } + testQuickFixes(`FROM index ${setWrapping('metadata _i_ndex')}`, ['_index']); + testQuickFixes(`FROM index ${setWrapping('metadata _id, _i_ndex')}`, ['_index']); + testQuickFixes(`FROM index ${setWrapping('METADATA _id, _i_ndex')}`, ['_index']); + } }); }); @@ -214,6 +219,15 @@ describe('quick fixes logic', () => { testQuickFixes(`FROM index | ENRICH poli`, ['policy']); testQuickFixes(`FROM index | ENRICH mypolicy`, ['policy']); testQuickFixes(`FROM index | ENRICH policy[`, ['policy', 'policy[]']); + + describe('modes', () => { + testQuickFixes(`FROM index | ENRICH _ann:policy`, ['_any']); + const modes = ['_any', '_coordinator', '_remote']; + for (const mode of modes) { + testQuickFixes(`FROM index | ENRICH ${mode.replace('_', '@')}:policy`, [mode]); + } + testQuickFixes(`FROM index | ENRICH unknown:policy`, modes); + }); }); describe('fixing function spellchecks', () => { @@ -281,4 +295,39 @@ describe('quick fixes logic', () => { testQuickFixes('FROM index | DROP any#Char$Field', ['`any#Char$Field`']); testQuickFixes('FROM index | DROP numberField, any#Char$Field', ['`any#Char$Field`']); }); + + describe('callbacks', () => { + it('should not crash if callback functions are not passed', async () => { + const callbackMocks = getCallbackMocks(); + const statement = `from a | eval b = a | enrich policy | dissect stringField "%{firstWord}"`; + const { model, range } = createModelAndRange(statement); + const { errors } = await validateAst(statement, getAstAndErrors, callbackMocks); + const monacoErrors = wrapAsMonacoMessage('error', statement, errors); + const context = createMonacoContext(monacoErrors); + try { + await getActions(model, range, context, getAstAndErrors, { + getFieldsFor: undefined, + getSources: undefined, + getPolicies: undefined, + getMetaFields: undefined, + }); + } catch { + fail('Should not throw'); + } + }); + + it('should not crash no callbacks are passed', async () => { + const callbackMocks = getCallbackMocks(); + const statement = `from a | eval b = a | enrich policy | dissect stringField "%{firstWord}"`; + const { model, range } = createModelAndRange(statement); + const { errors } = await validateAst(statement, getAstAndErrors, callbackMocks); + const monacoErrors = wrapAsMonacoMessage('error', statement, errors); + const context = createMonacoContext(monacoErrors); + try { + await getActions(model, range, context, getAstAndErrors, undefined); + } catch { + fail('Should not throw'); + } + }); + }); }); diff --git a/packages/kbn-monaco/src/esql/lib/ast/code_actions/actions.ts b/packages/kbn-monaco/src/esql/lib/ast/code_actions/actions.ts index 5adcd2fdde7c6..6d27682511d57 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/code_actions/actions.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/code_actions/actions.ts @@ -13,7 +13,12 @@ import { getPolicyHelper, getSourcesHelper, } from '../shared/resources_helpers'; -import { getAllFunctions, isSourceItem, shouldBeQuotedText } from '../shared/helpers'; +import { + getAllFunctions, + getCommandDefinition, + isSourceItem, + shouldBeQuotedText, +} from '../shared/helpers'; import { ESQLCallbacks } from '../shared/types'; import { AstProviderFn, ESQLAst, ESQLCommand } from '../types'; import { buildQueryForFieldsFromSource } from '../validation/helpers'; @@ -81,25 +86,13 @@ export function getMetaFieldsRetriever( export const getCompatibleFunctionDefinitions = ( command: string, - option: string | undefined, - returnTypes?: string[], - ignored: string[] = [] + option: string | undefined ): string[] => { const fnSupportedByCommand = getAllFunctions({ type: ['eval', 'agg'] }).filter( ({ name, supportedCommands, supportedOptions }) => - (option ? supportedOptions?.includes(option) : supportedCommands.includes(command)) && - !ignored.includes(name) + option ? supportedOptions?.includes(option) : supportedCommands.includes(command) ); - 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); + return fnSupportedByCommand.map(({ name }) => name); }; function createAction( @@ -291,6 +284,32 @@ async function getSpellingActionForMetadata( return wrapIntoSpellingChangeAction(error, uri, possibleMetafields); } +async function getSpellingActionForEnrichMode( + error: monaco.editor.IMarkerData, + uri: monaco.Uri, + queryString: string, + ast: ESQLAst, + _callbacks: Callbacks +) { + const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1); + const commandContext = + ast.find((command) => command.location.max > error.endColumn) || ast[ast.length - 1]; + if (!commandContext) { + return []; + } + const commandDef = getCommandDefinition(commandContext.name); + const allModes = + commandDef.modes?.flatMap(({ values, prefix }) => + values.map(({ name }) => `${prefix || ''}${name}`) + ) || []; + const possibleEnrichModes = await getSpellingPossibilities(async () => allModes, errorText); + // if no possible solution is found, push all modes + if (!possibleEnrichModes.length) { + possibleEnrichModes.push(...allModes); + } + return wrapIntoSpellingChangeAction(error, uri, possibleEnrichModes); +} + function wrapIntoSpellingChangeAction( error: monaco.editor.IMarkerData, uri: monaco.Uri, @@ -414,6 +433,16 @@ export async function getActions( ) ); break; + case 'unsupportedSettingCommandValue': + const enrichModeSpellChanges = await getSpellingActionForEnrichMode( + error, + model.uri, + innerText, + ast, + callbacks + ); + actions.push(...enrichModeSpellChanges); + break; default: break; } 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 841d6728fc5ed..c8357574e7fcc 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/builtin.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/builtin.ts @@ -13,13 +13,13 @@ function createMathDefinition( name: string, types: Array, description: string, - warning?: FunctionDefinition['warning'] + validate?: FunctionDefinition['validate'] ): FunctionDefinition { return { type: 'builtin', name, description, - supportedCommands: ['eval', 'where', 'row'], + supportedCommands: ['eval', 'where', 'row', 'stats'], supportedOptions: ['by'], signatures: types.map((type) => { if (Array.isArray(type)) { @@ -39,7 +39,7 @@ function createMathDefinition( returnType: type, }; }), - warning, + validate, }; } @@ -51,7 +51,7 @@ function createComparisonDefinition( name: string; description: string; }, - warning?: FunctionDefinition['warning'] + validate?: FunctionDefinition['validate'] ): FunctionDefinition { return { type: 'builtin' as const, @@ -59,6 +59,7 @@ function createComparisonDefinition( description, supportedCommands: ['eval', 'where', 'row'], supportedOptions: ['by'], + validate, signatures: [ { params: [ 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 e405519d79ae9..35e9649f7f189 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/commands.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/commands.ts @@ -7,9 +7,15 @@ */ import { i18n } from '@kbn/i18n'; -import { isColumnItem, isSettingItem } from '../shared/helpers'; -import { ESQLColumn, ESQLCommand, ESQLCommandMode, ESQLMessage } from '../types'; -import { ccqMode } from './settings'; +import { + getFunctionDefinition, + isAssignment, + isAssignmentComplete, + isColumnItem, + isFunctionItem, +} from '../shared/helpers'; +import type { ESQLColumn, ESQLCommand, ESQLAstItem, ESQLMessage } from '../types'; +import { enrichModes } from './settings'; import { appendSeparatorOption, asOption, @@ -88,6 +94,64 @@ export const commandDefinitions: CommandDefinition[] = [ code: 'statsNoArguments', }); } + + // now that all functions are supported, there's a specific check to perform + // unfortunately the logic here is a bit complex as it needs to dig deeper into the args + // until an agg function is detected + // in the long run this might be integrated into the validation function + const fnArg = command.args.filter(isFunctionItem); + if (fnArg.length) { + function isAggFunction(arg: ESQLAstItem) { + return isFunctionItem(arg) && getFunctionDefinition(arg.name)?.type === 'agg'; + } + function isOtherFunction(arg: ESQLAstItem) { + return isFunctionItem(arg) && getFunctionDefinition(arg.name)?.type !== 'agg'; + } + function isOtherFunctionWithAggInside(arg: ESQLAstItem) { + return ( + isFunctionItem(arg) && + isOtherFunction(arg) && + arg.args.filter(isFunctionItem).some( + // this is recursive as builtin fns can be wrapped one withins another + (subArg): boolean => + isAggFunction(subArg) || + (isOtherFunction(subArg) ? isOtherFunctionWithAggInside(subArg) : false) + ) + ); + } + // which is the presence of at least one agg type function at root level + const hasAggFunction = fnArg.some(isAggFunction); + // or as builtin function arg with an agg function as sub arg + const hasAggFunctionWithinBuiltin = fnArg + .filter((arg) => !isAssignment(arg)) + .some(isOtherFunctionWithAggInside); + + // assignment requires a special handling + const hasAggFunctionWithinAssignment = fnArg + .filter((arg) => isAssignment(arg) && isAssignmentComplete(arg)) + // extract the right hand side of the assignments + .flatMap((arg) => arg.args[1]) + .filter(isFunctionItem) + // now check that they are either agg functions + // or builtin functions with an agg function as sub arg + .some((arg) => isAggFunction(arg) || isOtherFunctionWithAggInside(arg)); + + if (!hasAggFunction && !hasAggFunctionWithinBuiltin && !hasAggFunctionWithinAssignment) { + messages.push({ + location: command.location, + text: i18n.translate('monaco.esql.validation.noNestedArgumentSupport', { + defaultMessage: + "Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [{name}] of type [{argType}]", + values: { + name: fnArg[0].name, + argType: getFunctionDefinition(fnArg[0].name)?.signatures[0].returnType, + }, + }), + type: 'error', + code: 'noNestedArgumentSupport', + }); + } + } return messages; }, }, @@ -149,22 +213,6 @@ export const commandDefinitions: CommandDefinition[] = [ multipleParams: true, params: [{ name: 'column', type: 'column', wildcards: true }], }, - validate: (command: ESQLCommand) => { - // the command name is automatically converted into KEEP by the ast_walker - // so validate the actual text - const messages: ESQLMessage[] = []; - if (/^project/.test(command.text.toLowerCase())) { - messages.push({ - location: command.location, - text: i18n.translate('monaco.esql.validation.projectCommandDeprecated', { - defaultMessage: 'PROJECT command is no longer supported, please use KEEP instead', - }), - type: 'warning', - code: 'projectCommandDeprecated', - }); - } - return messages; - }, }, { name: 'drop', @@ -304,41 +352,10 @@ export const commandDefinitions: CommandDefinition[] = [ '… | enrich my-policy on pivotField with a = enrichFieldA, b = enrichFieldB', ], options: [onOption, withOption], - modes: [ccqMode], + modes: [enrichModes], signature: { multipleParams: false, params: [{ name: 'policyName', type: 'source', innerType: 'policy' }], }, - validate: (command: ESQLCommand) => { - const messages: ESQLMessage[] = []; - if (command.args.some(isSettingItem)) { - const settings = command.args.filter(isSettingItem); - const settingCounters: Record = {}; - const settingLookup: Record = {}; - for (const setting of settings) { - if (!settingCounters[setting.name]) { - settingCounters[setting.name] = 0; - settingLookup[setting.name] = setting; - } - settingCounters[setting.name]++; - } - const duplicateSettings = Object.entries(settingCounters).filter(([_, count]) => count > 1); - messages.push( - ...duplicateSettings.map(([name]) => ({ - location: settingLookup[name].location, - text: i18n.translate('monaco.esql.validation.duplicateSettingWarning', { - defaultMessage: - 'Multiple definition of setting [{name}]. Only last one will be applied.', - values: { - name, - }, - }), - type: 'warning' as const, - code: 'duplicateSettingWarning', - })) - ); - } - return messages; - }, }, ]; 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 25cc3ce27d77c..64382fa831a64 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/functions.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/functions.ts @@ -7,8 +7,32 @@ */ import { i18n } from '@kbn/i18n'; +import { isLiteralItem } from '../shared/helpers'; +import { ESQLFunction } from '../types'; import { FunctionDefinition } from './types'; +const validateLogFunctions = (fnDef: ESQLFunction) => { + const messages = []; + // do not really care here about the base and field + // just need to check both values are not negative + for (const arg of fnDef.args) { + if (isLiteralItem(arg) && arg.value < 0) { + messages.push({ + type: 'warning' as const, + code: 'logOfNegativeValue', + text: i18n.translate('monaco.esql.divide.warning.logOfNegativeValue', { + defaultMessage: 'Log of a negative number results in null: {value}', + values: { + value: arg.value, + }, + }), + location: arg.location, + }); + } + } + return messages; +}; + export const evalFunctionsDefinitions: FunctionDefinition[] = [ { name: 'round', @@ -68,6 +92,29 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [ examples: [`from index | eval log10_value = log10(field)`], }, ], + validate: validateLogFunctions, + }, + + { + name: 'log', + description: i18n.translate('monaco.esql.definitions.logDoc', { + defaultMessage: + 'A scalar function log(based, value) returns the logarithm of a value for a particular base, as specified in the argument', + }), + signatures: [ + { + params: [ + { name: 'baseOrField', type: 'number' }, + { name: 'field', type: 'number', optional: true }, + ], + returnType: 'number', + examples: [ + `from index | eval log2_value = log(2, field)`, + `from index | eval loge_value = log(field)`, + ], + }, + ], + validate: validateLogFunctions, }, { name: 'pow', @@ -1050,7 +1097,7 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [ .sort(({ name: a }, { name: b }) => a.localeCompare(b)) .map((def) => ({ ...def, - supportedCommands: ['eval', 'where', 'row'], + supportedCommands: ['stats', '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 efcfbb9c60243..cac6978970ce0 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/options.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/options.ts @@ -33,10 +33,20 @@ export const metadataOption: CommandOptionsDefinition = { params: [{ name: 'column', type: 'column' }], }, optional: true, - wrapped: ['[', ']'], skipCommonValidation: true, validate: (option, command, references) => { const messages: ESQLMessage[] = []; + // need to test the parent command here + if (/\[metadata/i.test(command.text)) { + messages.push({ + location: option.location, + text: i18n.translate('monaco.esql.validation.metadataBracketsDeprecation', { + defaultMessage: "Square brackets '[]' need to be removed from FROM METADATA declaration", + }), + type: 'warning', + code: 'metadataBracketsDeprecation', + }); + } const fields = option.args.filter(isColumnItem); const metadataFieldsAvailable = references as unknown as Set; if (metadataFieldsAvailable.size > 0) { diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/settings.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/settings.ts index 5dad48913c408..4c76a87d9283c 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/settings.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/settings.ts @@ -9,30 +9,30 @@ import { i18n } from '@kbn/i18n'; import { CommandModeDefinition } from './types'; -export const ccqMode: CommandModeDefinition = { +export const enrichModes: CommandModeDefinition = { name: 'ccq.mode', description: i18n.translate('monaco.esql.definitions.ccqModeDoc', { defaultMessage: 'Cross-clusters query mode', }), - signature: { - multipleParams: false, - params: [ - { - name: 'mode', - type: 'string', - values: ['any', 'coordinator', 'remote'], - valueDescriptions: [ - i18n.translate('monaco.esql.definitions.ccqAnyDoc', { - defaultMessage: 'Enrich takes place on any cluster', - }), - i18n.translate('monaco.esql.definitions.ccqCoordinatorDoc', { - defaultMessage: 'Enrich takes place on the coordinating cluster receiving an ES|QL', - }), - i18n.translate('monaco.esql.definitions.ccqRemoteDoc', { - defaultMessage: 'Enrich takes place on the cluster hosting the target index.', - }), - ], - }, - ], - }, + prefix: '_', + values: [ + { + name: 'any', + description: i18n.translate('monaco.esql.definitions.ccqAnyDoc', { + defaultMessage: 'Enrich takes place on any cluster', + }), + }, + { + name: 'coordinator', + description: i18n.translate('monaco.esql.definitions.ccqCoordinatorDoc', { + defaultMessage: 'Enrich takes place on the coordinating cluster receiving an ES|QL', + }), + }, + { + name: 'remote', + description: i18n.translate('monaco.esql.definitions.ccqRemoteDoc', { + defaultMessage: 'Enrich takes place on the cluster hosting the target index.', + }), + }, + ], }; 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 f77afa8731bb7..0c086cfe5e679 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/types.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/types.ts @@ -29,7 +29,7 @@ export interface FunctionDefinition { returnType: string; examples?: string[]; }>; - warning?: (fnDef: ESQLFunction) => ESQLMessage[]; + validate?: (fnDef: ESQLFunction) => ESQLMessage[]; } export interface CommandBaseDefinition { @@ -64,9 +64,11 @@ export interface CommandOptionsDefinition extends CommandBaseDefinition { ) => ESQLMessage[]; } -export interface CommandModeDefinition extends CommandBaseDefinition { +export interface CommandModeDefinition { name: string; description: string; + values: Array<{ name: string; description: string }>; + prefix?: string; } export interface CommandDefinition extends CommandBaseDefinition { diff --git a/packages/kbn-monaco/src/esql/lib/ast/hover/hover.test.ts b/packages/kbn-monaco/src/esql/lib/ast/hover/hover.test.ts index 9acc9039ec81f..960adfeba0a25 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/hover/hover.test.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/hover/hover.test.ts @@ -14,6 +14,7 @@ import { AstListener } from '../ast_factory'; import { getHoverItem } from './hover'; import { getFunctionDefinition } from '../shared/helpers'; import { getFunctionSignatures } from '../definitions/helpers'; +import { enrichModes } from '../definitions/settings'; const fields: Array<{ name: string; type: string; suggestedAs?: string }> = [ ...['string', 'number', 'date', 'boolean', 'ip'].map((type) => ({ @@ -187,6 +188,16 @@ describe('hover', () => { testSuggestions(`from a | enrich policy`, 'policy', createPolicyContent); testSuggestions(`from a | enrich policy on b `, 'policy', createPolicyContent); testSuggestions(`from a | enrich policy on b `, 'non-policy', createPolicyContent); + + describe('ccq mode', () => { + for (const mode of enrichModes.values) { + testSuggestions( + `from a | enrich ${enrichModes.prefix || ''}${mode.name}:policy`, + `${enrichModes.prefix || ''}${mode.name}`, + () => [enrichModes.description, `**${mode.name}**: ${mode.description}`] + ); + } + }); }); describe('functions', () => { function createFunctionContent(fn: string) { diff --git a/packages/kbn-monaco/src/esql/lib/ast/hover/hover.ts b/packages/kbn-monaco/src/esql/lib/ast/hover/hover.ts index c612571e13ce7..bcc2f8c76ec8a 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/hover/hover.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/hover/hover.ts @@ -10,7 +10,13 @@ import { i18n } from '@kbn/i18n'; import type { monaco } from '../../../../monaco_imports'; import { getFunctionSignatures } from '../definitions/helpers'; import { getAstContext } from '../shared/context'; -import { monacoPositionToOffset, getFunctionDefinition, isSourceItem } from '../shared/helpers'; +import { + monacoPositionToOffset, + getFunctionDefinition, + isSourceItem, + isSettingItem, + getCommandDefinition, +} from '../shared/helpers'; import { getPolicyHelper } from '../shared/resources_helpers'; import { ESQLCallbacks } from '../shared/types'; import type { AstProviderFn } from '../types'; @@ -47,32 +53,47 @@ export async function getHoverItem( } if (astContext.type === 'expression') { - if ( - astContext.node && - isSourceItem(astContext.node) && - astContext.node.sourceType === 'policy' - ) { - const policyMetadata = await getPolicyMetadata(astContext.node.name); - if (policyMetadata) { - return { - contents: [ - { - value: `${i18n.translate('monaco.esql.hover.policyIndexes', { - defaultMessage: '**Indexes**', - })}: ${policyMetadata.sourceIndices.join(', ')}`, - }, - { - value: `${i18n.translate('monaco.esql.hover.policyMatchingField', { - defaultMessage: '**Matching field**', - })}: ${policyMetadata.matchField}`, - }, - { - value: `${i18n.translate('monaco.esql.hover.policyEnrichedFields', { - defaultMessage: '**Fields**', - })}: ${policyMetadata.enrichFields.join(', ')}`, - }, - ], - }; + if (astContext.node) { + if (isSourceItem(astContext.node) && astContext.node.sourceType === 'policy') { + const policyMetadata = await getPolicyMetadata(astContext.node.name); + if (policyMetadata) { + return { + contents: [ + { + value: `${i18n.translate('monaco.esql.hover.policyIndexes', { + defaultMessage: '**Indexes**', + })}: ${policyMetadata.sourceIndices.join(', ')}`, + }, + { + value: `${i18n.translate('monaco.esql.hover.policyMatchingField', { + defaultMessage: '**Matching field**', + })}: ${policyMetadata.matchField}`, + }, + { + value: `${i18n.translate('monaco.esql.hover.policyEnrichedFields', { + defaultMessage: '**Fields**', + })}: ${policyMetadata.enrichFields.join(', ')}`, + }, + ], + }; + } + } + if (isSettingItem(astContext.node)) { + const commandDef = getCommandDefinition(astContext.command.name); + const settingDef = commandDef?.modes.find(({ values }) => + values.some(({ name }) => name === astContext.node!.name) + ); + if (settingDef) { + const mode = settingDef.values.find(({ name }) => name === astContext.node!.name)!; + return { + contents: [ + { value: settingDef.description }, + { + value: `**${mode.name}**: ${mode.description}`, + }, + ], + }; + } } } } 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 aecf9340b945c..d81c65184c8c5 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/shared/context.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/shared/context.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { enrichModes } from '../definitions/settings'; import type { ESQLAstItem, ESQLSingleAstItem, @@ -152,8 +153,9 @@ export function getAstContext(innerText: string, ast: ESQLAst, offset: number) { // command ... by return { type: 'option' as const, command, node, option, setting }; } - if (node.type === 'mode' || option) { - // command [ + // for now it's only an enrich thing + if (node.type === 'source' && node.text === enrichModes.prefix) { + // command _ return { type: 'setting' as const, command, node, option, setting }; } } @@ -184,9 +186,6 @@ export function getAstContext(innerText: string, ast: ESQLAst, offset: number) { if (option) { return { type: 'option' as const, command, node, option, setting }; } - if (setting?.incomplete) { - return { type: 'setting' as const, command, node, option, setting }; - } } // command a ... OR command a = ... 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 f4c73a3f95776..ec51d4ec66c90 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/shared/helpers.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/shared/helpers.ts @@ -22,10 +22,8 @@ import { withOption, appendSeparatorOption, } from '../definitions/options'; -import { ccqMode } from '../definitions/settings'; import { CommandDefinition, - CommandModeDefinition, CommandOptionsDefinition, FunctionDefinition, SignatureArgType, @@ -219,10 +217,6 @@ export function getCommandOption(optionName: CommandOptionsDefinition['name']) { ); } -export function getCommandMode(settingName: CommandModeDefinition['name']) { - return [ccqMode].find(({ name }) => name === settingName); -} - function compareLiteralType(argTypes: string, item: ESQLLiteral) { if (item.literalType !== 'string') { return argTypes === item.literalType; diff --git a/packages/kbn-monaco/src/esql/lib/ast/types.ts b/packages/kbn-monaco/src/esql/lib/ast/types.ts index da5bff7551cad..32057a9ee3b65 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/types.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/types.ts @@ -46,7 +46,6 @@ export interface ESQLCommandOption extends ESQLAstBaseItem { export interface ESQLCommandMode extends ESQLAstBaseItem { type: 'mode'; - args: ESQLAstItem[]; } export interface ESQLFunction extends ESQLAstBaseItem { 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 f04d3d0eb9b22..0749c16496c94 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/validation/errors.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/validation/errors.ts @@ -59,8 +59,13 @@ function getMessageAndTypeFromId({ return { message: i18n.translate('monaco.esql.validation.wrongArgumentNumber', { defaultMessage: - 'Error building [{fn}]: expects exactly {numArgs, plural, one {one argument} other {{numArgs} arguments}}, passed {passedArgs} instead.', - values: { fn: out.fn, numArgs: out.numArgs, passedArgs: out.passedArgs }, + 'Error building [{fn}]: expects {canHaveMoreArgs, plural, =0 {exactly } other {}}{numArgs, plural, one {one argument} other {{numArgs} arguments}}, passed {passedArgs} instead.', + values: { + fn: out.fn, + numArgs: out.numArgs, + passedArgs: out.passedArgs, + canHaveMoreArgs: out.exactly, + }, }), }; case 'noNestedArgumentSupport': @@ -209,9 +214,8 @@ function getMessageAndTypeFromId({ return { message: i18n.translate('monaco.esql.validation.unsupportedSettingValue', { defaultMessage: - 'Unrecognized value [{value}], {command} [{setting}] needs to be one of [{expected}]', + 'Unrecognized value [{value}] for {command}, mode needs to be one of [{expected}]', values: { - setting: out.setting, expected: out.expected, value: out.value, command: out.command, diff --git a/packages/kbn-monaco/src/esql/lib/ast/validation/resources.ts b/packages/kbn-monaco/src/esql/lib/ast/validation/resources.ts index 76806b5385513..85133e81fd4e9 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/validation/resources.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/validation/resources.ts @@ -101,7 +101,7 @@ export async function retrieveFieldsFromStringSources( commands: ESQLCommand[], callbacks?: ESQLCallbacks ): Promise> { - if (!callbacks) { + if (!callbacks || !callbacks?.getMetaFields) { return new Map(); } const customQuery = buildQueryForFieldsForStringSources(queryString, commands); diff --git a/packages/kbn-monaco/src/esql/lib/ast/validation/types.ts b/packages/kbn-monaco/src/esql/lib/ast/validation/types.ts index 04b40a5bfbbac..d632d3a31d085 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/validation/types.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/validation/types.ts @@ -47,7 +47,7 @@ export interface ValidationErrors { }; wrongArgumentNumber: { message: string; - type: { fn: string; numArgs: number; passedArgs: number }; + type: { fn: string; numArgs: number; passedArgs: number; exactly: number }; }; unknownColumn: { message: string; @@ -123,7 +123,7 @@ export interface ValidationErrors { }; unsupportedSettingCommandValue: { message: string; - type: { command: string; setting: string; value: string; expected: string }; + type: { command: string; value: string; expected: string }; }; } diff --git a/packages/kbn-monaco/src/esql/lib/ast/validation/validation.test.ts b/packages/kbn-monaco/src/esql/lib/ast/validation/validation.test.ts index c1ea7d1bdec8c..7c339a8ab9fb8 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/validation/validation.test.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/validation/validation.test.ts @@ -27,21 +27,26 @@ const fieldTypes = ['number', 'date', 'boolean', 'ip', 'string', 'cartesian_poin 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' }] - : [ - ...fieldTypes.map((type) => ({ name: `${camelCase(type)}Field`, type })), - { name: 'any#Char$Field', type: 'number' }, - { name: 'kubernetes.something.something', type: 'number' }, - { name: '@timestamp', type: 'date' }, - ] - ), + getFieldsFor: jest.fn(async ({ query }) => { + if (/enrich/.test(query)) { + return [ + { name: 'otherField', type: 'string' }, + { name: 'yetAnotherField', type: 'number' }, + ]; + } + if (/unsupported_index/.test(query)) { + return [{ name: 'unsupported_field', type: 'unsupported' }]; + } + if (/dissect|grok/.test(query)) { + return [{ name: 'firstWord', type: 'string' }]; + } + return [ + ...fieldTypes.map((type) => ({ name: `${camelCase(type)}Field`, type })), + { name: 'any#Char$Field', type: 'number' }, + { name: 'kubernetes.something.something', type: 'number' }, + { name: '@timestamp', type: 'date' }, + ]; + }), getSources: jest.fn(async () => ['a', 'index', 'otherIndex', '.secretIndex', 'my-index', 'unsupported_index'].map((name) => ({ name, @@ -250,7 +255,7 @@ describe('validation logic', () => { "SyntaxError: missing {QUOTED_IDENTIFIER, FROM_UNQUOTED_IDENTIFIER} at ''", ]); testErrorsAndWarnings(`from assignment = 1`, [ - 'SyntaxError: expected {, PIPE, COMMA, OPENING_BRACKET} but found "="', + 'SyntaxError: expected {, PIPE, COMMA, OPENING_BRACKET, METADATA} but found "="', 'Unknown index [assignment]', ]); testErrorsAndWarnings(`from index`, []); @@ -262,21 +267,53 @@ describe('validation logic', () => { testErrorsAndWarnings(`from index, missingIndex`, ['Unknown index [missingIndex]']); testErrorsAndWarnings(`from fn()`, ['Unknown index [fn()]']); testErrorsAndWarnings(`from average()`, ['Unknown index [average()]']); - testErrorsAndWarnings(`from index [METADATA _id]`, []); - testErrorsAndWarnings(`from index [metadata _id]`, []); + for (const isWrapped of [true, false]) { + function setWrapping(option: string) { + return isWrapped ? `[${option}]` : option; + } + function addBracketsWarning() { + return isWrapped + ? ["Square brackets '[]' need to be removed from FROM METADATA declaration"] + : []; + } + testErrorsAndWarnings(`from index ${setWrapping('METADATA _id')}`, [], addBracketsWarning()); + testErrorsAndWarnings(`from index ${setWrapping('metadata _id')}`, [], addBracketsWarning()); - testErrorsAndWarnings(`from index [METADATA _id, _source]`, []); - testErrorsAndWarnings(`from index [METADATA _id, _source2]`, [ - 'Metadata field [_source2] is not available. Available metadata fields are: [_id, _source]', - ]); - testErrorsAndWarnings(`from index [metadata _id, _source] [METADATA _id2]`, [ - 'SyntaxError: expected {, PIPE} but found "["', - ]); - testErrorsAndWarnings(`from index metadata _id`, [ - 'SyntaxError: expected {, PIPE, COMMA, OPENING_BRACKET} but found "metadata"', - ]); + testErrorsAndWarnings( + `from index ${setWrapping('METADATA _id, _source')}`, + [], + addBracketsWarning() + ); + testErrorsAndWarnings( + `from index ${setWrapping('METADATA _id, _source2')}`, + [ + 'Metadata field [_source2] is not available. Available metadata fields are: [_id, _source]', + ], + addBracketsWarning() + ); + testErrorsAndWarnings( + `from index ${setWrapping('metadata _id, _source')} ${setWrapping('METADATA _id2')}`, + [ + isWrapped + ? 'SyntaxError: expected {COMMA, CLOSING_BRACKET} but found "["' + : 'SyntaxError: expected {, PIPE, COMMA} but found "METADATA"', + ], + addBracketsWarning() + ); + + testErrorsAndWarnings( + `from remote-ccs:indexes ${setWrapping('METADATA _id')}`, + [], + addBracketsWarning() + ); + testErrorsAndWarnings( + `from *:indexes ${setWrapping('METADATA _id')}`, + [], + addBracketsWarning() + ); + } testErrorsAndWarnings(`from index (metadata _id)`, [ - 'SyntaxError: expected {, PIPE, COMMA, OPENING_BRACKET} but found "(metadata"', + 'SyntaxError: expected {, PIPE, COMMA, OPENING_BRACKET, METADATA} but found "(metadata"', ]); testErrorsAndWarnings(`from ind*, other*`, []); testErrorsAndWarnings(`from index*`, []); @@ -289,8 +326,6 @@ describe('validation logic', () => { testErrorsAndWarnings(`from remote-*:indexes`, []); testErrorsAndWarnings(`from remote-ccs:indexes`, []); testErrorsAndWarnings(`from a, remote-ccs:indexes`, []); - testErrorsAndWarnings(`from remote-ccs:indexes [METADATA _id]`, []); - testErrorsAndWarnings(`from *:indexes [METADATA _id]`, []); testErrorsAndWarnings('from .secretIndex', []); testErrorsAndWarnings('from my-index', []); testErrorsAndWarnings('from numberField', ['Unknown index [numberField]']); @@ -374,7 +409,7 @@ describe('validation logic', () => { ); testErrorsAndWarnings(`row var = ${signatureStringCorrect}`, []); - testErrorsAndWarnings(`row ${signatureStringCorrect}`); + testErrorsAndWarnings(`row ${signatureStringCorrect}`, []); if (alias) { for (const otherName of alias) { @@ -412,7 +447,7 @@ describe('validation logic', () => { )[0].declaration ); - testErrorsAndWarnings(`row var = ${signatureString}`); + testErrorsAndWarnings(`row var = ${signatureString}`, []); const wrongFieldMapping = params.map(({ name: _name, type, ...rest }) => { const typeString = type; @@ -580,26 +615,18 @@ describe('validation logic', () => { 'Unknown column [missingField]', ]); testErrorsAndWarnings('from index | keep `any#Char$Field`', []); - testErrorsAndWarnings( - 'from index | project ', - [`SyntaxError: missing {QUOTED_IDENTIFIER, UNQUOTED_ID_PATTERN} at ''`], - ['PROJECT command is no longer supported, please use KEEP instead'] - ); - testErrorsAndWarnings( - 'from index | project stringField, numberField, dateField', - [], - ['PROJECT command is no longer supported, please use KEEP instead'] - ); - testErrorsAndWarnings( - 'from index | PROJECT stringField, numberField, dateField', - [], - ['PROJECT command is no longer supported, please use KEEP instead'] - ); - testErrorsAndWarnings( - 'from index | project missingField, numberField, dateField', - ['Unknown column [missingField]'], - ['PROJECT command is no longer supported, please use KEEP instead'] - ); + testErrorsAndWarnings('from index | project ', [ + `SyntaxError: expected {DISSECT, DROP, ENRICH, EVAL, GROK, INLINESTATS, KEEP, LIMIT, MV_EXPAND, RENAME, SORT, STATS, WHERE} but found \"project\"`, + ]); + testErrorsAndWarnings('from index | project stringField, numberField, dateField', [ + `SyntaxError: expected {DISSECT, DROP, ENRICH, EVAL, GROK, INLINESTATS, KEEP, LIMIT, MV_EXPAND, RENAME, SORT, STATS, WHERE} but found \"project\"`, + ]); + testErrorsAndWarnings('from index | PROJECT stringField, numberField, dateField', [ + `SyntaxError: expected {DISSECT, DROP, ENRICH, EVAL, GROK, INLINESTATS, KEEP, LIMIT, MV_EXPAND, RENAME, SORT, STATS, WHERE} but found \"PROJECT\"`, + ]); + testErrorsAndWarnings('from index | project missingField, numberField, dateField', [ + `SyntaxError: expected {DISSECT, DROP, ENRICH, EVAL, GROK, INLINESTATS, KEEP, LIMIT, MV_EXPAND, RENAME, SORT, STATS, WHERE} but found \"project\"`, + ]); testErrorsAndWarnings('from index | keep s*', []); testErrorsAndWarnings('from index | keep *Field', []); testErrorsAndWarnings('from index | keep s*Field', []); @@ -736,27 +763,28 @@ describe('validation logic', () => { "SyntaxError: missing STRING at '%'", ]); // Do not try to validate the dissect pattern string - testErrorsAndWarnings('from a | dissect stringField "%{a}"', []); - testErrorsAndWarnings('from a | dissect numberField "%{a}"', [ + testErrorsAndWarnings('from a | dissect stringField "%{firstWord}"', []); + testErrorsAndWarnings('from a | dissect numberField "%{firstWord}"', [ 'DISSECT only supports string type values, found [numberField] of type number', ]); - testErrorsAndWarnings('from a | dissect stringField "%{a}" option ', [ + testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" option ', [ 'SyntaxError: expected {ASSIGN} but found ""', ]); - testErrorsAndWarnings('from a | dissect stringField "%{a}" option = ', [ + testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" option = ', [ 'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET} but found ""', 'Invalid option for DISSECT: [option]', ]); - testErrorsAndWarnings('from a | dissect stringField "%{a}" option = 1', [ + testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" option = 1', [ 'Invalid option for DISSECT: [option]', ]); - testErrorsAndWarnings('from a | dissect stringField "%{a}" append_separator = "-"', []); - testErrorsAndWarnings('from a | dissect stringField "%{a}" ignore_missing = true', [ + testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" append_separator = "-"', []); + testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" ignore_missing = true', [ 'Invalid option for DISSECT: [ignore_missing]', ]); - testErrorsAndWarnings('from a | dissect stringField "%{a}" append_separator = true', [ + testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" append_separator = true', [ 'Invalid value for DISSECT append_separator: expected a string, but was [true]', ]); + testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" | keep firstWord', []); // testErrorsAndWarnings('from a | dissect s* "%{a}"', [ // 'Using wildcards (*) in dissect is not allowed [s*]', // ]); @@ -776,10 +804,11 @@ describe('validation logic', () => { ]); testErrorsAndWarnings('from a | grok stringField %a', ["SyntaxError: missing STRING at '%'"]); // Do not try to validate the grok pattern string - testErrorsAndWarnings('from a | grok stringField "%{a}"', []); - testErrorsAndWarnings('from a | grok numberField "%{a}"', [ + testErrorsAndWarnings('from a | grok stringField "%{firstWord}"', []); + testErrorsAndWarnings('from a | grok numberField "%{firstWord}"', [ 'GROK only supports string type values, found [numberField] of type number', ]); + testErrorsAndWarnings('from a | grok stringField "%{firstWord}" | keep firstWord', []); // testErrorsAndWarnings('from a | grok s* "%{a}"', [ // 'Using wildcards (*) in grok is not allowed [s*]', // ]); @@ -1047,7 +1076,7 @@ describe('validation logic', () => { } for (const { name, alias, signatures, ...defRest } of evalFunctionsDefinitions) { - for (const { params, returnType } of signatures) { + for (const { params, returnType, infiniteParams, minParams } of signatures) { const fieldMapping = getFieldMapping(params); testErrorsAndWarnings( `from a | eval var = ${ @@ -1128,6 +1157,40 @@ describe('validation logic', () => { }`, expectedErrors ); + + if (!infiniteParams && !minParams) { + // test that additional args are spotted + const fieldMappingWithOneExtraArg = getFieldMapping(params).concat({ + name: 'extraArg', + type: 'number', + }); + // get the expected args from the first signature in case of errors + const expectedArgs = signatures[0].params.filter(({ optional }) => !optional).length; + const shouldBeExactly = signatures[0].params.length; + testErrorsAndWarnings( + `from a | eval ${ + getFunctionSignatures( + { + name, + ...defRest, + signatures: [{ params: fieldMappingWithOneExtraArg, returnType }], + }, + { withTypes: false } + )[0].declaration + }`, + [ + `Error building [${name}]: expects ${ + shouldBeExactly - expectedArgs === 0 ? 'exactly ' : '' + }${ + expectedArgs === 1 + ? 'one argument' + : expectedArgs === 0 + ? '0 arguments' + : `${expectedArgs} arguments` + }, passed ${fieldMappingWithOneExtraArg.length} instead.`, + ] + ); + } } // test that wildcard won't work as arg @@ -1151,6 +1214,37 @@ describe('validation logic', () => { } } } + testErrorsAndWarnings( + 'from a | eval log10(-1)', + [], + ['Log of a negative number results in null: -1'] + ); + testErrorsAndWarnings( + 'from a | eval log(-1)', + [], + ['Log of a negative number results in null: -1'] + ); + testErrorsAndWarnings( + 'from a | eval log(-1, 20)', + [], + ['Log of a negative number results in null: -1'] + ); + testErrorsAndWarnings( + 'from a | eval log(-1, -20)', + [], + [ + 'Log of a negative number results in null: -1', + 'Log of a negative number results in null: -20', + ] + ); + testErrorsAndWarnings( + 'from a | eval var0 = log(-1, -20)', + [], + [ + 'Log of a negative number results in null: -1', + 'Log of a negative number results in null: -20', + ] + ); for (const op of ['>', '>=', '<', '<=', '==']) { testErrorsAndWarnings(`from a | eval numberField ${op} 0`, []); testErrorsAndWarnings(`from a | eval NOT numberField ${op} 0`, []); @@ -1304,9 +1398,11 @@ describe('validation logic', () => { ]); testErrorsAndWarnings('from a | stats numberField=', [ 'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, LP, NOT, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER} but found ""', + "Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [=] of type [void]", ]); testErrorsAndWarnings('from a | stats numberField=5 by ', [ 'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, LP, NOT, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER} but found ""', + "Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [=] of type [void]", ]); testErrorsAndWarnings('from a | stats avg(numberField) by wrongField', [ 'Unknown column [wrongField]', @@ -1347,6 +1443,12 @@ describe('validation logic', () => { 'from a | stats avg(numberField), percentile(numberField, 50) BY ipField', [] ); + for (const op of ['+', '-', '*', '/', '%']) { + testErrorsAndWarnings( + `from a | stats avg(numberField) ${op} percentile(numberField, 50) BY ipField`, + [] + ); + } testErrorsAndWarnings('from a | stats count(* + 1) BY ipField', [ 'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, LP, NOT, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER} but found "+"', ]); @@ -1359,15 +1461,33 @@ describe('validation logic', () => { testErrorsAndWarnings('from a | stats count(count(*)) BY ipField', [ `Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [number]`, ]); - testErrorsAndWarnings('from a | stats numberField + 1', ['STATS does not support function +']); + testErrorsAndWarnings('from a | stats numberField + 1', [ + `Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [+] of type [number]`, + ]); + + for (const nesting of [1, 2, 3, 4]) { + const moreBuiltinWrapping = Array(nesting).fill('+ 1').join(''); + testErrorsAndWarnings(`from a | stats 5 + avg(numberField) ${moreBuiltinWrapping}`, []); + testErrorsAndWarnings(`from a | stats 5 ${moreBuiltinWrapping} + avg(numberField)`, []); + testErrorsAndWarnings(`from a | stats 5 ${moreBuiltinWrapping} + numberField`, [ + "Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [+] of type [number]", + ]); + testErrorsAndWarnings(`from a | stats 5 + numberField ${moreBuiltinWrapping}`, [ + "Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [+] of type [number]", + ]); + } + + testErrorsAndWarnings('from a | stats 5 + numberField + 1', [ + "Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [+] of type [number]", + ]); testErrorsAndWarnings('from a | stats numberField + 1 by ipField', [ - 'STATS does not support function +', + `Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [+] of type [number]`, ]); testErrorsAndWarnings( 'from a | stats avg(numberField), percentile(numberField, 50) + 1 by ipField', - ['STATS does not support function +'] + [] ); testErrorsAndWarnings('from a | stats count(*)', []); @@ -1400,6 +1520,52 @@ describe('validation logic', () => { }`, [] ); + testErrorsAndWarnings( + `from a | stats var = round(${ + getFunctionSignatures( + { name, ...defRest, signatures: [{ params: fieldMapping, returnType }] }, + { withTypes: false } + )[0].declaration + })`, + [] + ); + testErrorsAndWarnings( + `from a | stats round(${ + getFunctionSignatures( + { name, ...defRest, signatures: [{ params: fieldMapping, returnType }] }, + { withTypes: false } + )[0].declaration + })`, + [] + ); + testErrorsAndWarnings( + `from a | stats var = round(${ + getFunctionSignatures( + { name, ...defRest, signatures: [{ params: fieldMapping, returnType }] }, + { withTypes: false } + )[0].declaration + }) + ${ + getFunctionSignatures( + { name, ...defRest, signatures: [{ params: fieldMapping, returnType }] }, + { withTypes: false } + )[0].declaration + }`, + [] + ); + testErrorsAndWarnings( + `from a | stats round(${ + getFunctionSignatures( + { name, ...defRest, signatures: [{ params: fieldMapping, returnType }] }, + { withTypes: false } + )[0].declaration + }) + ${ + getFunctionSignatures( + { name, ...defRest, signatures: [{ params: fieldMapping, returnType }] }, + { withTypes: false } + )[0].declaration + }`, + [] + ); if (alias) { for (const otherName of alias) { @@ -1623,55 +1789,50 @@ describe('validation logic', () => { describe('enrich', () => { testErrorsAndWarnings(`from a | enrich`, [ - 'SyntaxError: expected {OPENING_BRACKET, ENRICH_POLICY_NAME} but found ""', + "SyntaxError: missing ENRICH_POLICY_NAME at ''", ]); - testErrorsAndWarnings(`from a | enrich [`, [ - 'SyntaxError: expected {SETTING} but found ""', + testErrorsAndWarnings(`from a | enrich _`, ['Unknown policy [_]']); + testErrorsAndWarnings(`from a | enrich _:`, [ + "SyntaxError: token recognition error at: ':'", + 'Unknown policy [_]', ]); - testErrorsAndWarnings(`from a | enrich [ccq.mode`, [ - 'SyntaxError: expected {COLON} but found ""', + testErrorsAndWarnings(`from a | enrich _:policy`, [ + 'Unrecognized value [_] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]', ]); - testErrorsAndWarnings(`from a | enrich [ccq.mode:`, [ - 'SyntaxError: expected {SETTING} but found ""', + testErrorsAndWarnings(`from a | enrich :policy`, [ + "SyntaxError: token recognition error at: ':'", ]); - testErrorsAndWarnings(`from a | enrich [ccq.mode:any`, [ - 'SyntaxError: expected {CLOSING_BRACKET} but found ""', + testErrorsAndWarnings(`from a | enrich any:`, [ + "SyntaxError: token recognition error at: ':'", + 'Unknown policy [any]', ]); - testErrorsAndWarnings(`from a | enrich [ccq.mode:any] `, [ - "SyntaxError: extraneous input '' expecting {OPENING_BRACKET, ENRICH_POLICY_NAME}", + testErrorsAndWarnings(`from a | enrich _any:`, [ + "SyntaxError: token recognition error at: ':'", + 'Unknown policy [_any]', ]); - testErrorsAndWarnings(`from a | enrich policy `, []); - testErrorsAndWarnings(`from a | enrich [ccq.mode:value] policy `, [ - 'Unrecognized value [value], ENRICH [ccq.mode] needs to be one of [ANY, COORDINATOR, REMOTE]', + testErrorsAndWarnings(`from a | enrich any:policy`, [ + 'Unrecognized value [any] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]', ]); + testErrorsAndWarnings(`from a | enrich policy `, []); for (const value of ['any', 'coordinator', 'remote']) { - testErrorsAndWarnings(`from a | enrich [ccq.mode:${value}] policy `, []); - testErrorsAndWarnings(`from a | enrich [ccq.mode:${value.toUpperCase()}] policy `, []); + testErrorsAndWarnings(`from a | enrich _${value}:policy `, []); + testErrorsAndWarnings(`from a | enrich _${value} : policy `, [ + "SyntaxError: token recognition error at: ':'", + "SyntaxError: extraneous input 'policy' expecting ", + `Unknown policy [_${value}]`, + ]); + testErrorsAndWarnings(`from a | enrich _${value}: policy `, [ + "SyntaxError: token recognition error at: ':'", + "SyntaxError: extraneous input 'policy' expecting ", + `Unknown policy [_${value}]`, + ]); + testErrorsAndWarnings(`from a | enrich _${camelCase(value)}:policy `, []); + testErrorsAndWarnings(`from a | enrich _${value.toUpperCase()}:policy `, []); } - testErrorsAndWarnings(`from a | enrich [setting:value policy`, [ - 'SyntaxError: expected {CLOSING_BRACKET} but found "policy"', - 'Unsupported setting [setting], expected [ccq.mode]', - ]); - - testErrorsAndWarnings(`from a | enrich [ccq.mode:any policy`, [ - 'SyntaxError: expected {CLOSING_BRACKET} but found "policy"', - ]); - - testErrorsAndWarnings(`from a | enrich [ccq.mode:any policy`, [ - 'SyntaxError: expected {CLOSING_BRACKET} but found "policy"', - ]); - - testErrorsAndWarnings(`from a | enrich [setting:value] policy`, [ - 'Unsupported setting [setting], expected [ccq.mode]', + testErrorsAndWarnings(`from a | enrich _unknown:policy`, [ + 'Unrecognized value [_unknown] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]', ]); - testErrorsAndWarnings(`from a | enrich [ccq.mode:any] policy[]`, []); - - testErrorsAndWarnings( - `from a | enrich [ccq.mode:any][ccq.mode:coordinator] policy[]`, - [], - ['Multiple definition of setting [ccq.mode]. Only last one will be applied.'] - ); testErrorsAndWarnings(`from a | enrich missing-policy `, ['Unknown policy [missing-policy]']); testErrorsAndWarnings(`from a | enrich policy on `, [ "SyntaxError: missing {QUOTED_IDENTIFIER, UNQUOTED_ID_PATTERN} at ''", @@ -1749,7 +1910,7 @@ describe('validation logic', () => { expect(callbackMocks.getSources).not.toHaveBeenCalled(); }); - it(`should fetch policies if no enrich command is found`, async () => { + it(`should not fetch policies if no enrich command is found`, async () => { const callbackMocks = getCallbackMocks(); await validateAst(`row a = 1 | eval a`, getAstAndErrors, callbackMocks); expect(callbackMocks.getPolicies).not.toHaveBeenCalled(); @@ -1794,5 +1955,33 @@ describe('validation logic', () => { query: `from enrichIndex1 | keep otherField, yetAnotherField`, }); }); + + it(`should not crash if no callbacks are available`, async () => { + try { + await validateAst( + `from a | eval b = a | enrich policy | dissect stringField "%{firstWord}"`, + getAstAndErrors, + { + getFieldsFor: undefined, + getSources: undefined, + getPolicies: undefined, + getMetaFields: undefined, + } + ); + } catch { + fail('Should not throw'); + } + }); + + it(`should not crash if no callbacks are passed`, async () => { + try { + await validateAst( + `from a | eval b = a | enrich policy | dissect stringField "%{firstWord}"`, + getAstAndErrors + ); + } catch { + fail('Should not throw'); + } + }); }); }); 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 67a31afd7fea9..1deae7be97b4e 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/validation/validation.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/validation/validation.ts @@ -10,6 +10,7 @@ import uniqBy from 'lodash/uniqBy'; import { CommandModeDefinition, CommandOptionsDefinition, + FunctionDefinition, SignatureArgType, } from '../definitions/types'; import { @@ -134,8 +135,9 @@ function validateNestedFunctionArg( ) { // The isSupported check ensure the definition exists const argFn = getFunctionDefinition(actualArg.name)!; - - if ('noNestingFunctions' in argDef && argDef.noNestingFunctions) { + const fnDef = getFunctionDefinition(astFunction.name)!; + // no nestying criteria should be enforced only for same type function + if ('noNestingFunctions' in argDef && argDef.noNestingFunctions && fnDef.type === argFn.type) { messages.push( getMessageFromId({ messageId: 'noNestedArgumentSupport', @@ -170,51 +172,53 @@ function validateFunctionColumnArg( parentCommand: string ) { const messages: ESQLMessage[] = []; - if (isColumnItem(actualArg) && actualArg.name) { - const { hit: columnCheck, nameHit } = columnExists(actualArg, references); - if (!columnCheck) { - messages.push( - getMessageFromId({ - messageId: 'unknownColumn', - values: { - name: actualArg.name, - }, - locations: actualArg.location, - }) - ); - } else { - if (actualArg.name === '*') { - // if function does not support wildcards return a specific error - if (!('supportsWildcard' in argDef) || !argDef.supportsWildcard) { - messages.push( - getMessageFromId({ - messageId: 'noWildcardSupportAsArg', - values: { - name: astFunction.name, - }, - locations: actualArg.location, - }) - ); - } - // do not validate any further for now, only count() accepts wildcard as args... + if (isColumnItem(actualArg)) { + if (actualArg.name) { + const { hit: columnCheck, nameHit } = columnExists(actualArg, references); + if (!columnCheck) { + messages.push( + getMessageFromId({ + messageId: 'unknownColumn', + values: { + name: actualArg.name, + }, + locations: actualArg.location, + }) + ); } else { - // guaranteed by the check above - const columnHit = getColumnHit(nameHit!, references); - // check the type of the column hit - const typeHit = columnHit!.type; - if (!isEqualType(actualArg, argDef, references, parentCommand)) { - messages.push( - getMessageFromId({ - messageId: 'wrongArgumentType', - values: { - name: astFunction.name, - argType: argDef.type, - value: actualArg.name, - givenType: typeHit, - }, - locations: actualArg.location, - }) - ); + if (actualArg.name === '*') { + // if function does not support wildcards return a specific error + if (!('supportsWildcard' in argDef) || !argDef.supportsWildcard) { + messages.push( + getMessageFromId({ + messageId: 'noWildcardSupportAsArg', + values: { + name: astFunction.name, + }, + locations: actualArg.location, + }) + ); + } + // do not validate any further for now, only count() accepts wildcard as args... + } else { + // guaranteed by the check above + const columnHit = getColumnHit(nameHit!, references); + // check the type of the column hit + const typeHit = columnHit!.type; + if (!isEqualType(actualArg, argDef, references, parentCommand)) { + messages.push( + getMessageFromId({ + messageId: 'wrongArgumentType', + values: { + name: astFunction.name, + argType: argDef.type, + value: actualArg.name, + givenType: typeHit, + }, + locations: actualArg.location, + }) + ); + } } } } @@ -222,6 +226,24 @@ function validateFunctionColumnArg( return messages; } +function extractCompatibleSignaturesForFunction( + fnDef: FunctionDefinition, + astFunction: ESQLFunction +) { + return fnDef.signatures.filter((def) => { + if (def.infiniteParams && astFunction.args.length > 0) { + return true; + } + if (def.minParams && astFunction.args.length >= def.minParams) { + return true; + } + if (astFunction.args.length === def.params.length) { + return true; + } + return astFunction.args.length === def.params.filter(({ optional }) => !optional).length; + }); +} + function validateFunction( astFunction: ESQLFunction, parentCommand: string, @@ -235,16 +257,8 @@ function validateFunction( return messages; } const fnDefinition = getFunctionDefinition(astFunction.name)!; - const supportNestedFunctions = - fnDefinition?.signatures.some(({ params }) => - params.some(({ noNestingFunctions }) => !noNestingFunctions) - ) || true; - const isFnSupported = isSupportedFunction( - astFunction.name, - isNested && !supportNestedFunctions ? 'eval' : parentCommand, - parentOption - ); + const isFnSupported = isSupportedFunction(astFunction.name, parentCommand, parentOption); if (!isFnSupported.supported) { if (isFnSupported.reason === 'unknownFunction') { @@ -282,18 +296,7 @@ function validateFunction( return messages; } } - const matchingSignatures = fnDefinition.signatures.filter((def) => { - if (def.infiniteParams && astFunction.args.length > 0) { - return true; - } - if (def.minParams && astFunction.args.length >= def.minParams) { - return true; - } - if (astFunction.args.length === def.params.length) { - return true; - } - return astFunction.args.length >= def.params.filter(({ optional }) => !optional).length; - }); + const matchingSignatures = extractCompatibleSignaturesForFunction(fnDefinition, astFunction); if (!matchingSignatures.length) { const numArgs = fnDefinition.signatures[0].params.filter(({ optional }) => !optional).length; messages.push( @@ -303,6 +306,7 @@ function validateFunction( fn: astFunction.name, numArgs, passedArgs: astFunction.args.length, + exactly: fnDefinition.signatures[0].params.length - numArgs, }, locations: astFunction.location, }) @@ -325,9 +329,9 @@ function validateFunction( } } } - // check if the definition has some warning to show: - if (fnDefinition.warning) { - const payloads = fnDefinition.warning(astFunction); + // check if the definition has some specific validation to apply: + if (fnDefinition.validate) { + const payloads = fnDefinition.validate(astFunction); if (payloads.length) { messages.push(...payloads); } @@ -399,6 +403,7 @@ function validateFunction( failingSignatures.push(failingSignature); } } + if (failingSignatures.length && failingSignatures.length === matchingSignatures.length) { const failingSignatureOrderedByErrorCount = failingSignatures .map((arr, index) => ({ index, count: arr.length })) @@ -435,27 +440,27 @@ function validateSetting( ); return messages; } - setting.args.forEach((arg, index) => { - if (!Array.isArray(arg)) { - const argDef = settingDef.signature.params[index]; - const value = 'value' in arg ? arg.value : arg.name; - if (argDef.values && !argDef.values?.includes(String(value).toLowerCase())) { - messages.push( - getMessageFromId({ - messageId: 'unsupportedSettingCommandValue', - values: { - setting: setting.name, - command: command.name.toUpperCase(), - value: String(value), - // for some reason all this enums are uppercase in ES - expected: (argDef.values?.join(', ') || argDef.type).toUpperCase(), - }, - locations: arg.location, - }) - ); - } - } - }); + if ( + settingDef.values.every(({ name }) => name !== setting.name) || + // enforce the check on the prefix if present + (settingDef.prefix && !setting.text.startsWith(settingDef.prefix)) + ) { + messages.push( + getMessageFromId({ + messageId: 'unsupportedSettingCommandValue', + values: { + command: command.name.toUpperCase(), + value: setting.text, + // for some reason all this enums are uppercase in ES + expected: settingDef.values + .map(({ name }) => `${settingDef.prefix || ''}${name}`) + .join(', ') + .toUpperCase(), + }, + locations: setting.location, + }) + ); + } return messages; } @@ -657,14 +662,7 @@ function validateCommand(command: ESQLCommand, references: ReferenceMaps): ESQLM } if (isSettingItem(arg)) { - messages.push( - ...validateSetting( - arg, - commandDef.modes?.find(({ name }) => name === arg.name), - command, - references - ) - ); + messages.push(...validateSetting(arg, commandDef.modes[0], command, references)); } if (isOptionItem(arg)) { @@ -790,7 +788,7 @@ export async function validateAst( retrieveMetadataFields(callbacks), ]); - if (availablePolicies.size && ast.filter(({ name }) => name === 'enrich')) { + if (availablePolicies.size) { const fieldsFromPoliciesMap = await retrievePoliciesFields(ast, availablePolicies, callbacks); fieldsFromPoliciesMap.forEach((value, key) => availableFields.set(key, value)); } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 7910a362f5274..8b90dcbbe1a08 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -90,7 +90,7 @@ pageLoadAssetSize: licensing: 29004 links: 44490 lists: 22900 - logsExplorer: 50000 + logsExplorer: 55000 logsShared: 281060 logstash: 53548 management: 46112 diff --git a/packages/kbn-search-api-panels/components/code_box.tsx b/packages/kbn-search-api-panels/components/code_box.tsx index 21c4085f44a9b..5a4ff7cc13240 100644 --- a/packages/kbn-search-api-panels/components/code_box.tsx +++ b/packages/kbn-search-api-panels/components/code_box.tsx @@ -23,6 +23,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { ConsolePluginStart } from '@kbn/console-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import { LanguageDefinition } from '../types'; @@ -38,6 +39,7 @@ interface CodeBoxProps { setSelectedLanguage: (language: LanguageDefinition) => void; assetBasePath: string; application?: ApplicationStart; + consolePlugin?: ConsolePluginStart; sharePlugin: SharePluginStart; consoleRequest?: string; } @@ -45,6 +47,7 @@ interface CodeBoxProps { export const CodeBox: React.FC = ({ application, codeSnippet, + consolePlugin, languageType, languages, assetBasePath, @@ -117,6 +120,7 @@ export const CodeBox: React.FC = ({ diff --git a/packages/kbn-search-api-panels/components/ingest_data.tsx b/packages/kbn-search-api-panels/components/ingest_data.tsx index f8ba59d29bf30..0700d2d56d661 100644 --- a/packages/kbn-search-api-panels/components/ingest_data.tsx +++ b/packages/kbn-search-api-panels/components/ingest_data.tsx @@ -11,6 +11,7 @@ import React from 'react'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { ConsolePluginStart } from '@kbn/console-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import { CodeBox } from './code_box'; import { LanguageDefinition } from '../types'; @@ -26,6 +27,7 @@ interface IngestDataProps { }; assetBasePath: string; application?: ApplicationStart; + consolePlugin?: ConsolePluginStart; sharePlugin: SharePluginStart; languages: LanguageDefinition[]; consoleRequest?: string; @@ -39,6 +41,7 @@ export const IngestData: React.FC = ({ docLinks, assetBasePath, application, + consolePlugin, sharePlugin, languages, consoleRequest, @@ -58,6 +61,7 @@ export const IngestData: React.FC = ({ setSelectedLanguage={setSelectedLanguage} assetBasePath={assetBasePath} application={application} + consolePlugin={consolePlugin} sharePlugin={sharePlugin} /> } diff --git a/packages/kbn-search-api-panels/components/try_in_console_button.tsx b/packages/kbn-search-api-panels/components/try_in_console_button.tsx index fe109e025e2e5..7007c306a2cd6 100644 --- a/packages/kbn-search-api-panels/components/try_in_console_button.tsx +++ b/packages/kbn-search-api-panels/components/try_in_console_button.tsx @@ -11,6 +11,7 @@ import React from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import type { ApplicationStart } from '@kbn/core-application-browser'; import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { ConsolePluginStart } from '@kbn/console-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { compressToEncodedURIComponent } from 'lz-string'; @@ -18,11 +19,13 @@ import { compressToEncodedURIComponent } from 'lz-string'; export interface TryInConsoleButtonProps { request: string; application?: ApplicationStart; + consolePlugin?: ConsolePluginStart; sharePlugin: SharePluginStart; } export const TryInConsoleButton = ({ request, application, + consolePlugin, sharePlugin, }: TryInConsoleButtonProps) => { const { url } = sharePlugin; @@ -39,8 +42,20 @@ export const TryInConsoleButton = ({ ); if (!consolePreviewLink) return null; + const onClick = () => { + const embeddedConsoleAvailable = + (consolePlugin?.openEmbeddedConsole !== undefined && + consolePlugin?.isEmbeddedConsoleAvailable?.()) ?? + false; + if (embeddedConsoleAvailable) { + consolePlugin!.openEmbeddedConsole!(request); + } else { + window.open(consolePreviewLink, '_blank', 'noreferrer'); + } + }; + return ( - + { + const mockClient = { + transport: { + request: jest.fn(), + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + it('should delete a connector secret', async () => { + mockClient.transport.request.mockImplementation(() => ({ + result: 'deleted', + })); + + await expect( + deleteConnectorSecret(mockClient as unknown as ElasticsearchClient, 'secret-id') + ).resolves.toEqual({ result: 'deleted' }); + expect(mockClient.transport.request).toHaveBeenCalledWith({ + method: 'DELETE', + path: '/_connector/_secret/secret-id', + }); + jest.useRealTimers(); + }); +}); diff --git a/packages/kbn-search-connectors/lib/delete_connector_secret.ts b/packages/kbn-search-connectors/lib/delete_connector_secret.ts new file mode 100644 index 0000000000000..d3ecbe8da73f5 --- /dev/null +++ b/packages/kbn-search-connectors/lib/delete_connector_secret.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { ConnectorsAPIUpdateResponse } from '../types/connectors_api'; + +export const deleteConnectorSecret = async (client: ElasticsearchClient, id: string) => { + return await client.transport.request({ + method: 'DELETE', + path: `/_connector/_secret/${id}`, + }); +}; diff --git a/packages/kbn-search-connectors/lib/index.ts b/packages/kbn-search-connectors/lib/index.ts index 3e929d5bc6834..e7269a0620b62 100644 --- a/packages/kbn-search-connectors/lib/index.ts +++ b/packages/kbn-search-connectors/lib/index.ts @@ -11,6 +11,7 @@ export * from './create_connector'; export * from './create_connector_document'; export * from './create_connector_secret'; export * from './delete_connector'; +export * from './delete_connector_secret'; export * from './fetch_connectors'; export * from './fetch_sync_jobs'; export * from './update_filtering'; @@ -22,5 +23,6 @@ export * from './update_connector_configuration'; export * from './update_connector_index_name'; export * from './update_connector_name_and_description'; export * from './update_connector_scheduling'; +export * from './update_connector_secret'; export * from './update_connector_service_type'; export * from './update_connector_status'; diff --git a/packages/kbn-search-connectors/lib/update_connector_secret.test.ts b/packages/kbn-search-connectors/lib/update_connector_secret.test.ts new file mode 100644 index 0000000000000..80eb98a37babd --- /dev/null +++ b/packages/kbn-search-connectors/lib/update_connector_secret.test.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 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; + +import { updateConnectorSecret } from './update_connector_secret'; + +describe('updateConnectorSecret lib function', () => { + const mockClient = { + transport: { + request: jest.fn(), + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + it('should update a connector secret', async () => { + mockClient.transport.request.mockImplementation(() => ({ + result: 'created', + })); + + await expect( + updateConnectorSecret(mockClient as unknown as ElasticsearchClient, 'my-secret', 'secret-id') + ).resolves.toEqual({ result: 'created' }); + expect(mockClient.transport.request).toHaveBeenCalledWith({ + method: 'PUT', + path: '/_connector/_secret/secret-id', + body: { + value: 'my-secret', + }, + }); + jest.useRealTimers(); + }); +}); diff --git a/packages/kbn-search-connectors/lib/update_connector_secret.ts b/packages/kbn-search-connectors/lib/update_connector_secret.ts new file mode 100644 index 0000000000000..516818b7e9b8d --- /dev/null +++ b/packages/kbn-search-connectors/lib/update_connector_secret.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { ConnectorsAPIUpdateResponse } from '../types/connectors_api'; + +export const updateConnectorSecret = async ( + client: ElasticsearchClient, + value: string, + secretId: string +) => { + return await client.transport.request({ + method: 'PUT', + path: `/_connector/_secret/${secretId}`, + body: { + value, + }, + }); +}; diff --git a/packages/kbn-search-connectors/types/native_connectors.ts b/packages/kbn-search-connectors/types/native_connectors.ts index 62ac7445e8b06..849e669c4a810 100644 --- a/packages/kbn-search-connectors/types/native_connectors.ts +++ b/packages/kbn-search-connectors/types/native_connectors.ts @@ -1793,7 +1793,7 @@ export const NATIVE_CONNECTOR_DEFINITIONS: Record JSON.stringify(substr).slice(1, -1) + ); +} diff --git a/packages/kbn-security-hardening/index.ts b/packages/kbn-security-hardening/index.ts new file mode 100644 index 0000000000000..14c740887c21c --- /dev/null +++ b/packages/kbn-security-hardening/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 { unsafeConsole } from './console'; diff --git a/packages/kbn-security-hardening/kibana.jsonc b/packages/kbn-security-hardening/kibana.jsonc new file mode 100644 index 0000000000000..42b778b24fcc6 --- /dev/null +++ b/packages/kbn-security-hardening/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/security-hardening", + "owner": "@elastic/kibana-security" +} diff --git a/packages/kbn-security-hardening/package.json b/packages/kbn-security-hardening/package.json new file mode 100644 index 0000000000000..9fa800115404d --- /dev/null +++ b/packages/kbn-security-hardening/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/security-hardening", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/kbn-security-hardening/tsconfig.json b/packages/kbn-security-hardening/tsconfig.json new file mode 100644 index 0000000000000..33dcb5e36274e --- /dev/null +++ b/packages/kbn-security-hardening/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [] +} diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index df9ed4cab4f51..de4e6032ba52f 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -105,7 +105,7 @@ module.exports = { transformIgnorePatterns: [ // ignore all node_modules except monaco-editor, monaco-yaml and react-monaco-editor which requires babel transforms to handle dynamic import() // since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842) - '[/\\\\]node_modules(?![\\/\\\\](byte-size|monaco-editor|monaco-yaml|monaco-languageserver-types|monaco-marker-data-provider|monaco-worker-manager|vscode-languageserver-types|react-monaco-editor|d3-interpolate|d3-color|langchain|langsmith|@cfworker|gpt-tokenizer))[/\\\\].+\\.js$', + '[/\\\\]node_modules(?![\\/\\\\](byte-size|monaco-editor|monaco-yaml|monaco-languageserver-types|monaco-marker-data-provider|monaco-worker-manager|vscode-languageserver-types|react-monaco-editor|d3-interpolate|d3-color|langchain|langsmith|@cfworker|gpt-tokenizer|flat))[/\\\\].+\\.js$', 'packages/kbn-pm/dist/index.js', '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith))/dist/[/\\\\].+\\.js$', '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith))/dist/util/[/\\\\].+\\.js$', diff --git a/packages/kbn-test/jest_integration_node/jest-preset.js b/packages/kbn-test/jest_integration_node/jest-preset.js index 631b2c4f9350e..6472237c5dd17 100644 --- a/packages/kbn-test/jest_integration_node/jest-preset.js +++ b/packages/kbn-test/jest_integration_node/jest-preset.js @@ -22,7 +22,7 @@ module.exports = { // An array of regexp pattern strings that are matched against, matched files will skip transformation: transformIgnorePatterns: [ // since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842) - '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|gpt-tokenizer))[/\\\\].+\\.js$', + '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|gpt-tokenizer|flat))[/\\\\].+\\.js$', '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith))/dist/[/\\\\].+\\.js$', '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith))/dist/util/[/\\\\].+\\.js$', ], diff --git a/packages/kbn-text-based-editor/src/editor_footer.tsx b/packages/kbn-text-based-editor/src/editor_footer.tsx index 351b7bbe251c1..210988ac94e42 100644 --- a/packages/kbn-text-based-editor/src/editor_footer.tsx +++ b/packages/kbn-text-based-editor/src/editor_footer.tsx @@ -201,6 +201,7 @@ interface EditorFooterProps { editorIsInline?: boolean; isSpaceReduced?: boolean; isLoading?: boolean; + allowQueryCancellation?: boolean; } export const EditorFooter = memo(function EditorFooter({ @@ -216,6 +217,7 @@ export const EditorFooter = memo(function EditorFooter({ editorIsInline, isSpaceReduced, isLoading, + allowQueryCancellation, }: EditorFooterProps) { const { euiTheme } = useEuiTheme(); const [isErrorPopoverOpen, setIsErrorPopoverOpen] = useState(false); @@ -333,8 +335,8 @@ export const EditorFooter = memo(function EditorFooter({ size="s" fill onClick={runQuery} - isLoading={isLoading} - isDisabled={Boolean(disableSubmitAction)} + isLoading={isLoading && !allowQueryCancellation} + isDisabled={Boolean(disableSubmitAction && !allowQueryCancellation)} data-test-subj="TextBasedLangEditor-run-query-button" minWidth={isSpaceReduced ? false : undefined} > @@ -345,7 +347,11 @@ export const EditorFooter = memo(function EditorFooter({ justifyContent="spaceBetween" > - {isSpaceReduced + {allowQueryCancellation && isLoading + ? i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.cancel', { + defaultMessage: 'Cancel', + }) + : isSpaceReduced ? i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.run', { defaultMessage: 'Run', }) @@ -361,7 +367,7 @@ export const EditorFooter = memo(function EditorFooter({ size="xs" css={css` border: 1px solid - ${Boolean(disableSubmitAction) + ${Boolean(disableSubmitAction && !allowQueryCancellation) ? euiTheme.colors.disabled : euiTheme.colors.emptyShade}; padding: 0 ${euiTheme.size.xs}; @@ -370,7 +376,7 @@ export const EditorFooter = memo(function EditorFooter({ border-radius: ${euiTheme.size.xs}; `} > - {COMMAND_KEY}⏎ + {allowQueryCancellation && isLoading ? 'X' : `${COMMAND_KEY}⏎`} diff --git a/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx b/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx index 6f96c1366800d..2c47f7f309531 100644 --- a/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx +++ b/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx @@ -585,9 +585,12 @@ FROM employees defaultMessage: `### STATS ... BY Use \`STATS ... BY\` to group rows according to a common value and calculate one or more aggregated values over the grouped rows. +**Examples**: + \`\`\` FROM employees -| STATS count = COUNT(languages) BY languages +| STATS count = COUNT(emp_no) BY languages +| SORT languages \`\`\` If \`BY\` is omitted, the output table contains exactly one row with the aggregations applied over the entire dataset: @@ -615,6 +618,40 @@ FROM employees \`\`\` Refer to **Aggregation functions** for a list of functions that can be used with \`STATS ... BY\`. + +Both the aggregating functions and the grouping expressions accept other functions. This is useful for using \`STATS...BY\` on multivalue columns. For example, to calculate the average salary change, you can use \`MV_AVG\` to first average the multiple values per employee, and use the result with the \`AVG\` function: + +\`\`\` +FROM employees +| STATS avg_salary_change = AVG(MV_AVG(salary_change)) +\`\`\` + +An example of grouping by an expression is grouping employees on the first letter of their last name: + +\`\`\` +FROM employees +| STATS my_count = COUNT() BY LEFT(last_name, 1) +| SORT \`LEFT(last_name, 1)\` +\`\`\` + +Specifying the output column name is optional. If not specified, the new column name is equal to the expression. The following query returns a column named \`AVG(salary)\`: + +\`\`\` +FROM employees +| STATS AVG(salary) +\`\`\` + +Because this name contains special characters, it needs to be quoted with backticks (\`) when using it in subsequent commands: + +\`\`\` +FROM employees +| STATS AVG(salary) +| EVAL avg_salary_rounded = ROUND(\`AVG(salary)\`) +\`\`\` + +**Note**: \`STATS\` without any groups is much faster than adding a group. + +**Note**: Grouping on a single expression is currently much more optimized than grouping on many expressions. `, description: 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', @@ -934,7 +971,7 @@ ROW a=1.8 | EVAL a=CEIL(a) \`\`\` -Note: This is a noop for \`long\` (including unsigned) and \`integer\`. For \`double\` this picks the the closest \`double\` value to the integer similar to Java's \`Math.ceil\`. +Note: This is a noop for \`long\` (including unsigned) and \`integer\`. For \`double\` this picks the closest \`double\` value to the integer similar to Java's \`Math.ceil\`. `, description: 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', @@ -1081,6 +1118,33 @@ ROW a=1.8 /> ), }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dateDiffFunction', + { + defaultMessage: 'DATE_DIFF', + } + ), + description: ( + + ), + }, { label: i18n.translate( 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dateExtractFunction', @@ -1099,6 +1163,13 @@ Extracts parts of a date, like year, month, day, hour. The supported field types \`\`\` ROW date = DATE_PARSE("yyyy-MM-dd", "2022-05-06") | EVAL year = DATE_EXTRACT("year", date) +\`\`\` + +For example, to find all events that occurred outside of business hours (before 9 AM or after 5 PM), on any given date: + +\`\`\` +FROM sample_data +| WHERE DATE_EXTRACT("hour_of_day", @timestamp) < 9 AND DATE_EXTRACT("hour_of_day", @timestamp) >= 17 \`\`\` `, description: @@ -1145,12 +1216,13 @@ FROM employees ), description: ( + ), + }, + { + label: i18n.translate( + 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.stCentroidFunction', + { + defaultMessage: 'ST_CENTROID', + } + ), + description: ( + { if (ast) { - const execution = expressions.run(ast, null); + const executionContract = expressions.execute(ast, null); + + if (abortController) { + abortController.signal.onabort = () => { + executionContract.cancel(); + }; + } + + const execution = executionContract.getData(); let finalData: Datatable; let error: string | undefined; execution.pipe(pluck('result')).subscribe((resp) => { diff --git a/packages/kbn-text-based-editor/src/helpers.ts b/packages/kbn-text-based-editor/src/helpers.ts index 88df8ef6a75e4..dffddc9514449 100644 --- a/packages/kbn-text-based-editor/src/helpers.ts +++ b/packages/kbn-text-based-editor/src/helpers.ts @@ -116,6 +116,17 @@ export const parseErrors = (errors: Error[], code: string): MonacoMessage[] => { endLineNumber: Number(lineNumber), severity: monaco.MarkerSeverity.Error, }; + } else if (error.message.includes('expression was aborted')) { + return { + message: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.aborted', { + defaultMessage: 'Request was aborted', + }), + startColumn: 1, + startLineNumber: 1, + endColumn: 10, + endLineNumber: 1, + severity: monaco.MarkerSeverity.Warning, + }; } else { // unknown error message return { 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 146e020d4fe82..241679734e248 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 @@ -71,7 +71,10 @@ export interface TextBasedLanguagesEditorProps { /** Callback running everytime the query changes */ onTextLangQueryChange: (query: AggregateQuery) => void; /** Callback running when the user submits the query */ - onTextLangQuerySubmit: (query?: AggregateQuery) => void; + onTextLangQuerySubmit: ( + query?: AggregateQuery, + abortController?: AbortController + ) => Promise; /** Can be used to expand/minimize the editor */ expandCodeEditor: (status: boolean) => void; /** If it is true, the editor initializes with height EDITOR_INITIAL_HEIGHT_EXPANDED */ @@ -105,6 +108,9 @@ export interface TextBasedLanguagesEditorProps { editorIsInline?: boolean; /** Disables the submit query action*/ disableSubmitAction?: boolean; + + /** when set to true enables query cancellation **/ + allowQueryCancellation?: boolean; } interface TextBasedEditorDeps { @@ -158,12 +164,14 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ editorIsInline, disableSubmitAction, dataTestSubj, + allowQueryCancellation, }: TextBasedLanguagesEditorProps) { const { euiTheme } = useEuiTheme(); const language = getAggregateQueryMode(query); const queryString: string = query[language] ?? ''; const kibana = useKibana(); - const { dataViews, expressions, indexManagementApiService, application } = kibana.services; + const { dataViews, expressions, indexManagementApiService, application, docLinks } = + kibana.services; const [code, setCode] = useState(queryString ?? ''); const [codeOneLiner, setCodeOneLiner] = useState(''); // To make server side errors less "sticky", register the state of the code when submitting @@ -175,7 +183,8 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ const [showLineNumbers, setShowLineNumbers] = useState(isCodeEditorExpanded); const [isCompactFocused, setIsCompactFocused] = useState(isCodeEditorExpanded); const [isCodeEditorExpandedFocused, setIsCodeEditorExpandedFocused] = useState(false); - + const [isQueryLoading, setIsQueryLoading] = useState(true); + const [abortController, setAbortController] = useState(new AbortController()); const [editorMessages, setEditorMessages] = useState<{ errors: MonacoMessage[]; warnings: MonacoMessage[]; @@ -185,12 +194,25 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ }); const onQuerySubmit = useCallback(() => { - const currentValue = editor1.current?.getValue(); - if (currentValue != null) { - setCodeStateOnSubmission(currentValue); + if (isQueryLoading && allowQueryCancellation) { + abortController?.abort(); + setIsQueryLoading(false); + } else { + setIsQueryLoading(true); + const abc = new AbortController(); + setAbortController(abc); + + const currentValue = editor1.current?.getValue(); + if (currentValue != null) { + setCodeStateOnSubmission(currentValue); + } + onTextLangQuerySubmit({ [language]: currentValue } as AggregateQuery, abc); } - onTextLangQuerySubmit({ [language]: currentValue } as AggregateQuery); - }, [language, onTextLangQuerySubmit]); + }, [language, onTextLangQuerySubmit, abortController, isQueryLoading, allowQueryCancellation]); + + useEffect(() => { + if (!isLoading) setIsQueryLoading(false); + }, [isLoading]); const [documentationSections, setDocumentationSections] = useState(); @@ -310,12 +332,13 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ const { cache: esqlFieldsCache, memoizedFieldsFromESQL } = useMemo(() => { // need to store the timing of the first request so we can atomically clear the cache per query const fn = memoize( - (...args: [{ esql: string }, ExpressionsStart]) => ({ + (...args: [{ esql: string }, ExpressionsStart, undefined, AbortController?]) => ({ timestamp: Date.now(), result: fetchFieldsFromESQL(...args), }), ({ esql }) => esql ); + return { cache: fn.cache, memoizedFieldsFromESQL: fn }; }, []); @@ -333,7 +356,12 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ // Check if there's a stale entry and clear it clearCacheWhenOld(esqlFieldsCache, esqlQuery.esql); try { - const table = await memoizedFieldsFromESQL(esqlQuery, expressions).result; + const table = await memoizedFieldsFromESQL( + esqlQuery, + expressions, + undefined, + abortController + ).result; return table?.columns.map((c) => ({ name: c.name, type: c.meta.type })) || []; } catch (e) { // no action yet @@ -351,7 +379,14 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ return policies.map(({ type, query: policyQuery, ...rest }) => rest); }, }), - [dataViews, expressions, indexManagementApiService, esqlFieldsCache, memoizedFieldsFromESQL] + [ + dataViews, + expressions, + indexManagementApiService, + esqlFieldsCache, + memoizedFieldsFromESQL, + abortController, + ] ); const queryValidation = useCallback( @@ -680,6 +715,9 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ language={getLanguageDisplayName(String(language))} sections={documentationSections} searchInDescription + linkToDocumentation={ + language === 'esql' ? docLinks?.links?.query?.queryESQL : '' + } buttonProps={{ color: 'text', size: 's', @@ -815,6 +853,17 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ } }); + // this is fixing a bug between the EUIPopover and the monaco editor + // when the user clicks the editor, we force it to focus and the onDidFocusEditorText + // to fire, the timeout is needed because otherwise it refocuses on the popover icon + // and the user needs to click again the editor. + // IMPORTANT: The popover needs to be wrapped with the EuiOutsideClickDetector component. + editor.onMouseDown(() => { + setTimeout(() => { + editor.focus(); + }, 100); + }); + editor.onDidFocusEditorText(() => { onEditorFocus(); }); @@ -852,7 +901,8 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ disableSubmitAction={disableSubmitAction} hideRunQueryText={hideRunQueryText} isSpaceReduced={isSpaceReduced} - isLoading={isLoading} + isLoading={isQueryLoading} + allowQueryCancellation={allowQueryCancellation} /> )}
@@ -903,6 +953,9 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ language={ String(language) === 'esql' ? 'ES|QL' : String(language).toUpperCase() } + linkToDocumentation={ + language === 'esql' ? docLinks?.links?.query?.queryESQL : '' + } searchInDescription sections={documentationSections} buttonProps={{ @@ -936,13 +989,16 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ lines={lines} containerCSS={styles.bottomContainer} onErrorClick={onErrorClick} - runQuery={onQuerySubmit} + runQuery={() => { + onQuerySubmit(); + }} detectTimestamp={detectTimestamp} hideRunQueryText={hideRunQueryText} editorIsInline={editorIsInline} disableSubmitAction={disableSubmitAction} isSpaceReduced={isSpaceReduced} - isLoading={isLoading} + isLoading={isQueryLoading} + allowQueryCancellation={allowQueryCancellation} {...editorMessages} /> )} diff --git a/packages/kbn-unified-data-table/index.ts b/packages/kbn-unified-data-table/index.ts index 823520884f63e..ce166f212e2ae 100644 --- a/packages/kbn-unified-data-table/index.ts +++ b/packages/kbn-unified-data-table/index.ts @@ -25,3 +25,5 @@ export { getRowsPerPageOptions } from './src/utils/rows_per_page'; export { popularizeField } from './src/utils/popularize_field'; export { useColumns } from './src/hooks/use_data_grid_columns'; +export { OPEN_DETAILS, SELECT_ROW } from './src/components/data_table_columns'; +export { DataTableRowControl } from './src/components/data_table_row_control'; diff --git a/packages/kbn-unified-data-table/src/components/data_table.test.tsx b/packages/kbn-unified-data-table/src/components/data_table.test.tsx index e69d1a88200ba..506cd046e9f1c 100644 --- a/packages/kbn-unified-data-table/src/components/data_table.test.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table.test.tsx @@ -24,7 +24,10 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { servicesMock } from '../../__mocks__/services'; import { buildDataTableRecord, getDocId } from '@kbn/discover-utils'; import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types'; -import { testLeadingControlColumn } from '../../__mocks__/external_control_columns'; +import { + testLeadingControlColumn, + testTrailingControlColumns, +} from '../../__mocks__/external_control_columns'; const mockUseDataGridColumnsCellActions = jest.fn((prop: unknown) => []); jest.mock('@kbn/cell-actions', () => ({ @@ -418,6 +421,69 @@ describe('UnifiedDataTable', () => { }); }); + describe('customControlColumnsConfiguration', () => { + const customControlColumnsConfiguration = jest.fn(); + it('should be able to customise the leading control column', async () => { + const component = await getComponent({ + ...getProps(), + expandedDoc: { + id: 'test', + raw: { + _index: 'test_i', + _id: 'test', + }, + flattened: { test: jest.fn() }, + }, + setExpandedDoc: jest.fn(), + renderDocumentView: jest.fn(), + externalControlColumns: [testLeadingControlColumn], + customControlColumnsConfiguration: customControlColumnsConfiguration.mockImplementation( + () => { + return { + leadingControlColumns: [testLeadingControlColumn, testTrailingControlColumns[0]], + trailingControlColumns: [], + }; + } + ), + }); + + expect(findTestSubject(component, 'test-body-control-column-cell').exists()).toBeTruthy(); + expect( + findTestSubject(component, 'test-trailing-column-popover-button').exists() + ).toBeTruthy(); + }); + + it('should be able to customise the trailing control column', async () => { + const component = await getComponent({ + ...getProps(), + expandedDoc: { + id: 'test', + raw: { + _index: 'test_i', + _id: 'test', + }, + flattened: { test: jest.fn() }, + }, + setExpandedDoc: jest.fn(), + renderDocumentView: jest.fn(), + externalControlColumns: [testLeadingControlColumn], + customControlColumnsConfiguration: customControlColumnsConfiguration.mockImplementation( + () => { + return { + leadingControlColumns: [], + trailingControlColumns: [testLeadingControlColumn, testTrailingControlColumns[0]], + }; + } + ), + }); + + expect(findTestSubject(component, 'test-body-control-column-cell').exists()).toBeTruthy(); + expect( + findTestSubject(component, 'test-trailing-column-popover-button').exists() + ).toBeTruthy(); + }); + }); + describe('externalControlColumns', () => { it('should render external leading control columns', async () => { const component = await getComponent({ diff --git a/packages/kbn-unified-data-table/src/components/data_table.tsx b/packages/kbn-unified-data-table/src/components/data_table.tsx index 33ff451adc972..7df0488b904c9 100644 --- a/packages/kbn-unified-data-table/src/components/data_table.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table.tsx @@ -52,12 +52,14 @@ import { DataTableColumnTypes, CustomCellRenderer, CustomGridColumnsConfiguration, + CustomControlColumnConfiguration, } from '../types'; import { getDisplayedColumns } from '../utils/columns'; import { convertValueToString } from '../utils/convert_value_to_string'; import { getRowsPerPageOptions } from '../utils/rows_per_page'; import { getRenderCellValueFn } from '../utils/get_render_cell_value'; import { + getAllControlColumns, getEuiGridColumns, getLeadControlColumns, getVisibleColumns, @@ -334,6 +336,10 @@ export interface UnifiedDataTableProps { * An optional settings for customising the column */ customGridColumnsConfiguration?: CustomGridColumnsConfiguration; + /** + * An optional settings to control which columns to render as trailing and leading control columns + */ + customControlColumnsConfiguration?: CustomControlColumnConfiguration; /** * Name of the UnifiedDataTable consumer component or application */ @@ -412,6 +418,7 @@ export const UnifiedDataTable = ({ gridStyleOverride, rowLineHeightOverride, customGridColumnsConfiguration, + customControlColumnsConfiguration, }: UnifiedDataTableProps) => { const { fieldFormats, toastNotifications, dataViewFieldEditor, uiSettings, storage, data } = services; @@ -746,19 +753,31 @@ export const UnifiedDataTable = ({ onSort: onTableSort, }; } - return { columns: sortingColumns, onSort: () => {} }; + return { + columns: sortingColumns, + onSort: () => {}, + }; }, [isSortEnabled, sortingColumns, isPlainRecord, inmemorySortingColumns, onTableSort]); const canSetExpandedDoc = Boolean(setExpandedDoc && !!renderDocumentView); - const leadingControlColumns = useMemo(() => { + const leadingControlColumns: EuiDataGridControlColumn[] = useMemo(() => { const internalControlColumns = getLeadControlColumns(canSetExpandedDoc).filter(({ id }) => controlColumnIds.includes(id) ); return externalControlColumns ? [...internalControlColumns, ...externalControlColumns] : internalControlColumns; - }, [canSetExpandedDoc, externalControlColumns, controlColumnIds]); + }, [canSetExpandedDoc, controlColumnIds, externalControlColumns]); + + const controlColumnsConfig = customControlColumnsConfiguration?.({ + controlColumns: getAllControlColumns(), + }); + + const customLeadingControlColumn = + controlColumnsConfig?.leadingControlColumns ?? leadingControlColumns; + const customTrailingControlColumn = + controlColumnsConfig?.trailingControlColumns ?? trailingControlColumns; const additionalControls = useMemo(() => { if (!externalAdditionalControls && !usedSelectedDocs.length) { @@ -907,7 +926,7 @@ export const UnifiedDataTable = ({ columns={euiGridColumns} columnVisibility={columnsVisibility} data-test-subj="docTable" - leadingControlColumns={leadingControlColumns} + leadingControlColumns={customLeadingControlColumn} onColumnResize={onResize} pagination={paginationObj} renderCellValue={renderCellValue} @@ -921,7 +940,7 @@ export const UnifiedDataTable = ({ gridStyle={gridStyleOverride ?? GRID_STYLE} renderCustomGridBody={renderCustomGridBody} renderCustomToolbar={renderCustomToolbarFn} - trailingControlColumns={trailingControlColumns} + trailingControlColumns={customTrailingControlColumn} />
{loadingState !== DataLoadingState.loading && diff --git a/packages/kbn-unified-data-table/src/components/data_table_columns.tsx b/packages/kbn-unified-data-table/src/components/data_table_columns.tsx index 7393d3c034fa3..4c4208e30ec14 100644 --- a/packages/kbn-unified-data-table/src/components/data_table_columns.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_columns.tsx @@ -17,7 +17,7 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import { ToastsStart, IUiSettingsClient } from '@kbn/core/public'; import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import { ExpandButton } from './data_table_expand_button'; -import { CustomGridColumnsConfiguration, UnifiedDataTableSettings } from '../types'; +import { ControlColumns, CustomGridColumnsConfiguration, UnifiedDataTableSettings } from '../types'; import type { ValueToStringConverter, DataTableColumnTypes } from '../types'; import { buildCellActions } from './default_cell_actions'; import { getSchemaByKbnType } from './data_table_schema'; @@ -30,8 +30,11 @@ import { DataTableColumnHeader, DataTableTimeColumnHeader } from './data_table_c const DataTableColumnHeaderMemoized = React.memo(DataTableColumnHeader); const DataTableTimeColumnHeaderMemoized = React.memo(DataTableTimeColumnHeader); +export const OPEN_DETAILS = 'openDetails'; +export const SELECT_ROW = 'select'; + const openDetails = { - id: 'openDetails', + id: OPEN_DETAILS, width: 26, headerCellRender: () => ( @@ -46,7 +49,7 @@ const openDetails = { }; const select = { - id: 'select', + id: SELECT_ROW, width: 24, rowCellRender: SelectButton, headerCellRender: () => ( @@ -60,6 +63,13 @@ const select = { ), }; +export function getAllControlColumns(): ControlColumns { + return { + [SELECT_ROW]: select, + [OPEN_DETAILS]: openDetails, + }; +} + export function getLeadControlColumns(canSetExpandedDoc: boolean) { if (!canSetExpandedDoc) { return [select]; diff --git a/packages/kbn-unified-data-table/src/components/data_table_expand_button.tsx b/packages/kbn-unified-data-table/src/components/data_table_expand_button.tsx index c44ea74791b33..9538df4d3da03 100644 --- a/packages/kbn-unified-data-table/src/components/data_table_expand_button.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_expand_button.tsx @@ -11,6 +11,7 @@ import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@el import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { UnifiedDataTableContext } from '../table_context'; +import { DataTableRowControl } from './data_table_row_control'; /** * Button to expand a given row @@ -62,7 +63,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle } return ( -
+ -
+ ); }; diff --git a/packages/kbn-unified-data-table/src/components/data_table_row_control.tsx b/packages/kbn-unified-data-table/src/components/data_table_row_control.tsx new file mode 100644 index 0000000000000..4ceadea549dce --- /dev/null +++ b/packages/kbn-unified-data-table/src/components/data_table_row_control.tsx @@ -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. + */ + +import React from 'react'; + +export const DataTableRowControl = ({ children }: { children: React.ReactNode }) => { + return {children}; +}; diff --git a/packages/kbn-unified-data-table/src/types.ts b/packages/kbn-unified-data-table/src/types.ts index a27f1db510cbe..38d305d0c6177 100644 --- a/packages/kbn-unified-data-table/src/types.ts +++ b/packages/kbn-unified-data-table/src/types.ts @@ -11,6 +11,7 @@ import { EuiDataGridCellValueElementProps, type EuiDataGridColumn } from '@elast import type { DataTableRecord } from '@kbn/discover-utils/src/types'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { EuiDataGridControlColumn } from '@elastic/eui/src/components/datagrid/data_grid_types'; /** * User configurable state of data grid, persisted in saved search @@ -55,3 +56,17 @@ export type CustomGridColumnsConfiguration = Record< string, (props: CustomGridColumnProps) => EuiDataGridColumn >; + +export interface ControlColumns { + select: EuiDataGridControlColumn; + openDetails: EuiDataGridControlColumn; +} + +export interface ControlColumnsProps { + controlColumns: ControlColumns; +} + +export type CustomControlColumnConfiguration = (props: ControlColumnsProps) => { + leadingControlColumns: EuiDataGridControlColumn[]; + trailingControlColumns?: EuiDataGridControlColumn[]; +}; diff --git a/packages/kbn-utils/index.ts b/packages/kbn-utils/index.ts index d23c71fad55bc..492f6d321ef0e 100644 --- a/packages/kbn-utils/index.ts +++ b/packages/kbn-utils/index.ts @@ -5,6 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +export * from './src/json'; export * from './src/path'; export * from './src/streams'; diff --git a/packages/kbn-utils/src/json/index.test.ts b/packages/kbn-utils/src/json/index.test.ts new file mode 100644 index 0000000000000..d8a81883e3826 --- /dev/null +++ b/packages/kbn-utils/src/json/index.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { prettyPrintAndSortKeys } from '.'; + +describe('prettyPrintAndSortKeys', () => { + it('pretty prints JSON and consistently sorts the keys', () => { + const object1 = { + zap: { z: 1, b: 2 }, + foo: 'foo', + bar: 'bar', + }; + const object2 = { + foo: 'foo', + zap: { z: 1, b: 2 }, + bar: 'bar', + }; + expect(prettyPrintAndSortKeys(object1)).toMatchInlineSnapshot(` + "{ + \\"bar\\": \\"bar\\", + \\"foo\\": \\"foo\\", + \\"zap\\": { + \\"b\\": 2, + \\"z\\": 1 + } + }" + `); + + expect(prettyPrintAndSortKeys(object1)).toEqual(prettyPrintAndSortKeys(object2)); + }); + + it('pretty prints and sorts nested objects with arrays', () => { + const object = { + zap: { + a: { + b: 1, + a: 2, + }, + }, + foo: 'foo', + bar: 'bar', + baz: [ + { + c: 2, + b: 1, + }, + { + c: { + b: 1, + a: 2, + }, + }, + ], + }; + expect(prettyPrintAndSortKeys(object)).toMatchInlineSnapshot(` + "{ + \\"bar\\": \\"bar\\", + \\"baz\\": [ + { + \\"b\\": 1, + \\"c\\": 2 + }, + { + \\"c\\": { + \\"a\\": 2, + \\"b\\": 1 + } + } + ], + \\"foo\\": \\"foo\\", + \\"zap\\": { + \\"a\\": { + \\"a\\": 2, + \\"b\\": 1 + } + } + }" + `); + }); +}); diff --git a/packages/kbn-utils/src/json/index.ts b/packages/kbn-utils/src/json/index.ts new file mode 100644 index 0000000000000..7a8afd42283ae --- /dev/null +++ b/packages/kbn-utils/src/json/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +/** + * Given a JS object, will return a JSON.stringified result with consistently + * sorted keys. + */ +export function prettyPrintAndSortKeys(object: object): string { + const keys = new Set(); + JSON.stringify(object, (key, value) => { + keys.add(key); + return value; + }); + return JSON.stringify(object, Array.from(keys).sort(), 2); +} diff --git a/packages/serverless/settings/search_project/index.ts b/packages/serverless/settings/search_project/index.ts index a26c658501617..5a16690fd5f76 100644 --- a/packages/serverless/settings/search_project/index.ts +++ b/packages/serverless/settings/search_project/index.ts @@ -7,5 +7,9 @@ */ import { COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX_ID } from '@kbn/management-settings-ids'; +import { ENABLE_DOCKED_CONSOLE_UI_SETTING_ID } from '@kbn/dev-tools-plugin/common'; -export const SEARCH_PROJECT_SETTINGS = [COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX_ID]; +export const SEARCH_PROJECT_SETTINGS = [ + COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX_ID, + ENABLE_DOCKED_CONSOLE_UI_SETTING_ID, +]; diff --git a/packages/serverless/settings/search_project/tsconfig.json b/packages/serverless/settings/search_project/tsconfig.json index 16d6022e3d9bc..7d290d255a784 100644 --- a/packages/serverless/settings/search_project/tsconfig.json +++ b/packages/serverless/settings/search_project/tsconfig.json @@ -15,5 +15,6 @@ ], "kbn_references": [ "@kbn/management-settings-ids", + "@kbn/dev-tools-plugin", ] } diff --git a/src/core/server/integration_tests/elasticsearch/error_logging.test.ts b/src/core/server/integration_tests/elasticsearch/error_logging.test.ts index f8deaa41ee862..8201851530e30 100644 --- a/src/core/server/integration_tests/elasticsearch/error_logging.test.ts +++ b/src/core/server/integration_tests/elasticsearch/error_logging.test.ts @@ -11,6 +11,7 @@ import { type TestElasticsearchUtils, type TestKibanaUtils, } from '@kbn/core-test-helpers-kbn-server'; +import { unsafeConsole } from '@kbn/security-hardening'; describe('Error logging', () => { describe('ES client errors', () => { @@ -19,7 +20,7 @@ describe('Error logging', () => { let kibanaServer: TestKibanaUtils; beforeAll(async () => { - mockConsoleLog = jest.spyOn(global.console, 'log'); + mockConsoleLog = jest.spyOn(unsafeConsole, 'log'); const { startES, startKibana } = createTestServers({ adjustTimeout: jest.setTimeout, diff --git a/src/core/server/integration_tests/elasticsearch/version_compatibility.test.ts b/src/core/server/integration_tests/elasticsearch/version_compatibility.test.ts index 54bd5c0fc4a9b..eb6cc767d5817 100644 --- a/src/core/server/integration_tests/elasticsearch/version_compatibility.test.ts +++ b/src/core/server/integration_tests/elasticsearch/version_compatibility.test.ts @@ -15,6 +15,7 @@ import { esTestConfig } from '@kbn/test'; import { firstValueFrom, Subject } from 'rxjs'; import { CliArgs } from '@kbn/config'; import Semver from 'semver'; +import { unsafeConsole } from '@kbn/security-hardening'; function nextMinor() { return Semver.inc(esTestConfig.getVersion(), 'minor') || '10.0.0'; @@ -36,7 +37,7 @@ describe('Version Compatibility', () => { let consoleSpy: jest.SpyInstance; beforeEach(() => { - consoleSpy = jest.spyOn(console, 'log'); + consoleSpy = jest.spyOn(unsafeConsole, 'log'); }); afterEach(async () => { diff --git a/src/core/server/integration_tests/http/logging.test.ts b/src/core/server/integration_tests/http/logging.test.ts index dfd29256b4b3e..c00c3ac9825a9 100644 --- a/src/core/server/integration_tests/http/logging.test.ts +++ b/src/core/server/integration_tests/http/logging.test.ts @@ -8,12 +8,13 @@ import { schema } from '@kbn/config-schema'; import { createRoot, request } from '@kbn/core-test-helpers-kbn-server'; +import { unsafeConsole } from '@kbn/security-hardening'; describe('request logging', () => { let mockConsoleLog: jest.SpyInstance; beforeAll(() => { - mockConsoleLog = jest.spyOn(global.console, 'log'); + mockConsoleLog = jest.spyOn(unsafeConsole, 'log'); }); afterEach(() => { diff --git a/src/core/server/integration_tests/logging/logging.test.ts b/src/core/server/integration_tests/logging/logging.test.ts index ffae94adc624f..b7b972b6f3530 100644 --- a/src/core/server/integration_tests/logging/logging.test.ts +++ b/src/core/server/integration_tests/logging/logging.test.ts @@ -10,6 +10,7 @@ import type { LoggerContextConfigInput } from '@kbn/core-logging-server'; import { createRoot as createkbnTestServerRoot } from '@kbn/core-test-helpers-kbn-server'; import { InternalCoreSetup } from '@kbn/core-lifecycle-server-internal'; import { Subject } from 'rxjs'; +import { unsafeConsole } from '@kbn/security-hardening'; function createRoot() { return createkbnTestServerRoot({ @@ -45,7 +46,7 @@ describe('logging service', () => { let root: ReturnType; let mockConsoleLog: jest.SpyInstance; beforeAll(async () => { - mockConsoleLog = jest.spyOn(global.console, 'log'); + mockConsoleLog = jest.spyOn(unsafeConsole, 'log'); root = createRoot(); await root.preboot(); @@ -148,7 +149,7 @@ describe('logging service', () => { }; beforeAll(async () => { - mockConsoleLog = jest.spyOn(global.console, 'log'); + mockConsoleLog = jest.spyOn(unsafeConsole, 'log'); root = createRoot(); await root.preboot(); diff --git a/src/core/server/integration_tests/node/logging.test.ts b/src/core/server/integration_tests/node/logging.test.ts index e5158e6d2d8c4..2041688652dbf 100644 --- a/src/core/server/integration_tests/node/logging.test.ts +++ b/src/core/server/integration_tests/node/logging.test.ts @@ -7,6 +7,7 @@ */ import { createRoot as createkbnTestServerRoot } from '@kbn/core-test-helpers-kbn-server'; +import { unsafeConsole } from '@kbn/security-hardening'; function createRootWithRoles(roles: string[]) { return createkbnTestServerRoot({ @@ -39,7 +40,7 @@ describe('node service global context', () => { let mockConsoleLog: jest.SpyInstance; beforeAll(async () => { - mockConsoleLog = jest.spyOn(global.console, 'log'); + mockConsoleLog = jest.spyOn(unsafeConsole, 'log'); root = createRootWithRoles(roles); await root.preboot(); diff --git a/src/core/server/integration_tests/status/routes/status.test.ts b/src/core/server/integration_tests/status/routes/status.test.ts index 755b9fc9b46f6..29a55743dd0d5 100644 --- a/src/core/server/integration_tests/status/routes/status.test.ts +++ b/src/core/server/integration_tests/status/routes/status.test.ts @@ -210,6 +210,7 @@ describe('GET /api/status', () => { build_number: 1234, build_snapshot: true, build_date: new Date('2023-05-15T23:12:09.000Z').toISOString(), + build_flavor: 'traditional', }); const metricsMockValue = await firstValueFrom(metrics.getOpsMetrics$()); expect(result.body.metrics).toEqual({ diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index 06e6cf68d3a94..67aeaa971d102 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -157,6 +157,7 @@ "@kbn/core-plugins-contracts-server", "@kbn/dev-utils", "@kbn/server-http-tools", + "@kbn/security-hardening", "@kbn/core-base-server-mocks", ], "exclude": [ diff --git a/src/dev/build/lib/build.test.ts b/src/dev/build/lib/build.test.ts index 1292f1439642e..76fa06b04affa 100644 --- a/src/dev/build/lib/build.test.ts +++ b/src/dev/build/lib/build.test.ts @@ -30,6 +30,7 @@ const config = new Config( { buildNumber: 1234, buildSha: 'abcd1234', + buildShaShort: 'abcd', buildVersion: '8.0.0', buildDate: '2023-05-15T23:12:09+0000', }, diff --git a/src/dev/build/lib/config.ts b/src/dev/build/lib/config.ts index 19fbd6ae8bd37..ced8e10afab4b 100644 --- a/src/dev/build/lib/config.ts +++ b/src/dev/build/lib/config.ts @@ -228,6 +228,13 @@ export class Config { return this.versionInfo.buildSha; } + /** + * Get the first 12 digits of the git sha for this build + */ + getBuildShaShort() { + return this.versionInfo.buildShaShort; + } + /** * Get the ISO 8601 date for this build */ diff --git a/src/dev/build/lib/version_info.ts b/src/dev/build/lib/version_info.ts index 305eb54c9a491..3f79f85f8d76b 100644 --- a/src/dev/build/lib/version_info.ts +++ b/src/dev/build/lib/version_info.ts @@ -36,6 +36,7 @@ export async function getVersionInfo({ isRelease, versionQualifier, pkg }: Optio return { buildSha, + buildShaShort: buildSha.slice(0, 12), buildVersion, buildNumber: await getBuildNumber(), buildDate: new Date().toISOString(), diff --git a/src/dev/build/tasks/create_cdn_assets_task.ts b/src/dev/build/tasks/create_cdn_assets_task.ts index e03e1f6775672..a9ec8beb0955c 100644 --- a/src/dev/build/tasks/create_cdn_assets_task.ts +++ b/src/dev/build/tasks/create_cdn_assets_task.ts @@ -23,10 +23,10 @@ export const CreateCdnAssets: Task = { async run(config, log, build) { const buildSource = build.resolvePath(); - const buildNum = config.getBuildNumber(); + const buildSha = config.getBuildShaShort(); const buildVersion = config.getBuildVersion(); const assets = config.resolveFromRepo('build', 'cdn-assets'); - const bundles = resolve(assets, String(buildNum), 'bundles'); + const bundles = resolve(assets, buildSha, 'bundles'); await del(assets); await mkdirp(assets); @@ -83,7 +83,7 @@ export const CreateCdnAssets: Task = { // packages/core/apps/core-apps-server-internal/src/core_app.ts await copyAll( resolve(buildSource, 'node_modules/@kbn/core-apps-server-internal/assets'), - resolve(assets, 'ui') + resolve(assets, buildSha, 'ui') ); await compressTar({ diff --git a/src/dev/build/tasks/fetch_agent_versions_list.test.ts b/src/dev/build/tasks/fetch_agent_versions_list.test.ts index 2604dadf3302e..e27442beff0b4 100644 --- a/src/dev/build/tasks/fetch_agent_versions_list.test.ts +++ b/src/dev/build/tasks/fetch_agent_versions_list.test.ts @@ -34,6 +34,7 @@ const config = new Config( { buildNumber: 1234, buildSha: 'abcd1234', + buildShaShort: 'abcd', buildVersion: '8.0.0', buildDate: '2023-05-15T23:12:09.000Z', }, diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile index b02efe3f5b5b3..d0bf01692ae8f 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile @@ -137,8 +137,8 @@ RUN set -e ; \ chown -R 1000:0 /usr/share/kibana/openssl-3.0.8 # Enable FIPS for Kibana only. In the future we can override OS wide with ENV OPENSSL_CONF -RUN echo -e '\n--enable-fips' >> config/node.options -RUN echo '--openssl-config=/usr/share/kibana/openssl-3.0.8/nodejs.cnf' >> config/node.options +RUN /usr/bin/echo -e '\n--enable-fips' >> config/node.options +RUN /usr/bin/echo '--openssl-config=/usr/share/kibana/openssl-3.0.8/nodejs.cnf' >> config/node.options COPY --chown=1000:0 openssl/nodejs.cnf /usr/share/kibana/openssl-3.0.8/nodejs.cnf ENV OPENSSL_MODULES=/usr/local/lib64/ossl-modules @@ -158,8 +158,8 @@ COPY --chown=1000:0 config/serverless.es.yml /usr/share/kibana/config/serverless COPY --chown=1000:0 config/serverless.oblt.yml /usr/share/kibana/config/serverless.oblt.yml COPY --chown=1000:0 config/serverless.security.yml /usr/share/kibana/config/serverless.security.yml # Supportability enhancement: enable capturing heap snapshots. See https://nodejs.org/api/cli.html#--heapsnapshot-signalsignal -RUN echo -e '\n--heapsnapshot-signal=SIGUSR2' >> config/node.options -RUN echo '--diagnostic-dir=./data' >> config/node.options +RUN /usr/bin/echo -e '\n--heapsnapshot-signal=SIGUSR2' >> config/node.options +RUN /usr/bin/echo '--diagnostic-dir=./data' >> config/node.options {{/serverless}} {{^opensslLegacyProvider}} RUN sed 's/\(--openssl-legacy-provider\)/#\1/' -i config/node.options @@ -221,7 +221,7 @@ ENTRYPOINT ["/bin/tini", "--"] CMD ["/app/kibana.sh"] # Generate a stub command that will be overwritten at runtime RUN mkdir /app && \ - echo -e '#!/bin/bash\nexec /usr/local/bin/kibana-docker' > /app/kibana.sh && \ + /usr/bin/echo -e '#!/bin/bash\nexec /usr/local/bin/kibana-docker' > /app/kibana.sh && \ chmod 0555 /app/kibana.sh {{/cloud}} diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index 436fc25abf955..6b1be5c45934a 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -85,7 +85,7 @@ export const LICENSE_OVERRIDES = { 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint '@elastic/ems-client@8.5.1': ['Elastic License 2.0'], - '@elastic/eui@92.2.1': ['SSPL-1.0 OR Elastic License 2.0'], + '@elastic/eui@93.0.0': ['SSPL-1.0 OR Elastic License 2.0'], 'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry 'buffers@0.1.1': ['MIT'], // license in importing module https://www.npmjs.com/package/binary '@bufbuild/protobuf@1.2.1': ['Apache-2.0'], // license (Apache-2.0 AND BSD-3-Clause) diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index f7516711490da..e311ca1d1cc5d 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -108,7 +108,7 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { } const readQueryParams = () => { - const [, queryString] = (window.location.hash || '').split('?'); + const [, queryString] = (window.location.hash || window.location.search || '').split('?'); return parse(queryString || '', { sort: false }) as Required; }; diff --git a/src/plugins/console/public/application/containers/embeddable/_variables.scss b/src/plugins/console/public/application/containers/embeddable/_variables.scss index b987037af4d62..33ecd64b999c9 100644 --- a/src/plugins/console/public/application/containers/embeddable/_variables.scss +++ b/src/plugins/console/public/application/containers/embeddable/_variables.scss @@ -2,7 +2,7 @@ $embeddableConsoleBackground: lightOrDarkTheme($euiColorDarkestShade, $euiColorI $embeddableConsoleText: lighten(makeHighContrastColor($euiColorLightestShade, $embeddableConsoleBackground), 20%); $embeddableConsoleBorderColor: transparentize($euiColorGhost, .8); $embeddableConsoleInitialHeight: $euiSizeXXL; -$embeddableConsoleMaxHeight: calc(100vh - #{$euiSize * 5}); +$embeddableConsoleMaxHeight: calc(100vh - var(--euiFixedHeadersOffset, 0)); // Pixel heights ensure no blurriness caused by half pixel offsets $embeddableConsoleHeights: ( diff --git a/src/plugins/console/public/application/containers/embeddable/console_wrapper.tsx b/src/plugins/console/public/application/containers/embeddable/console_wrapper.tsx index 340b20c91b6cd..3618194e194dc 100644 --- a/src/plugins/console/public/application/containers/embeddable/console_wrapper.tsx +++ b/src/plugins/console/public/application/containers/embeddable/console_wrapper.tsx @@ -14,8 +14,10 @@ import { I18nStart, CoreTheme, DocLinksStart, + CoreStart, } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; +import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; import { ObjectStorageClient } from '../../../../common/types'; @@ -62,10 +64,10 @@ interface ConsoleDependencies { trackUiMetric: MetricsTracker; } -const loadDependencies = async ({ - core, - usageCollection, -}: EmbeddableConsoleDependencies): Promise => { +const loadDependencies = async ( + core: CoreStart, + usageCollection?: UsageCollectionStart +): Promise => { const { docLinks: { DOC_LINK_VERSION, links }, http, @@ -107,10 +109,12 @@ const loadDependencies = async ({ }; }; -export const ConsoleWrapper = (props: EmbeddableConsoleDependencies): React.ReactElement => { +type ConsoleWrapperProps = Omit; + +export const ConsoleWrapper: React.FunctionComponent = (props) => { const [dependencies, setDependencies] = useState(null); useEffect(() => { - loadDependencies(props).then(setDependencies); + loadDependencies(props.core, props.usageCollection).then(setDependencies); }, [setDependencies, props]); useEffect(() => { return () => { diff --git a/src/plugins/console/public/application/containers/embeddable/embeddable_console.tsx b/src/plugins/console/public/application/containers/embeddable/embeddable_console.tsx index 2577c9d4841d7..2167ec12c52c0 100644 --- a/src/plugins/console/public/application/containers/embeddable/embeddable_console.tsx +++ b/src/plugins/console/public/application/containers/embeddable/embeddable_console.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState } from 'react'; +import React, { useReducer, useEffect } from 'react'; import classNames from 'classnames'; import useObservable from 'react-use/lib/useObservable'; import { @@ -25,6 +25,9 @@ import { EmbeddableConsoleDependencies, } from '../../../types/embeddable_console'; +import * as store from '../../stores/embeddable_console'; +import { setLoadFromParameter, removeLoadFromParameter } from '../../lib/load_from'; + import { ConsoleWrapper } from './console_wrapper'; import './_index.scss'; @@ -36,10 +39,31 @@ export const EmbeddableConsole = ({ size = 'm', core, usageCollection, + setDispatch, }: EmbeddableConsoleProps & EmbeddableConsoleDependencies) => { - const [isConsoleOpen, setIsConsoleOpen] = useState(false); - const toggleConsole = () => setIsConsoleOpen(!isConsoleOpen); + const [consoleState, consoleDispatch] = useReducer( + store.reducer, + store.initialValue, + (value) => ({ ...value }) + ); const chromeStyle = useObservable(core.chrome.getChromeStyle$()); + useEffect(() => { + setDispatch(consoleDispatch); + return () => setDispatch(null); + }, [setDispatch, consoleDispatch]); + useEffect(() => { + if (consoleState.isOpen && consoleState.loadFromContent) { + setLoadFromParameter(consoleState.loadFromContent); + } else if (!consoleState.isOpen) { + removeLoadFromParameter(); + } + }, [consoleState.isOpen, consoleState.loadFromContent]); + + const isConsoleOpen = consoleState.isOpen; + const setIsConsoleOpen = (value: boolean) => { + consoleDispatch(value ? { type: 'open' } : { type: 'close' }); + }; + const toggleConsole = () => setIsConsoleOpen(!isConsoleOpen); const onKeyDown = (event: any) => { if (event.key === keys.ESCAPE) { diff --git a/src/plugins/console/public/application/lib/load_from.test.ts b/src/plugins/console/public/application/lib/load_from.test.ts new file mode 100644 index 0000000000000..289718f46cf10 --- /dev/null +++ b/src/plugins/console/public/application/lib/load_from.test.ts @@ -0,0 +1,236 @@ +/* + * 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 { compressToEncodedURIComponent } from 'lz-string'; +import { setLoadFromParameter, removeLoadFromParameter } from './load_from'; + +const baseMockWindow = () => { + return { + history: { + pushState: jest.fn(), + }, + location: { + host: 'my-kibana.elastic.co', + pathname: '', + protocol: 'https:', + search: '', + hash: '', + }, + }; +}; +let windowSpy: jest.SpyInstance; +let mockWindow = baseMockWindow(); + +describe('load from lib', () => { + beforeEach(() => { + mockWindow = baseMockWindow(); + windowSpy = jest.spyOn(globalThis, 'window', 'get'); + windowSpy.mockImplementation(() => mockWindow); + }); + + afterEach(() => { + windowSpy.mockRestore(); + }); + + describe('setLoadFromParameter', () => { + it('adds load_from as expected', () => { + mockWindow.location = { + ...mockWindow.location, + pathname: '/foo/app/dev_tools', + hash: '#/console', + }; + const codeSnippet = 'GET /_stats'; + const expectedUrl = + 'https://my-kibana.elastic.co/foo/app/dev_tools#/console?load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgzgFwIYLkA'; + + setLoadFromParameter(codeSnippet); + expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1); + expect(mockWindow.history.pushState).toHaveBeenCalledWith( + { + path: expectedUrl, + }, + '', + expectedUrl + ); + }); + it('can replace an existing value', () => { + mockWindow.location = { + ...mockWindow.location, + pathname: '/foo/app/dev_tools', + hash: `#/console?load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgxgQwC5QJYDsAmq4FMDOQA`, + }; + const codeSnippet = 'GET /_stats'; + const expectedUrl = + 'https://my-kibana.elastic.co/foo/app/dev_tools#/console?load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgzgFwIYLkA'; + + setLoadFromParameter(codeSnippet); + expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1); + expect(mockWindow.history.pushState).toHaveBeenCalledWith( + { + path: expectedUrl, + }, + '', + expectedUrl + ); + }); + it('works with other query params', () => { + mockWindow.location = { + ...mockWindow.location, + pathname: '/foo/app/dev_tools', + hash: '#/console?foo=bar', + }; + const codeSnippet = 'GET /_stats'; + const expectedUrl = + 'https://my-kibana.elastic.co/foo/app/dev_tools#/console?foo=bar&load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgzgFwIYLkA'; + + setLoadFromParameter(codeSnippet); + expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1); + expect(mockWindow.history.pushState).toHaveBeenCalledWith( + { + path: expectedUrl, + }, + '', + expectedUrl + ); + }); + it('works with a non-hash route', () => { + mockWindow.location = { + ...mockWindow.location, + pathname: '/foo/app/enterprise_search/overview', + }; + const codeSnippet = 'GET /_stats'; + const expectedUrl = + 'https://my-kibana.elastic.co/foo/app/enterprise_search/overview?load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgzgFwIYLkA'; + + setLoadFromParameter(codeSnippet); + expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1); + expect(mockWindow.history.pushState).toHaveBeenCalledWith( + { + path: expectedUrl, + }, + '', + expectedUrl + ); + }); + it('works with a non-hash route and other params', () => { + mockWindow.location = { + ...mockWindow.location, + pathname: '/foo/app/enterprise_search/overview', + search: '?foo=bar', + }; + const codeSnippet = 'GET /_stats'; + const expectedUrl = + 'https://my-kibana.elastic.co/foo/app/enterprise_search/overview?foo=bar&load_from=data%3Atext%2Fplain%2COIUQKgBA9A%2BgzgFwIYLkA'; + + setLoadFromParameter(codeSnippet); + expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1); + expect(mockWindow.history.pushState).toHaveBeenCalledWith( + { + path: expectedUrl, + }, + '', + expectedUrl + ); + }); + }); + + describe('removeLoadFromParameter', () => { + it('leaves other params in place', () => { + mockWindow.location = { + ...mockWindow.location, + pathname: '/foo/app/dev_tools', + search: `?foo=bar&load_from=data:text/plain,${compressToEncodedURIComponent( + 'GET /_cat/indices' + )}`, + }; + + const expectedUrl = 'https://my-kibana.elastic.co/foo/app/dev_tools?foo=bar'; + + removeLoadFromParameter(); + expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1); + expect(mockWindow.history.pushState).toHaveBeenCalledWith( + { + path: expectedUrl, + }, + '', + expectedUrl + ); + }); + it('leaves other params with a hashroute', () => { + mockWindow.location = { + ...mockWindow.location, + pathname: '/foo/app/dev_tools', + hash: `#/console?foo=bar&load_from=data:text/plain,${compressToEncodedURIComponent( + 'GET /_cat/indices' + )}`, + }; + + const expectedUrl = 'https://my-kibana.elastic.co/foo/app/dev_tools#/console?foo=bar'; + + removeLoadFromParameter(); + expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1); + expect(mockWindow.history.pushState).toHaveBeenCalledWith( + { + path: expectedUrl, + }, + '', + expectedUrl + ); + }); + it('removes ? if load_from was the only param', () => { + mockWindow.location = { + ...mockWindow.location, + pathname: '/foo/app/dev_tools', + search: `?load_from=data:text/plain,${compressToEncodedURIComponent('GET /_cat/indices')}`, + }; + + const expectedUrl = 'https://my-kibana.elastic.co/foo/app/dev_tools'; + + removeLoadFromParameter(); + expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1); + expect(mockWindow.history.pushState).toHaveBeenCalledWith( + { + path: expectedUrl, + }, + '', + expectedUrl + ); + }); + it('removes ? if load_from was the only param in a hashroute', () => { + mockWindow.location = { + ...mockWindow.location, + pathname: '/foo/app/dev_tools', + hash: `#/console?load_from=data:text/plain,${compressToEncodedURIComponent( + 'GET /_cat/indices' + )}`, + }; + + const expectedUrl = 'https://my-kibana.elastic.co/foo/app/dev_tools#/console'; + + removeLoadFromParameter(); + expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1); + expect(mockWindow.history.pushState).toHaveBeenCalledWith( + { + path: expectedUrl, + }, + '', + expectedUrl + ); + }); + it('noop if load_from not currently defined on QS', () => { + mockWindow.location = { + ...mockWindow.location, + pathname: '/foo/app/dev_tools', + hash: `#/console?foo=bar`, + }; + + removeLoadFromParameter(); + expect(mockWindow.history.pushState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/plugins/console/public/application/lib/load_from.ts b/src/plugins/console/public/application/lib/load_from.ts new file mode 100644 index 0000000000000..c601eafb6f3a9 --- /dev/null +++ b/src/plugins/console/public/application/lib/load_from.ts @@ -0,0 +1,49 @@ +/* + * 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 qs from 'query-string'; +import { compressToEncodedURIComponent } from 'lz-string'; + +function getBaseUrl() { + return `${window.location.protocol}//${window.location.host}${window.location.pathname}`; +} +function parseQueryString() { + const [hashRoute, queryString] = (window.location.hash || window.location.search || '').split( + '?' + ); + + const parsedQueryString = qs.parse(queryString || '', { sort: false }); + return { + hasHash: !!window.location.hash, + hashRoute, + queryString: parsedQueryString, + }; +} + +export const setLoadFromParameter = (value: string) => { + const baseUrl = getBaseUrl(); + const { hasHash, hashRoute, queryString } = parseQueryString(); + const consoleDataUri = compressToEncodedURIComponent(value); + queryString.load_from = `data:text/plain,${consoleDataUri}`; + const params = `?${qs.stringify(queryString)}`; + const newUrl = hasHash ? `${baseUrl}${hashRoute}${params}` : `${baseUrl}${params}`; + + window.history.pushState({ path: newUrl }, '', newUrl); +}; + +export const removeLoadFromParameter = () => { + const baseUrl = getBaseUrl(); + const { hasHash, hashRoute, queryString } = parseQueryString(); + if (queryString.load_from) { + delete queryString.load_from; + + const params = Object.keys(queryString).length ? `?${qs.stringify(queryString)}` : ''; + const newUrl = hasHash ? `${baseUrl}${hashRoute}${params}` : `${baseUrl}${params}`; + window.history.pushState({ path: newUrl }, '', newUrl); + } +}; diff --git a/src/plugins/console/public/application/stores/embeddable_console.ts b/src/plugins/console/public/application/stores/embeddable_console.ts new file mode 100644 index 0000000000000..4bfb38d094c13 --- /dev/null +++ b/src/plugins/console/public/application/stores/embeddable_console.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Reducer } from 'react'; +import { produce } from 'immer'; +import { identity } from 'fp-ts/lib/function'; + +import { EmbeddedConsoleAction, EmbeddedConsoleStore } from '../../types/embeddable_console'; + +export const initialValue: EmbeddedConsoleStore = produce( + { + isOpen: false, + }, + identity +); + +export const reducer: Reducer = (state, action) => + produce(state, (draft) => { + switch (action.type) { + case 'open': + if (!state.isOpen) { + draft.isOpen = true; + draft.loadFromContent = action.payload?.content; + return; + } + break; + case 'close': + if (state.isOpen) { + draft.isOpen = false; + draft.loadFromContent = undefined; + return; + } + break; + } + return draft; + }); diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts index 711b4304af9b0..ca097322486ea 100644 --- a/src/plugins/console/public/plugin.ts +++ b/src/plugins/console/public/plugin.ts @@ -5,9 +5,9 @@ * 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 { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import { ENABLE_DOCKED_CONSOLE_UI_SETTING_ID } from '@kbn/dev-tools-plugin/public'; import { renderEmbeddableConsole } from './application/containers/embeddable'; import { @@ -20,10 +20,12 @@ import { EmbeddableConsoleProps, EmbeddableConsoleDependencies, } from './types'; -import { AutocompleteInfo, setAutocompleteInfo } from './services'; +import { AutocompleteInfo, setAutocompleteInfo, EmbeddableConsoleInfo } from './services'; export class ConsoleUIPlugin implements Plugin { private readonly autocompleteInfo = new AutocompleteInfo(); + private _embeddableConsole: EmbeddableConsoleInfo = new EmbeddableConsoleInfo(); + constructor(private ctx: PluginInitializerContext) {} public setup( @@ -108,19 +110,30 @@ export class ConsoleUIPlugin implements Plugin(); const consoleStart: ConsolePluginStart = {}; + const embeddedConsoleUiSetting = core.uiSettings.get( + ENABLE_DOCKED_CONSOLE_UI_SETTING_ID + ); const embeddedConsoleAvailable = isConsoleUiEnabled && isEmbeddedConsoleEnabled && - core.application.capabilities?.dev_tools?.show === true; + core.application.capabilities?.dev_tools?.show === true && + embeddedConsoleUiSetting; if (embeddedConsoleAvailable) { consoleStart.renderEmbeddableConsole = (props?: EmbeddableConsoleProps) => { const consoleDeps: EmbeddableConsoleDependencies = { core, usageCollection: deps.usageCollection, + setDispatch: (d) => { + this._embeddableConsole.setDispatch(d); + }, }; return renderEmbeddableConsole(props, consoleDeps); }; + consoleStart.isEmbeddedConsoleAvailable = () => + this._embeddableConsole.isEmbeddedConsoleAvailable(); + consoleStart.openEmbeddedConsole = (content?: string) => + this._embeddableConsole.openEmbeddedConsole(content); } return consoleStart; diff --git a/src/plugins/console/public/services/embeddable_console.test.ts b/src/plugins/console/public/services/embeddable_console.test.ts new file mode 100644 index 0000000000000..7df8230b6dbdf --- /dev/null +++ b/src/plugins/console/public/services/embeddable_console.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { EmbeddableConsoleInfo } from './embeddable_console'; + +describe('EmbeddableConsoleInfo', () => { + let eConsole: EmbeddableConsoleInfo; + beforeEach(() => { + eConsole = new EmbeddableConsoleInfo(); + }); + describe('isEmbeddedConsoleAvailable', () => { + it('returns true if dispatch has been set', () => { + eConsole.setDispatch(jest.fn()); + expect(eConsole.isEmbeddedConsoleAvailable()).toBe(true); + }); + it('returns false if dispatch has not been set', () => { + expect(eConsole.isEmbeddedConsoleAvailable()).toBe(false); + }); + it('returns false if dispatch has been cleared', () => { + eConsole.setDispatch(jest.fn()); + eConsole.setDispatch(null); + expect(eConsole.isEmbeddedConsoleAvailable()).toBe(false); + }); + }); + describe('openEmbeddedConsole', () => { + const mockDispatch = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + + eConsole.setDispatch(mockDispatch); + }); + it('dispatches open action', () => { + eConsole.openEmbeddedConsole(); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledWith({ type: 'open' }); + }); + it('can set content', () => { + eConsole.openEmbeddedConsole('GET /_cat/_indices'); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'open', + payload: { content: 'GET /_cat/_indices' }, + }); + }); + }); +}); diff --git a/src/plugins/console/public/services/embeddable_console.ts b/src/plugins/console/public/services/embeddable_console.ts new file mode 100644 index 0000000000000..6bc32b8475eef --- /dev/null +++ b/src/plugins/console/public/services/embeddable_console.ts @@ -0,0 +1,29 @@ +/* + * 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 { Dispatch } from 'react'; + +import { EmbeddedConsoleAction as EmbeddableConsoleAction } from '../types/embeddable_console'; + +export class EmbeddableConsoleInfo { + private _dispatch: Dispatch | null = null; + + public setDispatch(d: Dispatch | null) { + this._dispatch = d; + } + + public isEmbeddedConsoleAvailable(): boolean { + return this._dispatch !== null; + } + + public openEmbeddedConsole(content?: string) { + // Embedded Console is not rendered on the page, nothing to do + if (!this._dispatch) return; + + this._dispatch({ type: 'open', payload: content ? { content } : undefined }); + } +} diff --git a/src/plugins/console/public/services/index.ts b/src/plugins/console/public/services/index.ts index 73929f89e386f..669ed890729dc 100644 --- a/src/plugins/console/public/services/index.ts +++ b/src/plugins/console/public/services/index.ts @@ -16,3 +16,4 @@ export { setAutocompleteInfo, ENTITIES, } from './autocomplete'; +export { EmbeddableConsoleInfo } from './embeddable_console'; diff --git a/src/plugins/console/public/types/embeddable_console.ts b/src/plugins/console/public/types/embeddable_console.ts index da0e3346a7bd2..71a755b7dd544 100644 --- a/src/plugins/console/public/types/embeddable_console.ts +++ b/src/plugins/console/public/types/embeddable_console.ts @@ -7,6 +7,7 @@ */ import type { CoreStart } from '@kbn/core/public'; import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import type { Dispatch } from 'react'; /** * EmbeddableConsoleProps are optional props used when rendering the embeddable developer console. @@ -21,4 +22,14 @@ export interface EmbeddableConsoleProps { export interface EmbeddableConsoleDependencies { core: CoreStart; usageCollection?: UsageCollectionStart; + setDispatch: (dispatch: Dispatch | null) => void; +} + +export type EmbeddedConsoleAction = + | { type: 'open'; payload?: { content?: string } } + | { type: 'close' }; + +export interface EmbeddedConsoleStore { + isOpen: boolean; + loadFromContent?: string; } diff --git a/src/plugins/console/public/types/plugin_dependencies.ts b/src/plugins/console/public/types/plugin_dependencies.ts index e4f65d44cb727..199e59d4b9b92 100644 --- a/src/plugins/console/public/types/plugin_dependencies.ts +++ b/src/plugins/console/public/types/plugin_dependencies.ts @@ -47,4 +47,14 @@ export interface ConsolePluginStart { * render an embeddable version of the developer console on the page. */ renderEmbeddableConsole?: (props?: EmbeddableConsoleProps) => ReactElement | null; + /** + * isEmbeddedConsoleAvailable is available if the embedded console can be rendered. Returns true when + * called if the Embedded Console is currently rendered. + */ + isEmbeddedConsoleAvailable?: () => boolean; + /** + * openEmbeddedConsole is available if the embedded console can be rendered. Calling + * this function will open the embedded console on the page if it is currently rendered. + */ + openEmbeddedConsole?: (content?: string) => void; } diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx index 7df2dec31ae90..7fe5c9343aa64 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx @@ -255,6 +255,7 @@ export const EditorMenu = ({ const getEditorMenuPanels = (closePopover: () => void) => { const initialPanelItems = [ ...visTypeAliases.map(getVisTypeAliasMenuItem), + ...getAddPanelActionMenuItems(api, addPanelActions, closePopover), ...toolVisTypes.map(getVisTypeMenuItem), ...ungroupedFactories.map((factory) => { return getEmbeddableFactoryMenuItem(factory, closePopover); @@ -265,9 +266,7 @@ export const EditorMenu = ({ panel: panelId, 'data-test-subj': `dashboardEditorMenu-${id}Group`, })), - ...promotedVisTypes.map(getVisTypeMenuItem), - ...getAddPanelActionMenuItems(api, addPanelActions, closePopover), ]; if (aggsBasedVisTypes.length > 0) { initialPanelItems.push({ diff --git a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap index 6863540ad4a71..d478b18ef2ddb 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -20,13 +20,13 @@ exports[`DashboardEmptyScreen renders correctly with edit mode 1`] = ` class="emotion-euiPageSection__content-l-center" >
+

+ This dashboard is empty. Let’s fill it up! +

+
-

- This dashboard is empty. Let’s fill it up! -

-
-
- - Create a visualization of your data, or add one from the library. - -
+ + Create a visualization of your data, or add one from the library. +
+
+
+
-
-
- -
-
+ +
+
+ -
+ +
@@ -145,13 +141,13 @@ exports[`DashboardEmptyScreen renders correctly with readonly and edit mode 1`] class="emotion-euiPageSection__content-l-center" >
+

+ This dashboard is empty. +

+
-

- This dashboard is empty. -

-
-
- - You need additional privileges to edit this dashboard. - -
+ + You need additional privileges to edit this dashboard. +
@@ -219,13 +211,13 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` class="emotion-euiPageSection__content-l-center" >
+

+ This dashboard is empty. +

+
-

- This dashboard is empty. -

-
-
- - You need additional privileges to edit this dashboard. - -
+ + You need additional privileges to edit this dashboard. +
@@ -293,13 +281,13 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = ` class="emotion-euiPageSection__content-l-center" >
+

+ Add visualizations to your dashboard +

+
-

- Add visualizations to your dashboard -

-
-
- - Enter edit mode, and then start adding your visualizations. - -
+ + Enter edit mode, and then start adding your visualizations. +
-
- -
+ +
diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx index e7a26c4a6bcd2..4da2d77825f47 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx @@ -88,7 +88,6 @@ test('when showWriteControls is true, table list view is passed editing function createItem: expect.any(Function), deleteItems: expect.any(Function), editItem: expect.any(Function), - itemIsEditable: expect.any(Function), }), expect.any(Object) // react context ); diff --git a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx index f30ed1b21f1d9..5eb0d661ef473 100644 --- a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx @@ -147,7 +147,6 @@ describe('useDashboardListingTable', () => { initialPageSize: 5, listingLimit: 20, onFetchSuccess: expect.any(Function), - itemIsEditable: expect.any(Function), setPageDataTestSubject: expect.any(Function), title: 'Dashboard List', urlStateEnabled: false, diff --git a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx index 80b39ff0e9d62..92856edf92881 100644 --- a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx @@ -283,7 +283,6 @@ export const useDashboardListingTable = ({ createItem: !showWriteControls || !showCreateDashboardButton ? undefined : createItem, deleteItems: !showWriteControls ? undefined : deleteItems, editItem: !showWriteControls ? undefined : editItem, - itemIsEditable: () => showWriteControls, emptyPrompt, entityName, entityNamePlural, diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index 8bd049df006fc..837cee1a78aff 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -2,32 +2,28 @@ exports[`Inspector Data View component should render empty state 1`] = `
+

+ No data available +

+
-

- No data available -

-
-
-

- The element did not provide any data. -

-
+

+ The element did not provide any data. +

diff --git a/src/plugins/dev_tools/common/constants.ts b/src/plugins/dev_tools/common/constants.ts new file mode 100644 index 0000000000000..b5e7c9adde842 --- /dev/null +++ b/src/plugins/dev_tools/common/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +/** + * The UI Setting prefix and category for dev tools UI Settings + */ +export const DEV_TOOLS_FEATURE_ID = 'devTools'; +/** + * UI Setting ID for enabling / disabling the docked console in Kibana + */ +export const ENABLE_DOCKED_CONSOLE_UI_SETTING_ID = 'devTools:enableDockedConsole'; diff --git a/src/plugins/dev_tools/common/index.ts b/src/plugins/dev_tools/common/index.ts new file mode 100644 index 0000000000000..dc6ec6152e519 --- /dev/null +++ b/src/plugins/dev_tools/common/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 { DEV_TOOLS_FEATURE_ID, ENABLE_DOCKED_CONSOLE_UI_SETTING_ID } from './constants'; diff --git a/src/plugins/dev_tools/public/index.ts b/src/plugins/dev_tools/public/index.ts index 9e685ee4ff507..03f0981a4a845 100644 --- a/src/plugins/dev_tools/public/index.ts +++ b/src/plugins/dev_tools/public/index.ts @@ -12,6 +12,7 @@ import { PluginInitializerContext } from '@kbn/core/public'; import { DevToolsPlugin } from './plugin'; export * from './plugin'; +export * from '../common/constants'; export function plugin(initializerContext: PluginInitializerContext) { return new DevToolsPlugin(initializerContext); diff --git a/src/plugins/dev_tools/server/plugin.ts b/src/plugins/dev_tools/server/plugin.ts index f4157691e2a87..dbb4098f74e79 100644 --- a/src/plugins/dev_tools/server/plugin.ts +++ b/src/plugins/dev_tools/server/plugin.ts @@ -6,12 +6,17 @@ * Side Public License, v 1. */ -import { PluginInitializerContext, Plugin } from '@kbn/core/server'; +import { PluginInitializerContext, Plugin, CoreSetup } from '@kbn/core/server'; +import { uiSettings } from './ui_settings'; export class DevToolsServerPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} - public setup() { + public setup(core: CoreSetup) { + /** + * Register Dev Tools UI Settings + */ + core.uiSettings.register(uiSettings); return {}; } diff --git a/src/plugins/dev_tools/server/ui_settings.ts b/src/plugins/dev_tools/server/ui_settings.ts new file mode 100644 index 0000000000000..ff7d04d847b46 --- /dev/null +++ b/src/plugins/dev_tools/server/ui_settings.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '@kbn/core/types'; +import { i18n } from '@kbn/i18n'; + +import { DEV_TOOLS_FEATURE_ID, ENABLE_DOCKED_CONSOLE_UI_SETTING_ID } from '../common/constants'; + +/** + * uiSettings definitions for Dev Tools + */ +export const uiSettings: Record> = { + [ENABLE_DOCKED_CONSOLE_UI_SETTING_ID]: { + category: [DEV_TOOLS_FEATURE_ID], + description: i18n.translate('devTools.uiSettings.dockedConsole.description', { + defaultMessage: + 'Docks the Console in the Kibana UI. This setting does not affect the standard Console in Dev Tools.', + }), + name: i18n.translate('devTools.uiSettings.dockedConsole.name', { + defaultMessage: 'Docked Console', + }), + requiresPageReload: true, + schema: schema.boolean(), + value: true, + }, +}; diff --git a/src/plugins/dev_tools/tsconfig.json b/src/plugins/dev_tools/tsconfig.json index b8dee30d16e73..13e30f92c8d6e 100644 --- a/src/plugins/dev_tools/tsconfig.json +++ b/src/plugins/dev_tools/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "target/types", }, - "include": ["public/**/*", "server/**/*"], + "include": ["common/*", "public/**/*", "server/**/*"], "kbn_references": [ "@kbn/core", "@kbn/url-forwarding-plugin", diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index 524c01be98a5e..62c38fe43a3b7 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -120,10 +120,16 @@ describe('Discover documents layout', () => { }), }; + const customControlColumnsConfiguration = () => ({ + leadingControlColumns: [], + trailingControlColumns: [], + }); + const customization: DiscoverCustomization = { id: 'data_table', customCellRenderer, customGridColumnsConfiguration, + customControlColumnsConfiguration, }; customisationService.set(customization); @@ -135,5 +141,8 @@ describe('Discover documents layout', () => { expect(discoverGridComponent.prop('customGridColumnsConfiguration')).toEqual( customGridColumnsConfiguration ); + expect(discoverGridComponent.prop('customControlColumnsConfiguration')).toEqual( + customControlColumnsConfiguration + ); }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 636748a44123b..604d2b0a27d53 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -255,9 +255,11 @@ function DiscoverDocumentsComponent({ [dataView, onAddColumn, onAddFilter, onRemoveColumn, query, savedSearch.id, setExpandedDoc] ); - const externalCustomRenderers = useDiscoverCustomization('data_table')?.customCellRenderer; - const customGridColumnsConfiguration = - useDiscoverCustomization('data_table')?.customGridColumnsConfiguration; + const { + customCellRenderer: externalCustomRenderers, + customGridColumnsConfiguration, + customControlColumnsConfiguration, + } = useDiscoverCustomization('data_table') || {}; const documents = useObservable(stateContainer.dataState.data$.documents$); @@ -427,6 +429,7 @@ function DiscoverDocumentsComponent({ headerRowHeight={3} externalCustomRenderers={externalCustomRenderers} customGridColumnsConfiguration={customGridColumnsConfiguration} + customControlColumnsConfiguration={customControlColumnsConfiguration} /> diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index 985b0c303cd21..ee80e467e7fb4 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -19,6 +19,7 @@ import { RuleCreationValidConsumer, STACK_ALERTS_FEATURE_ID, } from '@kbn/rule-data-utils'; +import { RuleTypeMetaData } from '@kbn/alerting-plugin/common'; import { DiscoverStateContainer } from '../../services/discover_state'; import { DiscoverServices } from '../../../../build_services'; @@ -42,7 +43,7 @@ interface AlertsPopoverProps { isPlainRecord?: boolean; } -interface EsQueryAlertMetaData { +interface EsQueryAlertMetaData extends RuleTypeMetaData { isManagementPage?: boolean; adHocDataViewList: DataView[]; } @@ -110,11 +111,11 @@ export function AlertsPopover({ metadata: discoverMetadata, consumer: 'alerts', onClose: (_, metadata) => { - onFinishFlyoutInteraction(metadata as EsQueryAlertMetaData); + onFinishFlyoutInteraction(metadata!); onClose(); }, onSave: async (metadata) => { - onFinishFlyoutInteraction(metadata as EsQueryAlertMetaData); + onFinishFlyoutInteraction(metadata!); }, canChangeTrigger: false, ruleTypeId: ES_QUERY_ID, diff --git a/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts b/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts index ef88aba74d7db..ecae2208bffcc 100644 --- a/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts +++ b/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts @@ -8,6 +8,7 @@ import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { BehaviorSubject } from 'rxjs'; +import { cloneDeep } from 'lodash'; import { COMPARE_ALL_OPTIONS, FilterCompareOptions } from '@kbn/es-query'; import type { SearchSourceFields } from '@kbn/data-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common'; @@ -110,6 +111,11 @@ export interface DiscoverSavedSearchContainer { * @param params */ update: (params: UpdateParams) => SavedSearch; + /** + * Passes filter manager filters to saved search filters + * @param params + */ + updateWithFilterManagerFilters: () => SavedSearch; } export function getSavedSearchContainer({ @@ -169,6 +175,26 @@ export function getSavedSearchContainer({ } return { id }; }; + + const assignNextSavedSearch = ({ nextSavedSearch }: { nextSavedSearch: SavedSearch }) => { + const hasChanged = !isEqualSavedSearch(savedSearchInitial$.getValue(), nextSavedSearch); + hasChanged$.next(hasChanged); + savedSearchCurrent$.next(nextSavedSearch); + }; + + const updateWithFilterManagerFilters = () => { + const nextSavedSearch: SavedSearch = { + ...getState(), + }; + + nextSavedSearch.searchSource.setField('filter', cloneDeep(services.filterManager.getFilters())); + + assignNextSavedSearch({ nextSavedSearch }); + + addLog('[savedSearch] updateWithFilterManagerFilters done', nextSavedSearch); + return nextSavedSearch; + }; + const update = ({ nextDataView, nextState, useFilterAndQueryServices }: UpdateParams) => { addLog('[savedSearch] update', { nextDataView, nextState }); @@ -186,9 +212,7 @@ export function getSavedSearchContainer({ useFilterAndQueryServices, }); - const hasChanged = !isEqualSavedSearch(savedSearchInitial$.getValue(), nextSavedSearch); - hasChanged$.next(hasChanged); - savedSearchCurrent$.next(nextSavedSearch); + assignNextSavedSearch({ nextSavedSearch }); addLog('[savedSearch] update done', nextSavedSearch); return nextSavedSearch; @@ -221,6 +245,7 @@ export function getSavedSearchContainer({ persist, set, update, + updateWithFilterManagerFilters, }; } diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index af3675156a93d..79577a8f8e616 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -357,6 +357,7 @@ export function getDiscoverStateContainer({ dataStateContainer, internalStateContainer, savedSearchContainer, + globalStateContainer, services, setDataView, }); diff --git a/src/plugins/discover/public/application/main/services/load_saved_search.ts b/src/plugins/discover/public/application/main/services/load_saved_search.ts index a921e7a69e58c..30ed1792a50e0 100644 --- a/src/plugins/discover/public/application/main/services/load_saved_search.ts +++ b/src/plugins/discover/public/application/main/services/load_saved_search.ts @@ -22,6 +22,7 @@ import { DiscoverAppStateContainer, getInitialState, } from './discover_app_state_container'; +import { DiscoverGlobalStateContainer } from './discover_global_state_container'; import { DiscoverServices } from '../../../build_services'; interface LoadSavedSearchDeps { @@ -29,6 +30,7 @@ interface LoadSavedSearchDeps { dataStateContainer: DiscoverDataStateContainer; internalStateContainer: DiscoverInternalStateContainer; savedSearchContainer: DiscoverSavedSearchContainer; + globalStateContainer: DiscoverGlobalStateContainer; services: DiscoverServices; setDataView: DiscoverStateContainer['actions']['setDataView']; } @@ -44,7 +46,13 @@ export const loadSavedSearch = async ( ): Promise => { addLog('[discoverState] loadSavedSearch'); const { savedSearchId } = params ?? {}; - const { appStateContainer, internalStateContainer, savedSearchContainer, services } = deps; + const { + appStateContainer, + internalStateContainer, + savedSearchContainer, + globalStateContainer, + services, + } = deps; const appStateExists = !appStateContainer.isEmptyURL(); const appState = appStateExists ? appStateContainer.getState() : undefined; @@ -59,6 +67,15 @@ export const loadSavedSearch = async ( services.filterManager.setAppFilters([]); services.data.query.queryString.clearQuery(); + // Sync global filters (coming from URL) to filter manager. + // It needs to be done manually here as `syncGlobalQueryStateWithUrl` is being called after this `loadSavedSearch` function. + const globalFilters = globalStateContainer?.get()?.filters; + const shouldUpdateWithGlobalFilters = + globalFilters?.length && !services.filterManager.getGlobalFilters()?.length; + if (shouldUpdateWithGlobalFilters) { + services.filterManager.setGlobalFilters(globalFilters); + } + // reset appState in case a saved search with id is loaded and // the url is empty so the saved search is loaded in a clean // state else it might be updated by the previous app state @@ -103,6 +120,10 @@ export const loadSavedSearch = async ( // Update all other services and state containers by the next saved search updateBySavedSearch(nextSavedSearch, deps); + if (!appState && shouldUpdateWithGlobalFilters) { + nextSavedSearch = savedSearchContainer.updateWithFilterManagerFilters(); + } + return nextSavedSearch; }; @@ -125,6 +146,7 @@ function updateBySavedSearch(savedSearch: SavedSearch, deps: LoadSavedSearchDeps // set data service filters const filters = savedSearch.searchSource.getField('filter'); if (Array.isArray(filters) && filters.length) { + // Saved search SO persists all filters as app filters services.data.query.filterManager.setAppFilters(cloneDeep(filters)); } // some filters may not be valid for this context, so update @@ -134,6 +156,7 @@ function updateBySavedSearch(savedSearch: SavedSearch, deps: LoadSavedSearchDeps if (!isEqual(currentFilters, validFilters)) { services.filterManager.setFilters(validFilters); } + // set data service query const query = savedSearch.searchSource.getField('query'); if (query) { diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx b/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx index 01aabdba43fc5..45a870fbc2806 100644 --- a/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx +++ b/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx @@ -295,7 +295,7 @@ export const DiscoverTourStepFooterAction: React.FC<{ onClick={onFinishTour} data-test-subj="discoverTourButtonSkip" > - {EuiI18n({ token: 'core.euiTourStep.skipTour', default: 'Skip tour' })} + {EuiI18n({ token: 'core.euiTourFooter.skipTour', default: 'Skip tour' })} )} @@ -306,7 +306,7 @@ export const DiscoverTourStepFooterAction: React.FC<{ onClick={onFinishTour} data-test-subj="discoverTourButtonEnd" > - {EuiI18n({ token: 'core.euiTourStep.endTour', default: 'End tour' })} + {EuiI18n({ token: 'core.euiTourFooter.endTour', default: 'End tour' })} ) : ( - {EuiI18n({ token: 'core.euiTourStep.nextStep', default: 'Next' })} + {EuiI18n({ token: 'core.euiTourFooter.nextStep', default: 'Next' })} )} diff --git a/src/plugins/discover/public/customizations/customization_types/data_table_customisation.ts b/src/plugins/discover/public/customizations/customization_types/data_table_customisation.ts index 4e783a832f0b8..2ed371fd2109e 100644 --- a/src/plugins/discover/public/customizations/customization_types/data_table_customisation.ts +++ b/src/plugins/discover/public/customizations/customization_types/data_table_customisation.ts @@ -6,10 +6,15 @@ * Side Public License, v 1. */ -import { CustomCellRenderer, CustomGridColumnsConfiguration } from '@kbn/unified-data-table'; +import { + CustomCellRenderer, + CustomControlColumnConfiguration, + CustomGridColumnsConfiguration, +} from '@kbn/unified-data-table'; export interface DataTableCustomization { id: 'data_table'; customCellRenderer?: CustomCellRenderer; customGridColumnsConfiguration?: CustomGridColumnsConfiguration; + customControlColumnsConfiguration?: CustomControlColumnConfiguration; } diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 5fff8866e7fbc..b21a76fe9e596 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -17,6 +17,7 @@ export function plugin(initializerContext: PluginInitializerContext) { export type { ISearchEmbeddable, SearchInput } from './embeddable'; export type { DiscoverAppState } from './application/main/services/discover_app_state_container'; export type { DiscoverStateContainer } from './application/main/services/discover_state'; +export type { DataDocumentsMsg } from './application/main/services/discover_data_state_container'; export type { DiscoverContainerProps } from './components/discover_container'; export type { CustomizationCallback, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 12e419822c5c8..1269a68e41c17 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -628,4 +628,8 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'keyword', _meta: { description: 'Non-default value of setting.' }, }, + 'devTools:enableDockedConsole': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index f850103afb14b..177a01b81af19 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -166,4 +166,5 @@ export interface UsageStats { 'observability:profilingCostPervCPUPerHour': number; 'observability:profilingAWSCostDiscountRate': number; 'data_views:fields_excluded_data_tiers': string; + 'devTools:enableDockedConsole': boolean; } diff --git a/src/plugins/saved_objects_management/common/types/v1.ts b/src/plugins/saved_objects_management/common/types/v1.ts index 241188d035a6e..1c44559ee07c4 100644 --- a/src/plugins/saved_objects_management/common/types/v1.ts +++ b/src/plugins/saved_objects_management/common/types/v1.ts @@ -46,6 +46,7 @@ export interface SavedObjectWithMetadata { error?: SavedObjectError; created_at?: string; updated_at?: string; + managed?: boolean; attributes: T; namespaces?: string[]; references: SavedObjectReference[]; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.test.tsx index 8f3209c5fee1b..260ebfc5dc956 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.test.tsx @@ -15,17 +15,20 @@ import { DeleteConfirmModal } from './delete_confirm_modal'; interface CreateObjectOptions { namespaces?: string[]; hiddenType?: boolean; + managed?: boolean; } const createObject = ({ namespaces, hiddenType = false, + managed = false, }: CreateObjectOptions = {}): SavedObjectWithMetadata => ({ id: 'foo', type: 'bar', attributes: {}, references: [], namespaces, + managed, meta: { hiddenType, }, @@ -197,6 +200,34 @@ describe('DeleteConfirmModal', () => { }); }); + it('excludes the managed objects from the table and displays a callout', () => { + const objs = [ + createObject({ managed: true }), + createObject({ managed: false }), + createObject({ managed: true }), + createObject({ hiddenType: true }), + ]; + + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('.euiTableRow')).toHaveLength(1); + + const callout = findTestSubject(wrapper, 'cannotDeleteObjectsConfirmWarning'); + expect(callout).toHaveLength(1); + + expect(callout.text()).toMatchInlineSnapshot( + `"Some objects have been excluded1 object is hidden and cannot be deleted.2 objects are managed by Elastic and cannot be deleted."` + ); + }); + describe('shared objects warning', () => { it('does not display a callout when no objects are shared', () => { const objs = [ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx index a422526c7afad..83adac8880aab 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx @@ -28,6 +28,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; import type { SavedObjectWithMetadata, SavedObjectManagementTypeInfo } from '../../../../common'; import { getSavedObjectLabel } from '../../../lib'; @@ -48,12 +49,15 @@ export const DeleteConfirmModal: FC = ({ allowedTypes, showPlainSpinner, }) => { - const undeletableObjects = useMemo(() => { + const hiddenObjects = useMemo(() => { return selectedObjects.filter((obj) => obj.meta.hiddenType); }, [selectedObjects]); + const managedObjects = useMemo(() => { + return selectedObjects.filter((obj) => obj.managed); + }, [selectedObjects]); const deletableObjects = useMemo(() => { return selectedObjects - .filter((obj) => !obj.meta.hiddenType) + .filter((obj) => !obj.meta.hiddenType && !obj.managed) .map(({ type, id, meta, namespaces = [] }) => { const { title = '', icon = 'apps' } = meta; const isShared = namespaces.length > 1 || namespaces.includes('*'); @@ -84,26 +88,42 @@ export const DeleteConfirmModal: FC = ({ - {undeletableObjects.length > 0 && ( + {hiddenObjects.length + managedObjects.length > 0 && ( <> } iconType="warning" color="warning" > -

- -

+ {hiddenObjects.length > 0 && ( +

+ +

+ )} + + {managedObjects.length > 0 && ( +

+ +

+ )}
@@ -159,7 +179,7 @@ export const DeleteConfirmModal: FC = ({ field: 'id', name: i18n.translate( 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', - { defaultMessage: 'Id' } + { defaultMessage: 'ID' } ), }, { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx index 86f2b766002ac..4b72e11b0968e 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx @@ -14,6 +14,8 @@ import { httpServiceMock } from '@kbn/core/public/mocks'; import { actionServiceMock } from '../../../services/action_service.mock'; import { columnServiceMock } from '../../../services/column_service.mock'; import { Table, TableProps } from './table'; +import { render, screen, waitFor } from '@testing-library/react'; +import { I18nProvider } from '@kbn/i18n-react'; const defaultProps: TableProps = { basePath: httpServiceMock.createSetupContract().basePath, @@ -112,9 +114,9 @@ describe('Table', () => { it(`prevents saved objects from being deleted`, () => { const selectedSavedObjects = [ - { type: 'visualization' }, - { type: 'search' }, - { type: 'index-pattern' }, + { type: 'visualization', meta: { hiddenType: false } }, + { type: 'search', meta: { hiddenType: false } }, + { type: 'index-pattern', meta: { hiddenType: false } }, ] as any; const customizedProps = { ...defaultProps, @@ -154,4 +156,76 @@ describe('Table', () => { someAction.onClick(); expect(onActionRefresh).toHaveBeenCalled(); }); + + describe('managed content', () => { + it('keeps the delete button disabled when the selection only contains managed and hidden SOs', async () => { + const managedSavedObjects = [ + { type: 'visualization', managed: true, meta: { hiddenType: false } }, + { type: 'search', managed: true, meta: { hiddenType: false } }, + { type: 'index-pattern', managed: true, meta: { hiddenType: false } }, + ] as any; + + const hiddenSavedObjects = [ + { type: 'visualization', managed: false, meta: { hiddenType: true } }, + { type: 'search', managed: false, meta: { hiddenType: true } }, + { type: 'index-pattern', managed: false, meta: { hiddenType: true } }, + ] as any; + + const { rerender } = render( + + + + ); + + await waitFor(() => { + expect(screen.getByTestId('savedObjectsManagementDelete')).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Export' })).toBeEnabled(); + }); + + rerender( + +
+ + ); + + await waitFor(() => { + expect(screen.getByTestId('savedObjectsManagementDelete')).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Export' })).toBeEnabled(); + }); + + rerender( + +
+ + ); + + await waitFor(() => { + expect(screen.getByTestId('savedObjectsManagementDelete')).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Export' })).toBeEnabled(); + }); + }); + + it('enables the delete button when the selection contains at least one unmanaged, non-hidden SO', async () => { + const selectedSavedObjects = [ + { type: 'visualization', managed: true, meta: { hiddenType: false } }, + { type: 'search', managed: true, meta: { hiddenType: false } }, + { type: 'index-pattern', managed: false, meta: { hiddenType: true } }, + { type: 'lens', managed: false, meta: { hiddenType: false } }, // deletable! + ] as any; + + render( + +
+ + ); + + await waitFor(() => { + expect(screen.getByTestId('savedObjectsManagementDelete')).toBeEnabled(); + expect(screen.getByRole('button', { name: 'Export' })).toBeEnabled(); + }); + }); + }); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index 79e4d32643108..8015bf11d0585 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -402,7 +402,9 @@ export class Table extends PureComponent { color="danger" onClick={onDelete} isDisabled={ - selectedSavedObjects.length === 0 || !capabilities.savedObjectsManagement.delete + selectedSavedObjects.filter( + ({ managed, meta: { hiddenType } }) => !managed && !hiddenType + ).length === 0 || !capabilities.savedObjectsManagement.delete } title={ capabilities.savedObjectsManagement.delete diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index c4ec166799f3e..8b70c5ed2af66 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -538,6 +538,7 @@ describe('SavedObjectsTable', () => { { id: '1', type: 'index-pattern', meta: {} }, { id: '3', type: 'dashboard', meta: {} }, { id: '4', type: 'dashboard', meta: { hiddenType: false } }, + { id: '5', type: 'dashboard', managed: true, meta: {} }, ] as SavedObjectWithMetadata[]; const mockSavedObjects = mockSelectedSavedObjects.map((obj) => ({ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index e84d957f39239..e4daf584888c3 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -528,7 +528,7 @@ export class SavedObjectsTable extends Component !object.meta.hiddenType) + .filter((object) => !object.meta.hiddenType && !object.managed) .map(({ id, type }) => ({ id, type })) ); diff --git a/src/plugins/saved_objects_management/server/lib/to_saved_object_with_meta.ts b/src/plugins/saved_objects_management/server/lib/to_saved_object_with_meta.ts index 03a900b0ddc97..792cd3ccd9086 100644 --- a/src/plugins/saved_objects_management/server/lib/to_saved_object_with_meta.ts +++ b/src/plugins/saved_objects_management/server/lib/to_saved_object_with_meta.ts @@ -16,6 +16,7 @@ export function toSavedObjectWithMeta(so: SavedObject): SavedObjectWithMetadata namespaces: so.namespaces, references: so.references, updated_at: so.updated_at, + managed: so.managed, attributes: so.attributes, created_at: so.created_at, error: so.error, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 659c791645ca9..b5108f3d9269d 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -10257,6 +10257,12 @@ "_meta": { "description": "Non-default value of setting." } + }, + "devTools:enableDockedConsole": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } } } }, diff --git a/src/plugins/unified_search/public/filter_badge/filter_badge.tsx b/src/plugins/unified_search/public/filter_badge/filter_badge.tsx index 7b20eab971e9c..e20429d5e9f36 100644 --- a/src/plugins/unified_search/public/filter_badge/filter_badge.tsx +++ b/src/plugins/unified_search/public/filter_badge/filter_badge.tsx @@ -67,7 +67,7 @@ function FilterBadge({ `} > - {!hideAlias && filter.meta.alias !== null ? ( + {filter.meta.alias && !hideAlias ? ( <> {prefix} diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index 0e4020f5c70fa..dd9fb37258ed3 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -717,7 +717,7 @@ export const QueryBarTopRow = React.memo( errors={props.textBasedLanguageModeErrors} warning={props.textBasedLanguageModeWarning} detectTimestamp={detectTimestamp} - onTextLangQuerySubmit={() => + onTextLangQuerySubmit={async () => onSubmit({ query: queryRef.current, dateRange: dateRangeRef.current, diff --git a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap index fb9bdb537acf3..533cc7d1bba3f 100644 --- a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap +++ b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap @@ -6,14 +6,14 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = ` data-test-subj="visNoResult" >
-
- No results found -
+ No results found
diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts index 7ef70fb8fc9b6..b8fc2c3f1032a 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts @@ -61,16 +61,19 @@ export function mapHitSource( id, references, updatedAt, + managed, }: { attributes: SavedObjectAttributes; id: string; references: SavedObjectReference[]; updatedAt?: string; + managed?: boolean; } ) { const newAttributes: { id: string; references: SavedObjectReference[]; + managed?: boolean; url: string; savedObjectType?: string; editor?: { editUrl?: string }; @@ -86,6 +89,7 @@ export function mapHitSource( references, url: urlFor(id), updatedAt, + managed, ...attributes, }; diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx index 4145612132259..bf07e1151c298 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx @@ -64,6 +64,7 @@ const toTableListViewSavedObject = (savedObject: Record): Visua return { id: savedObject.id as string, updatedAt: savedObject.updatedAt as string, + managed: savedObject.managed as boolean, references: savedObject.references as Array<{ id: string; type: string; name: string }>, type: savedObject.savedObjectType as string, icon: savedObject.icon as string, @@ -90,7 +91,7 @@ type CustomTableViewProps = Pick< | 'editItem' | 'contentEditor' | 'emptyPrompt' - | 'itemIsEditable' + | 'rowItemActions' >; const useTableListViewProps = ( @@ -260,7 +261,8 @@ const useTableListViewProps = ( editItem, emptyPrompt: noItemsFragment, createItem: createNewVis, - itemIsEditable: ({ attributes: { readOnly } }) => visualizeCapabilities.save && !readOnly, + rowItemActions: ({ attributes: { readOnly } }) => + !visualizeCapabilities.save || readOnly ? { edit: { enabled: false } } : undefined, }; return props; diff --git a/src/setup_node_env/index.js b/src/setup_node_env/index.js index 176785e10246a..3efbab18ea3b4 100644 --- a/src/setup_node_env/index.js +++ b/src/setup_node_env/index.js @@ -14,3 +14,5 @@ require('./dns_ipv4_first'); require('@kbn/babel-register').install(); require('./polyfill'); + +require('@kbn/security-hardening'); diff --git a/src/setup_node_env/tsconfig.json b/src/setup_node_env/tsconfig.json index 931afbdfaf0a3..694f5928bc94c 100644 --- a/src/setup_node_env/tsconfig.json +++ b/src/setup_node_env/tsconfig.json @@ -13,6 +13,7 @@ "kbn_references": [ { "path": "../../tsconfig.json" }, "@kbn/babel-register", + "@kbn/security-hardening", ], "exclude": [ "target/**/*", diff --git a/test/functional/apps/discover/group1/_url_state.ts b/test/functional/apps/discover/group1/_url_state.ts index 027e767e8fe3d..e97ac332e8b6e 100644 --- a/test/functional/apps/discover/group1/_url_state.ts +++ b/test/functional/apps/discover/group1/_url_state.ts @@ -11,6 +11,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const deployment = getService('deployment'); const browser = getService('browser'); const log = getService('log'); const retry = getService('retry'); @@ -19,6 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); + const dataGrid = getService('dataGrid'); const PageObjects = getPageObjects([ 'common', 'discover', @@ -30,6 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const defaultSettings = { defaultIndex: 'logstash-*', + hideAnnouncements: true, }; describe('discover URL state', () => { @@ -117,5 +120,104 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); expect(await PageObjects.discover.getHitCount()).to.be('11,268'); }); + + it('should merge custom global filters with saved search filters', async () => { + await kibanaServer.uiSettings.update({ + 'timepicker:timeDefaults': + '{ "from": "Sep 18, 2015 @ 19:37:13.000", "to": "Sep 23, 2015 @ 02:30:09.000"}', + }); + await PageObjects.common.navigateToApp('discover'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await filterBar.addFilter({ + field: 'bytes', + operation: 'is between', + value: { from: '1000', to: '2000' }, + }); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('extension'); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('bytes'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + const totalHitsForOneFilter = '737'; + const totalHitsForTwoFilters = '137'; + + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter); + + await PageObjects.discover.saveSearch('testFilters'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter); + + await browser.refresh(); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter); + + const url = await browser.getCurrentUrl(); + const savedSearchIdMatch = url.match(/view\/([^?]+)\?/); + const savedSearchId = savedSearchIdMatch?.length === 2 ? savedSearchIdMatch[1] : null; + + expect(typeof savedSearchId).to.be('string'); + + await browser.openNewTab(); + await browser.get(`${deployment.getHostPort()}/app/discover#/view/${savedSearchId}`); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + expect(await dataGrid.getRowsText()).to.eql([ + 'Sep 22, 2015 @ 20:44:05.521jpg1,808', + 'Sep 22, 2015 @ 20:41:53.463png1,969', + 'Sep 22, 2015 @ 20:40:22.952jpg1,576', + 'Sep 22, 2015 @ 20:11:39.532png1,708', + 'Sep 22, 2015 @ 19:45:13.813php1,406', + ]); + + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter); + + await browser.openNewTab(); + await browser.get( + `${deployment.getHostPort()}/app/discover#/view/${savedSearchId}` + + "?_g=(filters:!(('$state':(store:globalState)," + + "meta:(alias:!n,disabled:!f,field:extension.raw,index:'logstash-*'," + + 'key:extension.raw,negate:!f,params:!(png,css),type:phrases,value:!(png,css)),' + + 'query:(bool:(minimum_should_match:1,should:!((match_phrase:(extension.raw:png)),' + + "(match_phrase:(extension.raw:css))))))),query:(language:kuery,query:'')," + + "refreshInterval:(pause:!t,value:60000),time:(from:'2015-09-19T06:31:44.000Z'," + + "to:'2015-09-23T18:31:44.000Z'))" + ); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + const filteredRows = [ + 'Sep 22, 2015 @ 20:41:53.463png1,969', + 'Sep 22, 2015 @ 20:11:39.532png1,708', + 'Sep 22, 2015 @ 18:50:22.335css1,841', + 'Sep 22, 2015 @ 18:40:32.329css1,945', + 'Sep 22, 2015 @ 18:13:35.361css1,752', + ]; + + expect(await dataGrid.getRowsText()).to.eql(filteredRows); + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForTwoFilters); + await testSubjects.existOrFail('unsavedChangesBadge'); + + await browser.refresh(); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + expect(await dataGrid.getRowsText()).to.eql(filteredRows); + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForTwoFilters); + await testSubjects.existOrFail('unsavedChangesBadge'); + }); }); } diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 653b0213bc430..98ac4d0abfe04 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -332,14 +332,16 @@ export class CommonPageObject extends FtrService { } currentUrl = (await this.browser.getCurrentUrl()).replace(/\/\/\w+:\w+@/, '//'); + const decodedAppUrl = decodeURIComponent(appUrl); + const decodedCurrentUrl = decodeURIComponent(currentUrl); - const navSuccessful = currentUrl + const navSuccessful = decodedCurrentUrl .replace(':80/', '/') .replace(':443/', '/') - .startsWith(appUrl.replace(':80/', '/').replace(':443/', '/')); + .startsWith(decodedAppUrl.replace(':80/', '/').replace(':443/', '/')); if (!navSuccessful) { - const msg = `App failed to load: ${appName} in ${this.defaultFindTimeout}ms appUrl=${appUrl} currentUrl=${currentUrl}`; + const msg = `App failed to load: ${appName} in ${this.defaultFindTimeout}ms appUrl=${decodedAppUrl} currentUrl=${decodedCurrentUrl}`; this.log.debug(msg); throw new Error(msg); } diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index 88b95fc8a4ab6..b139392c0f5c9 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -15,9 +15,11 @@ export interface TabbedGridData { columns: string[]; rows: string[][]; } + interface SelectOptions { isAnchorRow?: boolean; rowIndex?: number; + columnIndex?: number; renderMoreRows?: boolean; } @@ -242,13 +244,14 @@ export class DataGridService extends FtrService { } public async clickRowToggle( - options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } + options: SelectOptions = { isAnchorRow: false, rowIndex: 0, columnIndex: 0 } ): Promise { - const row = await this.getRow(options); + const rowColumns = await this.getRow(options); const testSubj = options.isAnchorRow ? '~docTableExpandToggleColumnAnchor' : '~docTableExpandToggleColumn'; - const toggle = await row[0].findByTestSubject(testSubj); + + const toggle = await rowColumns[options.columnIndex ?? 0].findByTestSubject(testSubj); await toggle.scrollIntoViewIfNecessary(); await toggle.click(); @@ -272,7 +275,20 @@ export class DataGridService extends FtrService { const cellText = await cell.getVisibleText(); textArr.push(cellText.trim()); } - return Promise.resolve(textArr); + return textArr; + } + + public async getControlColumnHeaderFields(): Promise { + const result = await this.find.allByCssSelector( + '.euiDataGridHeaderCell--controlColumn > .euiDataGridHeaderCell__content' + ); + + const textArr = []; + for (const cell of result) { + const cellText = await cell.getVisibleText(); + textArr.push(cellText.trim()); + } + return textArr; } public async getRowActions( @@ -393,6 +409,7 @@ export class DataGridService extends FtrService { const detailRows = await this.getDetailsRows(); return detailRows[0]; } + public async addInclusiveFilter(detailsRow: WebElementWrapper, fieldName: string): Promise { const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow); diff --git a/test/functional/services/listing_table.ts b/test/functional/services/listing_table.ts index f83de38fb468b..220abf4f41fc3 100644 --- a/test/functional/services/listing_table.ts +++ b/test/functional/services/listing_table.ts @@ -69,9 +69,14 @@ export class ListingTableService extends FtrService { private async getAllSelectableItemsNamesOnCurrentPage(): Promise { const visualizationNames = []; - const links = await this.find.allByCssSelector('.euiTableRow-isSelectable .euiLink'); - for (let i = 0; i < links.length; i++) { - visualizationNames.push(await links[i].getVisibleText()); + // TODO - use .euiTableRow-isSelectable when it's working again (https://github.com/elastic/eui/issues/7515) + const rows = await this.find.allByCssSelector('.euiTableRow'); + for (let i = 0; i < rows.length; i++) { + const checkbox = await rows[i].findByCssSelector('.euiCheckbox__input'); + if (await checkbox.isEnabled()) { + const link = await rows[i].findByCssSelector('.euiLink'); + visualizationNames.push(await link.getVisibleText()); + } } this.log.debug(`Found ${visualizationNames.length} selectable visualizations on current page`); return visualizationNames; @@ -165,6 +170,19 @@ export class ListingTableService extends FtrService { await inspectButtons[index].click(); } + public async inspectorFieldsReadonly() { + const disabledValues = await Promise.all([ + this.testSubjects.getAttribute('nameInput', 'readonly'), + this.testSubjects.getAttribute('descriptionInput', 'readonly'), + ]); + + return disabledValues.every((value) => value === 'true'); + } + + public async closeInspector() { + await this.testSubjects.click('closeFlyoutButton'); + } + /** * Edit Visualization title and description in the flyout */ diff --git a/tsconfig.base.json b/tsconfig.base.json index bb1f7b7877026..e496444be723e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1342,6 +1342,8 @@ "@kbn/searchprofiler-plugin/*": ["x-pack/plugins/searchprofiler/*"], "@kbn/security-api-integration-helpers": ["x-pack/test/security_api_integration/packages/helpers"], "@kbn/security-api-integration-helpers/*": ["x-pack/test/security_api_integration/packages/helpers/*"], + "@kbn/security-hardening": ["packages/kbn-security-hardening"], + "@kbn/security-hardening/*": ["packages/kbn-security-hardening/*"], "@kbn/security-plugin": ["x-pack/plugins/security"], "@kbn/security-plugin/*": ["x-pack/plugins/security/*"], "@kbn/security-plugin-types-common": ["x-pack/packages/security/plugin_types_common"], diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/message_processor/index.ts b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/message_processor/index.ts index 346a7451a6364..3eb97560cad73 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/message_processor/index.ts +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/message_processor/index.ts @@ -12,6 +12,7 @@ import { badHost } from './lib/events/bad_host'; import { weightedSample } from '../common/weighted_sample'; import { Doc, GeneratorFunction, EventFunction, EventTemplate } from '../../../types'; +import { addEphemeralProjectId } from '../../../lib/add_ephemeral_project_id'; let firstRun = true; @@ -31,7 +32,7 @@ function getTemplate(name: string) { return GOOD_EVENT_TEMPLATES; } -export const generateEvent: GeneratorFunction = (_config, schedule, _index, timestamp) => { +export const generateEvent: GeneratorFunction = (config, schedule, _index, timestamp) => { let startupEvents: Doc[] = []; if (firstRun) { firstRun = false; @@ -40,7 +41,10 @@ export const generateEvent: GeneratorFunction = (_config, schedule, _index, time const template = getTemplate(schedule.template); const fn = weightedSample(template) as EventFunction; - const events = fn(schedule, timestamp).flat(); + const events = addEphemeralProjectId( + config.indexing.ephemeralProjectIds || 0, + fn(schedule, timestamp).flat() + ); return [...startupEvents, ...events]; }; diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/fields/subset.yml b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/fields/subset.yml index dbc663c5949a1..87291dbb1a819 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/fields/subset.yml +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/fields/subset.yml @@ -7,6 +7,12 @@ fields: fields: level: {} logger: {} + http: + fields: + response: + fields: + status_code: {} + bytes: {} host: fields: name: {} diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/beats/fields.ecs.yml b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/beats/fields.ecs.yml index 9558edd94c2da..2dd03a644f0e4 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/beats/fields.ecs.yml +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/beats/fields.ecs.yml @@ -72,6 +72,26 @@ It can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.' + - name: http + title: HTTP + group: 2 + description: Fields related to HTTP activity. Use the `url` field set to store + the url of the request. + type: group + default_field: true + fields: + - name: response.bytes + level: extended + type: long + format: bytes + description: Total size in bytes of the response (body and headers). + example: 1437 + - name: response.status_code + level: extended + type: long + format: string + description: HTTP response status code. + example: 404 - name: log title: Log group: 2 diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/csv/fields.csv b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/csv/fields.csv index e66fb987788df..4a8d0e8275426 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/csv/fields.csv +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/csv/fields.csv @@ -4,5 +4,7 @@ ECS_Version,Indexed,Field_Set,Field,Type,Level,Normalization,Example,Description 8.0.0,true,base,message,match_only_text,core,,Hello World,Log message optimized for viewing in a log viewer. 8.0.0,true,base,tags,keyword,core,array,"[""production"", ""env2""]",List of keywords used to tag each event. 8.0.0,true,host,host.name,keyword,core,,,Name of the host. +8.0.0,true,http,http.response.bytes,long,extended,,1437,Total size in bytes of the response (body and headers). +8.0.0,true,http,http.response.status_code,long,extended,,404,HTTP response status code. 8.0.0,true,log,log.level,keyword,core,,error,Log level of the log event. 8.0.0,true,log,log.logger,keyword,core,,org.elasticsearch.bootstrap.Bootstrap,Name of the logger. diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/ecs_flat.yml b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/ecs_flat.yml index 3d22bd599c6ce..5fd84750e06d4 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/ecs_flat.yml +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/ecs_flat.yml @@ -30,6 +30,28 @@ host.name: normalize: [] short: Name of the host. type: keyword +http.response.bytes: + dashed_name: http-response-bytes + description: Total size in bytes of the response (body and headers). + example: 1437 + flat_name: http.response.bytes + format: bytes + level: extended + name: response.bytes + normalize: [] + short: Total size in bytes of the response (body and headers). + type: long +http.response.status_code: + dashed_name: http-response-status-code + description: HTTP response status code. + example: 404 + flat_name: http.response.status_code + format: string + level: extended + name: response.status_code + normalize: [] + short: HTTP response status code. + type: long labels: dashed_name: labels description: 'Custom key/value pairs. diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/ecs_nested.yml b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/ecs_nested.yml index 9066a8b73fcd7..b204a419a007d 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/ecs_nested.yml +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/ecs_nested.yml @@ -109,6 +109,38 @@ host: short: Fields describing the relevant computing instance. title: Host type: group +http: + description: Fields related to HTTP activity. Use the `url` field set to store the + url of the request. + fields: + http.response.bytes: + dashed_name: http-response-bytes + description: Total size in bytes of the response (body and headers). + example: 1437 + flat_name: http.response.bytes + format: bytes + level: extended + name: response.bytes + normalize: [] + short: Total size in bytes of the response (body and headers). + type: long + http.response.status_code: + dashed_name: http-response-status-code + description: HTTP response status code. + example: 404 + flat_name: http.response.status_code + format: string + level: extended + name: response.status_code + normalize: [] + short: HTTP response status code. + type: long + group: 2 + name: http + prefix: http. + short: Fields describing an HTTP request. + title: HTTP + type: group log: description: 'Details about the event''s logging mechanism or logging transport. diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/subset/nginx_proxy/ecs_flat.yml b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/subset/nginx_proxy/ecs_flat.yml index 3d22bd599c6ce..5fd84750e06d4 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/subset/nginx_proxy/ecs_flat.yml +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/subset/nginx_proxy/ecs_flat.yml @@ -30,6 +30,28 @@ host.name: normalize: [] short: Name of the host. type: keyword +http.response.bytes: + dashed_name: http-response-bytes + description: Total size in bytes of the response (body and headers). + example: 1437 + flat_name: http.response.bytes + format: bytes + level: extended + name: response.bytes + normalize: [] + short: Total size in bytes of the response (body and headers). + type: long +http.response.status_code: + dashed_name: http-response-status-code + description: HTTP response status code. + example: 404 + flat_name: http.response.status_code + format: string + level: extended + name: response.status_code + normalize: [] + short: HTTP response status code. + type: long labels: dashed_name: labels description: 'Custom key/value pairs. diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/subset/nginx_proxy/ecs_nested.yml b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/subset/nginx_proxy/ecs_nested.yml index 9066a8b73fcd7..b204a419a007d 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/subset/nginx_proxy/ecs_nested.yml +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/ecs/subset/nginx_proxy/ecs_nested.yml @@ -109,6 +109,38 @@ host: short: Fields describing the relevant computing instance. title: Host type: group +http: + description: Fields related to HTTP activity. Use the `url` field set to store the + url of the request. + fields: + http.response.bytes: + dashed_name: http-response-bytes + description: Total size in bytes of the response (body and headers). + example: 1437 + flat_name: http.response.bytes + format: bytes + level: extended + name: response.bytes + normalize: [] + short: Total size in bytes of the response (body and headers). + type: long + http.response.status_code: + dashed_name: http-response-status-code + description: HTTP response status code. + example: 404 + flat_name: http.response.status_code + format: string + level: extended + name: response.status_code + normalize: [] + short: HTTP response status code. + type: long + group: 2 + name: http + prefix: http. + short: Fields describing an HTTP request. + title: HTTP + type: group log: description: 'Details about the event''s logging mechanism or logging transport. diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/elasticsearch/composable/component/http.json b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/elasticsearch/composable/component/http.json new file mode 100644 index 0000000000000..819512388f79d --- /dev/null +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/elasticsearch/composable/component/http.json @@ -0,0 +1,26 @@ +{ + "_meta": { + "documentation": "https://www.elastic.co/guide/en/ecs/current/ecs-http.html", + "ecs_version": "8.0.0" + }, + "template": { + "mappings": { + "properties": { + "http": { + "properties": { + "response": { + "properties": { + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + } + } + } + } + } + } +} diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/elasticsearch/composable/template.json b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/elasticsearch/composable/template.json index 52c9d48879d1f..e598c3317a7b7 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/elasticsearch/composable/template.json +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/elasticsearch/composable/template.json @@ -6,6 +6,7 @@ "composed_of": [ "ecs_8.0.0_base", "ecs_8.0.0_log", + "ecs_8.0.0_http", "ecs_8.0.0_host" ], "index_patterns": [ diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/elasticsearch/legacy/template.json b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/elasticsearch/legacy/template.json index 08a3534017b85..e8b04233d57e2 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/elasticsearch/legacy/template.json +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/ecs/generated/elasticsearch/legacy/template.json @@ -36,6 +36,20 @@ } } }, + "http": { + "properties": { + "response": { + "properties": { + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + } + } + }, "labels": { "type": "object" }, diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/lib/events/create_nginx_log.ts b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/lib/events/create_nginx_log.ts index b6af0163d4670..c202196370d09 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/lib/events/create_nginx_log.ts +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/nginx_proxy/lib/events/create_nginx_log.ts @@ -37,6 +37,7 @@ export const createNginxLog = ( )} - ${userId} ${domain} to: ${hostWithPort}: "${method} ${path} HTTP/1.1" ${statusCode} ${bytes} "${url}" "${userAgent}"`, log: { level: 'INFO', logger: NGINX_PROXY }, host: { name: host }, + http: { response: { status_code: statusCode, bytes } }, }, ]; }; diff --git a/x-pack/packages/kbn-data-forge/src/lib/add_ephemeral_project_id.ts b/x-pack/packages/kbn-data-forge/src/lib/add_ephemeral_project_id.ts index 0ba789c5a50b0..4226c8da7cebc 100644 --- a/x-pack/packages/kbn-data-forge/src/lib/add_ephemeral_project_id.ts +++ b/x-pack/packages/kbn-data-forge/src/lib/add_ephemeral_project_id.ts @@ -6,6 +6,7 @@ */ import { random, times } from 'lodash'; +import moment from 'moment'; import { v4 } from 'uuid'; import { Doc } from '../types'; @@ -18,7 +19,7 @@ export function addEphemeralProjectId(totalProjectIds: number, events: Doc[]) { return events; } - const now = Date.now(); + const now = (events[0] && moment(events[0]['@timestamp']).valueOf()) || Date.now(); const workingIds = projectIds.filter(([_id, expirationDate]) => expirationDate > now); const numberOfIdsToCreate = totalProjectIds - workingIds.length; diff --git a/x-pack/packages/ml/anomaly_utils/anomaly_utils.ts b/x-pack/packages/ml/anomaly_utils/anomaly_utils.ts index 4865aed1e4e97..8ef9fee14a273 100644 --- a/x-pack/packages/ml/anomaly_utils/anomaly_utils.ts +++ b/x-pack/packages/ml/anomaly_utils/anomaly_utils.ts @@ -59,6 +59,10 @@ export interface MlEntityField { * Optional entity field operation */ operation?: MlEntityFieldOperation; + /** + * Optional cardinality of field + */ + cardinality?: number; } // List of function descriptions for which actual values from record level results should be displayed. diff --git a/x-pack/plugins/actions/docs/openapi/bundled.json b/x-pack/plugins/actions/docs/openapi/bundled.json index d165392087670..d910d5ad6501e 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled.json +++ b/x-pack/plugins/actions/docs/openapi/bundled.json @@ -2240,7 +2240,7 @@ "defaultModel": { "type": "string", "description": "The generative artificial intelligence model for Amazon Bedrock to use. Current support is for the Anthropic Claude models.\n", - "default": "anthropic.claude-v2" + "default": "anthropic.claude-v2:1" } } }, @@ -6841,4 +6841,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/plugins/actions/docs/openapi/bundled.yaml b/x-pack/plugins/actions/docs/openapi/bundled.yaml index 58ea32fe25764..cd55a90afa483 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled.yaml +++ b/x-pack/plugins/actions/docs/openapi/bundled.yaml @@ -1498,7 +1498,7 @@ components: type: string description: | The generative artificial intelligence model for Amazon Bedrock to use. Current support is for the Anthropic Claude models. - default: anthropic.claude-v2 + default: anthropic.claude-v2:1 secrets_properties_bedrock: title: Connector secrets properties for an Amazon Bedrock connector description: Defines secrets for connectors when type is `.bedrock`. diff --git a/x-pack/plugins/actions/docs/openapi/bundled_serverless.json b/x-pack/plugins/actions/docs/openapi/bundled_serverless.json index acde35b764a5e..ba7d2b16be139 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled_serverless.json +++ b/x-pack/plugins/actions/docs/openapi/bundled_serverless.json @@ -1226,7 +1226,7 @@ "defaultModel": { "type": "string", "description": "The generative artificial intelligence model for Amazon Bedrock to use. Current support is for the Anthropic Claude models.\n", - "default": "anthropic.claude-v2" + "default": "anthropic.claude-v2:1" } } }, @@ -4377,4 +4377,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/plugins/actions/docs/openapi/bundled_serverless.yaml b/x-pack/plugins/actions/docs/openapi/bundled_serverless.yaml index 3d9be12c8077e..564b121ec663b 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled_serverless.yaml +++ b/x-pack/plugins/actions/docs/openapi/bundled_serverless.yaml @@ -857,7 +857,7 @@ components: type: string description: | The generative artificial intelligence model for Amazon Bedrock to use. Current support is for the Anthropic Claude models. - default: anthropic.claude-v2 + default: anthropic.claude-v2:1 secrets_properties_bedrock: title: Connector secrets properties for an Amazon Bedrock connector description: Defines secrets for connectors when type is `.bedrock`. diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_bedrock.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_bedrock.yaml index 25b279c423739..189a5d5e2e05e 100644 --- a/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_bedrock.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_bedrock.yaml @@ -12,4 +12,4 @@ properties: description: > The generative artificial intelligence model for Amazon Bedrock to use. Current support is for the Anthropic Claude models. - default: anthropic.claude-v2 \ No newline at end of file + default: anthropic.claude-v2:1 diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_disable/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_disable/schemas/v1.ts index 3e74ed88ff0f8..41a727b8b19b6 100644 --- a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_disable/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_disable/schemas/v1.ts @@ -10,4 +10,5 @@ import { schema } from '@kbn/config-schema'; export const bulkDisableRulesRequestBodySchema = schema.object({ filter: schema.maybe(schema.string()), ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1, maxSize: 1000 })), + untrack: schema.maybe(schema.boolean({ defaultValue: false })), }); diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index 7cec5bdbdd7a6..6a66b39720402 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -19,6 +19,7 @@ export type { ActionVariable } from '@kbn/alerting-types'; export type RuleTypeState = Record; export type RuleTypeParams = Record; +export type RuleTypeMetaData = Record; // rule type defined alert fields to persist in alerts index export type RuleAlertData = Record; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.test.ts index 81a8466e23a22..3f43d6077eb35 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.test.ts @@ -41,14 +41,6 @@ import { import { migrateLegacyActions } from '../../../../rules_client/lib'; import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; -jest.mock('../../../../task_runner/alert_task_instance', () => ({ - taskInstanceToAlertTaskInstance: jest.fn(), -})); - -const { taskInstanceToAlertTaskInstance } = jest.requireMock( - '../../../../task_runner/alert_task_instance' -); - jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => { return { migrateLegacyActions: jest.fn().mockResolvedValue({ @@ -63,6 +55,12 @@ jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invali bulkMarkApiKeysForInvalidation: jest.fn(), })); +jest.mock('../../../../rules_client/lib/untrack_rule_alerts', () => ({ + untrackRuleAlerts: jest.fn(), +})); + +const { untrackRuleAlerts } = jest.requireMock('../../../../rules_client/lib/untrack_rule_alerts'); + const taskManager = taskManagerMock.createStart(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); @@ -192,6 +190,76 @@ describe('bulkDisableRules', () => { }); }); + test('should call untrack alert if untrack is true', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [disabledRuleForBulkDisable1, disabledRuleForBulkDisable2], + }); + + const result = await rulesClient.bulkDisableRules({ filter: 'fake_filter', untrack: true }); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + id: 'id1', + attributes: expect.objectContaining({ + enabled: false, + }), + }), + expect.objectContaining({ + id: 'id2', + attributes: expect.objectContaining({ + enabled: false, + }), + }), + ]), + { overwrite: true } + ); + + expect(result).toStrictEqual({ + errors: [], + rules: [returnedRuleForBulkDisable1, returnedRuleForBulkDisable2], + total: 2, + }); + + expect(untrackRuleAlerts).toHaveBeenCalled(); + }); + + test('should not call untrack alert if untrack is false', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [disabledRuleForBulkDisable1, disabledRuleForBulkDisable2], + }); + + const result = await rulesClient.bulkDisableRules({ filter: 'fake_filter', untrack: true }); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + id: 'id1', + attributes: expect.objectContaining({ + enabled: false, + }), + }), + expect.objectContaining({ + id: 'id2', + attributes: expect.objectContaining({ + enabled: false, + }), + }), + ]), + { overwrite: true } + ); + + expect(result).toStrictEqual({ + errors: [], + rules: [returnedRuleForBulkDisable1, returnedRuleForBulkDisable2], + total: 2, + }); + + expect(untrackRuleAlerts).toHaveBeenCalled(); + }); + test('should try to disable rules, one successful and one with 500 error', async () => { unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [disabledRuleForBulkDisable1, savedObjectWith500Error], @@ -585,51 +653,6 @@ describe('bulkDisableRules', () => { }); }); - describe('recoverRuleAlerts', () => { - beforeEach(() => { - taskInstanceToAlertTaskInstance.mockImplementation(() => ({ - state: { - alertInstances: { - '1': { - meta: { - lastScheduledActions: { - group: 'default', - date: new Date().toISOString(), - }, - }, - state: { bar: false }, - }, - }, - }, - })); - }); - test('should call logEvent', async () => { - unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: [disabledRuleForBulkDisable1, disabledRuleForBulkDisable2], - }); - - await rulesClient.bulkDisableRules({ filter: 'fake_filter' }); - - expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); - }); - - test('should call logger.warn', async () => { - eventLogger.logEvent.mockImplementation(() => { - throw new Error('UPS'); - }); - unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: [disabledRuleForBulkDisable1, disabledRuleForBulkDisable2], - }); - - await rulesClient.bulkDisableRules({ filter: 'fake_filter' }); - - expect(logger.warn).toHaveBeenCalledTimes(2); - expect(logger.warn).toHaveBeenLastCalledWith( - "rulesClient.disable('id2') - Could not write untrack events - UPS" - ); - }); - }); - describe('legacy actions migration for SIEM', () => { test('should call migrateLegacyActions', async () => { encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.ts index 0ac84ce2ef6d7..84229c4dc665e 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/bulk_disable_rules.ts @@ -50,7 +50,7 @@ export const bulkDisableRules = async ( throw Boom.badRequest(`Error validating bulk disable data - ${error.message}`); } - const { ids, filter } = options; + const { ids, filter, untrack = false } = options; const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); const authorizationFilter = await getAuthorizationFilter(context, { action: 'DISABLE' }); @@ -72,7 +72,7 @@ export const bulkDisableRules = async ( action: 'DISABLE', logger: context.logger, bulkOperation: (filterKueryNode: KueryNode | null) => - bulkDisableRulesWithOCC(context, { filter: filterKueryNode }), + bulkDisableRulesWithOCC(context, { filter: filterKueryNode, untrack }), filter: kueryNodeFilterWithAuth, }) ); @@ -120,7 +120,13 @@ export const bulkDisableRules = async ( const bulkDisableRulesWithOCC = async ( context: RulesClientContext, - { filter }: { filter: KueryNode | null } + { + filter, + untrack = false, + }: { + filter: KueryNode | null; + untrack: boolean; + } ) => { const additionalFilter = nodeBuilder.is('alert.attributes.enabled', 'true'); @@ -151,7 +157,9 @@ const bulkDisableRulesWithOCC = async ( for await (const response of rulesFinder.find()) { await pMap(response.saved_objects, async (rule) => { try { - await untrackRuleAlerts(context, rule.id, rule.attributes); + if (untrack) { + await untrackRuleAlerts(context, rule.id, rule.attributes); + } if (rule.attributes.name) { ruleNameToRuleIdMapping[rule.id] = rule.attributes.name; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/schemas/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/schemas/index.ts index 3e74ed88ff0f8..41a727b8b19b6 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/schemas/index.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_disable/schemas/index.ts @@ -10,4 +10,5 @@ import { schema } from '@kbn/config-schema'; export const bulkDisableRulesRequestBodySchema = schema.object({ filter: schema.maybe(schema.string()), ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1, maxSize: 1000 })), + untrack: schema.maybe(schema.boolean({ defaultValue: false })), }); diff --git a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap index 70ffc475d01d6..6360d65c0e66c 100644 --- a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap +++ b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap @@ -3064,6 +3064,101 @@ Object { "presence": "optional", }, "keys": Object { + "filter": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "items": Array [ + Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "meta": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "key": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "value": Object { + "flags": Object { + "error": [Function], + }, + "type": "any", + }, + }, + "name": "entries", + }, + ], + "type": "record", + }, + "query": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "rules": Array [ + Object { + "args": Object { + "key": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "value": Object { + "flags": Object { + "error": [Function], + }, + "type": "any", + }, + }, + "name": "entries", + }, + ], + "type": "record", + }, + }, + "preferences": Object { + "stripUnknown": Object { + "objects": false, + }, + }, + "type": "object", + }, + ], + "type": "array", + }, "index": Object { "flags": Object { "error": [Function], diff --git a/x-pack/plugins/alerting/server/lib/get_es_request_timeout.test.ts b/x-pack/plugins/alerting/server/lib/get_es_request_timeout.test.ts new file mode 100644 index 0000000000000..deb5dc4d3b0a2 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/get_es_request_timeout.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { getEsRequestTimeout } from './get_es_request_timeout'; + +describe('getEsRequestTimeout', () => { + const logger = loggingSystemMock.create().get(); + test('should return undefined if the timeout is not passed in', () => { + expect(getEsRequestTimeout(logger)).toBe(undefined); + }); + test('should return timeout in ms', () => { + expect(getEsRequestTimeout(logger, '5s')).toBe(5000); + }); + test('should return timeout that is not > 5m', () => { + expect(getEsRequestTimeout(logger, '10m')).toBe(300000); + }); + test('should log error and return undefined for invalid timeout', () => { + expect(getEsRequestTimeout(logger, '5z')).toBe(undefined); + expect(logger.debug).toBeCalledTimes(1); + expect(logger.debug).toBeCalledWith( + 'Invalid format for the rule ES requestTimeout duration: "5z"' + ); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/get_es_request_timeout.ts b/x-pack/plugins/alerting/server/lib/get_es_request_timeout.ts new file mode 100644 index 0000000000000..7fb82f55993fc --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/get_es_request_timeout.ts @@ -0,0 +1,26 @@ +/* + * 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 { Logger } from '@kbn/core/server'; +import { parseDuration } from '../../common'; +import { DEFAULT_EXECUTION_TIMEOUT } from './get_rule_task_timeout'; + +export function getEsRequestTimeout(logger: Logger, timeout?: string): number | undefined { + if (!timeout) { + return undefined; + } + + try { + const maxRequestTimeout = parseDuration(DEFAULT_EXECUTION_TIMEOUT); + const requestTimeout = parseDuration(timeout); + // return the ES request timeout in ms that is capped at the default execution timeout (5 min). + return requestTimeout > maxRequestTimeout ? maxRequestTimeout : requestTimeout; + } catch (error) { + logger.debug(`Invalid format for the rule ES requestTimeout duration: "${timeout}"`); + return undefined; + } +} diff --git a/x-pack/plugins/alerting/server/lib/get_rule_task_timeout.ts b/x-pack/plugins/alerting/server/lib/get_rule_task_timeout.ts index e6721d65e309f..52919bd54de0b 100644 --- a/x-pack/plugins/alerting/server/lib/get_rule_task_timeout.ts +++ b/x-pack/plugins/alerting/server/lib/get_rule_task_timeout.ts @@ -7,7 +7,7 @@ import { RulesConfig } from '../config'; -const DEFAULT_EXECUTION_TIMEOUT = '5m'; +export const DEFAULT_EXECUTION_TIMEOUT = '5m'; export const getRuleTaskTimeout = ({ config, diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerting/server/lib/index.ts index 4257f3ca916a4..d83cc889015d4 100644 --- a/x-pack/plugins/alerting/server/lib/index.ts +++ b/x-pack/plugins/alerting/server/lib/index.ts @@ -48,3 +48,4 @@ export { getAlertsForNotification } from './get_alerts_for_notification'; export { trimRecoveredAlerts } from './trim_recovered_alerts'; export { createGetAlertIndicesAliasFn } from './create_get_alert_indices_alias'; export type { GetAlertIndicesAlias } from './create_get_alert_indices_alias'; +export { getEsRequestTimeout } from './get_es_request_timeout'; diff --git a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts index 1880db1e69a4e..da0b65f550deb 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.test.ts @@ -58,14 +58,19 @@ describe('wrapScopedClusterClient', () => { rule, logger, abortController, + requestTimeout: 5000, }).client(); await wrappedSearchClient.asInternalUser.search(esQuery); expect(asInternalUserWrappedSearchFn).toHaveBeenCalledWith(esQuery, { signal: abortController.signal, + requestTimeout: 5000, }); expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `executing query for rule .test-rule-type:abcdefg in space my-space - {\"body\":{\"query\":{\"bool\":{\"filter\":{\"range\":{\"@timestamp\":{\"gte\":0}}}}}}} - with options {} and 5000ms requestTimeout` + ); }); test('uses asCurrentUser when specified', async () => { @@ -78,14 +83,19 @@ describe('wrapScopedClusterClient', () => { rule, logger, abortController, + requestTimeout: 5000, }).client(); await wrappedSearchClient.asCurrentUser.search(esQuery); expect(asCurrentUserWrappedSearchFn).toHaveBeenCalledWith(esQuery, { signal: abortController.signal, + requestTimeout: 5000, }); expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `executing query for rule .test-rule-type:abcdefg in space my-space - {\"body\":{\"query\":{\"bool\":{\"filter\":{\"range\":{\"@timestamp\":{\"gte\":0}}}}}}} - with options {} and 5000ms requestTimeout` + ); }); test('uses search options when specified', async () => { @@ -98,12 +108,17 @@ describe('wrapScopedClusterClient', () => { rule, logger, abortController, + requestTimeout: 5000, }).client(); - await wrappedSearchClient.asInternalUser.search(esQuery, { ignore: [404] }); + await wrappedSearchClient.asInternalUser.search(esQuery, { + ignore: [404], + requestTimeout: 10000, + }); expect(asInternalUserWrappedSearchFn).toHaveBeenCalledWith(esQuery, { ignore: [404], signal: abortController.signal, + requestTimeout: 5000, }); expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); @@ -205,6 +220,83 @@ describe('wrapScopedClusterClient', () => { }); describe('eql.search', () => { + test('uses asInternalUser when specified', async () => { + const { abortController, scopedClusterClient, childClient } = getMockClusterClients(); + + const asInternalUserWrappedSearchFn = childClient.eql.search; + + const wrappedSearchClient = createWrappedScopedClusterClientFactory({ + scopedClusterClient, + rule, + logger, + abortController, + requestTimeout: 5000, + }).client(); + + await wrappedSearchClient.asInternalUser.eql.search(eqlQuery); + + expect(asInternalUserWrappedSearchFn).toHaveBeenCalledWith(eqlQuery, { + signal: abortController.signal, + requestTimeout: 5000, + }); + expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); + expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + 'executing eql query for rule .test-rule-type:abcdefg in space my-space - {"index":"foo","query":"process where process.name == \\"regsvr32.exe\\""} - with options {} and 5000ms requestTimeout' + ); + }); + + test('uses asCurrentUser when specified', async () => { + const { abortController, scopedClusterClient, childClient } = getMockClusterClients(true); + + const asCurrentUserWrappedSearchFn = childClient.eql.search; + + const wrappedSearchClient = createWrappedScopedClusterClientFactory({ + scopedClusterClient, + rule, + logger, + abortController, + requestTimeout: 5000, + }).client(); + await wrappedSearchClient.asCurrentUser.eql.search(eqlQuery); + + expect(asCurrentUserWrappedSearchFn).toHaveBeenCalledWith(eqlQuery, { + signal: abortController.signal, + requestTimeout: 5000, + }); + expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); + expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + 'executing eql query for rule .test-rule-type:abcdefg in space my-space - {"index":"foo","query":"process where process.name == \\"regsvr32.exe\\""} - with options {} and 5000ms requestTimeout' + ); + }); + + test('uses search options when specified', async () => { + const { abortController, scopedClusterClient, childClient } = getMockClusterClients(); + + const asInternalUserWrappedSearchFn = childClient.eql.search; + + const wrappedSearchClient = createWrappedScopedClusterClientFactory({ + scopedClusterClient, + rule, + logger, + abortController, + requestTimeout: 5000, + }).client(); + await wrappedSearchClient.asInternalUser.eql.search(eqlQuery, { + ignore: [404], + requestTimeout: 10000, + }); + + expect(asInternalUserWrappedSearchFn).toHaveBeenCalledWith(eqlQuery, { + ignore: [404], + signal: abortController.signal, + requestTimeout: 5000, + }); + expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); + expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + }); + test('re-throws error when an error is thrown', async () => { const { abortController, scopedClusterClient, childClient } = getMockClusterClients(); @@ -278,6 +370,83 @@ describe('wrapScopedClusterClient', () => { describe('transport.request', () => { describe('ES|QL', () => { + test('uses asInternalUser when specified', async () => { + const { abortController, scopedClusterClient, childClient } = getMockClusterClients(); + + const asInternalUserWrappedSearchFn = childClient.transport.request; + + const wrappedSearchClient = createWrappedScopedClusterClientFactory({ + scopedClusterClient, + rule, + logger, + abortController, + requestTimeout: 5000, + }).client(); + + await wrappedSearchClient.asInternalUser.transport.request(esqlQueryRequest); + + expect(asInternalUserWrappedSearchFn).toHaveBeenCalledWith(esqlQueryRequest, { + signal: abortController.signal, + requestTimeout: 5000, + }); + expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); + expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + 'executing ES|QL query for rule .test-rule-type:abcdefg in space my-space - {"method":"POST","path":"/_query","body":{"query":"from .kibana_task_manager"}} - with options {} and 5000ms requestTimeout' + ); + }); + + test('uses asCurrentUser when specified', async () => { + const { abortController, scopedClusterClient, childClient } = getMockClusterClients(true); + + const asCurrentUserWrappedSearchFn = childClient.transport.request; + + const wrappedSearchClient = createWrappedScopedClusterClientFactory({ + scopedClusterClient, + rule, + logger, + abortController, + requestTimeout: 5000, + }).client(); + await wrappedSearchClient.asCurrentUser.transport.request(esqlQueryRequest); + + expect(asCurrentUserWrappedSearchFn).toHaveBeenCalledWith(esqlQueryRequest, { + signal: abortController.signal, + requestTimeout: 5000, + }); + expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); + expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + 'executing ES|QL query for rule .test-rule-type:abcdefg in space my-space - {"method":"POST","path":"/_query","body":{"query":"from .kibana_task_manager"}} - with options {} and 5000ms requestTimeout' + ); + }); + + test('uses search options when specified', async () => { + const { abortController, scopedClusterClient, childClient } = getMockClusterClients(); + + const asInternalUserWrappedSearchFn = childClient.transport.request; + + const wrappedSearchClient = createWrappedScopedClusterClientFactory({ + scopedClusterClient, + rule, + logger, + abortController, + requestTimeout: 5000, + }).client(); + await wrappedSearchClient.asInternalUser.transport.request(esqlQueryRequest, { + ignore: [404], + requestTimeout: 10000, + }); + + expect(asInternalUserWrappedSearchFn).toHaveBeenCalledWith(esqlQueryRequest, { + ignore: [404], + signal: abortController.signal, + requestTimeout: 5000, + }); + expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); + expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + }); + test('re-throws error when an error is thrown', async () => { const { abortController, scopedClusterClient, childClient } = getMockClusterClients(); @@ -352,6 +521,86 @@ describe('wrapScopedClusterClient', () => { }); }); + test('uses asInternalUser when specified', async () => { + const { abortController, scopedClusterClient, childClient } = getMockClusterClients(); + + const asInternalUserWrappedSearchFn = childClient.transport.request; + + const wrappedSearchClient = createWrappedScopedClusterClientFactory({ + scopedClusterClient, + rule, + logger, + abortController, + requestTimeout: 5000, + }).client(); + + await wrappedSearchClient.asInternalUser.transport.request({ method: '', path: '' }); + + expect(asInternalUserWrappedSearchFn).toHaveBeenCalledWith( + { method: '', path: '' }, + { + requestTimeout: 5000, + } + ); + expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); + expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + }); + + test('uses asCurrentUser when specified', async () => { + const { abortController, scopedClusterClient, childClient } = getMockClusterClients(true); + + const asCurrentUserWrappedSearchFn = childClient.transport.request; + + const wrappedSearchClient = createWrappedScopedClusterClientFactory({ + scopedClusterClient, + rule, + logger, + abortController, + requestTimeout: 5000, + }).client(); + await wrappedSearchClient.asCurrentUser.transport.request({ method: '', path: '' }); + + expect(asCurrentUserWrappedSearchFn).toHaveBeenCalledWith( + { method: '', path: '' }, + { + requestTimeout: 5000, + } + ); + expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); + expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + }); + + test('uses search options when specified', async () => { + const { abortController, scopedClusterClient, childClient } = getMockClusterClients(); + + const asInternalUserWrappedSearchFn = childClient.transport.request; + + const wrappedSearchClient = createWrappedScopedClusterClientFactory({ + scopedClusterClient, + rule, + logger, + abortController, + requestTimeout: 5000, + }).client(); + await wrappedSearchClient.asInternalUser.transport.request( + { method: '', path: '' }, + { + ignore: [404], + requestTimeout: 10000, + } + ); + + expect(asInternalUserWrappedSearchFn).toHaveBeenCalledWith( + { method: '', path: '' }, + { + ignore: [404], + requestTimeout: 5000, + } + ); + expect(scopedClusterClient.asInternalUser.search).not.toHaveBeenCalled(); + expect(scopedClusterClient.asCurrentUser.search).not.toHaveBeenCalled(); + }); + test('re-throws error when an error is thrown', async () => { const { abortController, scopedClusterClient, childClient } = getMockClusterClients(); diff --git a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts index 55f9d7f4a7c07..2150f415f1039 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_scoped_cluster_client.ts @@ -32,6 +32,7 @@ interface WrapScopedClusterClientFactoryOpts { rule: RuleInfo; logger: Logger; abortController: AbortController; + requestTimeout?: number; } type WrapScopedClusterClientOpts = WrapScopedClusterClientFactoryOpts & { @@ -105,6 +106,7 @@ function wrapEsClient(opts: WrapEsClientOpts): ElasticsearchClient { function getWrappedTransportRequestFn(opts: WrapEsClientOpts) { const originalRequestFn = opts.esClient.transport.request; + const requestTimeout = opts.requestTimeout; // A bunch of overloads to make TypeScript happy async function request( @@ -131,10 +133,17 @@ function getWrappedTransportRequestFn(opts: WrapEsClientOpts) { opts.logger.debug( `executing ES|QL query for rule ${opts.rule.alertTypeId}:${opts.rule.id} in space ${ opts.rule.spaceId - } - ${JSON.stringify(params)} - with options ${JSON.stringify(requestOptions)}` + } - ${JSON.stringify(params)} - with options ${JSON.stringify(requestOptions)}${ + requestTimeout ? ` and ${requestTimeout}ms requestTimeout` : '' + }` ); const result = (await originalRequestFn.call(opts.esClient.transport, params, { ...requestOptions, + ...(requestTimeout + ? { + requestTimeout, + } + : {}), signal: opts.abortController.signal, })) as Promise | TransportResult; @@ -152,11 +161,14 @@ function getWrappedTransportRequestFn(opts: WrapEsClientOpts) { } // No wrap - return (await originalRequestFn.call( - opts.esClient.transport, - params, - options - )) as Promise; + return (await originalRequestFn.call(opts.esClient.transport, params, { + ...options, + ...(requestTimeout + ? { + requestTimeout, + } + : {}), + })) as Promise; } return request; @@ -164,6 +176,7 @@ function getWrappedTransportRequestFn(opts: WrapEsClientOpts) { function getWrappedEqlSearchFn(opts: WrapEsClientOpts) { const originalEqlSearch = opts.esClient.eql.search; + const requestTimeout = opts.requestTimeout; // A bunch of overloads to make TypeScript happy async function search( @@ -188,10 +201,17 @@ function getWrappedEqlSearchFn(opts: WrapEsClientOpts) { opts.logger.debug( `executing eql query for rule ${opts.rule.alertTypeId}:${opts.rule.id} in space ${ opts.rule.spaceId - } - ${JSON.stringify(params)} - with options ${JSON.stringify(searchOptions)}` + } - ${JSON.stringify(params)} - with options ${JSON.stringify(searchOptions)}${ + requestTimeout ? ` and ${requestTimeout}ms requestTimeout` : '' + }` ); const result = (await originalEqlSearch.call(opts.esClient, params, { ...searchOptions, + ...(requestTimeout + ? { + requestTimeout, + } + : {}), signal: opts.abortController.signal, })) as TransportResult, unknown> | EqlSearchResponse; @@ -222,6 +242,7 @@ function getWrappedEqlSearchFn(opts: WrapEsClientOpts) { function getWrappedSearchFn(opts: WrapEsClientOpts) { const originalSearch = opts.esClient.search; + const requestTimeout = opts.requestTimeout; // A bunch of overloads to make TypeScript happy async function search< @@ -261,10 +282,17 @@ function getWrappedSearchFn(opts: WrapEsClientOpts) { opts.logger.debug( `executing query for rule ${opts.rule.alertTypeId}:${opts.rule.id} in space ${ opts.rule.spaceId - } - ${JSON.stringify(params)} - with options ${JSON.stringify(searchOptions)}` + } - ${JSON.stringify(params)} - with options ${JSON.stringify(searchOptions)}${ + requestTimeout ? ` and ${requestTimeout}ms requestTimeout` : '' + }` ); const result = (await originalSearch.call(opts.esClient, params, { ...searchOptions, + ...(requestTimeout + ? { + requestTimeout, + } + : {}), signal: opts.abortController.signal, })) as | TransportResult, unknown> diff --git a/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts b/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts index f3187eddc9d9a..21fe0e7ecb02f 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_search_source_client.test.ts @@ -64,6 +64,31 @@ describe('wrapSearchSourceClient', () => { }); }); + test('searches with provided request timeout', async () => { + const abortController = new AbortController(); + const { searchSourceMock, searchSourceClientMock } = createSearchSourceClientMock(); + + const { searchSourceClient } = wrapSearchSourceClient({ + logger, + rule, + searchSourceClient: searchSourceClientMock, + abortController, + requestTimeout: 5000, + }); + const wrappedSearchSource = await searchSourceClient.createEmpty(); + await wrappedSearchSource.fetch(); + + expect(searchSourceMock.fetch$).toHaveBeenCalledWith({ + abortSignal: abortController.signal, + transport: { + requestTimeout: 5000, + }, + }); + expect(logger.debug).toHaveBeenCalledWith( + `executing query for rule .test-rule-type:abcdefg in space my-space - with options {} and 5000ms requestTimeout` + ); + }); + test('uses search options when specified', async () => { const abortController = new AbortController(); const { searchSourceMock, searchSourceClientMock } = createSearchSourceClientMock(); @@ -73,13 +98,15 @@ describe('wrapSearchSourceClient', () => { rule, searchSourceClient: searchSourceClientMock, abortController, + requestTimeout: 5000, }); const wrappedSearchSource = await searchSourceClient.create(); - await wrappedSearchSource.fetch({ isStored: true }); + await wrappedSearchSource.fetch({ isStored: true, transport: { requestTimeout: 10000 } }); expect(searchSourceMock.fetch$).toHaveBeenCalledWith({ isStored: true, abortSignal: abortController.signal, + transport: { requestTimeout: 5000 }, }); }); diff --git a/x-pack/plugins/alerting/server/lib/wrap_search_source_client.ts b/x-pack/plugins/alerting/server/lib/wrap_search_source_client.ts index 442f0c3e292bf..00577d39aaa97 100644 --- a/x-pack/plugins/alerting/server/lib/wrap_search_source_client.ts +++ b/x-pack/plugins/alerting/server/lib/wrap_search_source_client.ts @@ -21,6 +21,7 @@ interface Props { rule: RuleInfo; abortController: AbortController; searchSourceClient: ISearchStartSearchSource; + requestTimeout?: number; } interface WrapParams { @@ -29,6 +30,7 @@ interface WrapParams { abortController: AbortController; pureSearchSource: T; logMetrics: (metrics: LogSearchMetricsOpts) => void; + requestTimeout?: number; } export function wrapSearchSourceClient({ @@ -36,6 +38,7 @@ export function wrapSearchSourceClient({ rule, abortController, searchSourceClient: pureSearchSourceClient, + requestTimeout, }: Props) { let numSearches: number = 0; let esSearchDurationMs: number = 0; @@ -52,6 +55,7 @@ export function wrapSearchSourceClient({ logger, rule, abortController, + requestTimeout, }; const wrappedSearchSourceClient: ISearchStartSearchSource = Object.create(pureSearchSourceClient); @@ -137,6 +141,7 @@ function wrapFetch$({ abortController, pureSearchSource, logMetrics, + requestTimeout, }: WrapParams) { return (options?: ISearchOptions) => { const searchOptions = options ?? {}; @@ -145,12 +150,19 @@ function wrapFetch$({ logger.debug( `executing query for rule ${rule.alertTypeId}:${rule.id} in space ${ rule.spaceId - } - with options ${JSON.stringify(searchOptions)}` + } - with options ${JSON.stringify(searchOptions)}${ + requestTimeout ? ` and ${requestTimeout}ms requestTimeout` : '' + }` ); return pureSearchSource .fetch$({ ...searchOptions, + ...(requestTimeout + ? { + transport: { requestTimeout }, + } + : {}), abortSignal: abortController.signal, }) .pipe( diff --git a/x-pack/plugins/alerting/server/routes/disable_rule.test.ts b/x-pack/plugins/alerting/server/routes/disable_rule.test.ts index 6f4806b8e17f5..6186fbd8a5db7 100644 --- a/x-pack/plugins/alerting/server/routes/disable_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/disable_rule.test.ts @@ -52,6 +52,7 @@ describe('disableRuleRoute', () => { Array [ Object { "id": "1", + "untrack": false, }, ] `); diff --git a/x-pack/plugins/alerting/server/routes/disable_rule.ts b/x-pack/plugins/alerting/server/routes/disable_rule.ts index 99795e7ee2bf2..726b080bedbf1 100644 --- a/x-pack/plugins/alerting/server/routes/disable_rule.ts +++ b/x-pack/plugins/alerting/server/routes/disable_rule.ts @@ -15,6 +15,14 @@ const paramSchema = schema.object({ id: schema.string(), }); +const bodySchema = schema.nullable( + schema.maybe( + schema.object({ + untrack: schema.maybe(schema.boolean({ defaultValue: false })), + }) + ) +); + export const disableRuleRoute = ( router: IRouter, licenseState: ILicenseState @@ -24,14 +32,16 @@ export const disableRuleRoute = ( path: `${BASE_ALERTING_API_PATH}/rule/{id}/_disable`, validate: { params: paramSchema, + body: bodySchema, }, }, router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesClient = (await context.alerting).getRulesClient(); const { id } = req.params; + const { untrack = false } = req.body || {}; try { - await rulesClient.disable({ id }); + await rulesClient.disable({ id, untrack }); return res.noContent(); } catch (e) { if (e instanceof RuleTypeDisabledError) { diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts index 03afb4a95d25a..39b81ee74fe91 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_disable/bulk_disable_rules_route.ts @@ -39,10 +39,10 @@ export const bulkDisableRulesRoute = ({ const rulesClient = (await context.alerting).getRulesClient(); const body: BulkDisableRulesRequestBodyV1 = req.body; - const { filter, ids } = body; + const { filter, ids, untrack } = body; try { - const bulkDisableResults = await rulesClient.bulkDisableRules({ filter, ids }); + const bulkDisableResults = await rulesClient.bulkDisableRules({ filter, ids, untrack }); const resultBody: BulkDisableRulesResponseV1 = { body: { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/disable.ts b/x-pack/plugins/alerting/server/rules_client/methods/disable.ts index 38b0dcc7e17d6..88396559031c8 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/disable.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/disable.ts @@ -15,15 +15,33 @@ import { untrackRuleAlerts, updateMeta, migrateLegacyActions } from '../lib'; import { RuleAttributes } from '../../data/rule/types'; import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; -export async function disable(context: RulesClientContext, { id }: { id: string }): Promise { +export async function disable( + context: RulesClientContext, + { + id, + untrack = false, + }: { + id: string; + untrack?: boolean; + } +): Promise { return await retryIfConflicts( context.logger, `rulesClient.disable('${id}')`, - async () => await disableWithOCC(context, { id }) + async () => await disableWithOCC(context, { id, untrack }) ); } -async function disableWithOCC(context: RulesClientContext, { id }: { id: string }) { +async function disableWithOCC( + context: RulesClientContext, + { + id, + untrack = false, + }: { + id: string; + untrack?: boolean; + } +) { let attributes: RawRule; let version: string | undefined; let references: SavedObjectReference[]; @@ -70,7 +88,9 @@ async function disableWithOCC(context: RulesClientContext, { id }: { id: string throw error; } - await untrackRuleAlerts(context, id, attributes as RuleAttributes); + if (untrack) { + await untrackRuleAlerts(context, id, attributes as RuleAttributes); + } context.auditLogger?.log( ruleAuditEvent({ diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 0a2da42d7e424..39b4353525f7e 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -160,7 +160,7 @@ export class RulesClient { public updateApiKey = (options: { id: string }) => updateApiKey(this.context, options); public enable = (options: { id: string }) => enable(this.context, options); - public disable = (options: { id: string }) => disable(this.context, options); + public disable = (options: { id: string; untrack?: boolean }) => disable(this.context, options); public snooze = (options: SnoozeRuleOptions) => snoozeRule(this.context, options); public unsnooze = (options: UnsnoozeParams) => unsnoozeRule(this.context, options); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index 4f539f5a9f7b1..9e7073da8a18d 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -299,7 +299,7 @@ describe('disable()', () => { }, ownerId: null, }); - await rulesClient.disable({ id: '1' }); + await rulesClient.disable({ id: '1', untrack: true }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith( RULE_SAVED_OBJECT_TYPE, @@ -388,7 +388,7 @@ describe('disable()', () => { test('disables the rule even if unable to retrieve task manager doc to generate untrack event log events', async () => { taskManager.get.mockRejectedValueOnce(new Error('Fail')); - await rulesClient.disable({ id: '1' }); + await rulesClient.disable({ id: '1', untrack: true }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith( RULE_SAVED_OBJECT_TYPE, @@ -440,6 +440,55 @@ describe('disable()', () => { ); }); + test('should not untrack rule alert if untrack is false', async () => { + await rulesClient.disable({ id: '1', untrack: false }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith( + RULE_SAVED_OBJECT_TYPE, + '1', + { + namespace: 'default', + } + ); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + RULE_SAVED_OBJECT_TYPE, + '1', + { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + meta: { + versionApiKeyLastmodified: 'v7.10.0', + }, + revision: 0, + scheduledTaskId: '1', + apiKey: 'MTIzOmFiYw==', + apiKeyOwner: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + nextRun: null, + }, + { + version: '123', + } + ); + expect(taskManager.bulkDisable).toHaveBeenCalledWith(['1'], false); + expect(taskManager.get).not.toHaveBeenCalled(); + expect(taskManager.removeIfExists).not.toHaveBeenCalled(); + }); + test('falls back when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await rulesClient.disable({ id: '1' }); diff --git a/x-pack/plugins/alerting/server/rules_settings_client.mock.ts b/x-pack/plugins/alerting/server/rules_settings_client.mock.ts index 12703161fdb46..5e3479b10e0ab 100644 --- a/x-pack/plugins/alerting/server/rules_settings_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_settings_client.mock.ts @@ -23,14 +23,10 @@ const createRulesSettingsClientMock = () => { const flappingMocked: RulesSettingsFlappingClientMock = { get: jest.fn().mockReturnValue(DEFAULT_FLAPPING_SETTINGS), update: jest.fn(), - getSettings: jest.fn(), - createSettings: jest.fn(), }; const queryDelayMocked: RulesSettingsQueryDelayClientMock = { get: jest.fn().mockReturnValue(DEFAULT_QUERY_DELAY_SETTINGS), update: jest.fn(), - getSettings: jest.fn(), - createSettings: jest.fn(), }; const mocked: RulesSettingsClientMock = { flapping: jest.fn().mockReturnValue(flappingMocked), diff --git a/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.test.ts b/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.test.ts index 19f978a8985f1..213d1a94a38eb 100644 --- a/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.test.ts +++ b/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.test.ts @@ -215,6 +215,7 @@ describe('RulesSettingsFlappingClient', () => { references: [], }); + // @ts-expect-error access private method const result = await client.createSettings(); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -249,6 +250,7 @@ describe('RulesSettingsFlappingClient', () => { attributes: mockAttributes, references: [], }); + // @ts-expect-error access private method const result = await client.getSettings(); expect(result.attributes).toEqual(mockAttributes); }); @@ -262,6 +264,7 @@ describe('RulesSettingsFlappingClient', () => { RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID ) ); + // @ts-expect-error access private method await expect(client.getSettings()).rejects.toThrowError(); }); diff --git a/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.ts b/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.ts index 0bf6f2af025fe..724da9cffa149 100644 --- a/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.ts +++ b/x-pack/plugins/alerting/server/rules_settings_client/flapping/rules_settings_flapping_client.ts @@ -130,19 +130,14 @@ export class RulesSettingsFlappingClient { } } - public async getSettings(): Promise> { - try { - return await this.savedObjectsClient.get( - RULES_SETTINGS_SAVED_OBJECT_TYPE, - RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID - ); - } catch (e) { - this.logger.error(`Failed to get flapping rules setting for current space. Error: ${e}`); - throw e; - } + private async getSettings(): Promise> { + return await this.savedObjectsClient.get( + RULES_SETTINGS_SAVED_OBJECT_TYPE, + RULES_SETTINGS_FLAPPING_SAVED_OBJECT_ID + ); } - public async createSettings(): Promise> { + private async createSettings(): Promise> { const modificationMetadata = await this.getModificationMetadata(); try { return await this.savedObjectsClient.create( diff --git a/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.test.ts b/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.test.ts index 213ece8cd6fe4..84d707d388a44 100644 --- a/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.test.ts +++ b/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.test.ts @@ -179,7 +179,7 @@ describe('RulesSettingsQueryDelayClient', () => { attributes: mockAttributes, references: [], }); - + // @ts-expect-error access private method const result = await client.createSettings(); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -221,7 +221,7 @@ describe('RulesSettingsQueryDelayClient', () => { attributes: mockAttributes, references: [], }); - + // @ts-expect-error access private method const result = await client.createSettings(); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -254,6 +254,7 @@ describe('RulesSettingsQueryDelayClient', () => { attributes: mockAttributes, references: [], }); + // @ts-expect-error access private method const result = await client.getSettings(); expect(result.attributes).toEqual(mockAttributes); }); diff --git a/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.ts b/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.ts index ac394dca17180..c8dc863a02920 100644 --- a/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.ts +++ b/x-pack/plugins/alerting/server/rules_settings_client/query_delay/rules_settings_query_delay_client.ts @@ -121,19 +121,14 @@ export class RulesSettingsQueryDelayClient { } } - public async getSettings(): Promise> { - try { - return await this.savedObjectsClient.get( - RULES_SETTINGS_SAVED_OBJECT_TYPE, - RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID - ); - } catch (e) { - this.logger.error(`Failed to get query delay rules setting for current space. Error: ${e}`); - throw e; - } + private async getSettings(): Promise> { + return await this.savedObjectsClient.get( + RULES_SETTINGS_SAVED_OBJECT_TYPE, + RULES_SETTINGS_QUERY_DELAY_SAVED_OBJECT_ID + ); } - public async createSettings(): Promise> { + private async createSettings(): Promise> { const modificationMetadata = await this.getModificationMetadata(); const defaultQueryDelaySettings = this.isServerless ? DEFAULT_SERVERLESS_QUERY_DELAY_SETTINGS diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 93f655965e92a..2733521eab88f 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -30,6 +30,7 @@ import { isRuleSnoozed, lastRunFromError, ruleExecutionStatusToRaw, + getEsRequestTimeout, } from '../lib'; import { IntervalSchedule, @@ -403,6 +404,8 @@ export class TaskRunner< }, logger: this.logger, abortController: this.searchAbortController, + // Set the ES request timeout to the rule task timeout + requestTimeout: getEsRequestTimeout(this.logger, this.ruleType.ruleTaskTimeout), }; const scopedClusterClient = this.context.elasticsearch.client.asScoped(fakeRequest); const wrappedScopedClusterClient = createWrappedScopedClusterClientFactory({ diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts index c0c3c032a0e61..5c50e79c145aa 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/service_inventory/service_inventory.cy.ts @@ -115,7 +115,8 @@ describe('Service inventory', () => { }); }); - describe('Table search', () => { + // Skipping this until we enable the table search on the Service inventory view + describe.skip('Table search', () => { beforeEach(() => { cy.updateAdvancedSettings({ 'observability:apmEnableTableSearchBar': true, diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx index 95820bf8f84d4..c671bc2dda540 100644 --- a/x-pack/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ApmRuleType } from '@kbn/rule-data-utils'; +import type { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { APM_SERVER_FEATURE_ID } from '../../../../../common/rules/apm_rule_types'; import { getInitialAlertValues } from '../../utils/get_initial_alert_values'; import { ApmPluginStartDeps } from '../../../../plugin'; @@ -35,7 +36,7 @@ export function AlertingFlyout(props: Props) { const { start, end } = useTimeRange({ rangeFrom, rangeTo, optional: true }); const environment = - 'environment' in query ? query.environment : ENVIRONMENT_ALL.value; + 'environment' in query ? query.environment! : ENVIRONMENT_ALL.value; const transactionType = 'transactionType' in query ? query.transactionType : undefined; const transactionName = @@ -53,7 +54,10 @@ export function AlertingFlyout(props: Props) { const addAlertFlyout = useMemo( () => ruleType && - services.triggersActionsUi.getAddRuleFlyout({ + services.triggersActionsUi.getAddRuleFlyout< + RuleTypeParams, + AlertMetadata + >({ consumer: APM_SERVER_FEATURE_ID, onClose: onCloseAddFlyout, ruleTypeId: ruleType, @@ -67,7 +71,7 @@ export function AlertingFlyout(props: Props) { errorGroupingKey, start, end, - } as AlertMetadata, + }, useRuleProducer: true, }), /* eslint-disable-next-line react-hooks/exhaustive-deps */ diff --git a/x-pack/plugins/apm/public/components/alerting/utils/helper.ts b/x-pack/plugins/apm/public/components/alerting/utils/helper.ts index 7cc0d958aaf9e..66cfe522388f7 100644 --- a/x-pack/plugins/apm/public/components/alerting/utils/helper.ts +++ b/x-pack/plugins/apm/public/components/alerting/utils/helper.ts @@ -6,9 +6,11 @@ */ import { TIME_UNITS } from '@kbn/triggers-actions-ui-plugin/public'; +import type { RuleTypeMetaData } from '@kbn/alerting-plugin/common'; + import moment from 'moment'; -export interface AlertMetadata { +export interface AlertMetadata extends RuleTypeMetaData { environment: string; serviceName?: string; transactionType?: string; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index 08e7a840b5dfb..ba55defaaf4d7 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -357,6 +357,7 @@ export function ServiceList({ const tableSearchBar: TableSearchBar = useMemo(() => { return { + isEnabled: false, fieldsToSearch: ['serviceName'], maxCountExceeded, onChangeSearchQuery, diff --git a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx index 7d6307d32ffb8..ae14f63f8d72b 100644 --- a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx @@ -111,7 +111,7 @@ function UnoptimizedManagedTable(props: { const { core } = useApmPluginContext(); const isTableSearchBarEnabled = core.uiSettings.get( apmEnableTableSearchBar, - false + true ); const { diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index ba7e98d775efb..b4a21607a293c 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -82,7 +82,7 @@ export const INTERNAL_DELETE_FILE_ATTACHMENTS_URL = export const INTERNAL_GET_CASE_CATEGORIES_URL = `${CASES_INTERNAL_URL}/categories` as const; export const INTERNAL_CASE_METRICS_URL = `${CASES_INTERNAL_URL}/metrics` as const; export const INTERNAL_CASE_METRICS_DETAILS_URL = `${CASES_INTERNAL_URL}/metrics/{case_id}` as const; - +export const INTERNAL_PUT_CUSTOM_FIELDS_URL = `${CASES_INTERNAL_URL}/{case_id}/custom_fields/{custom_field_id}`; /** * Action routes */ @@ -201,10 +201,9 @@ export const SEARCH_DEBOUNCE_MS = 500; * Local storage keys */ export const LOCAL_STORAGE_KEYS = { - casesQueryParams: 'cases.list.queryParams', - casesFilterOptions: 'cases.list.filterOptions', casesTableColumns: 'cases.list.tableColumns', casesTableFiltersConfig: 'cases.list.tableFiltersConfig', + casesTableState: 'cases.list.state', }; /** diff --git a/x-pack/plugins/cases/common/types/api/case/v1.ts b/x-pack/plugins/cases/common/types/api/case/v1.ts index acb8049e01737..0dff1cac0d95d 100644 --- a/x-pack/plugins/cases/common/types/api/case/v1.ts +++ b/x-pack/plugins/cases/common/types/api/case/v1.ts @@ -51,7 +51,7 @@ const CaseCustomFieldTextWithValidationRt = rt.strict({ const CustomFieldRt = rt.union([CaseCustomFieldTextWithValidationRt, CaseCustomFieldToggleRt]); -const CustomFieldsRt = limitedArraySchema({ +export const CaseRequestCustomFieldsRt = limitedArraySchema({ codec: CustomFieldRt, fieldName: 'customFields', min: 0, @@ -124,7 +124,7 @@ export const CasePostRequestRt = rt.intersection([ /** * The list of custom field values of the case. */ - customFields: CustomFieldsRt, + customFields: CaseRequestCustomFieldsRt, }) ), ]); @@ -418,7 +418,7 @@ export const CasePatchRequestRt = rt.intersection([ /** * Custom fields of the case */ - customFields: CustomFieldsRt, + customFields: CaseRequestCustomFieldsRt, }) ), /** @@ -510,6 +510,7 @@ export type GetReportersResponse = rt.TypeOf; export type CasesBulkGetRequest = rt.TypeOf; export type CasesBulkGetResponse = rt.TypeOf; export type GetRelatedCasesByAlertResponse = rt.TypeOf; -export type CaseRequestCustomFields = rt.TypeOf; +export type CaseRequestCustomFields = rt.TypeOf; +export type CaseRequestCustomField = rt.TypeOf; export type BulkCreateCasesRequest = rt.TypeOf; export type BulkCreateCasesResponse = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts b/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts index e2f54761d6670..83d9a437c998d 100644 --- a/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts +++ b/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts @@ -7,7 +7,7 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH } from '../../../constants'; -import { CaseCustomFieldTextWithValidationValueRt } from './v1'; +import { CaseCustomFieldTextWithValidationValueRt, CustomFieldPutRequestRt } from './v1'; describe('Custom Fields', () => { describe('CaseCustomFieldTextWithValidationValueRt', () => { @@ -38,4 +38,66 @@ describe('Custom Fields', () => { ); }); }); + + describe('CustomFieldPutRequestRt', () => { + const defaultRequest = { + caseVersion: 'WzQ3LDFd', + value: 'this is a text field value', + }; + + it('has expected attributes in request', () => { + const query = CustomFieldPutRequestRt.decode(defaultRequest); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + + it('has expected attributes of toggle field in request', () => { + const newRequest = { + caseVersion: 'WzQ3LDFd', + value: false, + }; + const query = CustomFieldPutRequestRt.decode(newRequest); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: newRequest, + }); + }); + + it('removes foo:bar attributes from request', () => { + const query = CustomFieldPutRequestRt.decode({ ...defaultRequest, foo: 'bar' }); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + + it(`throws an error when a text customField is longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { + expect( + PathReporter.report( + CustomFieldPutRequestRt.decode({ + caseVersion: 'WzQ3LDFd', + value: '#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1), + }) + ) + ).toContain( + `The length of the value is too long. The maximum length is ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}.` + ); + }); + + it('throws an error when a text customField is empty', () => { + expect( + PathReporter.report( + CustomFieldPutRequestRt.decode({ + caseVersion: 'WzQ3LDFd', + value: '', + }) + ) + ).toContain('The value field cannot be an empty string.'); + }); + }); }); diff --git a/x-pack/plugins/cases/common/types/api/custom_field/v1.ts b/x-pack/plugins/cases/common/types/api/custom_field/v1.ts index 4ee70642c86b1..fb59f187991b3 100644 --- a/x-pack/plugins/cases/common/types/api/custom_field/v1.ts +++ b/x-pack/plugins/cases/common/types/api/custom_field/v1.ts @@ -5,6 +5,7 @@ * 2.0. */ +import * as rt from 'io-ts'; import { MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH } from '../../../constants'; import { limitedStringSchema } from '../../../schema'; @@ -14,3 +15,14 @@ export const CaseCustomFieldTextWithValidationValueRt = (fieldName: string) => min: 1, max: MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH, }); + +/** + * Update custom_field + */ + +export const CustomFieldPutRequestRt = rt.strict({ + value: rt.union([rt.boolean, rt.null, CaseCustomFieldTextWithValidationValueRt('value')]), + caseVersion: rt.string, +}); + +export type CustomFieldPutRequest = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 2ab04f058179d..a6e747ac6e85b 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -137,18 +137,6 @@ export interface QueryParams extends SortingParams { page: number; perPage: number; } -export type PartialQueryParams = Partial; - -export interface UrlQueryParams extends SortingParams { - page: string; - perPage: string; -} - -export interface ParsedUrlQueryParams extends Partial { - [index: string]: string | string[] | undefined | null; -} - -export type LocalStorageQueryParams = Partial>; export interface SystemFilterOptions { search: string; @@ -171,11 +159,13 @@ export interface FilterOptions extends SystemFilterOptions { }; } -export type PartialFilterOptions = Partial; - export type SingleCaseMetrics = SingleCaseMetricsResponse; export type SingleCaseMetricsFeature = Exclude; +/** + * If you add a new value here and you want to support it on the URL + * you have to also add it here x-pack/plugins/cases/public/components/all_cases/schema.ts + */ export enum SortFieldCase { closedAt = 'closedAt', createdAt = 'createdAt', diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index 028849f48fdb4..6e96596a41ab9 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -39,7 +39,11 @@ import { useGetTags } from '../../containers/use_get_tags'; import { useGetCategories } from '../../containers/use_get_categories'; import { useUpdateCase } from '../../containers/use_update_case'; import { useGetCases } from '../../containers/use_get_cases'; -import { DEFAULT_QUERY_PARAMS, DEFAULT_FILTER_OPTIONS } from '../../containers/constants'; +import { + DEFAULT_QUERY_PARAMS, + DEFAULT_FILTER_OPTIONS, + DEFAULT_CASES_TABLE_STATE, +} from '../../containers/constants'; import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile'; import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock'; import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles'; @@ -202,7 +206,9 @@ describe('AllCasesListGeneric', () => { expect(screen.getByTestId('case-table-case-count')).toHaveTextContent( `Showing 10 of ${useGetCasesMockState.data.total} cases` ); + expect(screen.queryByTestId('all-cases-maximum-limit-warning')).not.toBeInTheDocument(); + expect(screen.queryByTestId('all-cases-clear-filters-link-icon')).not.toBeInTheDocument(); }); }); @@ -643,6 +649,22 @@ describe('AllCasesListGeneric', () => { expect(alertCounts.length).toBeGreaterThan(0); }); + it('should clear the filters correctly', async () => { + useLicenseMock.mockReturnValue({ isAtLeastPlatinum: () => true }); + + appMockRenderer.render(); + + userEvent.click(await screen.findByTestId('options-filter-popover-button-category')); + await waitForEuiPopoverOpen(); + userEvent.click(await screen.findByTestId('options-filter-popover-item-twix')); + + userEvent.click(await screen.findByTestId('all-cases-clear-filters-link-icon')); + + await waitFor(() => { + expect(useGetCasesMock).toHaveBeenLastCalledWith(DEFAULT_CASES_TABLE_STATE); + }); + }); + describe('Solutions', () => { it('should hide the solutions filter if the owner is provided', async () => { const { queryByTestId } = render( 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 735ff95a5edf7..ea4810b5a8db3 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 @@ -8,18 +8,17 @@ 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'; +import deepEqual from 'react-fast-compare'; import type { CaseUI, FilterOptions, CasesUI } from '../../../common/ui/types'; import type { EuiBasicTableOnChange } from './types'; import { SortFieldCase } from '../../../common/ui/types'; import type { CaseStatuses } from '../../../common/types/domain'; -import { caseStatuses } from '../../../common/types/domain'; import { useCasesColumns } from './use_cases_columns'; import { CasesTableFilters } from './table_filters'; -import { CASES_TABLE_PERPAGE_VALUES } from './types'; +import { CASES_TABLE_PER_PAGE_VALUES } from './types'; import { CasesTable } from './table'; import { useCasesContext } from '../cases_context/use_cases_context'; import { CasesMetrics } from './cases_metrics'; @@ -32,6 +31,8 @@ import { useIsLoadingCases } from './use_is_loading_cases'; import { useAllCasesState } from './use_all_cases_state'; import { useAvailableCasesOwners } from '../app/use_available_owners'; import { useCasesColumnsSelection } from './use_cases_columns_selection'; +import { DEFAULT_CASES_TABLE_STATE } from '../../containers/constants'; +import { CasesTableUtilityBar } from './utility_bar'; const ProgressLoader = styled(EuiProgress)` ${({ $isShow }: { $isShow: boolean }) => @@ -64,15 +65,9 @@ export const AllCasesList = React.memo( const hasOwner = !!owner.length; - const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses)); - const initialFilterOptions = { - ...(!isEmpty(hiddenStatuses) && firstAvailableStatus && { status: [firstAvailableStatus] }), - }; + const { queryParams, setQueryParams, filterOptions, setFilterOptions } = + useAllCasesState(isSelectorView); - const { queryParams, setQueryParams, filterOptions, setFilterOptions } = useAllCasesState( - isSelectorView, - initialFilterOptions - ); const [selectedCases, setSelectedCases] = useState([]); const { data = initialData, isFetching: isLoadingCases } = useGetCases({ @@ -164,7 +159,7 @@ export const AllCasesList = React.memo( pageIndex: queryParams.page - 1, pageSize: queryParams.perPage, totalItemCount: data.total ?? 0, - pageSizeOptions: CASES_TABLE_PERPAGE_VALUES, + pageSizeOptions: CASES_TABLE_PER_PAGE_VALUES, }), [data, queryParams] ); @@ -190,6 +185,15 @@ export const AllCasesList = React.memo( onRowClick?.(undefined, true); }, [onRowClick]); + const onClearFilters = useCallback(() => { + setFilterOptions(DEFAULT_CASES_TABLE_STATE.filterOptions); + }, [setFilterOptions]); + + const showClearFiltersButton = !deepEqual( + DEFAULT_CASES_TABLE_STATE.filterOptions, + filterOptions + ); + return ( <> ( currentUserProfile={currentUserProfile} filterOptions={filterOptions} /> + ( isSelectorView={isSelectorView} onChange={tableOnChangeCallback} pagination={pagination} - selectedCases={selectedCases} selection={euiBasicTableSelectionProps} sorting={sorting} tableRowProps={tableRowProps} - deselectCases={deselectCases} - selectedColumns={selectedColumns} - onSelectedColumnsChange={setSelectedColumns} /> ); diff --git a/x-pack/plugins/cases/public/components/all_cases/constants.ts b/x-pack/plugins/cases/public/components/all_cases/constants.ts new file mode 100644 index 0000000000000..d55a1c4810f35 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/constants.ts @@ -0,0 +1,18 @@ +/* + * 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 CUSTOM_FIELD_KEY_PREFIX = 'cf_'; +export const ALL_CASES_STATE_URL_KEY = 'cases'; + +export const LEGACY_SUPPORTED_STATE_KEYS = [ + 'status', + 'severity', + 'page', + 'perPage', + 'sortField', + 'sortOrder', +] as const; diff --git a/x-pack/plugins/cases/public/components/all_cases/multi_select_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/multi_select_filter.test.tsx index 5210e3d52e215..50ea3a82974bf 100644 --- a/x-pack/plugins/cases/public/components/all_cases/multi_select_filter.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/multi_select_filter.test.tsx @@ -23,6 +23,7 @@ describe('multi select filter', () => { { label: 'tag d', key: 'tag d' }, ], onChange, + isLoading: false, }; render(); @@ -46,6 +47,7 @@ describe('multi select filter', () => { selectedOptionKeys: ['tag a'], limit: 1, limitReachedMessage: 'Limit reached', + isLoading: false, }; const { rerender } = render(); @@ -76,6 +78,7 @@ describe('multi select filter', () => { selectedOptionKeys: ['tag a'], limit: 2, limitReachedMessage: 'Limit reached', + isLoading: false, }; const { rerender } = render(); @@ -109,6 +112,7 @@ describe('multi select filter', () => { selectedOptionKeys: ['tag a'], limit: 1, limitReachedMessage: 'Limit reached', + isLoading: false, }; render(); @@ -134,6 +138,7 @@ describe('multi select filter', () => { ], onChange, selectedOptionKeys: ['tag b'], + isLoading: false, }; const { rerender } = render(); @@ -154,6 +159,7 @@ describe('multi select filter', () => { ], onChange, renderOption, + isLoading: false, }; render(); @@ -173,6 +179,7 @@ describe('multi select filter', () => { ], onChange, selectedOptionKeys: ['tag b'], + isLoading: false, }; const { rerender } = render(); diff --git a/x-pack/plugins/cases/public/components/all_cases/multi_select_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/multi_select_filter.tsx index b56c926bab965..080fe6df352c7 100644 --- a/x-pack/plugins/cases/public/components/all_cases/multi_select_filter.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/multi_select_filter.tsx @@ -76,6 +76,7 @@ interface UseFilterParams { renderOption?: (option: FilterOption) => React.ReactNode; selectedOptionKeys?: string[]; transparentBackground?: boolean; + isLoading: boolean; } export const MultiSelectFilter = ({ buttonLabel, @@ -89,6 +90,7 @@ export const MultiSelectFilter = ({ selectedOptionKeys = [], renderOption, transparentBackground, + isLoading, }: UseFilterParams) => { const { euiTheme } = useEuiTheme(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -101,13 +103,14 @@ export const MultiSelectFilter = ({ const newSelectedOptions = selectedOptionKeys.filter((selectedOptionKey) => rawOptions.some(({ key: optionKey }) => optionKey === selectedOptionKey) ); - if (!isEqual(newSelectedOptions, selectedOptionKeys)) { + + if (!isEqual(newSelectedOptions, selectedOptionKeys) && !isLoading) { onChange({ filterId: id, selectedOptionKeys: newSelectedOptions, }); } - }, [selectedOptionKeys, rawOptions, id, onChange]); + }, [selectedOptionKeys, rawOptions, id, onChange, isLoading]); const _onChange = (newOptions: Array>) => { const newSelectedOptions = getEuiSelectableCheckedOptions(newOptions); diff --git a/x-pack/plugins/cases/public/components/all_cases/schema.test.ts b/x-pack/plugins/cases/public/components/all_cases/schema.test.ts new file mode 100644 index 0000000000000..bdf6e627131f8 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/schema.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; +import { omit, pick } from 'lodash'; +import { DEFAULT_CASES_TABLE_STATE } from '../../containers/constants'; +import { AllCasesURLQueryParamsRt, validateSchema } from './schema'; + +describe('Schema', () => { + const supportedFilterOptions = pick(DEFAULT_CASES_TABLE_STATE.filterOptions, [ + 'search', + 'severity', + 'status', + 'tags', + 'assignees', + 'category', + ]); + + const defaultState = { + ...supportedFilterOptions, + ...DEFAULT_CASES_TABLE_STATE.queryParams, + }; + + describe('AllCasesURLQueryParamsRt', () => { + it('decodes correctly with defaults', () => { + const [params, errors] = validateNonExact(defaultState, AllCasesURLQueryParamsRt); + + expect(params).toEqual(defaultState); + expect(errors).toEqual(null); + }); + + it('decodes correctly with values', () => { + const state = { + assignees: ['elastic'], + tags: ['a', 'b'], + category: ['my category'], + status: ['open'], + search: 'My title', + severity: ['high'], + customFields: { my_field: ['one', 'two'] }, + sortOrder: 'asc', + sortField: 'updatedAt', + page: 5, + perPage: 20, + }; + + const [params, errors] = validateNonExact(state, AllCasesURLQueryParamsRt); + + expect(params).toEqual(state); + expect(errors).toEqual(null); + }); + + it('does not throws an error when missing fields', () => { + for (const [key] of Object.entries(defaultState)) { + const stateWithoutKey = omit(defaultState, key); + const [params, errors] = validateNonExact(stateWithoutKey, AllCasesURLQueryParamsRt); + + expect(params).toEqual(stateWithoutKey); + expect(errors).toEqual(null); + } + }); + + it('removes unknown properties', () => { + const [params, errors] = validateNonExact({ page: 10, foo: 'bar' }, AllCasesURLQueryParamsRt); + + expect(params).toEqual({ page: 10 }); + expect(errors).toEqual(null); + }); + + it.each(['status', 'severity', 'sortOrder', 'sortField', 'page', 'perPage'])( + 'throws if %s has invalid value', + (key) => { + const [params, errors] = validateNonExact({ [key]: 'foo' }, AllCasesURLQueryParamsRt); + + expect(params).toEqual(null); + expect(errors).toEqual(`Invalid value "foo" supplied to "${key}"`); + } + ); + }); + + describe('validateSchema', () => { + it('validates schema correctly', () => { + const params = validateSchema(defaultState, AllCasesURLQueryParamsRt); + expect(params).toEqual(defaultState); + }); + + it('throws an error if the schema is not valid', () => { + const params = validateSchema({ severity: 'foo' }, AllCasesURLQueryParamsRt); + expect(params).toEqual(null); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/schema.ts b/x-pack/plugins/cases/public/components/all_cases/schema.ts new file mode 100644 index 0000000000000..75f8e2be12dd4 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/schema.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isLeft } from 'fp-ts/lib/Either'; +import * as rt from 'io-ts'; +import { CaseSeverityRt, CaseStatusRt } from '../../../common/types/domain'; + +export const AllCasesURLQueryParamsRt = rt.exact( + rt.partial({ + search: rt.string, + severity: rt.array(CaseSeverityRt), + status: rt.array(CaseStatusRt), + tags: rt.array(rt.string), + category: rt.array(rt.string), + assignees: rt.array(rt.union([rt.string, rt.null])), + customFields: rt.record(rt.string, rt.array(rt.string)), + sortOrder: rt.union([rt.literal('asc'), rt.literal('desc')]), + sortField: rt.union([ + rt.literal('closedAt'), + rt.literal('createdAt'), + rt.literal('updatedAt'), + rt.literal('severity'), + rt.literal('status'), + rt.literal('title'), + rt.literal('category'), + ]), + page: rt.number, + perPage: rt.number, + }) +); + +export const validateSchema = ( + obj: unknown, + schema: T +): rt.TypeOf | null => { + const decoded = schema.decode(obj); + if (isLeft(decoded)) { + return null; + } else { + return decoded.right; + } +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/search.test.tsx b/x-pack/plugins/cases/public/components/all_cases/search.test.tsx new file mode 100644 index 0000000000000..8ea775e5e10a6 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/search.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; +import { TableSearch } from './search'; + +describe('TableSearch', () => { + const onFilterOptionsChange = jest.fn(); + + let appMockRender: AppMockRenderer; + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + jest.clearAllMocks(); + }); + + it('renders with empty value correctly', async () => { + appMockRender.render( + + ); + + await screen.findByDisplayValue(''); + }); + + it('renders with initial value correctly', async () => { + appMockRender.render( + + ); + + await screen.findByDisplayValue('My search'); + }); + + it('calls onFilterOptionsChange correctly', async () => { + appMockRender.render( + + ); + + userEvent.type(await screen.findByTestId('search-cases'), 'My search{enter}'); + + expect(onFilterOptionsChange).toHaveBeenCalledWith({ search: 'My search' }); + }); + + it('calls onFilterOptionsChange if the search term is empty', async () => { + appMockRender.render( + + ); + + userEvent.type(await screen.findByTestId('search-cases'), ' {enter}'); + + expect(onFilterOptionsChange).toHaveBeenCalledWith({ search: '' }); + }); + + it('calls onFilterOptionsChange when clearing the search bar', async () => { + appMockRender.render( + + ); + + await screen.findByDisplayValue('My search'); + + userEvent.click(await screen.findByTestId('clearSearchButton')); + + expect(onFilterOptionsChange).toHaveBeenCalledWith({ search: '' }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/search.tsx b/x-pack/plugins/cases/public/components/all_cases/search.tsx new file mode 100644 index 0000000000000..265c42470cdbd --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/search.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EuiFieldSearch } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import * as i18n from './translations'; +import type { FilterOptions } from '../../containers/types'; + +interface TableSearchComponentProps { + filterOptionsSearch: string; + onFilterOptionsChange: (filterOptions: Partial) => void; +} + +const TableSearchComponent: React.FC = ({ + filterOptionsSearch, + onFilterOptionsChange, +}) => { + const [search, setSearch] = useState(filterOptionsSearch); + + const onSearch = useCallback( + (newSearch) => { + const trimSearch = newSearch.trim(); + setSearch(trimSearch); + onFilterOptionsChange({ search: trimSearch }); + }, + [onFilterOptionsChange] + ); + + return ( + setSearch(e.target.value)} + onSearch={onSearch} + value={search} + /> + ); +}; + +TableSearchComponent.displayName = 'TableSearchComponent'; + +export const TableSearch = React.memo(TableSearchComponent); diff --git a/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx index 6924cbd13f1c7..28be7c63f22b3 100644 --- a/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx @@ -14,7 +14,9 @@ import { screen, waitFor } from '@testing-library/react'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; import { SeverityFilter } from './severity_filter'; -describe('Severity form field', () => { +// FLAKY: https://github.com/elastic/kibana/issues/176336 +// FLAKY: https://github.com/elastic/kibana/issues/176337 +describe.skip('Severity form field', () => { const onChange = jest.fn(); let appMockRender: AppMockRenderer; const props = { diff --git a/x-pack/plugins/cases/public/components/all_cases/severity_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/severity_filter.tsx index 650e5215c6aac..dbc7bdef31c2a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/severity_filter.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/severity_filter.tsx @@ -40,6 +40,7 @@ export const SeverityFilter: React.FC = ({ selectedOptionKeys, onChange } options={options} renderOption={renderOption} selectedOptionKeys={selectedOptionKeys} + isLoading={false} /> ); }; diff --git a/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx index f2002e4c7899b..39e024161144f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/solution_filter.tsx @@ -63,6 +63,7 @@ export const SolutionFilterComponent = ({ options={options} renderOption={renderOption} selectedOptionKeys={selectedOptionKeys} + isLoading={false} /> ); }; diff --git a/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx index cc4b032f96c71..6e665a3c19236 100644 --- a/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx @@ -73,6 +73,7 @@ export const StatusFilterComponent = ({ options={options} renderOption={renderOption} selectedOptionKeys={selectedOptionKeys} + isLoading={false} /> ); }; 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 edd7ab7e9955b..b3a943a2c31d9 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -12,11 +12,9 @@ import { EuiEmptyPrompt, EuiSkeletonText, EuiBasicTable } from '@elastic/eui'; import classnames from 'classnames'; import styled from 'styled-components'; -import { CasesTableUtilityBar } from './utility_bar'; import { LinkButton } from '../links'; -import type { CasesFindResponseUI, CasesUI, CaseUI } from '../../../common/ui/types'; -import type { CasesColumnSelection } from './types'; +import type { CasesFindResponseUI, CaseUI } from '../../../common/ui/types'; import * as i18n from './translations'; import { useCreateCaseNavigation } from '../../common/navigation'; @@ -32,14 +30,10 @@ interface CasesTableProps { isSelectorView?: boolean; onChange: EuiBasicTableProps['onChange']; pagination: Pagination; - selectedCases: CasesUI; selection: EuiTableSelectionType; sorting: EuiBasicTableProps['sorting']; tableRef?: MutableRefObject; tableRowProps: EuiBasicTableProps['rowProps']; - deselectCases: () => void; - selectedColumns: CasesColumnSelection[]; - onSelectedColumnsChange: (columns: CasesColumnSelection[]) => void; isLoadingColumns: boolean; } @@ -57,14 +51,10 @@ export const CasesTable: FunctionComponent = ({ isSelectorView, onChange, pagination, - selectedCases, selection, sorting, tableRef, tableRowProps, - deselectCases, - selectedColumns, - onSelectedColumnsChange, isLoadingColumns, }) => { const { permissions } = useCasesContext(); @@ -87,15 +77,6 @@ export const CasesTable: FunctionComponent = ({
) : ( <> - >; activeFilters: string[]; + isLoading: boolean; onChange: (params: { filterId: string; selectedOptionKeys: string[] }) => void; }) => { return ( @@ -28,6 +30,7 @@ export const MoreFiltersSelectable = ({ options={options} selectedOptionKeys={activeFilters} transparentBackground={true} + isLoading={isLoading} /> ); }; diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_custom_fields_filter_config.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_custom_fields_filter_config.tsx index 6901bca807319..ed5fa48838602 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_custom_fields_filter_config.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_custom_fields_filter_config.tsx @@ -6,13 +6,12 @@ */ import React from 'react'; +import type { CasesConfigurationUI } from '../../../../common/ui'; import type { CustomFieldTypes } from '../../../../common/types/domain'; import { builderMap as customFieldsBuilder } from '../../custom_fields/builder'; -import { useGetCaseConfiguration } from '../../../containers/configure/use_get_case_configuration'; import type { FilterChangeHandler, FilterConfig, FilterConfigRenderParams } from './types'; import { MultiSelectFilter } from '../multi_select_filter'; - -export const CUSTOM_FIELD_KEY_PREFIX = 'cf_'; +import { deflattenCustomFieldKey, flattenCustomFieldKey } from '../utils'; interface CustomFieldFilterOptionFactoryProps { buttonLabel: string; @@ -20,6 +19,7 @@ interface CustomFieldFilterOptionFactoryProps { fieldKey: string; onFilterOptionsChange: FilterChangeHandler; type: CustomFieldTypes; + isLoading: boolean; } const customFieldFilterOptionFactory = ({ buttonLabel, @@ -27,9 +27,10 @@ const customFieldFilterOptionFactory = ({ fieldKey, onFilterOptionsChange, type, + isLoading, }: CustomFieldFilterOptionFactoryProps) => { return { - key: `${CUSTOM_FIELD_KEY_PREFIX}${fieldKey}`, // this prefix is set in case custom field has the same key as a system field + key: flattenCustomFieldKey(fieldKey), // this prefix is set in case custom field has the same key as a system field isActive: false, isAvailable: true, label: buttonLabel, @@ -53,7 +54,7 @@ const customFieldFilterOptionFactory = ({ }) => { onFilterOptionsChange({ customFields: { - [filterId.replace(CUSTOM_FIELD_KEY_PREFIX, '')]: { + [deflattenCustomFieldKey(filterId)]: { options: selectedOptionKeys, type, }, @@ -71,6 +72,7 @@ const customFieldFilterOptionFactory = ({ label: option.label, }))} selectedOptionKeys={filterOptions.customFields[fieldKey]?.options || []} + isLoading={isLoading} /> ); }, @@ -79,15 +81,15 @@ const customFieldFilterOptionFactory = ({ export const useCustomFieldsFilterConfig = ({ isSelectorView, + customFields, + isLoading, onFilterOptionsChange, }: { isSelectorView: boolean; + customFields: CasesConfigurationUI['customFields']; + isLoading: boolean; onFilterOptionsChange: FilterChangeHandler; }) => { - const { - data: { customFields }, - } = useGetCaseConfiguration(); - const customFieldsFilterConfig: FilterConfig[] = []; if (isSelectorView) { @@ -106,6 +108,7 @@ export const useCustomFieldsFilterConfig = ({ fieldKey, onFilterOptionsChange, type, + isLoading, }) ); } diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.test.tsx index 62dd688cae29a..25dd550a3ecf3 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.test.tsx @@ -9,17 +9,10 @@ import { renderHook } from '@testing-library/react-hooks'; import type { AppMockRenderer } from '../../../common/mock'; import { createAppMockRenderer } from '../../../common/mock'; import type { FilterConfig, FilterConfigRenderParams } from './types'; -import { getCaseConfigure } from '../../../containers/configure/api'; import { useFilterConfig } from './use_filter_config'; import type { FilterOptions } from '../../../../common/ui'; - -jest.mock('../../../containers/configure/api', () => { - const originalModule = jest.requireActual('../../../containers/configure/api'); - return { - ...originalModule, - getCaseConfigure: jest.fn(), - }; -}); +import { CUSTOM_FIELD_KEY_PREFIX } from '../constants'; +import { CustomFieldTypes } from '../../../../common/types/domain'; const emptyFilterOptions: FilterOptions = { search: '', @@ -33,9 +26,31 @@ const emptyFilterOptions: FilterOptions = { category: [], customFields: {}, }; -const getCaseConfigureMock = getCaseConfigure as jest.Mock; describe('useFilterConfig', () => { + const onFilterOptionsChange = jest.fn(); + const getEmptyOptions = jest.fn().mockReturnValue({ severity: [] }); + const filters: FilterConfig[] = [ + { + key: 'severity', + label: 'Severity', + isActive: true, + isAvailable: true, + getEmptyOptions, + render: ({ filterOptions }: FilterConfigRenderParams) => null, + }, + { + key: 'tags', + label: 'Tags', + isActive: true, + isAvailable: true, + getEmptyOptions() { + return { tags: ['initialValue'] }; + }, + render: ({ filterOptions }: FilterConfigRenderParams) => null, + }, + ]; + let appMockRender: AppMockRenderer; beforeEach(() => { @@ -48,32 +63,6 @@ describe('useFilterConfig', () => { }); it('should remove a selected option if the filter is deleted', async () => { - getCaseConfigureMock.mockReturnValue(() => { - return []; - }); - const onFilterOptionsChange = jest.fn(); - const getEmptyOptions = jest.fn().mockReturnValue({ severity: [] }); - const filters: FilterConfig[] = [ - { - key: 'severity', - label: 'Severity', - isActive: true, - isAvailable: true, - getEmptyOptions, - render: ({ filterOptions }: FilterConfigRenderParams) => null, - }, - { - key: 'tags', - label: 'Tags', - isActive: true, - isAvailable: true, - getEmptyOptions() { - return { tags: ['initialValue'] }; - }, - render: ({ filterOptions }: FilterConfigRenderParams) => null, - }, - ]; - const { rerender } = renderHook(useFilterConfig, { wrapper: ({ children }) => {children}, initialProps: { @@ -81,16 +70,22 @@ describe('useFilterConfig', () => { onFilterOptionsChange, isSelectorView: false, filterOptions: emptyFilterOptions, + customFields: [], + isLoading: false, }, }); expect(onFilterOptionsChange).not.toHaveBeenCalled(); + rerender({ systemFilterConfig: [], onFilterOptionsChange, isSelectorView: false, filterOptions: emptyFilterOptions, + customFields: [], + isLoading: false, }); + expect(getEmptyOptions).toHaveBeenCalledTimes(1); expect(onFilterOptionsChange).toHaveBeenCalledTimes(1); expect(onFilterOptionsChange).toHaveBeenCalledWith({ @@ -98,4 +93,38 @@ describe('useFilterConfig', () => { tags: ['initialValue'], }); }); + + it('should activate custom fields correctly when they are hidden', async () => { + const customFieldKey = 'toggleKey'; + const uiCustomFieldKey = `${CUSTOM_FIELD_KEY_PREFIX}${customFieldKey}`; + + localStorage.setItem( + 'testAppId.cases.list.tableFiltersConfig', + JSON.stringify([{ key: uiCustomFieldKey, isActive: false }]) + ); + + const { result } = renderHook(useFilterConfig, { + wrapper: ({ children }) => {children}, + initialProps: { + systemFilterConfig: filters, + onFilterOptionsChange, + isSelectorView: false, + filterOptions: { + ...emptyFilterOptions, + customFields: { [customFieldKey]: { type: CustomFieldTypes.TOGGLE, options: ['on'] } }, + }, + customFields: [ + { + key: customFieldKey, + type: CustomFieldTypes.TOGGLE, + required: false, + label: 'My toggle', + }, + ], + isLoading: false, + }, + }); + + expect(result.current.activeSelectableOptionKeys).toEqual([uiCustomFieldKey]); + }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.tsx index 8d721cd13daa7..6c342770a12f3 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_filter_config.tsx @@ -9,11 +9,12 @@ import type { SetStateAction } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; import useLocalStorage from 'react-use/lib/useLocalStorage'; import { merge, isEqual, isEmpty } from 'lodash'; -import type { FilterOptions } from '../../../../common/ui'; +import type { CasesConfigurationUI, FilterOptions } from '../../../../common/ui'; import { LOCAL_STORAGE_KEYS } from '../../../../common/constants'; import type { FilterConfig, FilterConfigState } from './types'; import { useCustomFieldsFilterConfig } from './use_custom_fields_filter_config'; import { useCasesContext } from '../../cases_context/use_cases_context'; +import { deflattenCustomFieldKey, isFlattenCustomField } from '../utils'; const mergeSystemAndCustomFieldConfigs = ({ systemFilterConfig, @@ -38,6 +39,13 @@ const shouldBeActive = ({ filter: FilterConfigState; filterOptions: FilterOptions; }) => { + if (isFlattenCustomField(filter.key)) { + return ( + !filter.isActive && + !isEmpty(filterOptions.customFields[deflattenCustomFieldKey(filter.key)]?.options) + ); + } + return !filter.isActive && !isEmpty(filterOptions[filter.key as keyof FilterOptions]); }; @@ -52,6 +60,7 @@ const useActiveByFilterKeyState = ({ filterOptions }: { filterOptions: FilterOpt * Activates filters that aren't active but have a value in the filterOptions */ const newActiveByFilterKey = [...(activeByFilterKey || [])]; + newActiveByFilterKey.forEach((filter) => { if (shouldBeActive({ filter, filterOptions })) { const currentIndex = newActiveByFilterKey.findIndex((_filter) => filter.key === _filter.key); @@ -98,25 +107,45 @@ export const useFilterConfig = ({ onFilterOptionsChange, systemFilterConfig, filterOptions, + customFields, + isLoading, }: { isSelectorView: boolean; + isLoading: boolean; onFilterOptionsChange: (params: Partial) => void; systemFilterConfig: FilterConfig[]; filterOptions: FilterOptions; + customFields: CasesConfigurationUI['customFields']; }) => { /** * Initially we won't save any order, it will use the default config as it is defined in the system. * Once the user adds/removes a filter, we start saving the order and the visible state. */ - const [activeByFilterKey, setActiveByFilterKey] = useActiveByFilterKeyState({ filterOptions }); + const [activeByFilterKey, setActiveByFilterKey] = useActiveByFilterKeyState({ + filterOptions, + }); + const { customFieldsFilterConfig } = useCustomFieldsFilterConfig({ isSelectorView, + customFields, + isLoading, onFilterOptionsChange, }); + const activeCustomFieldsConfig = customFieldsFilterConfig.map((customField) => { + return { + ...customField, + isActive: Object.entries(filterOptions.customFields).find( + ([key, _]) => key === deflattenCustomFieldKey(customField.key) + ) + ? true + : customField.isActive, + }; + }); + const filterConfigs = mergeSystemAndCustomFieldConfigs({ systemFilterConfig, - customFieldsFilterConfig, + customFieldsFilterConfig: activeCustomFieldsConfig, }); const prevFilterConfigs = usePrevious(filterConfigs) ?? new Map(); @@ -192,11 +221,14 @@ export const useFilterConfig = ({ if (a.label < b.label) return -1; return a.key > b.key ? 1 : -1; }); + const source = activeByFilterKey && activeByFilterKey.length > 0 ? activeByFilterKey : filterConfigArray; + const activeFilters = source .filter((filter) => filter.isActive && filterConfigs.has(filter.key)) .map((filter) => filterConfigs.get(filter.key)) as FilterConfig[]; + const activeFilterKeys = activeFilters.map((filter) => filter.key); return { diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_system_filter_config.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_system_filter_config.tsx index ba2ca2d5f363f..e186e5c990d06 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_system_filter_config.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filter_config/use_system_filter_config.tsx @@ -137,6 +137,7 @@ export const getSystemFilterConfig = ({ onChange={onSystemFilterChange} options={mapToMultiSelectOption(tags)} selectedOptionKeys={filterOptions?.tags} + isLoading={isLoading} /> ), }, @@ -159,6 +160,7 @@ export const getSystemFilterConfig = ({ onChange={onSystemFilterChange} options={mapToMultiSelectOption(categories)} selectedOptionKeys={filterOptions?.category} + isLoading={isLoading} /> ), }, diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx index 0abed867a29b3..d407d03517b25 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx @@ -17,30 +17,26 @@ import { SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER } from '../../../common/co import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer } from '../../common/mock'; import { DEFAULT_FILTER_OPTIONS } from '../../containers/constants'; +import type { CasesTableFiltersProps } from './table_filters'; import { CasesTableFilters } from './table_filters'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetCategories } from '../../containers/use_get_categories'; import { useSuggestUserProfiles } from '../../containers/user_profiles/use_suggest_user_profiles'; import { userProfiles } from '../../containers/user_profiles/api.mock'; -import { getCaseConfigure } from '../../containers/configure/api'; -import { CUSTOM_FIELD_KEY_PREFIX } from './table_filter_config/use_custom_fields_filter_config'; +import { CUSTOM_FIELD_KEY_PREFIX } from './constants'; +import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration'; +import { useCaseConfigureResponse } from '../configure_cases/__mock__'; jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/use_get_categories'); jest.mock('../../containers/user_profiles/use_suggest_user_profiles'); -jest.mock('../../containers/configure/api', () => { - const originalModule = jest.requireActual('../../containers/configure/api'); - return { - ...originalModule, - getCaseConfigure: jest.fn(), - }; -}); +jest.mock('../../containers/configure/use_get_case_configuration'); -const getCaseConfigureMock = getCaseConfigure as jest.Mock; +const useGetCaseConfigurationMock = useGetCaseConfiguration as jest.Mock; const onFilterChanged = jest.fn(); -const props = { +const props: CasesTableFiltersProps = { countClosedCases: 1234, countOpenCases: 99, countInProgressCases: 54, @@ -48,7 +44,6 @@ const props = { filterOptions: DEFAULT_FILTER_OPTIONS, availableSolutions: [], isLoading: false, - initialFilterOptions: DEFAULT_FILTER_OPTIONS, currentUserProfile: undefined, }; @@ -100,6 +95,8 @@ describe('CasesTableFilters ', () => { isLoading: false, }); (useSuggestUserProfiles as jest.Mock).mockReturnValue({ data: userProfiles, isLoading: false }); + + useGetCaseConfigurationMock.mockImplementation(() => useCaseConfigureResponse); }); afterEach(() => { @@ -107,22 +104,22 @@ describe('CasesTableFilters ', () => { window.localStorage.clear(); }); - it('should render the case status filter dropdown', () => { + it('should render the case status filter dropdown', async () => { appMockRender.render(); - expect(screen.getByTestId('options-filter-popover-button-status')).toBeInTheDocument(); + expect(await screen.findByTestId('options-filter-popover-button-status')).toBeInTheDocument(); }); - it('should render the case severity filter dropdown', () => { + it('should render the case severity filter dropdown', async () => { appMockRender.render(); - expect(screen.getByTestId('options-filter-popover-button-severity')).toBeTruthy(); + expect(await screen.findByTestId('options-filter-popover-button-severity')).toBeTruthy(); }); it('should call onFilterChange when the severity filter changes', async () => { appMockRender.render(); - userEvent.click(screen.getByTestId('options-filter-popover-button-severity')); + userEvent.click(await screen.findByTestId('options-filter-popover-button-severity')); await waitForEuiPopoverOpen(); - userEvent.click(screen.getByTestId('options-filter-popover-item-high')); + userEvent.click(await screen.findByTestId('options-filter-popover-item-high')); expect(onFilterChanged).toBeCalledWith({ ...DEFAULT_FILTER_OPTIONS, severity: ['high'] }); }); @@ -130,9 +127,9 @@ describe('CasesTableFilters ', () => { it('should call onFilterChange when selected tags change', async () => { appMockRender.render(); - userEvent.click(screen.getByTestId('options-filter-popover-button-tags')); + userEvent.click(await screen.findByTestId('options-filter-popover-button-tags')); await waitForEuiPopoverOpen(); - userEvent.click(screen.getByTestId('options-filter-popover-item-coke')); + userEvent.click(await screen.findByTestId('options-filter-popover-item-coke')); expect(onFilterChanged).toBeCalledWith({ ...DEFAULT_FILTER_OPTIONS, tags: ['coke'] }); }); @@ -140,9 +137,9 @@ describe('CasesTableFilters ', () => { it('should call onFilterChange when selected category changes', async () => { appMockRender.render(); - userEvent.click(screen.getByTestId('options-filter-popover-button-category')); + userEvent.click(await screen.findByTestId('options-filter-popover-button-category')); await waitForEuiPopoverOpen(); - userEvent.click(screen.getByTestId('options-filter-popover-item-twix')); + userEvent.click(await screen.findByTestId('options-filter-popover-item-twix')); expect(onFilterChanged).toBeCalledWith({ ...DEFAULT_FILTER_OPTIONS, category: ['twix'] }); }); @@ -184,17 +181,39 @@ describe('CasesTableFilters ', () => { it('should call onFilterChange when search changes', async () => { appMockRender.render(); - userEvent.type(screen.getByTestId('search-cases'), 'My search{enter}'); + userEvent.type(await screen.findByTestId('search-cases'), 'My search{enter}'); + + await waitFor(() => { + expect(onFilterChanged.mock.calls[0][0].search).toEqual('My search'); + }); + }); + + it('should change the initial value of search when the state changes', async () => { + const { rerender } = appMockRender.render( + + ); + + await screen.findByDisplayValue('My search'); + + rerender( + + ); - expect(onFilterChanged).toBeCalledWith({ search: 'My search' }); + await screen.findByDisplayValue('My new search'); }); it('should call onFilterChange when changing status', async () => { appMockRender.render(); - userEvent.click(screen.getByTestId('options-filter-popover-button-status')); + userEvent.click(await screen.findByTestId('options-filter-popover-button-status')); await waitForEuiPopoverOpen(); - userEvent.click(screen.getByTestId('options-filter-popover-item-closed')); + userEvent.click(await screen.findByTestId('options-filter-popover-item-closed')); expect(onFilterChanged).toBeCalledWith({ ...DEFAULT_FILTER_OPTIONS, @@ -205,10 +224,10 @@ describe('CasesTableFilters ', () => { it('should show in progress status only when "in p" is searched in the filter', async () => { appMockRender.render(); - userEvent.click(screen.getByTestId('options-filter-popover-button-status')); + userEvent.click(await screen.findByTestId('options-filter-popover-button-status')); await waitForEuiPopoverOpen(); - userEvent.type(screen.getByTestId('status-search-input'), 'in p'); + userEvent.type(await screen.findByTestId('status-search-input'), 'in p'); const allOptions = screen.getAllByRole('option'); expect(allOptions).toHaveLength(1); @@ -234,7 +253,7 @@ describe('CasesTableFilters ', () => { appMockRender = createAppMockRenderer({ license }); appMockRender.render(); - userEvent.click(screen.getByTestId('options-filter-popover-button-assignees')); + userEvent.click(await screen.findByTestId('options-filter-popover-button-assignees')); await waitForEuiPopoverOpen(); userEvent.click(screen.getByText('Physical Dinosaur')); @@ -261,7 +280,7 @@ describe('CasesTableFilters ', () => { }); describe('Solution filter', () => { - it('shows Solution filter when provided more than 1 availableSolutions', () => { + it('shows Solution filter when provided more than 1 availableSolutions', async () => { appMockRender = createAppMockRenderer({ owner: [SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER], }); @@ -271,7 +290,7 @@ describe('CasesTableFilters ', () => { availableSolutions={[SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER]} /> ); - expect(screen.getByTestId('options-filter-popover-button-owner')).toBeInTheDocument(); + expect(await screen.findByTestId('options-filter-popover-button-owner')).toBeInTheDocument(); }); it('does not show Solution filter when provided less than 1 availableSolutions', () => { @@ -282,7 +301,7 @@ describe('CasesTableFilters ', () => { expect(screen.queryByTestId('options-filter-popover-button-owner')).not.toBeInTheDocument(); }); - it('does not select a solution on initial render', () => { + it('does not select a solution on initial render', async () => { appMockRender = createAppMockRenderer({ owner: [SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER], }); @@ -293,7 +312,7 @@ describe('CasesTableFilters ', () => { /> ); - expect(screen.getByTestId('options-filter-popover-button-owner')).not.toHaveAttribute( + expect(await screen.findByTestId('options-filter-popover-button-owner')).not.toHaveAttribute( 'hasActiveFilters' ); }); @@ -375,10 +394,12 @@ describe('CasesTableFilters ', () => { appMockRender = createAppMockRenderer({ license }); appMockRender.render(); - expect(screen.getByTestId('options-filter-popover-button-assignees')).toBeInTheDocument(); + expect( + await screen.findByTestId('options-filter-popover-button-assignees') + ).toBeInTheDocument(); }); - it('shuld reset the assignees when deactivating the filter', async () => { + it('should reset the assignees when deactivating the filter', async () => { const overrideProps = { ...props, filterOptions: { @@ -411,7 +432,7 @@ describe('CasesTableFilters ', () => { expect(screen.queryByTestId('cases-table-add-case-filter-bar')).not.toBeInTheDocument(); }); - it('should render the create case button when isSelectorView is true and onCreateCasePressed are passed', () => { + it('should render the create case button when isSelectorView is true and onCreateCasePressed are passed', async () => { const onCreateCasePressed = jest.fn(); appMockRender.render( { onCreateCasePressed={onCreateCasePressed} /> ); - expect(screen.getByTestId('cases-table-add-case-filter-bar')).toBeInTheDocument(); + expect(await screen.findByTestId('cases-table-add-case-filter-bar')).toBeInTheDocument(); }); it('should call the onCreateCasePressed when create case is clicked', async () => { @@ -433,7 +454,7 @@ describe('CasesTableFilters ', () => { /> ); - userEvent.click(screen.getByTestId('cases-table-add-case-filter-bar')); + userEvent.click(await screen.findByTestId('cases-table-add-case-filter-bar')); await waitForComponentToUpdate(); // NOTE: intentionally checking no arguments are passed @@ -451,11 +472,14 @@ describe('CasesTableFilters ', () => { 'testAppId.cases.list.tableFiltersConfig', JSON.stringify(previousState) ); - getCaseConfigureMock.mockImplementation(() => { - return { + + useGetCaseConfigurationMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + data: { + ...useCaseConfigureResponse.data, customFields: [{ type: 'toggle', key: customFieldKey, label: 'Toggle', required: false }], - }; - }); + }, + })); }); afterEach(() => { @@ -481,7 +505,7 @@ describe('CasesTableFilters ', () => { userEvent.click(await screen.findByRole('button', { name: 'Toggle' })); await waitForEuiPopoverOpen(); - userEvent.click(screen.getByTestId('options-filter-popover-item-on')); + userEvent.click(await screen.findByTestId('options-filter-popover-item-on')); expect(onFilterChanged).toBeCalledWith({ ...DEFAULT_FILTER_OPTIONS, @@ -500,7 +524,7 @@ describe('CasesTableFilters ', () => { userEvent.click(await screen.findByRole('button', { name: 'Toggle' })); await waitForEuiPopoverOpen(); - userEvent.click(screen.getByTestId('options-filter-popover-item-off')); + userEvent.click(await screen.findByTestId('options-filter-popover-item-off')); expect(onFilterChanged).toBeCalledWith({ ...DEFAULT_FILTER_OPTIONS, @@ -531,7 +555,7 @@ describe('CasesTableFilters ', () => { userEvent.click(await screen.findByRole('button', { name: 'Toggle' })); await waitForEuiPopoverOpen(); - userEvent.click(screen.getByTestId('options-filter-popover-item-off')); + userEvent.click(await screen.findByTestId('options-filter-popover-item-off')); expect(onFilterChanged).toHaveBeenCalledWith({ ...DEFAULT_FILTER_OPTIONS, @@ -581,21 +605,23 @@ describe('CasesTableFilters ', () => { describe('custom filters configuration', () => { beforeEach(() => { - getCaseConfigureMock.mockImplementation(() => { - return { + useGetCaseConfigurationMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + data: { + ...useCaseConfigureResponse.data, customFields: [ { type: 'toggle', key: 'toggle', label: 'Toggle', required: false }, { type: 'text', key: 'text', label: 'Text', required: false }, ], - }; - }); + }, + })); }); afterEach(() => { jest.clearAllMocks(); }); - it('shouldnt render the more button when in selector view', async () => { + it('should not render the more button when in selector view', async () => { appMockRender.render(); expect(screen.queryByRole('button', { name: 'More' })).not.toBeInTheDocument(); }); @@ -607,9 +633,9 @@ describe('CasesTableFilters ', () => { userEvent.click(screen.getByRole('button', { name: 'More' })); await waitFor(() => expect(screen.getAllByRole('option')).toHaveLength(5)); - expect(screen.getByTestId('options-filter-popover-item-status')).toBeInTheDocument(); + expect(await screen.findByTestId('options-filter-popover-item-status')).toBeInTheDocument(); expect( - screen.getByTestId(`options-filter-popover-item-${CUSTOM_FIELD_KEY_PREFIX}toggle`) + await screen.findByTestId(`options-filter-popover-item-${CUSTOM_FIELD_KEY_PREFIX}toggle`) ).toBeInTheDocument(); }); @@ -632,7 +658,7 @@ describe('CasesTableFilters ', () => { userEvent.click(screen.getByRole('option', { name: 'Toggle' })); expect(screen.getByRole('button', { name: 'Toggle' })).toBeInTheDocument(); - const filterBar = screen.getByTestId('cases-table-filters'); + const filterBar = await screen.findByTestId('cases-table-filters'); const allFilters = within(filterBar).getAllByRole('button'); const orderedFilterLabels = ['Severity', 'Status', 'Tags', 'Categories', 'Toggle', 'More']; orderedFilterLabels.forEach((label, index) => { @@ -685,7 +711,7 @@ describe('CasesTableFilters ', () => { userEvent.click(screen.getByRole('option', { name: 'Status' })); expect(screen.queryByRole('button', { name: 'Status' })).not.toBeInTheDocument(); - const filterBar = screen.getByTestId('cases-table-filters'); + const filterBar = await screen.findByTestId('cases-table-filters'); const allFilters = within(filterBar).getAllByRole('button'); const orderedFilterLabels = ['Severity', 'Tags', 'Categories', 'More']; orderedFilterLabels.forEach((label, index) => { @@ -764,6 +790,7 @@ describe('CasesTableFilters ', () => { { key: 'status', isActive: false }, { key: 'severity', isActive: true }, ]; + localStorage.setItem( 'testAppId.cases.list.tableFiltersConfig', JSON.stringify(previousState) @@ -771,7 +798,7 @@ describe('CasesTableFilters ', () => { appMockRender.render(); - const filterBar = screen.getByTestId('cases-table-filters'); + const filterBar = await screen.findByTestId('cases-table-filters'); let allFilters: HTMLElement[]; await waitFor(() => { allFilters = within(filterBar).getAllByRole('button'); @@ -801,7 +828,7 @@ describe('CasesTableFilters ', () => { appMockRender.render(); - const filterBar = screen.getByTestId('cases-table-filters'); + const filterBar = await screen.findByTestId('cases-table-filters'); let allFilters: HTMLElement[]; await waitFor(() => { allFilters = within(filterBar).getAllByRole('button'); @@ -815,8 +842,10 @@ describe('CasesTableFilters ', () => { }); it('should sort the labels shown in the popover (on equal label, sort by key)', async () => { - getCaseConfigureMock.mockImplementation(() => { - return { + useGetCaseConfigurationMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + data: { + ...useCaseConfigureResponse.data, customFields: [ { type: 'toggle', key: 'za', label: 'ZToggle', required: false }, { type: 'toggle', key: 'tc', label: 'Toggle', required: false }, @@ -824,8 +853,9 @@ describe('CasesTableFilters ', () => { { type: 'toggle', key: 'tb', label: 'Toggle', required: false }, { type: 'toggle', key: 'aa', label: 'AToggle', required: false }, ], - }; - }); + }, + })); + appMockRender.render(); userEvent.click(screen.getByRole('button', { name: 'More' })); @@ -851,10 +881,10 @@ describe('CasesTableFilters ', () => { }); }); - it('when a filter is active and isnt last in the list, it should move the filter to last position after deactivating and activating', async () => { + it('when a filter is active and is not last in the list, it should move the filter to last position after deactivating and activating', async () => { appMockRender.render(); - const filterBar = screen.getByTestId('cases-table-filters'); + const filterBar = await screen.findByTestId('cases-table-filters'); let allFilters = within(filterBar).getAllByRole('button'); let orderedFilterLabels = ['Severity', 'Status', 'Tags', 'Categories', 'More']; orderedFilterLabels.forEach((label, index) => { @@ -876,17 +906,20 @@ describe('CasesTableFilters ', () => { }); it('should avoid key collisions between custom fields and default fields', async () => { - getCaseConfigureMock.mockImplementation(() => { - return { + useGetCaseConfigurationMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + data: { + ...useCaseConfigureResponse.data, customFields: [ { type: 'toggle', key: 'severity', label: 'Fake Severity', required: false }, { type: 'toggle', key: 'status', label: 'Fake Status', required: false }, ], - }; - }); + }, + })); + appMockRender.render(); - const filterBar = screen.getByTestId('cases-table-filters'); + const filterBar = await screen.findByTestId('cases-table-filters'); let allFilters: HTMLElement[]; await waitFor(() => { allFilters = within(filterBar).getAllByRole('button'); @@ -906,7 +939,7 @@ describe('CasesTableFilters ', () => { }); }); - it('should delete stored filters that dont exist anymore', async () => { + it('should delete stored filters that do not exist anymore', async () => { const previousState = [ { key: 'severity', isActive: true }, { key: 'status', isActive: false }, @@ -954,35 +987,111 @@ describe('CasesTableFilters ', () => { ] `); }); - }); - it('should activate a filter when there is a value in the global state as this means that it has a value set in the url', async () => { - const previousState = [ - { key: 'severity', isActive: false }, // notice severity filter not active - { key: 'status', isActive: false }, // notice status filter not active - { key: 'tags', isActive: true }, - { key: 'category', isActive: false }, - ]; + it('should activate all filters when there is a value in the global state and is not active in the local storage', async () => { + const license = licensingMock.createLicense({ + license: { type: 'platinum' }, + }); - localStorage.setItem('testAppId.cases.list.tableFiltersConfig', JSON.stringify(previousState)); + const previousState = [ + { key: 'severity', isActive: false }, // notice severity filter not active + { key: 'status', isActive: false }, // notice status filter not active + { key: 'tags', isActive: false }, + { key: 'category', isActive: false }, + { key: 'cf_toggle', isActive: false }, + { key: 'assignees', isActive: false }, + ]; - const overrideProps = { - ...props, - filterOptions: { - ...DEFAULT_FILTER_OPTIONS, - severity: [CaseSeverity.MEDIUM], // but they have values - status: [CaseStatuses.open, CaseStatuses['in-progress']], - }, - }; + localStorage.setItem( + 'testAppId.cases.list.tableFiltersConfig', + JSON.stringify(previousState) + ); - appMockRender.render(); + const overrideProps = { + ...props, + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + severity: [CaseSeverity.MEDIUM], // but they have values + status: [CaseStatuses.open, CaseStatuses['in-progress']], + tags: ['coke'], + category: ['twix'], + assignees: [userProfiles[0].uid], + customFields: { toggle: { type: CustomFieldTypes.TOGGLE, options: ['on'] } }, + }, + }; + + appMockRender = createAppMockRenderer({ license }); + appMockRender.render(); - const statusButton = await screen.findByRole('button', { name: 'Status' }); - expect(statusButton).toBeInTheDocument(); - expect(within(statusButton).getByLabelText('2 active filters')).toBeInTheDocument(); + const filters = [ + { name: 'Status', active: 2 }, + { name: 'Severity', active: 1 }, + { name: 'Tags', active: 1 }, + { name: 'Categories', active: 1 }, + { name: 'Toggle', active: 1 }, + { name: 'click to filter assignees', active: 1 }, + ]; + + await waitForComponentToUpdate(); + + const totalFilters = await screen.findAllByRole('button'); + // plus the more button + expect(totalFilters.length).toBe(filters.length + 1); + + for (const filter of filters) { + const button = await screen.findByRole('button', { name: filter.name }); + expect(button).toBeInTheDocument(); + expect( + await within(button).findByLabelText(`${filter.active} active filters`) + ).toBeInTheDocument(); + } + }); - const severityButton = await screen.findByRole('button', { name: 'Severity' }); - expect(severityButton).toBeInTheDocument(); - expect(within(severityButton).getByLabelText('1 active filters')).toBeInTheDocument(); + it('should activate all filters when there is a value in the global state and the local storage is empty', async () => { + const license = licensingMock.createLicense({ + license: { type: 'platinum' }, + }); + + localStorage.setItem('testAppId.cases.list.tableFiltersConfig', JSON.stringify([])); + + const overrideProps = { + ...props, + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + severity: [CaseSeverity.MEDIUM], // but they have values + status: [CaseStatuses.open, CaseStatuses['in-progress']], + tags: ['coke'], + category: ['twix'], + assignees: [userProfiles[0].uid], + customFields: { toggle: { type: CustomFieldTypes.TOGGLE, options: ['on'] } }, + }, + }; + + appMockRender = createAppMockRenderer({ license }); + appMockRender.render(); + + const filters = [ + { name: 'Status', active: 2 }, + { name: 'Severity', active: 1 }, + { name: 'Tags', active: 1 }, + { name: 'Categories', active: 1 }, + { name: 'Toggle', active: 1 }, + { name: 'click to filter assignees', active: 1 }, + ]; + + await waitForComponentToUpdate(); + + const totalFilters = await screen.findAllByRole('button'); + // plus the more button + expect(totalFilters.length).toBe(filters.length + 1); + + for (const filter of filters) { + const button = await screen.findByRole('button', { name: filter.name }); + expect(button).toBeInTheDocument(); + expect( + await within(button).findByLabelText(`${filter.active} active filters`) + ).toBeInTheDocument(); + } + }); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx index dbdc947418eca..adece041c67ad 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useCallback, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import { mergeWith, isEqual } from 'lodash'; import { MoreFiltersSelectable } from './table_filter_config/more_filters_selectable'; import type { CaseStatuses } from '../../../common/types/domain'; @@ -18,6 +18,8 @@ import type { CurrentUserProfile } from '../types'; import { useCasesFeatures } from '../../common/use_cases_features'; import { useSystemFilterConfig } from './table_filter_config/use_system_filter_config'; import { useFilterConfig } from './table_filter_config/use_filter_config'; +import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration'; +import { TableSearch } from './search'; export interface CasesTableFiltersProps { countClosedCases: number | null; @@ -52,10 +54,13 @@ const CasesTableFiltersComponent = ({ currentUserProfile, filterOptions, }: CasesTableFiltersProps) => { - const [search, setSearch] = useState(filterOptions.search); - const { data: tags = [] } = useGetTags(); - const { data: categories = [] } = useGetCategories(); + const { data: tags = [], isLoading: isLoadingTags } = useGetTags(); + const { data: categories = [], isLoading: isLoadingCategories } = useGetCategories(); const { caseAssignmentAuthorized } = useCasesFeatures(); + const { + data: { customFields }, + isFetching: isLoadingCasesConfiguration, + } = useGetCaseConfiguration(); const onFilterOptionsChange = useCallback( (partialFilterOptions: Partial) => { @@ -67,6 +72,9 @@ const CasesTableFiltersComponent = ({ [filterOptions, onFilterChanged] ); + const isLoadingFilters = + isLoading || isLoadingTags || isLoadingCategories || isLoadingCasesConfiguration; + const { systemFilterConfig } = useSystemFilterConfig({ availableSolutions, caseAssignmentAuthorized, @@ -76,7 +84,7 @@ const CasesTableFiltersComponent = ({ countOpenCases, currentUserProfile, hiddenStatuses, - isLoading, + isLoading: isLoadingFilters, isSelectorView, onFilterOptionsChange, tags, @@ -87,18 +95,14 @@ const CasesTableFiltersComponent = ({ selectableOptions, activeSelectableOptionKeys, onFilterConfigChange, - } = useFilterConfig({ systemFilterConfig, onFilterOptionsChange, isSelectorView, filterOptions }); - - const handleOnSearch = useCallback( - (newSearch) => { - const trimSearch = newSearch.trim(); - if (!isEqual(trimSearch, search)) { - setSearch(trimSearch); - onFilterChanged({ search: trimSearch }); - } - }, - [onFilterChanged, search] - ); + } = useFilterConfig({ + systemFilterConfig, + onFilterOptionsChange, + isSelectorView, + filterOptions, + customFields, + isLoading: isLoadingFilters, + }); const handleOnCreateCasePressed = useCallback(() => { if (onCreateCasePressed) { @@ -126,13 +130,15 @@ const CasesTableFiltersComponent = ({ ) : null} - {activeFilters.map((filter) => ( @@ -147,6 +153,7 @@ const CasesTableFiltersComponent = ({ options={selectableOptions} activeFilters={activeSelectableOptionKeys} onChange={onFilterConfigChange} + isLoading={isLoadingFilters} /> )} diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts index f0c402d097e8d..e29019516e911 100644 --- a/x-pack/plugins/cases/public/components/all_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts @@ -139,12 +139,9 @@ export const FILTER_ASSIGNEES_ARIA_LABEL = i18n.translate( } ); -export const CLEAR_FILTERS = i18n.translate( - 'xpack.cases.allCasesView.filterAssignees.clearFilters', - { - defaultMessage: 'Clear filters', - } -); +export const CLEAR_FILTERS = i18n.translate('xpack.cases.allCasesView.clearFilters', { + defaultMessage: 'Clear filters', +}); export const TOTAL_ASSIGNEES_FILTERED = (total: number) => i18n.translate('xpack.cases.allCasesView.totalFilteredUsers', { diff --git a/x-pack/plugins/cases/public/components/all_cases/types.ts b/x-pack/plugins/cases/public/components/all_cases/types.ts index c0872b63cd892..4a1dd61aa505c 100644 --- a/x-pack/plugins/cases/public/components/all_cases/types.ts +++ b/x-pack/plugins/cases/public/components/all_cases/types.ts @@ -5,9 +5,11 @@ * 2.0. */ -import type { SortOrder } from '../../../common/ui'; +import type * as rt from 'io-ts'; +import type { FilterOptions, QueryParams, SortOrder } from '../../../common/ui'; +import type { AllCasesURLQueryParamsRt } from './schema'; -export const CASES_TABLE_PERPAGE_VALUES = [10, 25, 50, 100]; +export const CASES_TABLE_PER_PAGE_VALUES = [10, 25, 50, 100]; export interface EuiBasicTableSortTypes { field: string; @@ -32,3 +34,21 @@ export interface CasesColumnSelection { name: string; isChecked: boolean; } + +type SupportedFilterOptionsInURL = Pick< + FilterOptions, + 'search' | 'severity' | 'status' | 'tags' | 'assignees' | 'category' +>; + +export interface AllCasesTableState { + filterOptions: FilterOptions; + queryParams: QueryParams; +} + +export interface AllCasesURLState { + filterOptions: Partial & + Partial>; + queryParams: Partial; +} + +export type AllCasesURLQueryParams = rt.TypeOf; diff --git a/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.test.tsx index 30de5acb0bfac..43aec66176c6f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.test.tsx @@ -6,351 +6,780 @@ */ import React from 'react'; -import { useHistory } from 'react-router-dom'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { waitFor } from '@testing-library/react'; +import { CaseStatuses } from '@kbn/cases-components'; import { TestProviders } from '../../common/mock'; -import { - useAllCasesState, - getQueryParamsLocalStorageKey, - getFilterOptionsLocalStorageKey, -} from './use_all_cases_state'; -import { - DEFAULT_FILTER_OPTIONS, - DEFAULT_QUERY_PARAMS, - DEFAULT_TABLE_ACTIVE_PAGE, - DEFAULT_TABLE_LIMIT, -} from '../../containers/constants'; -import { CaseStatuses } from '../../../common/types/domain'; +import { useAllCasesState } from './use_all_cases_state'; +import { DEFAULT_CASES_TABLE_STATE, DEFAULT_TABLE_LIMIT } from '../../containers/constants'; import { SortFieldCase } from '../../containers/types'; -import { stringifyToURL } from '../utils'; - -const LOCAL_STORAGE_QUERY_PARAMS_DEFAULTS = { - perPage: DEFAULT_QUERY_PARAMS.perPage, - sortOrder: DEFAULT_QUERY_PARAMS.sortOrder, -}; - -const LOCAL_STORAGE_FILTER_OPTIONS_DEFAULTS = { - severity: DEFAULT_FILTER_OPTIONS.severity, - status: DEFAULT_FILTER_OPTIONS.status, -}; - -const URL_DEFAULTS = { - ...DEFAULT_QUERY_PARAMS, - ...LOCAL_STORAGE_FILTER_OPTIONS_DEFAULTS, -}; +import { stringifyUrlParams } from './utils/stringify_url_params'; +import { CaseSeverity } from '../../../common'; +import type { AllCasesTableState } from './types'; +import { CustomFieldTypes } from '../../../common/types/domain'; +import { useCaseConfigureResponse } from '../configure_cases/__mock__'; +import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration'; const mockLocation = { search: '' }; +const mockPush = jest.fn(); +const mockReplace = jest.fn(); + +jest.mock('../../containers/configure/use_get_case_configuration'); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn().mockImplementation(() => { return mockLocation; }), - useHistory: jest.fn().mockReturnValue({ - replace: jest.fn(), - push: jest.fn(), + useHistory: jest.fn().mockImplementation(() => ({ + replace: mockReplace, + push: mockPush, location: { search: '', }, - }), + })), })); -const APP_ID = 'testAppId'; -const LOCALSTORAGE_QUERY_PARAMS_KEY = getQueryParamsLocalStorageKey(APP_ID); -const LOCALSTORAGE_FILTER_OPTIONS_KEY = getFilterOptionsLocalStorageKey(APP_ID); +const useGetCaseConfigurationMock = useGetCaseConfiguration as jest.Mock; + +const LS_KEY = 'testAppId.cases.list.state'; describe('useAllCasesQueryParams', () => { beforeEach(() => { localStorage.clear(); + mockLocation.search = ''; + + useGetCaseConfigurationMock.mockImplementation(() => useCaseConfigureResponse); }); afterEach(() => { jest.clearAllMocks(); }); - it('calls setState with default values on first run', () => { + it('returns default state with empty URL and local storage', () => { const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(result.current.queryParams).toStrictEqual(DEFAULT_QUERY_PARAMS); - expect(result.current.filterOptions).toStrictEqual(DEFAULT_FILTER_OPTIONS); + expect(result.current.queryParams).toStrictEqual(DEFAULT_CASES_TABLE_STATE.queryParams); + expect(result.current.filterOptions).toStrictEqual(DEFAULT_CASES_TABLE_STATE.filterOptions); }); - it('updates localstorage with default values on first run', () => { - expect(localStorage.getItem(LOCALSTORAGE_QUERY_PARAMS_KEY)).toStrictEqual(null); - expect(localStorage.getItem(LOCALSTORAGE_FILTER_OPTIONS_KEY)).toStrictEqual(null); + it('takes into account existing localStorage query params on first run', () => { + const existingLocalStorageValues = { + queryParams: { + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_TABLE_LIMIT + 10, + sortOrder: 'asc', + sortField: SortFieldCase.severity, + }, + filterOptions: DEFAULT_CASES_TABLE_STATE.filterOptions, + }; + + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); - renderHook(() => useAllCasesState(), { + const { result } = renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current.queryParams).toMatchObject(existingLocalStorageValues.queryParams); + }); + + it('takes into account existing localStorage filter options values on first run', () => { + const existingLocalStorageValues = { + queryParams: DEFAULT_CASES_TABLE_STATE.queryParams, + filterOptions: { + ...DEFAULT_CASES_TABLE_STATE.filterOptions, + severity: ['critical'], + status: ['open'], + }, + }; + + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); + + const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(JSON.parse(localStorage.getItem(LOCALSTORAGE_QUERY_PARAMS_KEY)!)).toMatchObject({ - ...LOCAL_STORAGE_QUERY_PARAMS_DEFAULTS, + expect(result.current.filterOptions).toMatchObject(existingLocalStorageValues.filterOptions); + }); + + it('takes into account existing url query params on first run', () => { + mockLocation.search = stringifyUrlParams({ page: 2, perPage: 15 }); + + const { result } = renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, }); - expect(JSON.parse(localStorage.getItem(LOCALSTORAGE_FILTER_OPTIONS_KEY)!)).toMatchObject({ - ...LOCAL_STORAGE_FILTER_OPTIONS_DEFAULTS, + + expect(result.current.queryParams).toMatchObject({ + ...DEFAULT_CASES_TABLE_STATE.queryParams, + ...{ page: 2, perPage: 15 }, }); }); - it('takes into account input filter options', () => { - const existingLocalStorageValues = { owner: ['foobar'], status: [CaseStatuses.open] }; + it('takes into account existing url filter options on first run', () => { + mockLocation.search = stringifyUrlParams({ + severity: [CaseSeverity.CRITICAL], + status: [CaseStatuses.open], + }); - const { result } = renderHook(() => useAllCasesState(false, existingLocalStorageValues), { + const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(result.current.filterOptions).toStrictEqual({ - ...DEFAULT_FILTER_OPTIONS, - ...existingLocalStorageValues, + expect(result.current.filterOptions).toMatchObject({ + ...DEFAULT_CASES_TABLE_STATE.filterOptions, + ...{ severity: ['critical'], status: ['open'] }, }); }); - it('calls history.replace on every run', () => { + it('takes into account legacy url filter option "all"', () => { + const nonDefaultUrlParams = new URLSearchParams(); + nonDefaultUrlParams.append('severity', 'all'); + nonDefaultUrlParams.append('status', 'all'); + nonDefaultUrlParams.append('status', 'open'); + nonDefaultUrlParams.append('severity', 'low'); + + mockLocation.search = nonDefaultUrlParams.toString(); + const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(useHistory().replace).toHaveBeenCalledTimes(1); - expect(useHistory().push).toHaveBeenCalledTimes(0); + expect(result.current.filterOptions).toMatchObject({ + ...DEFAULT_CASES_TABLE_STATE.filterOptions, + ...{ severity: ['low'], status: ['open'] }, + }); + }); + + it('preserves non cases state url parameters', () => { + mockLocation.search = `${stringifyUrlParams({ + status: [CaseStatuses.open], + })}&foo=bar&foo=baz&test=my-test`; + + const { result } = renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); act(() => { - result.current.setQueryParams({ perPage: DEFAULT_TABLE_LIMIT + 10 }); + result.current.setFilterOptions({ severity: [CaseSeverity.MEDIUM] }); }); - expect(useHistory().replace).toHaveBeenCalledTimes(2); - expect(useHistory().push).toHaveBeenCalledTimes(0); + expect(mockPush).toHaveBeenCalledWith({ + search: + 'cases=(page:1,perPage:10,severity:!(medium),sortField:createdAt,sortOrder:desc,status:!(open))&foo=bar&foo=baz&test=my-test', + }); }); - it('takes into account existing localStorage query params on first run', () => { + it('does not preserve cases state in the url when clearing filters', async () => { + const defaultStateWithValues: AllCasesTableState = { + filterOptions: { + search: 'my search', + searchFields: ['title'], + severity: [CaseSeverity.MEDIUM], + assignees: ['elastic'], + reporters: [], + status: [CaseStatuses.closed], + tags: ['test-tag'], + owner: ['cases'], + category: ['test-category'], + customFields: { + testCustomField: { options: ['foo'], type: CustomFieldTypes.TEXT }, + }, + }, + queryParams: { + page: DEFAULT_CASES_TABLE_STATE.queryParams.page + 10, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 50, + sortField: SortFieldCase.closedAt, + sortOrder: 'asc', + }, + }; + + const { result } = renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); + + act(() => { + result.current.setFilterOptions(defaultStateWithValues.filterOptions); + }); + + act(() => { + result.current.setQueryParams(defaultStateWithValues.queryParams); + }); + + await waitFor(() => { + expect(result.current.queryParams).toStrictEqual(defaultStateWithValues.queryParams); + expect(result.current.filterOptions).toStrictEqual(defaultStateWithValues.filterOptions); + }); + + act(() => { + result.current.setFilterOptions(DEFAULT_CASES_TABLE_STATE.filterOptions); + }); + + act(() => { + result.current.setQueryParams(DEFAULT_CASES_TABLE_STATE.queryParams); + }); + + await waitFor(() => { + expect(result.current.queryParams).toStrictEqual(DEFAULT_CASES_TABLE_STATE.queryParams); + expect(result.current.filterOptions).toStrictEqual(DEFAULT_CASES_TABLE_STATE.filterOptions); + }); + }); + + it('urlParams take precedence over localStorage query params values', () => { + mockLocation.search = stringifyUrlParams({ perPage: 15 }); + const existingLocalStorageValues = { - perPage: DEFAULT_TABLE_LIMIT + 10, - sortOrder: 'asc', - sortField: SortFieldCase.severity, + queryParams: { ...DEFAULT_CASES_TABLE_STATE.queryParams, perPage: 20 }, + filterOptions: DEFAULT_CASES_TABLE_STATE.filterOptions, }; - localStorage.setItem(LOCALSTORAGE_QUERY_PARAMS_KEY, JSON.stringify(existingLocalStorageValues)); + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); expect(result.current.queryParams).toMatchObject({ - ...LOCAL_STORAGE_QUERY_PARAMS_DEFAULTS, - ...existingLocalStorageValues, + ...DEFAULT_CASES_TABLE_STATE.queryParams, + ...{ perPage: 15 }, }); }); - it('takes into account existing localStorage filter options values on first run', () => { - const existingLocalStorageValues = { severity: ['critical'], status: ['open'] }; + it('urlParams take precedence over localStorage filter options values', () => { + mockLocation.search = stringifyUrlParams({ + severity: [CaseSeverity.HIGH], + status: [CaseStatuses.open], + }); - localStorage.setItem( - LOCALSTORAGE_FILTER_OPTIONS_KEY, - JSON.stringify(existingLocalStorageValues) - ); + const existingLocalStorageValues = { + filterOptions: { + ...DEFAULT_CASES_TABLE_STATE.filterOptions, + severity: ['low'], + status: ['closed'], + }, + queryParams: DEFAULT_CASES_TABLE_STATE.queryParams, + }; + + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(result.current.filterOptions).toMatchObject(existingLocalStorageValues); + expect(result.current.filterOptions).toMatchObject({ severity: ['high'], status: ['open'] }); }); - it('takes into account legacy localStorage filter values as string', () => { - const existingLocalStorageValues = { severity: 'critical', status: 'open' }; + it('loads the URL from the local storage when the URL is empty on first run', async () => { + const existingLocalStorageValues = { + queryParams: { + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }, + filterOptions: DEFAULT_CASES_TABLE_STATE.filterOptions, + }; - localStorage.setItem( - LOCALSTORAGE_FILTER_OPTIONS_KEY, - JSON.stringify(existingLocalStorageValues) - ); + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); - const { result } = renderHook(() => useAllCasesState(), { + renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(result.current.filterOptions).toMatchObject({ - severity: ['critical'], - status: ['open'], + expect(mockReplace).toHaveBeenCalledWith({ + search: 'cases=(page:1,perPage:30,sortField:createdAt,sortOrder:desc)', }); }); - it('takes into account legacy localStorage filter value all', () => { - const existingLocalStorageValues = { severity: 'all', status: 'all' }; + it('does not load the URL from the local storage when the URL is empty on the second run', async () => { + const existingLocalStorageValues = { + queryParams: { + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }, + filterOptions: DEFAULT_CASES_TABLE_STATE.filterOptions, + }; - localStorage.setItem( - LOCALSTORAGE_FILTER_OPTIONS_KEY, - JSON.stringify(existingLocalStorageValues) - ); + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); - const { result } = renderHook(() => useAllCasesState(), { + const { rerender } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(result.current.filterOptions).toMatchObject({ - severity: [], - status: [], - }); + rerender(); + + expect(mockReplace).toHaveBeenCalledTimes(1); }); - it('takes into account existing url query params on first run', () => { - const nonDefaultUrlParams = { - page: DEFAULT_TABLE_ACTIVE_PAGE + 1, - perPage: DEFAULT_TABLE_LIMIT + 5, + it('does not load the URL from the local storage when the URL is not empty', async () => { + mockLocation.search = stringifyUrlParams({ + severity: [CaseSeverity.HIGH], + status: [CaseStatuses.open], + }); + + const existingLocalStorageValues = { + queryParams: { + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }, + filterOptions: DEFAULT_CASES_TABLE_STATE.filterOptions, }; - const expectedUrl = { ...URL_DEFAULTS, ...nonDefaultUrlParams }; - mockLocation.search = stringifyToURL(nonDefaultUrlParams as unknown as Record); + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(useHistory().replace).toHaveBeenCalledWith({ - search: stringifyToURL(expectedUrl as unknown as Record), + expect(mockReplace).toHaveBeenCalledTimes(0); + }); + + it('loads the state from the URL correctly', () => { + mockLocation.search = stringifyUrlParams({ + severity: [CaseSeverity.HIGH], + status: [CaseStatuses['in-progress']], + }); + + const { result } = renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current.queryParams).toStrictEqual(DEFAULT_CASES_TABLE_STATE.queryParams); + expect(result.current.filterOptions).toStrictEqual({ + ...DEFAULT_CASES_TABLE_STATE.filterOptions, + status: [CaseStatuses['in-progress']], + severity: [CaseSeverity.HIGH], }); }); - it('takes into account existing url filter options on first run', () => { - const nonDefaultUrlParams = { severity: 'critical', status: 'open' }; + it('loads the state from the local storage if they URL is empty correctly', () => { + const existingLocalStorageValues = { + queryParams: { + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }, + filterOptions: DEFAULT_CASES_TABLE_STATE.filterOptions, + }; - mockLocation.search = stringifyToURL(nonDefaultUrlParams); + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); - renderHook(() => useAllCasesState(), { + const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(useHistory().replace).toHaveBeenCalledWith({ - search: 'severity=critical&status=open&page=1&perPage=10&sortField=createdAt&sortOrder=desc', + expect(result.current.queryParams).toStrictEqual({ + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, }); + expect(result.current.filterOptions).toStrictEqual(DEFAULT_CASES_TABLE_STATE.filterOptions); }); - it('takes into account legacy url filter option "all"', () => { - const nonDefaultUrlParams = new URLSearchParams(); - nonDefaultUrlParams.append('severity', 'all'); - nonDefaultUrlParams.append('status', 'all'); - nonDefaultUrlParams.append('status', 'open'); - nonDefaultUrlParams.append('severity', 'low'); + it('updates the query params correctly', () => { + const { result } = renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); - mockLocation.search = stringifyToURL(nonDefaultUrlParams); + act(() => { + result.current.setQueryParams({ + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }); + }); - renderHook(() => useAllCasesState(), { + expect(result.current.queryParams).toStrictEqual({ + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }); + expect(result.current.filterOptions).toStrictEqual(DEFAULT_CASES_TABLE_STATE.filterOptions); + }); + + it('updates URL when updating the query params', () => { + const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(useHistory().replace).toHaveBeenCalledWith({ - search: 'severity=low&status=open&page=1&perPage=10&sortField=createdAt&sortOrder=desc', + act(() => { + result.current.setQueryParams({ + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }); + }); + + expect(mockPush).toHaveBeenCalledWith({ + search: 'cases=(page:1,perPage:30,sortField:createdAt,sortOrder:desc)', }); }); - it('preserves other url parameters', () => { - const nonDefaultUrlParams = { - foo: 'bar', - }; + it('updates the local storage when updating the query params', () => { + const { result } = renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); - mockLocation.search = stringifyToURL(nonDefaultUrlParams); + act(() => { + result.current.setQueryParams({ + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }); + }); - renderHook(() => useAllCasesState(), { + const localStorageState = JSON.parse(localStorage.getItem(LS_KEY) ?? '{}'); + expect(localStorageState).toEqual({ + ...DEFAULT_CASES_TABLE_STATE, + queryParams: { + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }, + }); + }); + + it('updates the filter options correctly', () => { + const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(useHistory().replace).toHaveBeenCalledWith({ - search: 'foo=bar&page=1&perPage=10&sortField=createdAt&sortOrder=desc&severity=&status=', + act(() => { + result.current.setFilterOptions({ status: [CaseStatuses.closed] }); + }); + + expect(result.current.queryParams).toStrictEqual(DEFAULT_CASES_TABLE_STATE.queryParams); + expect(result.current.filterOptions).toStrictEqual({ + ...DEFAULT_CASES_TABLE_STATE.filterOptions, + status: [CaseStatuses.closed], }); }); - it('urlParams take precedence over localStorage query params values', () => { - const nonDefaultUrlParams = { - perPage: DEFAULT_TABLE_LIMIT + 5, - }; + it('updates the URL when updating the filter options', () => { + const { result } = renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); - mockLocation.search = stringifyToURL(nonDefaultUrlParams as unknown as Record); + act(() => { + result.current.setFilterOptions({ status: [CaseStatuses.closed] }); + }); - localStorage.setItem( - LOCALSTORAGE_QUERY_PARAMS_KEY, - JSON.stringify({ perPage: DEFAULT_TABLE_LIMIT + 10 }) - ); + expect(mockPush).toHaveBeenCalledWith({ + search: 'cases=(page:1,perPage:10,sortField:createdAt,sortOrder:desc,status:!(closed))', + }); + }); + it('updates the local storage when updating the filter options', () => { const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(result.current.queryParams).toMatchObject({ - ...DEFAULT_QUERY_PARAMS, - ...nonDefaultUrlParams, + act(() => { + result.current.setFilterOptions({ status: [CaseStatuses.closed] }); + }); + + const localStorageState = JSON.parse(localStorage.getItem(LS_KEY) ?? '{}'); + expect(localStorageState).toEqual({ + ...DEFAULT_CASES_TABLE_STATE, + filterOptions: { + ...DEFAULT_CASES_TABLE_STATE.filterOptions, + status: [CaseStatuses.closed], + }, }); }); - it('urlParams take precedence over localStorage filter options values', () => { - const nonDefaultUrlParams = { - severity: 'high', - status: 'open', + it('updates the local storage when navigating to a URL and the query params are not empty', () => { + mockLocation.search = stringifyUrlParams({ + severity: [CaseSeverity.HIGH], + status: [CaseStatuses['in-progress']], + customFields: { my_field: ['foo'] }, + }); + + useGetCaseConfigurationMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + data: { + ...useCaseConfigureResponse.data, + customFields: [ + { key: 'my_field', required: false, type: CustomFieldTypes.TEXT, label: 'foo' }, + ], + }, + })); + + renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); + + const localStorageState = JSON.parse(localStorage.getItem(LS_KEY) ?? '{}'); + + expect(localStorageState).toEqual({ + ...DEFAULT_CASES_TABLE_STATE, + filterOptions: { + ...DEFAULT_CASES_TABLE_STATE.filterOptions, + severity: [CaseSeverity.HIGH], + status: [CaseStatuses['in-progress']], + customFields: { my_field: { options: ['foo'], type: CustomFieldTypes.TEXT } }, + }, + }); + }); + + it('does not update the local storage when navigating to an empty URL', () => { + const lsSpy = jest.spyOn(Storage.prototype, 'setItem'); + + renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); + + // first call is the initial call made by useLocalStorage + expect(lsSpy).toBeCalledTimes(1); + }); + + it('does not update the local storage on the second run', () => { + mockLocation.search = stringifyUrlParams({ + severity: [CaseSeverity.HIGH], + status: [CaseStatuses['in-progress']], + }); + + const lsSpy = jest.spyOn(Storage.prototype, 'setItem'); + + const { rerender } = renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); + + rerender(); + + // first call is the initial call made by useLocalStorage + expect(lsSpy).toBeCalledTimes(2); + }); + + it('does not update the local storage when the URL and the local storage are the same', async () => { + mockLocation.search = stringifyUrlParams({ + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }); + + const lsSpy = jest.spyOn(Storage.prototype, 'setItem'); + + const existingLocalStorageValues = { + queryParams: { + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }, + filterOptions: DEFAULT_CASES_TABLE_STATE.filterOptions, }; - mockLocation.search = stringifyToURL(nonDefaultUrlParams); + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); - localStorage.setItem( - LOCALSTORAGE_FILTER_OPTIONS_KEY, - JSON.stringify({ severity: ['low'], status: ['closed'] }) - ); + renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); - const { result } = renderHook(() => useAllCasesState(), { + // first call is the initial call made by useLocalStorage + expect(lsSpy).toBeCalledTimes(1); + }); + + it('does not update the local storage when the custom field configuration is loading', async () => { + mockLocation.search = stringifyUrlParams({ + severity: [CaseSeverity.HIGH], + status: [CaseStatuses['in-progress']], + }); + + useGetCaseConfigurationMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + isFetching: true, + })); + + const lsSpy = jest.spyOn(Storage.prototype, 'setItem'); + + renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(result.current.filterOptions).toMatchObject({ severity: ['high'], status: ['open'] }); + // first call is the initial call made by useLocalStorage + expect(lsSpy).toBeCalledTimes(1); }); describe('validation', () => { it('localStorage perPage query param cannot be > 100', () => { - localStorage.setItem(LOCALSTORAGE_QUERY_PARAMS_KEY, JSON.stringify({ perPage: 1000 })); + const existingLocalStorageValues = { + queryParams: { ...DEFAULT_CASES_TABLE_STATE.queryParams, perPage: 1000 }, + filterOptions: DEFAULT_CASES_TABLE_STATE.filterOptions, + }; + + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); expect(result.current.queryParams).toMatchObject({ - ...LOCAL_STORAGE_QUERY_PARAMS_DEFAULTS, perPage: 100, }); }); it('url perPage query param cannot be > 100', () => { - mockLocation.search = stringifyToURL({ perPage: '1000' }); + mockLocation.search = stringifyUrlParams({ perPage: 1000 }); - renderHook(() => useAllCasesState(), { + const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(useHistory().replace).toHaveBeenCalledWith({ - search: 'perPage=100&page=1&sortField=createdAt&sortOrder=desc&severity=&status=', + expect(result.current.queryParams).toMatchObject({ + ...DEFAULT_CASES_TABLE_STATE.queryParams, + ...{ perPage: 100 }, }); - - mockLocation.search = ''; }); it('validate spelling of localStorage sortOrder', () => { - localStorage.setItem(LOCALSTORAGE_QUERY_PARAMS_KEY, JSON.stringify({ sortOrder: 'foobar' })); + const existingLocalStorageValues = { + queryParams: { ...DEFAULT_CASES_TABLE_STATE.queryParams, sortOrder: 'foobar' }, + filterOptions: DEFAULT_CASES_TABLE_STATE.filterOptions, + }; + + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); const { result } = renderHook(() => useAllCasesState(), { wrapper: ({ children }) => {children}, }); - expect(result.current.queryParams).toMatchObject({ - ...LOCAL_STORAGE_QUERY_PARAMS_DEFAULTS, - }); + expect(result.current.queryParams).toMatchObject({ sortOrder: 'desc' }); }); it('validate spelling of url sortOrder', () => { - mockLocation.search = stringifyToURL({ sortOrder: 'foobar' }); + // @ts-expect-error: testing invalid sortOrder + mockLocation.search = stringifyUrlParams({ sortOrder: 'foobar' }); + + const { result } = renderHook(() => useAllCasesState(), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current.queryParams).toMatchObject({ sortOrder: 'desc' }); + }); + }); + + describe('Modal', () => { + it('returns default state with empty URL and local storage', () => { + const { result } = renderHook(() => useAllCasesState(true), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current.queryParams).toStrictEqual(DEFAULT_CASES_TABLE_STATE.queryParams); + expect(result.current.filterOptions).toStrictEqual(DEFAULT_CASES_TABLE_STATE.filterOptions); + }); + + it('updates the query params correctly', () => { + const { result } = renderHook(() => useAllCasesState(true), { + wrapper: ({ children }) => {children}, + }); + + act(() => { + result.current.setQueryParams({ + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }); + }); + + expect(result.current.queryParams).toStrictEqual({ + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }); + expect(result.current.filterOptions).toStrictEqual(DEFAULT_CASES_TABLE_STATE.filterOptions); + }); + + it('updates the filter options correctly', () => { + const { result } = renderHook(() => useAllCasesState(true), { + wrapper: ({ children }) => {children}, + }); + + act(() => { + result.current.setFilterOptions({ status: [CaseStatuses.closed] }); + }); + + expect(result.current.queryParams).toStrictEqual(DEFAULT_CASES_TABLE_STATE.queryParams); + expect(result.current.filterOptions).toStrictEqual({ + ...DEFAULT_CASES_TABLE_STATE.filterOptions, + status: [CaseStatuses.closed], + }); + }); + + it('does not update the URL when changing the state of the table', () => { + const { result } = renderHook(() => useAllCasesState(true), { + wrapper: ({ children }) => {children}, + }); + + act(() => { + result.current.setQueryParams({ perPage: 20 }); + }); + + expect(mockPush).not.toHaveBeenCalled(); + }); + + it('does not update the local storage when changing the state of the table', () => { + const { result } = renderHook(() => useAllCasesState(true), { + wrapper: ({ children }) => {children}, + }); + + act(() => { + result.current.setQueryParams({ + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }); + }); + + const localStorageState = JSON.parse(localStorage.getItem(LS_KEY) ?? '{}'); + expect(localStorageState).toEqual(DEFAULT_CASES_TABLE_STATE); + }); + + it('does not load the URL from the local storage when the URL is empty on first run', () => { + const existingLocalStorageValues = { + queryParams: { + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }, + filterOptions: DEFAULT_CASES_TABLE_STATE.filterOptions, + }; + + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); - renderHook(() => useAllCasesState(), { + renderHook(() => useAllCasesState(true), { wrapper: ({ children }) => {children}, }); - expect(useHistory().replace).toHaveBeenCalledWith({ - search: 'sortOrder=desc&page=1&perPage=10&sortField=createdAt&severity=&status=', + expect(mockPush).not.toHaveBeenCalled(); + }); + + it('does not load the state from the URL', () => { + mockLocation.search = stringifyUrlParams({ + severity: [CaseSeverity.HIGH], + status: [CaseStatuses['in-progress']], }); + + const { result } = renderHook(() => useAllCasesState(true), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current.queryParams).toStrictEqual(DEFAULT_CASES_TABLE_STATE.queryParams); + expect(result.current.filterOptions).toStrictEqual(DEFAULT_CASES_TABLE_STATE.filterOptions); + }); + + it('does not load the state from the local storage', () => { + const existingLocalStorageValues = { + queryParams: { + ...DEFAULT_CASES_TABLE_STATE.queryParams, + perPage: DEFAULT_CASES_TABLE_STATE.queryParams.perPage + 20, + }, + filterOptions: DEFAULT_CASES_TABLE_STATE.filterOptions, + }; + + localStorage.setItem(LS_KEY, JSON.stringify(existingLocalStorageValues)); + + const { result } = renderHook(() => useAllCasesState(true), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current.queryParams).toStrictEqual(DEFAULT_CASES_TABLE_STATE.queryParams); + expect(result.current.filterOptions).toStrictEqual(DEFAULT_CASES_TABLE_STATE.filterOptions); + }); + + it('does not update the local storage when navigating to a URL and the query params are not empty', () => { + mockLocation.search = stringifyUrlParams({ + severity: [CaseSeverity.HIGH], + status: [CaseStatuses['in-progress']], + }); + + renderHook(() => useAllCasesState(true), { + wrapper: ({ children }) => {children}, + }); + + const localStorageState = JSON.parse(localStorage.getItem(LS_KEY) ?? '{}'); + + expect(localStorageState).toEqual(DEFAULT_CASES_TABLE_STATE); }); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.tsx b/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.tsx index b988cf501cddb..39bac2c05d569 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_all_cases_state.tsx @@ -5,244 +5,202 @@ * 2.0. */ -import { useCallback, useEffect, useRef, useState } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import { useEffect, useRef, useCallback, useMemo, useState } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; -import { isEqual } from 'lodash'; - +import deepEqual from 'react-fast-compare'; import useLocalStorage from 'react-use/lib/useLocalStorage'; - -import { removeLegacyValuesFromOptions, getStorableFilters } from './utils/sanitize_filter_options'; -import type { - FilterOptions, - PartialFilterOptions, - LocalStorageQueryParams, - QueryParams, - PartialQueryParams, - ParsedUrlQueryParams, -} from '../../../common/ui/types'; - -import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from '../../containers/constants'; -import { parseUrlQueryParams } from './utils'; -import { stringifyToURL, parseURL } from '../utils'; +import { isEmpty } from 'lodash'; + +import type { FilterOptions, QueryParams } from '../../../common/ui/types'; +import { + DEFAULT_CASES_TABLE_STATE, + DEFAULT_FILTER_OPTIONS, + DEFAULT_QUERY_PARAMS, +} from '../../containers/constants'; import { LOCAL_STORAGE_KEYS } from '../../../common/constants'; -import { SORT_ORDER_VALUES } from '../../../common/ui/types'; +import type { AllCasesTableState, AllCasesURLState } from './types'; +import { stringifyUrlParams } from './utils/stringify_url_params'; +import { allCasesUrlStateDeserializer } from './utils/all_cases_url_state_deserializer'; +import { allCasesUrlStateSerializer } from './utils/all_cases_url_state_serializer'; +import { parseUrlParams } from './utils/parse_url_params'; import { useCasesContext } from '../cases_context/use_cases_context'; -import { CASES_TABLE_PERPAGE_VALUES } from './types'; -import { parseURLWithFilterOptions } from './utils/parse_url_with_filter_options'; -import { serializeUrlParams } from './utils/serialize_url_params'; +import { sanitizeState } from './utils/sanitize_state'; +import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration'; + +interface UseAllCasesStateReturn { + filterOptions: FilterOptions; + setQueryParams: (queryParam: Partial) => void; + setFilterOptions: (filterOptions: Partial) => void; + queryParams: QueryParams; +} -export const getQueryParamsLocalStorageKey = (appId: string) => { - const filteringKey = LOCAL_STORAGE_KEYS.casesQueryParams; - return `${appId}.${filteringKey}`; -}; +export function useAllCasesState(isModalView: boolean = false): UseAllCasesStateReturn { + const isStateLoadedFromLocalStorage = useRef(false); + const isFirstRun = useRef(false); + const [tableState, setTableState] = useState(DEFAULT_CASES_TABLE_STATE); + const [urlState, setUrlState] = useAllCasesUrlState(); + const [localStorageState, setLocalStorageState] = useAllCasesLocalStorage(); + const { isFetching: isLoadingCasesConfiguration } = useGetCaseConfiguration(); + + const allCasesTableState: AllCasesTableState = useMemo( + () => (isModalView ? tableState : getAllCasesTableState(urlState, localStorageState)), + [isModalView, tableState, urlState, localStorageState] + ); -export const getFilterOptionsLocalStorageKey = (appId: string) => { - const filteringKey = LOCAL_STORAGE_KEYS.casesFilterOptions; - return `${appId}.${filteringKey}`; -}; + const setState = useCallback( + (state: AllCasesTableState) => { + if (isModalView) { + setTableState(state); + return; + } -const getQueryParams = ( - params: PartialQueryParams, - urlParams: PartialQueryParams, - localStorageQueryParams?: LocalStorageQueryParams -): QueryParams => { - const result = { ...DEFAULT_QUERY_PARAMS }; - - result.perPage = - params.perPage ?? - urlParams.perPage ?? - localStorageQueryParams?.perPage ?? - DEFAULT_QUERY_PARAMS.perPage; - - result.sortField = - params.sortField ?? - urlParams.sortField ?? - localStorageQueryParams?.sortField ?? - DEFAULT_QUERY_PARAMS.sortField; - - result.sortOrder = - params.sortOrder ?? - urlParams.sortOrder ?? - localStorageQueryParams?.sortOrder ?? - DEFAULT_QUERY_PARAMS.sortOrder; - - result.page = params.page ?? urlParams.page ?? DEFAULT_QUERY_PARAMS.page; - - return result; -}; + if (!deepEqual(state, urlState)) { + setUrlState(state); + } -const validateQueryParams = (queryParams: QueryParams): QueryParams => { - const perPage = Math.min( - queryParams.perPage, - CASES_TABLE_PERPAGE_VALUES[CASES_TABLE_PERPAGE_VALUES.length - 1] + if (!deepEqual(state, localStorageState)) { + setLocalStorageState(state); + } + }, + [localStorageState, urlState, isModalView, setLocalStorageState, setUrlState] ); - const sortOrder = !SORT_ORDER_VALUES.includes(queryParams.sortOrder) - ? DEFAULT_QUERY_PARAMS.sortOrder - : queryParams.sortOrder; - - return { ...queryParams, perPage, sortOrder }; -}; -/** - * Previously, 'status' and 'severity' were represented as single options (strings). - * To maintain backward compatibility while transitioning to the new type of string[], - * we map the legacy type to the new type. - */ -const convertToFilterOptionArray = (value: string | string[] | undefined) => { - if (typeof value === 'string') { - return [value]; + // use of useEffect because setUrlState calls history.push + useEffect(() => { + if ( + !isStateLoadedFromLocalStorage.current && + isURLStateEmpty(urlState) && + localStorageState && + !isModalView + ) { + setUrlState(localStorageState, 'replace'); + isStateLoadedFromLocalStorage.current = true; + } + }, [localStorageState, setUrlState, urlState, isModalView]); + + /** + * When navigating for the first time in a URL + * we need to persist the state on the local storage. + * We need to do it only on the first run and only when the URL is not empty. + * Otherwise we may introduce a race condition or loop with the above hook. + */ + if ( + !isFirstRun.current && + !isURLStateEmpty(urlState) && + localStorageState && + !deepEqual(allCasesTableState, localStorageState) && + !isLoadingCasesConfiguration && + !isModalView + ) { + setLocalStorageState(allCasesTableState); + isFirstRun.current = true; } - return value; -}; - -const getFilterOptions = ( - filterOptions: FilterOptions, - params: FilterOptions, - urlParams: PartialFilterOptions, - localStorageFilterOptions?: PartialFilterOptions -): FilterOptions => { - const severity = - params?.severity ?? - urlParams?.severity ?? - convertToFilterOptionArray(localStorageFilterOptions?.severity) ?? - DEFAULT_FILTER_OPTIONS.severity; - - const status = - params?.status ?? - urlParams?.status ?? - convertToFilterOptionArray(localStorageFilterOptions?.status) ?? - DEFAULT_FILTER_OPTIONS.status; return { - ...filterOptions, - ...params, - ...removeLegacyValuesFromOptions({ status, severity }), + ...allCasesTableState, + setQueryParams: (newQueryParams: Partial) => { + setState({ + filterOptions: allCasesTableState.filterOptions, + queryParams: { ...allCasesTableState.queryParams, ...newQueryParams }, + }); + }, + setFilterOptions: (newFilterOptions: Partial) => { + setState({ + filterOptions: { ...allCasesTableState.filterOptions, ...newFilterOptions }, + queryParams: allCasesTableState.queryParams, + }); + }, }; -}; +} -export function useAllCasesState( - isModalView: boolean = false, - initialFilterOptions?: PartialFilterOptions -) { - const { appId } = useCasesContext(); - const location = useLocation(); +const useAllCasesUrlState = (): [ + AllCasesURLState, + (updated: AllCasesTableState, mode?: 'push' | 'replace') => void +] => { const history = useHistory(); - const isFirstRenderRef = useRef(true); + const location = useLocation(); + const { + data: { customFields: customFieldsConfiguration }, + } = useGetCaseConfiguration(); + + const urlParams = parseUrlParams(new URLSearchParams(decodeURIComponent(location.search))); + const parsedUrlParams = allCasesUrlStateDeserializer(urlParams, customFieldsConfiguration); + + const updateQueryParams = useCallback( + (updated: AllCasesTableState, mode: 'push' | 'replace' = 'push') => { + const updatedQuery = allCasesUrlStateSerializer(updated); + const search = stringifyUrlParams(updatedQuery, location.search); + + history[mode]({ + ...location, + search, + }); + }, + [history, location] + ); - const [queryParams, setQueryParams] = useState({ ...DEFAULT_QUERY_PARAMS }); - const [filterOptions, setFilterOptions] = useState({ - ...DEFAULT_FILTER_OPTIONS, - ...initialFilterOptions, - }); + return [parsedUrlParams, updateQueryParams]; +}; - const [localStorageQueryParams, setLocalStorageQueryParams] = - useLocalStorage(getQueryParamsLocalStorageKey(appId)); +const getAllCasesTableState = ( + urlState: AllCasesURLState, + localStorageState?: AllCasesTableState +): AllCasesTableState => { + if (isURLStateEmpty(urlState)) { + return { + queryParams: { ...DEFAULT_CASES_TABLE_STATE.queryParams, ...localStorageState?.queryParams }, + filterOptions: { + ...DEFAULT_CASES_TABLE_STATE.filterOptions, + ...localStorageState?.filterOptions, + }, + }; + } - const [localStorageFilterOptions, setLocalStorageFilterOptions] = - useLocalStorage(getFilterOptionsLocalStorageKey(appId)); + return { + queryParams: { ...DEFAULT_CASES_TABLE_STATE.queryParams, ...urlState.queryParams }, + filterOptions: { ...DEFAULT_CASES_TABLE_STATE.filterOptions, ...urlState.filterOptions }, + }; +}; - const persistAndUpdateQueryParams = useCallback( - (params) => { - if (isModalView) { - setQueryParams((prevParams) => ({ ...prevParams, ...params })); - return; - } +const isURLStateEmpty = (urlState: AllCasesURLState) => { + if (isEmpty(urlState)) { + return true; + } - const parsedUrlParams: ParsedUrlQueryParams = parseURL(location.search); - const urlParams: PartialQueryParams = parseUrlQueryParams(parsedUrlParams); + if (isEmpty(urlState.filterOptions) && isEmpty(urlState.queryParams)) { + return true; + } - let newQueryParams: QueryParams = getQueryParams(params, urlParams, localStorageQueryParams); + return false; +}; - newQueryParams = validateQueryParams(newQueryParams); +const useAllCasesLocalStorage = (): [ + AllCasesTableState | undefined, + Dispatch> +] => { + const { appId } = useCasesContext(); - const newLocalStorageQueryParams = { - perPage: newQueryParams.perPage, - sortField: newQueryParams.sortField, - sortOrder: newQueryParams.sortOrder, - }; - setLocalStorageQueryParams(newLocalStorageQueryParams); - setQueryParams(newQueryParams); - }, - [isModalView, location.search, localStorageQueryParams, setLocalStorageQueryParams] + const [state, setState] = useLocalStorage( + getAllCasesTableStateLocalStorageKey(appId), + { queryParams: DEFAULT_QUERY_PARAMS, filterOptions: DEFAULT_FILTER_OPTIONS } ); - const persistAndUpdateFilterOptions = useCallback( - (params) => { - if (isModalView) { - setFilterOptions((prevParams) => ({ ...prevParams, ...params })); - return; - } + const sanitizedState = sanitizeState(state); - const newFilterOptions: FilterOptions = getFilterOptions( - filterOptions, - params, - parseURLWithFilterOptions(location.search), - localStorageFilterOptions - ); - - const newPersistedFilterOptions: PartialFilterOptions = getStorableFilters(newFilterOptions); - - const newLocalStorageFilterOptions: PartialFilterOptions = { - ...localStorageFilterOptions, - ...newPersistedFilterOptions, - }; - setLocalStorageFilterOptions(newLocalStorageFilterOptions); - setFilterOptions(newFilterOptions); + return [ + { + queryParams: { ...DEFAULT_CASES_TABLE_STATE.queryParams, ...sanitizedState.queryParams }, + filterOptions: { + ...DEFAULT_CASES_TABLE_STATE.filterOptions, + ...sanitizedState.filterOptions, + }, }, - [ - filterOptions, - isModalView, - localStorageFilterOptions, - location.search, - setLocalStorageFilterOptions, - ] - ); - - const updateLocation = useCallback(() => { - const parsedUrlParams = parseURLWithFilterOptions(location.search); - const stateUrlParams = { - ...parsedUrlParams, - ...queryParams, - ...getStorableFilters(filterOptions), - page: queryParams.page.toString(), - perPage: queryParams.perPage.toString(), - }; - - if (!isEqual(parsedUrlParams, stateUrlParams)) { - try { - const urlParams = serializeUrlParams({ - ...parsedUrlParams, - ...stateUrlParams, - }); - - const newHistory = { - ...location, - search: stringifyToURL(urlParams), - }; - history.replace(newHistory); - } catch { - // silently fail - } - } - }, [filterOptions, history, location, queryParams]); - - if (isFirstRenderRef.current) { - persistAndUpdateQueryParams(isModalView ? queryParams : {}); - persistAndUpdateFilterOptions(isModalView ? filterOptions : initialFilterOptions); - - isFirstRenderRef.current = false; - } - - useEffect(() => { - if (!isModalView) { - updateLocation(); - } - }, [isModalView, updateLocation]); + setState, + ]; +}; - return { - queryParams, - setQueryParams: persistAndUpdateQueryParams, - filterOptions, - setFilterOptions: persistAndUpdateFilterOptions, - }; -} +const getAllCasesTableStateLocalStorageKey = (appId: string) => { + const key = LOCAL_STORAGE_KEYS.casesTableState; + return `${appId}.${key}`; +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_selection.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_selection.tsx index a7c32ee939a19..7b81e88c0d383 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_selection.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns_selection.tsx @@ -12,7 +12,7 @@ import type { CasesColumnSelection } from './types'; import { LOCAL_STORAGE_KEYS } from '../../../common/constants'; import { useCasesContext } from '../cases_context/use_cases_context'; import { useCasesColumnsConfiguration } from './use_cases_columns_configuration'; -import { mergeSelectedColumnsWithConfiguration } from './utils'; +import { mergeSelectedColumnsWithConfiguration } from './utils/merge_selected_columns_with_configuration'; const getTableColumnsLocalStorageKey = (appId: string) => { const filteringKey = LOCAL_STORAGE_KEYS.casesTableColumns; diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.test.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.test.tsx index ce776e77e8304..f58e7aa2698cc 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, waitFor, screen } from '@testing-library/react'; +import { act, waitFor, screen, waitForElementToBeRemoved } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import type { AppMockRenderer } from '../../common/mock'; @@ -36,6 +36,8 @@ describe('Severity form field', () => { }, selectedColumns: [], onSelectedColumnsChange: jest.fn(), + onClearFilters: jest.fn(), + showClearFiltersButton: false, }; beforeEach(() => { @@ -44,11 +46,14 @@ describe('Severity form field', () => { it('renders', async () => { appMockRender.render(); - expect(screen.getByText('Showing 5 of 5 cases')).toBeInTheDocument(); - expect(screen.getByText('Selected 1 case')).toBeInTheDocument(); - expect(screen.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); - expect(screen.getByTestId('all-cases-refresh-link-icon')).toBeInTheDocument(); + + expect(await screen.findByText('Showing 5 of 5 cases')).toBeInTheDocument(); + expect(await screen.findByText('Selected 1 case')).toBeInTheDocument(); + expect(await screen.findByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); + expect(await screen.findByTestId('all-cases-refresh-link-icon')).toBeInTheDocument(); + expect(screen.queryByTestId('all-cases-maximum-limit-warning')).not.toBeInTheDocument(); + expect(screen.queryByTestId('all-cases-clear-filters-link-icon')).not.toBeInTheDocument(); }); it('renders showing cases correctly', async () => { @@ -60,9 +65,11 @@ describe('Severity form field', () => { totalItemCount: 20, }, }; + appMockRender.render(); - expect(screen.getByText('Showing 10 of 20 cases')).toBeInTheDocument(); - expect(screen.getByText('Selected 1 case')).toBeInTheDocument(); + + expect(await screen.findByText('Showing 10 of 20 cases')).toBeInTheDocument(); + expect(await screen.findByText('Selected 1 case')).toBeInTheDocument(); }); it('renders showing cases correctly for second page', async () => { @@ -75,13 +82,16 @@ describe('Severity form field', () => { totalItemCount: 20, }, }; + appMockRender.render(); - expect(screen.getByText('Showing 10 of 20 cases')).toBeInTheDocument(); - expect(screen.getByText('Selected 1 case')).toBeInTheDocument(); + + expect(await screen.findByText('Showing 10 of 20 cases')).toBeInTheDocument(); + expect(await screen.findByText('Selected 1 case')).toBeInTheDocument(); }); it('renders showing cases correctly when no cases available', async () => { const updatedProps = { + ...props, totalCases: 0, selectedCases: [], deselectCases, @@ -90,16 +100,15 @@ describe('Severity form field', () => { pageIndex: 1, totalItemCount: 0, }, - selectedColumns: [], - onSelectedColumnsChange: jest.fn(), }; + appMockRender.render(); - expect(screen.getByText('Showing 0 of 0 cases')).toBeInTheDocument(); + expect(await screen.findByText('Showing 0 of 0 cases')).toBeInTheDocument(); }); it('renders columns popover button when isSelectorView=False', async () => { appMockRender.render(); - expect(screen.getByTestId('column-selection-popover-button')).toBeInTheDocument(); + expect(await screen.findByTestId('column-selection-popover-button')).toBeInTheDocument(); }); it('does not render columns popover button when isSelectorView=True', async () => { @@ -110,34 +119,28 @@ describe('Severity form field', () => { it('opens the bulk actions correctly', async () => { appMockRender.render(); - userEvent.click(screen.getByTestId('case-table-bulk-actions-link-icon')); + userEvent.click(await screen.findByTestId('case-table-bulk-actions-link-icon')); - await waitFor(() => { - expect(screen.getByTestId('case-table-bulk-actions-context-menu')); - }); + expect(await screen.findByTestId('case-table-bulk-actions-context-menu')); }); it('closes the bulk actions correctly', async () => { appMockRender.render(); - userEvent.click(screen.getByTestId('case-table-bulk-actions-link-icon')); + userEvent.click(await screen.findByTestId('case-table-bulk-actions-link-icon')); - await waitFor(() => { - expect(screen.getByTestId('case-table-bulk-actions-context-menu')); - }); + expect(await screen.findByTestId('case-table-bulk-actions-context-menu')); - userEvent.click(screen.getByTestId('case-table-bulk-actions-link-icon')); + userEvent.click(await screen.findByTestId('case-table-bulk-actions-link-icon')); - await waitFor(() => { - expect(screen.queryByTestId('case-table-bulk-actions-context-menu')).toBeFalsy(); - }); + await waitForElementToBeRemoved(screen.queryByTestId('case-table-bulk-actions-context-menu')); }); it('refresh correctly', async () => { appMockRender.render(); const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); - userEvent.click(screen.getByTestId('all-cases-refresh-link-icon')); + userEvent.click(await screen.findByTestId('all-cases-refresh-link-icon')); await waitFor(() => { expect(deselectCases).toHaveBeenCalled(); @@ -151,28 +154,44 @@ describe('Severity form field', () => { appMockRender = createAppMockRenderer({ permissions: noCasesPermissions() }); appMockRender.render(); - expect(screen.queryByTestId('case-table-bulk-actions-link-icon')).toBeFalsy(); + expect(screen.queryByTestId('case-table-bulk-actions-link-icon')).not.toBeInTheDocument(); }); it('does show the bulk actions with only delete permissions', async () => { appMockRender = createAppMockRenderer({ permissions: onlyDeleteCasesPermission() }); appMockRender.render(); - expect(screen.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); + expect(await screen.findByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); }); it('does show the bulk actions with update permissions', async () => { appMockRender = createAppMockRenderer({ permissions: writeCasesPermissions() }); appMockRender.render(); - expect(screen.getByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); + expect(await screen.findByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); }); it('does not show the bulk actions if there are not selected cases', async () => { appMockRender.render(); - expect(screen.queryByTestId('case-table-bulk-actions-link-icon')).toBeFalsy(); - expect(screen.queryByText('Showing 0 cases')).toBeFalsy(); + expect(screen.queryByTestId('case-table-bulk-actions-link-icon')).not.toBeInTheDocument(); + expect(screen.queryByText('Showing 0 cases')).not.toBeInTheDocument(); + }); + + it('shows the clear filter button', async () => { + appMockRender.render(); + + expect(await screen.findByTestId('all-cases-clear-filters-link-icon')).toBeInTheDocument(); + }); + + it('clears the filters correctly', async () => { + appMockRender.render(); + + userEvent.click(await screen.findByTestId('all-cases-clear-filters-link-icon')); + + await waitFor(() => { + expect(props.onClearFilters).toHaveBeenCalled(); + }); }); describe('Maximum number of cases', () => { @@ -190,7 +209,7 @@ describe('Severity form field', () => { it.each(allCasesPageSize)( `does not show warning when totalCases = ${MAX_DOCS_PER_PAGE} but pageSize(%s) * pageIndex + 1 < ${MAX_DOCS_PER_PAGE}`, - (size) => { + async (size) => { const newPageIndex = MAX_DOCS_PER_PAGE / size - 2; appMockRender.render( @@ -203,15 +222,16 @@ describe('Severity form field', () => { ); expect( - screen.getByText(`Showing ${size} of ${MAX_DOCS_PER_PAGE} cases`) + await screen.findByText(`Showing ${size} of ${MAX_DOCS_PER_PAGE} cases`) ).toBeInTheDocument(); + expect(screen.queryByTestId('all-cases-maximum-limit-warning')).not.toBeInTheDocument(); } ); it.each(allCasesPageSize)( `shows warning when totalCases = ${MAX_DOCS_PER_PAGE} but pageSize(%s) * pageIndex + 1 = ${MAX_DOCS_PER_PAGE}`, - (size) => { + async (size) => { const newPageIndex = MAX_DOCS_PER_PAGE / size - 1; appMockRender.render( @@ -224,15 +244,16 @@ describe('Severity form field', () => { ); expect( - screen.getByText(`Showing ${size} of ${MAX_DOCS_PER_PAGE} cases`) + await screen.findByText(`Showing ${size} of ${MAX_DOCS_PER_PAGE} cases`) ).toBeInTheDocument(); - expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + + expect(await screen.findByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); } ); it.each(allCasesPageSize)( `shows warning when totalCases = ${MAX_DOCS_PER_PAGE} but pageSize(%s) * pageIndex + 1 > ${MAX_DOCS_PER_PAGE}`, - (size) => { + async (size) => { const newPageIndex = MAX_DOCS_PER_PAGE / size; appMockRender.render( @@ -245,13 +266,14 @@ describe('Severity form field', () => { ); expect( - screen.getByText(`Showing ${size} of ${MAX_DOCS_PER_PAGE} cases`) + await screen.findByText(`Showing ${size} of ${MAX_DOCS_PER_PAGE} cases`) ).toBeInTheDocument(); - expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + + expect(await screen.findByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); } ); - it('should show dismiss and do not show again buttons correctly', () => { + it('should show dismiss and do not show again buttons correctly', async () => { appMockRender.render( { /> ); - expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); - expect(screen.getByTestId('dismiss-warning')).toBeInTheDocument(); - - expect(screen.getByTestId('do-not-show-warning')).toBeInTheDocument(); + expect(await screen.findByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + expect(await screen.findByTestId('dismiss-warning')).toBeInTheDocument(); + expect(await screen.findByTestId('do-not-show-warning')).toBeInTheDocument(); }); - it('should dismiss warning correctly', () => { + it('should dismiss warning correctly', async () => { appMockRender.render( { /> ); - expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); - expect(screen.getByTestId('dismiss-warning')).toBeInTheDocument(); + expect(await screen.findByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + expect(await screen.findByTestId('dismiss-warning')).toBeInTheDocument(); - userEvent.click(screen.getByTestId('dismiss-warning')); + userEvent.click(await screen.findByTestId('dismiss-warning')); expect(screen.queryByTestId('all-cases-maximum-limit-warning')).not.toBeInTheDocument(); }); @@ -303,7 +324,7 @@ describe('Severity form field', () => { jest.clearAllMocks(); }); - it('should set storage key correctly', () => { + it('should set storage key correctly', async () => { appMockRender.render( { /> ); - expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); - expect(screen.getByTestId('do-not-show-warning')).toBeInTheDocument(); + expect(await screen.findByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + expect(await screen.findByTestId('do-not-show-warning')).toBeInTheDocument(); expect(localStorage.getItem(localStorageKey)).toBe(null); }); - it('should hide warning correctly when do not show button clicked', () => { + it('should hide warning correctly when do not show button clicked', async () => { appMockRender.render( { /> ); - expect(screen.getByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); - expect(screen.getByTestId('do-not-show-warning')).toBeInTheDocument(); + expect(await screen.findByTestId('all-cases-maximum-limit-warning')).toBeInTheDocument(); + expect(await screen.findByTestId('do-not-show-warning')).toBeInTheDocument(); - userEvent.click(screen.getByTestId('do-not-show-warning')); + userEvent.click(await screen.findByTestId('do-not-show-warning')); act(() => { jest.advanceTimersByTime(1000); diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx index ee80e0ffb38b1..afd9398d91cce 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx @@ -38,6 +38,8 @@ interface Props { pagination: Pagination; selectedColumns: CasesColumnSelection[]; onSelectedColumnsChange: (columns: CasesColumnSelection[]) => void; + onClearFilters: () => void; + showClearFiltersButton: boolean; } export const CasesTableUtilityBar: FunctionComponent = React.memo( @@ -49,6 +51,8 @@ export const CasesTableUtilityBar: FunctionComponent = React.memo( pagination, selectedColumns, onSelectedColumnsChange, + onClearFilters, + showClearFiltersButton, }) => { const { euiTheme } = useEuiTheme(); const refreshCases = useRefreshCases(); @@ -212,6 +216,20 @@ export const CasesTableUtilityBar: FunctionComponent = React.memo( {i18n.REFRESH} + {showClearFiltersButton ? ( + + + {i18n.CLEAR_FILTERS} + + + ) : null} diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_deserializer.test.ts b/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_deserializer.test.ts new file mode 100644 index 0000000000000..0b46456204dd2 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_deserializer.test.ts @@ -0,0 +1,217 @@ +/* + * 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 { CustomFieldTypes } from '../../../../common/types/domain'; +import { DEFAULT_CASES_TABLE_STATE } from '../../../containers/constants'; + +import { allCasesUrlStateDeserializer } from './all_cases_url_state_deserializer'; + +describe('allCasesUrlStateDeserializer', () => { + const { customFields, ...filterOptionsWithoutCustomFields } = + DEFAULT_CASES_TABLE_STATE.filterOptions; + + const defaultMap = { + ...filterOptionsWithoutCustomFields, + ...DEFAULT_CASES_TABLE_STATE.queryParams, + }; + + it('parses defaults correctly', () => { + expect(allCasesUrlStateDeserializer(defaultMap)).toMatchInlineSnapshot(` + Object { + "filterOptions": Object { + "assignees": Array [], + "category": Array [], + "owner": Array [], + "reporters": Array [], + "search": "", + "searchFields": Array [ + "title", + "description", + ], + "severity": Array [], + "status": Array [], + "tags": Array [], + }, + "queryParams": Object { + "page": 1, + "perPage": 10, + "sortField": "createdAt", + "sortOrder": "desc", + }, + } + `); + }); + + it('parses an empty object correctly', () => { + expect(allCasesUrlStateDeserializer({})).toMatchInlineSnapshot(` + Object { + "filterOptions": Object {}, + "queryParams": Object {}, + } + `); + }); + + it('does not return unknown values', () => { + // @ts-expect-error: testing unknown values + expect(allCasesUrlStateDeserializer({ foo: 'bar' })).toMatchInlineSnapshot(` + Object { + "filterOptions": Object {}, + "queryParams": Object {}, + } + `); + }); + + it('converts page to integer correctly', () => { + // @ts-expect-error: testing integer conversion + expect(allCasesUrlStateDeserializer({ page: '1' }).queryParams.page).toBe(1); + }); + + it('sets perPage to the maximum allowed value if it is set to over the limit', () => { + // @ts-expect-error: testing integer conversion + expect(allCasesUrlStateDeserializer({ perPage: '1000' }).queryParams.perPage).toBe(100); + }); + + it('converts perPage to integer correctly', () => { + // @ts-expect-error: testing integer conversion + expect(allCasesUrlStateDeserializer({ perPage: '2' }).queryParams.perPage).toBe(2); + }); + + it('sets the defaults to page and perPage correctly if they are not numbers', () => { + // @ts-expect-error: testing integer conversion + expect(allCasesUrlStateDeserializer({ page: 'foo', perPage: 'bar' }).queryParams.page).toBe( + DEFAULT_CASES_TABLE_STATE.queryParams.page + ); + + // @ts-expect-error: testing integer conversion + expect(allCasesUrlStateDeserializer({ page: 'foo', perPage: 'bar' }).queryParams.perPage).toBe( + DEFAULT_CASES_TABLE_STATE.queryParams.perPage + ); + }); + + it('does not return the page and perPage if they are not defined', () => { + expect(allCasesUrlStateDeserializer({})).toMatchInlineSnapshot(` + Object { + "filterOptions": Object {}, + "queryParams": Object {}, + } + `); + }); + + it('sets the sortOrder correctly', () => { + expect(allCasesUrlStateDeserializer({ sortOrder: 'asc' }).queryParams.sortOrder).toBe('asc'); + }); + + it('parses custom fields correctly', () => { + expect( + allCasesUrlStateDeserializer( + { + customFields: { + 'my-custom-field-1': ['foo', 'qux'], + 'my-custom-field-2': ['bar', 'baz'], + 'my-custom-field-4': [], + }, + }, + [ + { + key: 'my-custom-field-1', + type: CustomFieldTypes.TOGGLE, + required: false, + label: 'foo', + }, + { + key: 'my-custom-field-2', + type: CustomFieldTypes.TOGGLE, + required: false, + label: 'foo', + }, + { + key: 'my-custom-field-4', + type: CustomFieldTypes.TOGGLE, + required: false, + label: 'foo', + }, + ] + ) + ).toMatchInlineSnapshot(` + Object { + "filterOptions": Object { + "customFields": Object { + "my-custom-field-1": Object { + "options": Array [ + "foo", + "qux", + ], + "type": "toggle", + }, + "my-custom-field-2": Object { + "options": Array [ + "bar", + "baz", + ], + "type": "toggle", + }, + "my-custom-field-4": Object { + "options": Array [], + "type": "toggle", + }, + }, + }, + "queryParams": Object {}, + } + `); + }); + + it('removes unknown custom fields', () => { + expect( + allCasesUrlStateDeserializer( + { + customFields: { + 'my-custom-field-1': ['foo', 'qux'], + 'my-custom-field-2': ['bar', 'baz'], + }, + }, + [ + { + key: 'my-custom-field-1', + type: CustomFieldTypes.TOGGLE, + required: false, + label: 'foo', + }, + ] + ) + ).toMatchInlineSnapshot(` + Object { + "filterOptions": Object { + "customFields": Object { + "my-custom-field-1": Object { + "options": Array [ + "foo", + "qux", + ], + "type": "toggle", + }, + }, + }, + "queryParams": Object {}, + } + `); + }); + + it('parses none assignees correctly', () => { + expect(allCasesUrlStateDeserializer({ assignees: ['none', 'elastic'] })).toMatchInlineSnapshot(` + Object { + "filterOptions": Object { + "assignees": Array [ + null, + "elastic", + ], + }, + "queryParams": Object {}, + } + `); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_deserializer.ts b/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_deserializer.ts new file mode 100644 index 0000000000000..d07f693f2907c --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_deserializer.ts @@ -0,0 +1,85 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { NO_ASSIGNEES_FILTERING_KEYWORD } from '../../../../common/constants'; +import type { QueryParams, FilterOptions, CasesConfigurationUI } from '../../../../common/ui'; +import { DEFAULT_CASES_TABLE_STATE } from '../../../containers/constants'; +import type { AllCasesURLQueryParams, AllCasesURLState } from '../types'; +import { sanitizeState } from './sanitize_state'; +import { stringToIntegerWithDefault } from '.'; + +export const allCasesUrlStateDeserializer = ( + urlParamsMap: AllCasesURLQueryParams, + customFieldsConfiguration: CasesConfigurationUI['customFields'] = [] +): AllCasesURLState => { + const queryParams: Partial & Record = {}; + const filterOptions: Partial & Record = {}; + + for (const [key, value] of Object.entries(urlParamsMap)) { + if (Object.hasOwn(DEFAULT_CASES_TABLE_STATE.queryParams, key)) { + queryParams[key] = value; + } + + if (Object.hasOwn(DEFAULT_CASES_TABLE_STATE.filterOptions, key)) { + filterOptions[key] = value; + } + } + + const { page, perPage, ...restQueryParams } = queryParams; + const { assignees, customFields, ...restFilterOptions } = filterOptions; + + const queryParamsParsed: Partial = { + ...restQueryParams, + }; + + const filterOptionsParsed: Partial = { + ...restFilterOptions, + }; + + if (page) { + queryParamsParsed.page = stringToIntegerWithDefault( + page, + DEFAULT_CASES_TABLE_STATE.queryParams.page + ); + } + + if (perPage) { + queryParamsParsed.perPage = stringToIntegerWithDefault( + perPage, + DEFAULT_CASES_TABLE_STATE.queryParams.perPage + ); + } + + if (assignees) { + filterOptionsParsed.assignees = assignees.map((assignee) => + assignee === NO_ASSIGNEES_FILTERING_KEYWORD ? null : assignee + ); + } + + const customFieldsParams = Object.entries(customFields ?? {}).reduce((acc, [key, value]) => { + const foundCustomField = customFieldsConfiguration.find((cf) => cf.key === key); + + if (!foundCustomField) { + return acc; + } + + return { ...acc, [key]: { type: foundCustomField.type, options: value } }; + }, {}); + + const state: AllCasesURLState = { + queryParams: queryParamsParsed, + filterOptions: { + ...filterOptionsParsed, + ...(!isEmpty(customFieldsParams) && { + customFields: customFieldsParams, + }), + }, + }; + + return sanitizeState(state); +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_serializer.test.ts b/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_serializer.test.ts new file mode 100644 index 0000000000000..0e129cdc69489 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_serializer.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { CustomFieldTypes } from '../../../../common/types/domain'; +import { DEFAULT_QUERY_PARAMS, DEFAULT_FILTER_OPTIONS } from '../../../containers/constants'; + +import { allCasesUrlStateSerializer } from './all_cases_url_state_serializer'; + +describe('allCasesUrlStateSerializer', () => { + it('serializes correctly with default values', () => { + expect( + allCasesUrlStateSerializer({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: DEFAULT_QUERY_PARAMS, + }) + ).toMatchInlineSnapshot(` + Object { + "page": 1, + "perPage": 10, + "sortField": "createdAt", + "sortOrder": "desc", + } + `); + }); + + it('serializes custom fields correctly', () => { + expect( + allCasesUrlStateSerializer({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + customFields: { + foo: { type: CustomFieldTypes.TEXT, options: ['bar'] }, + bar: { type: CustomFieldTypes.TEXT, options: ['foo'] }, + }, + }, + queryParams: DEFAULT_QUERY_PARAMS, + }) + ).toMatchInlineSnapshot(` + Object { + "customFields": Object { + "bar": Array [ + "foo", + ], + "foo": Array [ + "bar", + ], + }, + "page": 1, + "perPage": 10, + "sortField": "createdAt", + "sortOrder": "desc", + } + `); + }); + + it('removes unsupported filter options', () => { + expect( + allCasesUrlStateSerializer({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + searchFields: ['title'], + reporters: [{ username: 'elastic', email: null, full_name: null }], + owner: ['cases'], + }, + queryParams: DEFAULT_QUERY_PARAMS, + }) + ).toMatchInlineSnapshot(` + Object { + "page": 1, + "perPage": 10, + "sortField": "createdAt", + "sortOrder": "desc", + } + `); + }); + + it('removes empty values', () => { + expect( + allCasesUrlStateSerializer({ + filterOptions: { ...DEFAULT_FILTER_OPTIONS, status: [], search: '', customFields: {} }, + queryParams: DEFAULT_QUERY_PARAMS, + }) + ).toMatchInlineSnapshot(` + Object { + "page": 1, + "perPage": 10, + "sortField": "createdAt", + "sortOrder": "desc", + } + `); + }); + + it('converts null assignees correctly', () => { + expect( + allCasesUrlStateSerializer({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + assignees: [null, 'elastic'], + }, + queryParams: DEFAULT_QUERY_PARAMS, + }) + ).toMatchInlineSnapshot(` + Object { + "assignees": Array [ + "none", + "elastic", + ], + "page": 1, + "perPage": 10, + "sortField": "createdAt", + "sortOrder": "desc", + } + `); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_serializer.ts b/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_serializer.ts new file mode 100644 index 0000000000000..e5faeb6f36f90 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/all_cases_url_state_serializer.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty, pick, isNumber } from 'lodash'; +import { NO_ASSIGNEES_FILTERING_KEYWORD } from '../../../../common/constants'; +import type { AllCasesURLQueryParams, AllCasesTableState } from '../types'; + +export const allCasesUrlStateSerializer = (state: AllCasesTableState): AllCasesURLQueryParams => { + const supportedFilterOptions = pick(state.filterOptions, [ + 'search', + 'severity', + 'status', + 'tags', + 'assignees', + 'category', + ]); + + const customFieldsAsQueryParams = Object.entries(state.filterOptions.customFields).reduce( + (acc, [key, value]) => { + if (isEmpty(value.options)) { + return acc; + } + + return { ...acc, [key]: value.options }; + }, + {} + ); + + const combinedState = { + ...state.queryParams, + page: state.queryParams.page, + perPage: state.queryParams.perPage, + ...supportedFilterOptions, + assignees: supportedFilterOptions.assignees.map((assignee) => + assignee === null ? NO_ASSIGNEES_FILTERING_KEYWORD : assignee + ), + customFields: customFieldsAsQueryParams, + }; + + // filters empty values + return Object.entries(combinedState).reduce((acc, [key, value]) => { + // isEmpty returns true for numbers + if (isEmpty(value) && !isNumber(value)) { + return acc; + } + + return Object.assign(acc, { [key]: value }); + }, {}); +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/index.test.ts b/x-pack/plugins/cases/public/components/all_cases/utils/index.test.ts new file mode 100644 index 0000000000000..98eeef78f9765 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/index.test.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 { + isFlattenCustomField, + flattenCustomFieldKey, + deflattenCustomFieldKey, + stringToInteger, + stringToIntegerWithDefault, +} from '.'; + +describe('utils', () => { + describe('isFlattenCustomField', () => { + it('returns true if the key is prefixed with cf_', () => { + expect(isFlattenCustomField('cf_foo')).toBe(true); + }); + + it('returns false if the key is not prefixed with cf_', () => { + expect(isFlattenCustomField('foo')).toBe(false); + }); + }); + + describe('flattenCustomFieldKey', () => { + it('flattens a custom field key correctly', () => { + expect(flattenCustomFieldKey('foo')).toBe('cf_foo'); + }); + }); + + describe('deflattenCustomFieldKey', () => { + it('deflattens a custom field key correctly', () => { + expect(deflattenCustomFieldKey('cf_foo')).toBe('foo'); + }); + }); + + describe('stringToInteger', () => { + it('converts a number correctly', () => { + expect(stringToInteger(5)).toBe(5); + }); + + it('converts a string to a number correctly', () => { + expect(stringToInteger('5')).toBe(5); + }); + + it('returns undefined if the value cannot converted to a number', () => { + expect(stringToInteger('foo')).toBe(undefined); + }); + }); + + describe('stringToIntegerWithDefault', () => { + it('converts a string to a number correctly', () => { + expect(stringToIntegerWithDefault('5', 10)).toBe(5); + }); + + it('sets the default value correctly if the number is zero', () => { + expect(stringToIntegerWithDefault(0, 10)).toBe(10); + }); + + it('sets the default value correctly if the value is not a number', () => { + expect(stringToIntegerWithDefault('foo', 10)).toBe(10); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/index.ts b/x-pack/plugins/cases/public/components/all_cases/utils/index.ts index bbc48210bfaa2..762882834b8ee 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utils/index.ts +++ b/x-pack/plugins/cases/public/components/all_cases/utils/index.ts @@ -5,71 +5,31 @@ * 2.0. */ -import { difference } from 'lodash'; -import type { ParsedUrlQueryParams, PartialQueryParams } from '../../../../common/ui/types'; -import type { CasesColumnSelection } from '../types'; -import type { CasesColumnsConfiguration } from '../use_cases_columns_configuration'; +import { CUSTOM_FIELD_KEY_PREFIX } from '../constants'; -export const parseUrlQueryParams = (parsedUrlParams: ParsedUrlQueryParams): PartialQueryParams => { - const urlParams: PartialQueryParams = { - ...(parsedUrlParams.sortField && { sortField: parsedUrlParams.sortField }), - ...(parsedUrlParams.sortOrder && { sortOrder: parsedUrlParams.sortOrder }), - }; +export const isFlattenCustomField = (key: string): boolean => + key.startsWith(CUSTOM_FIELD_KEY_PREFIX); - const intPage = parsedUrlParams.page && parseInt(parsedUrlParams.page, 10); - const intPerPage = parsedUrlParams.perPage && parseInt(parsedUrlParams.perPage, 10); +export const flattenCustomFieldKey = (key: string): string => `${CUSTOM_FIELD_KEY_PREFIX}${key}`; - // page=0 is deliberately ignored - if (intPage) { - urlParams.page = intPage; - } +export const deflattenCustomFieldKey = (key: string): string => + key.replace(CUSTOM_FIELD_KEY_PREFIX, ''); + +export const stringToInteger = (value?: string | number): number | undefined => { + const num = Number(value); - // perPage=0 is deliberately ignored - if (intPerPage) { - urlParams.perPage = intPerPage; + if (isNaN(num)) { + return; } - return urlParams; + return num; }; -export const mergeSelectedColumnsWithConfiguration = ({ - selectedColumns, - casesColumnsConfig, -}: { - selectedColumns: CasesColumnSelection[]; - casesColumnsConfig: CasesColumnsConfiguration; -}): CasesColumnSelection[] => { - const result = selectedColumns.reduce((accumulator, { field, isChecked }) => { - if ( - field in casesColumnsConfig && - casesColumnsConfig[field].field !== '' && - casesColumnsConfig[field].canDisplay - ) { - accumulator.push({ - field: casesColumnsConfig[field].field, - name: casesColumnsConfig[field].name, - isChecked, - }); - } - return accumulator; - }, [] as CasesColumnSelection[]); - - // This will include any new customFields and/or changes to the case attributes - const missingColumns = difference( - Object.keys(casesColumnsConfig), - selectedColumns.map(({ field }) => field) - ); - - missingColumns.forEach((field) => { - // can be an empty string - if (casesColumnsConfig[field].field && casesColumnsConfig[field].canDisplay) { - result.push({ - field: casesColumnsConfig[field].field, - name: casesColumnsConfig[field].name, - isChecked: casesColumnsConfig[field].isCheckedDefault, - }); - } - }); +export const stringToIntegerWithDefault = ( + value: string | number, + defaultValue: number +): number | undefined => { + const valueAsInteger = stringToInteger(value); - return result; + return valueAsInteger && valueAsInteger > 0 ? valueAsInteger : defaultValue; }; diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/merge_selected_columns_with_configuration.ts b/x-pack/plugins/cases/public/components/all_cases/utils/merge_selected_columns_with_configuration.ts new file mode 100644 index 0000000000000..c56c5e70aed7e --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/merge_selected_columns_with_configuration.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { difference } from 'lodash'; +import type { CasesColumnSelection } from '../types'; +import type { CasesColumnsConfiguration } from '../use_cases_columns_configuration'; + +export const mergeSelectedColumnsWithConfiguration = ({ + selectedColumns, + casesColumnsConfig, +}: { + selectedColumns: CasesColumnSelection[]; + casesColumnsConfig: CasesColumnsConfiguration; +}): CasesColumnSelection[] => { + const result = selectedColumns.reduce((accumulator, { field, isChecked }) => { + if ( + field in casesColumnsConfig && + casesColumnsConfig[field].field !== '' && + casesColumnsConfig[field].canDisplay + ) { + accumulator.push({ + field: casesColumnsConfig[field].field, + name: casesColumnsConfig[field].name, + isChecked, + }); + } + return accumulator; + }, [] as CasesColumnSelection[]); + + // This will include any new customFields and/or changes to the case attributes + const missingColumns = difference( + Object.keys(casesColumnsConfig), + selectedColumns.map(({ field }) => field) + ); + + missingColumns.forEach((field) => { + // can be an empty string + if (casesColumnsConfig[field].field && casesColumnsConfig[field].canDisplay) { + result.push({ + field: casesColumnsConfig[field].field, + name: casesColumnsConfig[field].name, + isChecked: casesColumnsConfig[field].isCheckedDefault, + }); + } + }); + + return result; +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_params.test.tsx b/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_params.test.tsx new file mode 100644 index 0000000000000..7daa7166f051b --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_params.test.tsx @@ -0,0 +1,254 @@ +/* + * 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 { encode } from '@kbn/rison'; +import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from '../../../containers/constants'; + +import { parseUrlParams } from './parse_url_params'; + +describe('parseUrlParams', () => { + const defaultValuesAsURL = new URLSearchParams({ + cases: encode({ + ...DEFAULT_FILTER_OPTIONS, + ...DEFAULT_QUERY_PARAMS, + }), + }); + + it('parses the default filter options and query params correctly', () => { + expect(parseUrlParams(defaultValuesAsURL)).toMatchInlineSnapshot(` + Object { + "assignees": Array [], + "category": Array [], + "customFields": Object {}, + "page": 1, + "perPage": 10, + "search": "", + "severity": Array [], + "sortField": "createdAt", + "sortOrder": "desc", + "status": Array [], + "tags": Array [], + } + `); + }); + + it('parses a mix of fields correctly', () => { + const state = { + assignees: ['elastic'], + tags: ['a', 'b'], + category: [], + status: ['open'], + search: 'My title', + owner: ['cases'], + customFields: { my_field: ['foo'] }, + }; + + const url = `cases=${encode(state)}`; + + expect(parseUrlParams(new URLSearchParams(url))).toMatchInlineSnapshot(` + Object { + "assignees": Array [ + "elastic", + ], + "category": Array [], + "customFields": Object { + "my_field": Array [ + "foo", + ], + }, + "search": "My title", + "status": Array [ + "open", + ], + "tags": Array [ + "a", + "b", + ], + } + `); + }); + + it('protects against prototype attacks', () => { + const firstUrl = 'cases=(customFields:(__proto__:!(foo)))'; + const secondUrl = 'cases=(customFields:(__proto__:(property:payload)))'; + + // @ts-expect-error: testing prototype attacks + expect(parseUrlParams(new URLSearchParams(firstUrl)).__proto__).toEqual({}); + // @ts-expect-error: testing prototype attacks + expect(parseUrlParams(new URLSearchParams(secondUrl)).__proto__).toEqual({}); + }); + + it('parses empty query params correctly', () => { + expect(parseUrlParams(new URLSearchParams())).toMatchInlineSnapshot(`Object {}`); + }); + + it('parses an empty string correctly', () => { + expect(parseUrlParams(new URLSearchParams(''))).toMatchInlineSnapshot(`Object {}`); + }); + + it('parses an unrecognized query param correctly', () => { + expect(parseUrlParams(new URLSearchParams('foo='))).toMatchInlineSnapshot(`Object {}`); + }); + + it('parses an empty string correctly in the cases object correctly', () => { + expect(parseUrlParams(new URLSearchParams({ cases: '' }))).toMatchInlineSnapshot(`Object {}`); + }); + + it('parses a malformed rison url correctly', () => { + expect(parseUrlParams(new URLSearchParams({ cases: '!' }))).toMatchInlineSnapshot(`Object {}`); + }); + + it('parses a rison url that is not an object correctly', () => { + for (const value of ['foo', true, false, ['bar'], null, 0]) { + expect(parseUrlParams(new URLSearchParams({ cases: encode(value) }))).toEqual({}); + } + }); + + it('validates the query params schema correctly', () => { + expect( + parseUrlParams(new URLSearchParams({ cases: encode({ status: 'foo' }) })) + ).toMatchInlineSnapshot(`Object {}`); + }); + + describe('legacy URLs', () => { + it('parses a legacy url with all legacy supported keys correctly', () => { + const url = 'status=open&severity=low&page=2&perPage=50&sortField=closedAt&sortOrder=asc'; + + expect(parseUrlParams(new URLSearchParams(url))).toMatchInlineSnapshot(` + Object { + "page": 2, + "perPage": 50, + "severity": Array [ + "low", + ], + "sortField": "closedAt", + "sortOrder": "asc", + "status": Array [ + "open", + ], + } + `); + }); + + it('parses a url with status=open,closed', () => { + const url = 'status=open,closed'; + + expect(parseUrlParams(new URLSearchParams(url))).toMatchInlineSnapshot(` + Object { + "status": Array [ + "open", + "closed", + ], + } + `); + }); + + it('parses a url with status=in-progress', () => { + const url = 'status=in-progress'; + + expect(parseUrlParams(new URLSearchParams(url))).toMatchInlineSnapshot(` + Object { + "status": Array [ + "in-progress", + ], + } + `); + }); + + it('parses a url with status=open&status=closed', () => { + const url = 'status=open&status=closed'; + + expect(parseUrlParams(new URLSearchParams(url))).toMatchInlineSnapshot(` + Object { + "status": Array [ + "open", + "closed", + ], + } + `); + }); + + it('parses a url with status=open,closed&status=in-progress', () => { + const url = 'status=open,closed&status=in-progress'; + + expect(parseUrlParams(new URLSearchParams(url))).toMatchInlineSnapshot(` + Object { + "status": Array [ + "open", + "closed", + "in-progress", + ], + } + `); + }); + + it('parses a url with severity=low,medium&severity=high,critical', () => { + const url = 'severity=low,medium&severity=high,critical'; + + expect(parseUrlParams(new URLSearchParams(url))).toMatchInlineSnapshot(` + Object { + "severity": Array [ + "low", + "medium", + "high", + "critical", + ], + } + `); + }); + + it('parses a url with severity=low,medium&severity=high&severity=critical', () => { + const url = 'severity=low,medium&severity=high&severity=critical'; + + expect(parseUrlParams(new URLSearchParams(url))).toMatchInlineSnapshot(` + Object { + "severity": Array [ + "low", + "medium", + "high", + "critical", + ], + } + `); + }); + + it('parses a url with page=2&page=5&perPage=4&perPage=20', () => { + const url = 'page=2&page=5&perPage=4&perPage=20'; + + expect(parseUrlParams(new URLSearchParams(url))).toMatchInlineSnapshot(` + Object { + "page": 2, + "perPage": 4, + } + `); + }); + + it('validates the query params schema correctly', () => { + const url = 'status=foo'; + + expect(parseUrlParams(new URLSearchParams(url))).toMatchInlineSnapshot(`Object {}`); + }); + + it('sets the defaults to page and perPage correctly if they are not numbers', () => { + const url = 'page=foo&perPage=bar'; + + expect(parseUrlParams(new URLSearchParams(url))).toMatchInlineSnapshot(` + Object { + "page": 1, + "perPage": 10, + } + `); + }); + + it('protects against prototype attacks', () => { + const url = '__proto__[property]=payload'; + + // @ts-expect-error: testing prototype attacks + expect(parseUrlParams(new URLSearchParams(url)).__proto__.property).toEqual(undefined); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_params.tsx b/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_params.tsx new file mode 100644 index 0000000000000..7e67cdfab89b2 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_params.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { safeDecode } from '@kbn/rison'; +import { isPlainObject } from 'lodash'; +import type { CaseStatuses } from '@kbn/cases-components'; +import type { CaseSeverity } from '../../../../common'; +import { DEFAULT_CASES_TABLE_STATE } from '../../../containers/constants'; +import { stringToIntegerWithDefault } from '.'; +import { SortFieldCase } from '../../../../common/ui'; +import { LEGACY_SUPPORTED_STATE_KEYS, ALL_CASES_STATE_URL_KEY } from '../constants'; +import { AllCasesURLQueryParamsRt, validateSchema } from '../schema'; +import type { AllCasesURLQueryParams } from '../types'; + +type LegacySupportedKeys = typeof LEGACY_SUPPORTED_STATE_KEYS[number]; + +const legacyDefaultState: Record = { + page: 1, + perPage: 10, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + status: [], + severity: [], +}; + +/** + * Parses legacy state in URL. + * + * - Parameters in the query string can have multiple formats: + * 1. Comma-separated values (e.g., "status=foo,bar") + * 2. A single value (e.g., "status=foo") + * 3. Repeated keys (e.g., "status=foo&status=bar") + * + */ +const parseLegacyUrl = (urlParams: URLSearchParams): AllCasesURLQueryParams => { + const urlParamsMap = new Map>(); + + urlParams.forEach((value, key) => { + if (LEGACY_SUPPORTED_STATE_KEYS.includes(key as LegacySupportedKeys)) { + const values = urlParamsMap.get(key) ?? new Set(); + + value + .split(',') + .filter(Boolean) + .forEach((urlValue) => values.add(urlValue)); + + urlParamsMap.set(key, values); + } + }); + + const entries = new Map( + [...urlParamsMap].map(([key, value]) => [ + key, + parseValue(value, legacyDefaultState[key as LegacySupportedKeys]), + ]) + ); + + const params = Object.fromEntries(entries.entries()); + const allCasesParams: AllCasesURLQueryParams = { ...params }; + + if (params.page) { + allCasesParams.page = stringToIntegerWithDefault( + Array.isArray(params.page) ? params.page[0] : params.page, + DEFAULT_CASES_TABLE_STATE.queryParams.page + ); + } + + if (params.perPage) { + allCasesParams.perPage = stringToIntegerWithDefault( + Array.isArray(params.perPage) ? params.perPage[0] : params.perPage, + DEFAULT_CASES_TABLE_STATE.queryParams.perPage + ); + } + + if (params.status) { + const statusAsArray = Array.isArray(params.status) ? params.status : [params.status]; + allCasesParams.status = statusAsArray.filter(notAll).filter(Boolean) as CaseStatuses[]; + } + + if (params.severity) { + const severityAsArray = Array.isArray(params.severity) ? params.severity : [params.severity]; + allCasesParams.severity = severityAsArray.filter(notAll).filter(Boolean) as CaseSeverity[]; + } + + return allCasesParams; +}; + +const parseValue = (values: Set, defaultValue: unknown): string | string[] => { + const valuesAsArray = Array.from(values.values()); + return Array.isArray(defaultValue) ? valuesAsArray : valuesAsArray[0] ?? ''; +}; + +const notAll = (option: string) => option !== 'all'; + +export function parseUrlParams(urlParams: URLSearchParams): AllCasesURLQueryParams { + const allCasesParams = urlParams.get(ALL_CASES_STATE_URL_KEY); + + if (!allCasesParams) { + return parseAndValidateLegacyUrl(urlParams); + } + + const parsedAllCasesParams = safeDecode(allCasesParams); + + if (!parsedAllCasesParams || !isPlainObject(parsedAllCasesParams)) { + return {}; + } + + const validatedAllCasesParams = validateSchema(parsedAllCasesParams, AllCasesURLQueryParamsRt); + + if (!validatedAllCasesParams) { + return {}; + } + + return validatedAllCasesParams; +} + +const parseAndValidateLegacyUrl = (urlParams: URLSearchParams): AllCasesURLQueryParams => { + const validatedUrlParams = validateSchema(parseLegacyUrl(urlParams), AllCasesURLQueryParamsRt); + + if (!validatedUrlParams) { + return {}; + } + + return validatedUrlParams; +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_with_filter_options.test.tsx b/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_with_filter_options.test.tsx deleted file mode 100644 index 2cfa14f3af328..0000000000000 --- a/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_with_filter_options.test.tsx +++ /dev/null @@ -1,56 +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. - */ - -// This file was contributed to by generative AI - -import { parseURLWithFilterOptions } from './parse_url_with_filter_options'; - -describe('parseURLWithFilterOptions', () => { - it('parses a url with search=foo', () => { - const url = 'search=foo'; - expect(parseURLWithFilterOptions(url)).toStrictEqual({ search: 'foo' }); - }); - - it('parses a url with status=foo,bar', () => { - const url = 'status=foo,bar'; - expect(parseURLWithFilterOptions(url)).toStrictEqual({ status: ['foo', 'bar'] }); - }); - - it('parses a url with status=foo', () => { - const url = 'status=foo'; - expect(parseURLWithFilterOptions(url)).toStrictEqual({ status: ['foo'] }); - }); - - it('parses a url with status=foo&status=bar', () => { - const url = 'status=foo&status=bar'; - expect(parseURLWithFilterOptions(url)).toStrictEqual({ status: ['foo', 'bar'] }); - }); - - it('parses a url with status=foo,bar&status=baz', () => { - const url = 'status=foo,bar&status=baz'; - expect(parseURLWithFilterOptions(url)).toStrictEqual({ status: ['foo', 'bar', 'baz'] }); - }); - - it('parses a url with status=foo,bar&status=baz,qux', () => { - const url = 'status=foo,bar&status=baz,qux'; - expect(parseURLWithFilterOptions(url)).toStrictEqual({ - status: ['foo', 'bar', 'baz', 'qux'], - }); - }); - - it('parses a url with status=foo,bar&status=baz,qux&status=quux', () => { - const url = 'status=foo,bar&status=baz,qux&status=quux'; - expect(parseURLWithFilterOptions(url)).toStrictEqual({ - status: ['foo', 'bar', 'baz', 'qux', 'quux'], - }); - }); - - it('parses a url with status=', () => { - const url = 'status='; - expect(parseURLWithFilterOptions(url)).toStrictEqual({ status: [] }); - }); -}); diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_with_filter_options.tsx b/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_with_filter_options.tsx deleted file mode 100644 index 6467dc99ab440..0000000000000 --- a/x-pack/plugins/cases/public/components/all_cases/utils/parse_url_with_filter_options.tsx +++ /dev/null @@ -1,42 +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 { DEFAULT_FILTER_OPTIONS } from '../../../containers/constants'; - -/** - * Parses filter options from a URL query string. - * - * The behavior is influenced by the predefined DEFAULT_FILTER_OPTIONS: - * - If an option is defined as an array there, it will always be returned as an array. - * - Parameters in the query string can have multiple formats: - * 1. Comma-separated values (e.g., "status=foo,bar") - * 2. A single value (e.g., "status=foo") - * 3. Repeated keys (e.g., "status=foo&status=bar") - * - * This function ensures the output respects the format indicated in DEFAULT_FILTER_OPTIONS. - */ -export const parseURLWithFilterOptions = (search: string) => { - const urlParams = new URLSearchParams(search); - - const paramKeysWithTypeArray = Object.entries(DEFAULT_FILTER_OPTIONS) - .map(([key, val]) => (Array.isArray(val) ? key : undefined)) - .filter(Boolean); - - const parsedUrlParams: { [key in string]: string[] | string } = {}; - for (const [key, value] of urlParams.entries()) { - if (paramKeysWithTypeArray.includes(key)) { - if (!parsedUrlParams[key]) parsedUrlParams[key] = []; - // only applies if the value is separated by commas (e.g., "foo,bar") - const splittedValues = value.split(',').filter(Boolean); - (parsedUrlParams[key] as string[]).push(...splittedValues); - } else { - parsedUrlParams[key] = value; - } - } - - return parsedUrlParams; -}; diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_filter_options.test.tsx b/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_filter_options.test.tsx deleted file mode 100644 index b96dbc40fe668..0000000000000 --- a/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_filter_options.test.tsx +++ /dev/null @@ -1,46 +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. - */ - -// This file was contributed to by generative AI - -import { CaseStatuses, CaseSeverity } from '../../../../common/types/domain'; -import { removeLegacyValuesFromOptions, getStorableFilters } from './sanitize_filter_options'; - -describe('removeLegacyValuesFromOptions', () => { - it('should remove legacy values from options', () => { - const options: { - status: Array; - severity: Array; - } = { - status: ['all', CaseStatuses.open, CaseStatuses['in-progress'], 'all'], - severity: ['all', CaseSeverity.LOW, 'all'], - }; - - expect(removeLegacyValuesFromOptions(options)).toEqual({ - status: ['open', 'in-progress'], - severity: ['low'], - }); - }); -}); - -describe('getStorableFilters', () => { - it('should return the filters if provided', () => { - expect( - getStorableFilters({ - status: [CaseStatuses.open, CaseStatuses['in-progress']], - severity: [CaseSeverity.LOW], - }) - ).toEqual({ - status: [CaseStatuses.open, CaseStatuses['in-progress']], - severity: [CaseSeverity.LOW], - }); - }); - - it('should return undefined if no filters are provided', () => { - expect(getStorableFilters({})).toEqual({ status: undefined, severity: undefined }); - }); -}); diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_filter_options.tsx b/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_filter_options.tsx deleted file mode 100644 index 498d2998a0a20..0000000000000 --- a/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_filter_options.tsx +++ /dev/null @@ -1,37 +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 { FilterOptions } from '../../../../common/ui/types'; -import type { CaseStatuses, CaseSeverity } from '../../../../common/types/domain'; - -const notAll = (option: string) => option !== 'all'; - -/** - * In earlier versions, the options 'status' and 'severity' could have a value of 'all'. - * This function ensures such legacy values are removed from the URL parameters to maintain - * backwards compatibility. - */ -export const removeLegacyValuesFromOptions = ({ - status: legacyStatus, - severity: legacySeverity, -}: { - status: Array; - severity: Array; -}): { status: CaseStatuses[]; severity: CaseSeverity[] } => { - return { - status: legacyStatus.filter(notAll).filter(Boolean) as CaseStatuses[], - severity: legacySeverity.filter(notAll).filter(Boolean) as CaseSeverity[], - }; -}; - -export const getStorableFilters = ( - filterOptions: Partial -): { status: CaseStatuses[] | undefined; severity: CaseSeverity[] | undefined } => { - const { status, severity } = filterOptions; - - return { severity, status }; -}; diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_state.test.ts b/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_state.test.ts new file mode 100644 index 0000000000000..a23cbed01f4e2 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_state.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseStatuses } from '@kbn/cases-components'; +import { CaseSeverity } from '../../../../common'; +import { DEFAULT_CASES_TABLE_STATE } from '../../../containers/constants'; +import { sanitizeState } from './sanitize_state'; + +describe('sanitizeState', () => { + it('sanitize default state correctly', () => { + expect(sanitizeState(DEFAULT_CASES_TABLE_STATE)).toEqual(DEFAULT_CASES_TABLE_STATE); + }); + + it('sanitize perPage query param correctly if it is bigger than 100', () => { + expect(sanitizeState({ queryParams: { perPage: 1000 } })).toEqual({ + filterOptions: {}, + queryParams: { perPage: 100 }, + }); + }); + + it('sanitize sortOrder correctly', () => { + // @ts-expect-error: need to check unrecognized values + expect(sanitizeState({ queryParams: { sortOrder: 'foo' } })).toEqual({ + filterOptions: {}, + queryParams: { sortOrder: 'desc' }, + }); + }); + + it('returns empty state with no arguments', () => { + expect(sanitizeState()).toEqual({ + filterOptions: {}, + queryParams: {}, + }); + }); + + it('sanitize status correctly', () => { + // @ts-expect-error: need to check unrecognized values + expect(sanitizeState({ filterOptions: { status: ['foo', CaseStatuses.open] } })).toEqual({ + filterOptions: { status: ['open'] }, + queryParams: {}, + }); + }); + + it('sanitize severity correctly', () => { + // @ts-expect-error: need to check unrecognized values + expect(sanitizeState({ filterOptions: { severity: ['foo', CaseSeverity.MEDIUM] } })).toEqual({ + filterOptions: { severity: ['medium'] }, + queryParams: {}, + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_state.ts b/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_state.ts new file mode 100644 index 0000000000000..a0553a5ade632 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/sanitize_state.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseStatuses } from '@kbn/cases-components'; +import { CaseSeverity } from '../../../../common'; +import { SORT_ORDER_VALUES } from '../../../../common/ui'; +import { DEFAULT_QUERY_PARAMS } from '../../../containers/constants'; +import type { AllCasesTableState } from '../types'; +import { CASES_TABLE_PER_PAGE_VALUES } from '../types'; + +interface PartialState { + queryParams?: Partial; + filterOptions?: Partial; +} + +interface PartialParams { + queryParams: Partial; + filterOptions: Partial; +} + +export const sanitizeState = (state: PartialState = {}): PartialParams => { + return { + queryParams: sanitizeQueryParams(state.queryParams) ?? {}, + filterOptions: sanitizeFilterOptions(state.filterOptions) ?? {}, + }; +}; + +const sanitizeQueryParams = ( + queryParams: PartialState['queryParams'] = {} +): PartialState['queryParams'] => { + const { perPage, sortOrder, ...restQueryParams } = queryParams; + + const queryParamsSanitized: PartialState['queryParams'] = { + ...restQueryParams, + }; + + if (perPage) { + queryParamsSanitized.perPage = Math.min( + perPage, + CASES_TABLE_PER_PAGE_VALUES[CASES_TABLE_PER_PAGE_VALUES.length - 1] + ); + } + + if (sortOrder) { + queryParamsSanitized.sortOrder = SORT_ORDER_VALUES.includes(sortOrder) + ? sortOrder + : DEFAULT_QUERY_PARAMS.sortOrder; + } + + return queryParamsSanitized; +}; + +const sanitizeFilterOptions = ( + filterOptions: PartialState['filterOptions'] = {} +): PartialState['filterOptions'] => { + const { status, severity, ...restFilterOptions } = filterOptions; + + const filterOptionsSanitized: PartialState['filterOptions'] = { + ...restFilterOptions, + }; + + if (status) { + filterOptionsSanitized.status = filterOutOptions(status, CaseStatuses); + } + + if (severity) { + filterOptionsSanitized.severity = filterOutOptions(severity, CaseSeverity); + } + + return filterOptionsSanitized; +}; + +const filterOutOptions = (collection: T[], validValues: Record): T[] => + collection.filter((value) => Object.values(validValues).includes(value)); diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/serialize_url_params.test.tsx b/x-pack/plugins/cases/public/components/all_cases/utils/serialize_url_params.test.tsx deleted file mode 100644 index ace5fdda934ab..0000000000000 --- a/x-pack/plugins/cases/public/components/all_cases/utils/serialize_url_params.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { serializeUrlParams } from './serialize_url_params'; - -describe('serializeUrlParams', () => { - const commonProps = { - page: '1', - perPage: '5', - sortField: 'createdAt', - sortOrder: 'desc', - }; - - it('empty severity and status', () => { - const urlParams = { - ...commonProps, - status: [], - severity: [], - }; - - expect(serializeUrlParams(urlParams).toString()).toEqual( - 'page=1&perPage=5&sortField=createdAt&sortOrder=desc&status=&severity=' - ); - }); - - it('severity and status with one value', () => { - const urlParams = { - ...commonProps, - status: ['open'], - severity: ['low'], - }; - - expect(serializeUrlParams(urlParams).toString()).toEqual( - 'page=1&perPage=5&sortField=createdAt&sortOrder=desc&status=open&severity=low' - ); - }); - - it('severity and status with multiple values', () => { - const urlParams = { - ...commonProps, - status: ['open', 'closed'], - severity: ['low', 'high'], - }; - - expect(serializeUrlParams(urlParams).toString()).toEqual( - 'page=1&perPage=5&sortField=createdAt&sortOrder=desc&status=open&status=closed&severity=low&severity=high' - ); - }); - - it('severity and status are undefined', () => { - const urlParams = { - ...commonProps, - status: undefined, - severity: undefined, - }; - - expect(serializeUrlParams(urlParams).toString()).toEqual( - 'page=1&perPage=5&sortField=createdAt&sortOrder=desc' - ); - }); - - it('severity and status are undefined but there are more filters to serialize', () => { - const urlParams = { - status: undefined, - severity: undefined, - ...commonProps, - }; - - expect(serializeUrlParams(urlParams).toString()).toEqual( - 'page=1&perPage=5&sortField=createdAt&sortOrder=desc' - ); - }); -}); diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/serialize_url_params.tsx b/x-pack/plugins/cases/public/components/all_cases/utils/serialize_url_params.tsx deleted file mode 100644 index 4b3e352b894d0..0000000000000 --- a/x-pack/plugins/cases/public/components/all_cases/utils/serialize_url_params.tsx +++ /dev/null @@ -1,27 +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. - */ - -export function serializeUrlParams(urlParams: { - [key in string]: string[] | string | undefined; -}) { - const urlSearchParams = new URLSearchParams(); - for (const [key, value] of Object.entries(urlParams)) { - if (value) { - if (Array.isArray(value)) { - if (value.length === 0) { - urlSearchParams.append(key, ''); - } else { - value.forEach((v) => urlSearchParams.append(key, v)); - } - } else { - urlSearchParams.append(key, value); - } - } - } - - return urlSearchParams; -} diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/stringify_url_params.test.tsx b/x-pack/plugins/cases/public/components/all_cases/utils/stringify_url_params.test.tsx new file mode 100644 index 0000000000000..4f67764260bb3 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/stringify_url_params.test.tsx @@ -0,0 +1,131 @@ +/* + * 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 { CaseStatuses } from '@kbn/cases-components'; +import { DEFAULT_CASES_TABLE_STATE } from '../../../containers/constants'; +import { CaseSeverity } from '../../../../common'; +import { SortFieldCase } from '../../../../common/ui'; +import { stringifyUrlParams } from './stringify_url_params'; + +describe('stringifyUrlParams', () => { + const commonProps = { + page: 1, + perPage: 5, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc' as const, + }; + + it('empty severity and status', () => { + const urlParams = { + ...commonProps, + status: [], + severity: [], + }; + + expect(stringifyUrlParams(urlParams)).toMatchInlineSnapshot( + `"cases=(page:1,perPage:5,severity:!(),sortField:createdAt,sortOrder:desc,status:!())"` + ); + }); + + it('severity and status with one value', () => { + const urlParams = { + ...commonProps, + status: [CaseStatuses.open], + severity: [CaseSeverity.LOW], + }; + + expect(stringifyUrlParams(urlParams)).toMatchInlineSnapshot( + `"cases=(page:1,perPage:5,severity:!(low),sortField:createdAt,sortOrder:desc,status:!(open))"` + ); + }); + + it('severity and status with multiple values', () => { + const urlParams = { + ...commonProps, + status: [CaseStatuses.open, CaseStatuses.closed], + severity: [CaseSeverity.LOW, CaseSeverity.HIGH], + }; + + expect(stringifyUrlParams(urlParams)).toMatchInlineSnapshot( + `"cases=(page:1,perPage:5,severity:!(low,high),sortField:createdAt,sortOrder:desc,status:!(open,closed))"` + ); + }); + + it('severity and status are undefined', () => { + const urlParams = { + ...commonProps, + status: undefined, + severity: undefined, + }; + + expect(stringifyUrlParams(urlParams)).toMatchInlineSnapshot( + `"cases=(page:1,perPage:5,sortField:createdAt,sortOrder:desc)"` + ); + }); + + it('severity and status are undefined but there are more filters to serialize', () => { + const urlParams = { + status: undefined, + severity: undefined, + ...commonProps, + }; + + expect(stringifyUrlParams(urlParams)).toMatchInlineSnapshot( + `"cases=(page:1,perPage:5,sortField:createdAt,sortOrder:desc)"` + ); + }); + + it('encodes defaults correctly', () => { + const { customFields, ...filterOptionsWithoutCustomFields } = + DEFAULT_CASES_TABLE_STATE.filterOptions; + + const urlParams = { + ...filterOptionsWithoutCustomFields, + ...DEFAULT_CASES_TABLE_STATE.queryParams, + customFields: { my_field: ['foo', 'bar'] }, + }; + + expect(stringifyUrlParams(urlParams)).toMatchInlineSnapshot( + `"cases=(assignees:!(),category:!(),customFields:(my_field:!(foo,bar)),owner:!(),page:1,perPage:10,reporters:!(),search:'',searchFields:!(title,description),severity:!(),sortField:createdAt,sortOrder:desc,status:!(),tags:!())"` + ); + }); + + it('replaces the cases query param correctly', () => { + expect( + stringifyUrlParams( + { + perPage: 100, + }, + 'cases=(perPage:5)' + ) + ).toMatchInlineSnapshot(`"cases=(perPage:100)"`); + }); + + it('removes legacy keys from URL', () => { + const search = 'status=foo&severity=foo&page=2&perPage=50&sortField=closedAt&sortOrder=asc'; + + expect( + stringifyUrlParams( + { + perPage: 100, + }, + search + ) + ).toMatchInlineSnapshot(`"cases=(perPage:100)"`); + }); + + it('keeps non cases state', () => { + expect( + stringifyUrlParams( + { + perPage: 100, + }, + 'foo=bar' + ) + ).toMatchInlineSnapshot(`"cases=(perPage:100)&foo=bar"`); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/stringify_url_params.tsx b/x-pack/plugins/cases/public/components/all_cases/utils/stringify_url_params.tsx new file mode 100644 index 0000000000000..8c43112c093aa --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/utils/stringify_url_params.tsx @@ -0,0 +1,35 @@ +/* + * 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 { encode } from '@kbn/rison'; +import { ALL_CASES_STATE_URL_KEY, LEGACY_SUPPORTED_STATE_KEYS } from '../constants'; +import type { AllCasesURLQueryParams } from '../types'; + +export function stringifyUrlParams( + allCasesUrlParams: AllCasesURLQueryParams, + currentSearch: string = '' +): string { + const encodedUrlParams = encode({ ...allCasesUrlParams }); + + const searchUrlParams = removeLegacyStateFromUrl( + new URLSearchParams(decodeURIComponent(currentSearch)) + ); + + searchUrlParams.delete(ALL_CASES_STATE_URL_KEY); + const casesQueryParam = `${ALL_CASES_STATE_URL_KEY}=${encodedUrlParams}`; + + return searchUrlParams.size > 0 + ? `${casesQueryParam}&${searchUrlParams.toString()}` + : casesQueryParam; +} + +const removeLegacyStateFromUrl = (urlParams: URLSearchParams): URLSearchParams => { + const newUrlParams = new URLSearchParams(urlParams); + LEGACY_SUPPORTED_STATE_KEYS.forEach((key) => newUrlParams.delete(key)); + + return newUrlParams; +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/utils/utils.test.tsx b/x-pack/plugins/cases/public/components/all_cases/utils/utils.test.tsx deleted file mode 100644 index 3fdb9f035f417..0000000000000 --- a/x-pack/plugins/cases/public/components/all_cases/utils/utils.test.tsx +++ /dev/null @@ -1,71 +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 { parseUrlQueryParams } from '.'; -import { DEFAULT_QUERY_PARAMS } from '../../../containers/constants'; - -const DEFAULT_STRING_QUERY_PARAMS = { - ...DEFAULT_QUERY_PARAMS, - page: String(DEFAULT_QUERY_PARAMS.page), - perPage: String(DEFAULT_QUERY_PARAMS.perPage), -}; - -describe('utils', () => { - describe('parseUrlQueryParams', () => { - it('valid input is processed correctly', () => { - expect(parseUrlQueryParams(DEFAULT_STRING_QUERY_PARAMS)).toStrictEqual(DEFAULT_QUERY_PARAMS); - }); - - it('empty string value for page/perPage is ignored', () => { - expect( - parseUrlQueryParams({ - ...DEFAULT_STRING_QUERY_PARAMS, - page: '', - perPage: '', - }) - ).toStrictEqual({ - sortField: DEFAULT_QUERY_PARAMS.sortField, - sortOrder: DEFAULT_QUERY_PARAMS.sortOrder, - }); - }); - - it('0 value for page/perPage is ignored', () => { - expect( - parseUrlQueryParams({ - ...DEFAULT_STRING_QUERY_PARAMS, - page: '0', - perPage: '0', - }) - ).toStrictEqual({ - sortField: DEFAULT_QUERY_PARAMS.sortField, - sortOrder: DEFAULT_QUERY_PARAMS.sortOrder, - }); - }); - - it('invalid string values for page/perPage are ignored', () => { - expect( - parseUrlQueryParams({ - ...DEFAULT_STRING_QUERY_PARAMS, - page: 'foo', - perPage: 'bar', - }) - ).toStrictEqual({ - sortField: DEFAULT_QUERY_PARAMS.sortField, - sortOrder: DEFAULT_QUERY_PARAMS.sortOrder, - }); - }); - - it('additional URL parameters are ignored', () => { - expect( - parseUrlQueryParams({ - ...DEFAULT_STRING_QUERY_PARAMS, - foo: 'bar', - }) - ).toStrictEqual(DEFAULT_QUERY_PARAMS); - }); - }); -}); diff --git a/x-pack/plugins/cases/public/components/app/routes.test.tsx b/x-pack/plugins/cases/public/components/app/routes.test.tsx index 9c435e2b163ba..91b4b1ef5227f 100644 --- a/x-pack/plugins/cases/public/components/app/routes.test.tsx +++ b/x-pack/plugins/cases/public/components/app/routes.test.tsx @@ -60,7 +60,8 @@ describe('Cases routes', () => { }); }); - describe('Case view', () => { + // FLAKY: https://github.com/elastic/kibana/issues/163263 + describe.skip('Case view', () => { it.each(getCaseViewPaths())( 'navigates to the cases view page for path: %s', async (path: string) => { @@ -84,7 +85,9 @@ describe('Cases routes', () => { ); }); - describe('Create case', () => { + // FLAKY: https://github.com/elastic/kibana/issues/175229 + // FLAKY: https://github.com/elastic/kibana/issues/175230 + describe.skip('Create case', () => { it('navigates to the create case page', () => { renderWithRouter(['/cases/create']); expect(screen.getByText('Create case')).toBeInTheDocument(); @@ -96,7 +99,9 @@ describe('Cases routes', () => { }); }); - describe('Cases settings', () => { + // FLAKY: https://github.com/elastic/kibana/issues/175231 + // FLAKY: https://github.com/elastic/kibana/issues/175232 + describe.skip('Cases settings', () => { it('navigates to the cases settings page', () => { renderWithRouter(['/cases/configure']); expect(screen.getByText('Settings')).toBeInTheDocument(); diff --git a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx index baad93c219b9d..f36620c033b7e 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx @@ -25,7 +25,8 @@ const defaultProps: EditTagsProps = { tags: [], }; -describe('EditTags ', () => { +// FLAKY: https://github.com/elastic/kibana/issues/175655 +describe.skip('EditTags ', () => { let appMockRender: AppMockRenderer; const sampleTags = ['coke', 'pepsi']; diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index e2db3717c009d..ba3e7850533c9 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -750,6 +750,7 @@ describe('ConfigureCases', () => { type: customFieldsConfigurationMock[0].type, label: `${customFieldsConfigurationMock[0].label}!!`, required: !customFieldsConfigurationMock[0].required, + defaultValue: customFieldsConfigurationMock[0].defaultValue, }, { ...customFieldsConfigurationMock[1] }, { ...customFieldsConfigurationMock[2] }, diff --git a/x-pack/plugins/cases/public/components/custom_fields/flyout.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/flyout.test.tsx index 3a25009450df7..508f124a7746c 100644 --- a/x-pack/plugins/cases/public/components/custom_fields/flyout.test.tsx +++ b/x-pack/plugins/cases/public/components/custom_fields/flyout.test.tsx @@ -108,6 +108,27 @@ describe('CustomFieldFlyout ', () => { }); }); + it('calls onSaveField with correct params when a custom field is NOT required and has a default value', async () => { + appMockRender.render(); + + userEvent.paste(await screen.findByTestId('custom-field-label-input'), 'Summary'); + userEvent.paste( + await screen.findByTestId('text-custom-field-default-value'), + 'Default value' + ); + userEvent.click(await screen.findByTestId('custom-field-flyout-save')); + + await waitFor(() => { + expect(props.onSaveField).toBeCalledWith({ + key: expect.anything(), + label: 'Summary', + required: false, + type: CustomFieldTypes.TEXT, + defaultValue: 'Default value', + }); + }); + }); + it('calls onSaveField with the correct params when a custom field is required', async () => { appMockRender.render(); @@ -202,6 +223,7 @@ describe('CustomFieldFlyout ', () => { label: 'Summary', required: false, type: CustomFieldTypes.TOGGLE, + defaultValue: false, }); }); }); diff --git a/x-pack/plugins/cases/public/components/custom_fields/form_fields.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/form_fields.test.tsx index 6c392a1ee7d7d..51f5f6dbddea6 100644 --- a/x-pack/plugins/cases/public/components/custom_fields/form_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/custom_fields/form_fields.test.tsx @@ -65,6 +65,7 @@ describe('FormFields ', () => { { label: 'hello', type: CustomFieldTypes.TOGGLE, + defaultValue: false, }, true ); diff --git a/x-pack/plugins/cases/public/components/custom_fields/text/configure.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/text/configure.test.tsx index 5d9d7166db270..455163f225a2b 100644 --- a/x-pack/plugins/cases/public/components/custom_fields/text/configure.test.tsx +++ b/x-pack/plugins/cases/public/components/custom_fields/text/configure.test.tsx @@ -44,6 +44,27 @@ describe('Configure ', () => { }); }); + it('updates field options with default value correctly when not required', async () => { + render( + + + + ); + + userEvent.paste(await screen.findByTestId('text-custom-field-default-value'), 'Default value'); + userEvent.click(await screen.findByTestId('form-test-component-submit-button')); + + await waitFor(() => { + // data, isValid + expect(onSubmit).toBeCalledWith( + { + defaultValue: 'Default value', + }, + true + ); + }); + }); + it('updates field options correctly when required', async () => { render( diff --git a/x-pack/plugins/cases/public/components/custom_fields/text/configure.tsx b/x-pack/plugins/cases/public/components/custom_fields/text/configure.tsx index 1253640d91b79..2ec61a4b80529 100644 --- a/x-pack/plugins/cases/public/components/custom_fields/text/configure.tsx +++ b/x-pack/plugins/cases/public/components/custom_fields/text/configure.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { UseField, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { CheckBoxField, TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; import type { CaseCustomFieldText } from '../../../../common/types/domain'; import type { CustomFieldType } from '../types'; @@ -14,7 +14,6 @@ import { getTextFieldConfig } from './config'; import * as i18n from '../translations'; const ConfigureComponent: CustomFieldType['Configure'] = () => { - const [{ required }] = useFormData<{ required: boolean }>(); const config = getTextFieldConfig({ required: false, label: i18n.DEFAULT_VALUE.toLocaleLowerCase(), @@ -34,19 +33,17 @@ const ConfigureComponent: CustomFieldType['Configure'] = () }, }} /> - {required && ( - - )} + ); }; diff --git a/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.test.tsx index 8153ca64a789c..77f1bde4e7b55 100644 --- a/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.test.tsx +++ b/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.test.tsx @@ -41,7 +41,12 @@ describe('Configure ', () => { await waitFor(() => { // data, isValid - expect(onSubmit).toBeCalledWith({}, true); + expect(onSubmit).toBeCalledWith( + { + defaultValue: false, + }, + true + ); }); }); diff --git a/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.tsx b/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.tsx index 83645ae185f1d..7b5980c2276ec 100644 --- a/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.tsx +++ b/x-pack/plugins/cases/public/components/custom_fields/toggle/configure.tsx @@ -6,15 +6,13 @@ */ import React from 'react'; -import { UseField, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { CheckBoxField, ToggleField } from '@kbn/es-ui-shared-plugin/static/forms/components'; import type { CaseCustomFieldToggle } from '../../../../common/types/domain'; import type { CustomFieldType } from '../types'; import * as i18n from '../translations'; const ConfigureComponent: CustomFieldType['Configure'] = () => { - const [{ required }] = useFormData<{ required: boolean }>(); - return ( <> ['Configure'] = }, }} /> - {required && ( - - )} + ); }; diff --git a/x-pack/plugins/cases/public/components/user_actions/user_actions_list.test.tsx b/x-pack/plugins/cases/public/components/user_actions/user_actions_list.test.tsx index 7a5a0b232589e..8e042c1b95a72 100644 --- a/x-pack/plugins/cases/public/components/user_actions/user_actions_list.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/user_actions_list.test.tsx @@ -31,7 +31,9 @@ const defaultProps = { jest.mock('../../common/lib/kibana'); -describe(`UserActionsList`, () => { +// FLAKY: https://github.com/elastic/kibana/issues/176524 +// FLAKY: https://github.com/elastic/kibana/issues/176525 +describe.skip(`UserActionsList`, () => { let appMockRender: AppMockRenderer; beforeEach(() => { diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index 97e84b8bbdc82..72fbbc24c15ec 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -179,6 +179,7 @@ export const removeItemFromSessionStorage = (key: string) => { export const stringifyToURL = (parsedParams: Record | URLSearchParams) => new URLSearchParams(parsedParams).toString(); + export const parseURL = (queryString: string) => Object.fromEntries(new URLSearchParams(queryString)); diff --git a/x-pack/plugins/cases/public/containers/constants.ts b/x-pack/plugins/cases/public/containers/constants.ts index 76d95a8bd0375..54e7cebba9025 100644 --- a/x-pack/plugins/cases/public/containers/constants.ts +++ b/x-pack/plugins/cases/public/containers/constants.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { AllCasesTableState } from '../components/all_cases/types'; import type { FilterOptions, QueryParams, SingleCaseMetricsFeature } from './types'; import { SortFieldCase } from './types'; @@ -66,6 +67,7 @@ export const casesMutationsKeys = { const DEFAULT_SEARCH_FIELDS = ['title', 'description']; +// TODO: Remove reporters. Move searchFields to API. export const DEFAULT_FILTER_OPTIONS: FilterOptions = { search: '', searchFields: DEFAULT_SEARCH_FIELDS, @@ -85,3 +87,8 @@ export const DEFAULT_QUERY_PARAMS: QueryParams = { sortField: SortFieldCase.createdAt, sortOrder: 'desc', }; + +export const DEFAULT_CASES_TABLE_STATE: AllCasesTableState = { + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: DEFAULT_QUERY_PARAMS, +}; diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts index 2c19df9b4c0f1..4819ca3fc1672 100644 --- a/x-pack/plugins/cases/server/client/cases/client.ts +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Case, Cases, User } from '../../../common/types/domain'; +import type { Case, CaseCustomField, Cases, User } from '../../../common/types/domain'; import type { CasePostRequest, CasesFindResponse, @@ -34,6 +34,8 @@ import type { PushParams } from './push'; import { push } from './push'; import { update } from './update'; import { bulkCreate } from './bulk_create'; +import type { ReplaceCustomFieldArgs } from './replace_custom_field'; +import { replaceCustomField } from './replace_custom_field'; /** * API for interacting with the cases entities. @@ -96,6 +98,10 @@ export interface CasesSubClient { * Retrieves the cases ID and title that have the requested alert attached to them */ getCasesByAlertID(params: CasesByAlertIDParams): Promise; + /** + * Replace custom field with specific customFieldId and CaseId + */ + replaceCustomField(params: ReplaceCustomFieldArgs): Promise; } /** @@ -122,6 +128,8 @@ export const createCasesSubClient = ( getCategories: (params: AllCategoriesFindRequest) => getCategories(params, clientArgs), getReporters: (params: AllReportersFindRequest) => getReporters(params, clientArgs), getCasesByAlertID: (params: CasesByAlertIDParams) => getCasesByAlertID(params, clientArgs), + replaceCustomField: (params: ReplaceCustomFieldArgs) => + replaceCustomField(params, clientArgs, casesClient), }; return Object.freeze(casesSubClient); diff --git a/x-pack/plugins/cases/server/client/cases/replace_custom_field.test.ts b/x-pack/plugins/cases/server/client/cases/replace_custom_field.test.ts new file mode 100644 index 0000000000000..f4c3666db7083 --- /dev/null +++ b/x-pack/plugins/cases/server/client/cases/replace_custom_field.test.ts @@ -0,0 +1,394 @@ +/* + * 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 { CustomFieldTypes } from '../../../common/types/domain'; +import { MAX_USER_ACTIONS_PER_CASE } from '../../../common/constants'; +import { mockCases } from '../../mocks'; +import { createCasesClientMock, createCasesClientMockArgs } from '../mocks'; +import { replaceCustomField } from './replace_custom_field'; + +describe('Replace custom field', () => { + const customFields = [ + { + key: 'first_key', + type: CustomFieldTypes.TEXT as const, + value: 'this is a text field value', + }, + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE as const, + value: null, + }, + ]; + + const theCase = { ...mockCases[0], attributes: { ...mockCases[0].attributes, customFields } }; + const clientArgs = createCasesClientMockArgs(); + const casesClient = createCasesClientMock(); + + beforeEach(() => { + jest.clearAllMocks(); + clientArgs.services.caseService.getCase.mockResolvedValue(theCase); + clientArgs.services.userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({ + [mockCases[0].id]: 1, + }); + + casesClient.configure.get = jest.fn().mockResolvedValue([ + { + owner: mockCases[0].attributes.owner, + customFields: [ + { + key: 'first_key', + type: CustomFieldTypes.TEXT, + label: 'missing field 1', + required: true, + }, + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE, + label: 'foo', + required: false, + }, + ], + }, + ]); + }); + + it('can replace text customField', async () => { + clientArgs.services.caseService.patchCase.mockResolvedValue({ + ...theCase, + }); + + await expect( + replaceCustomField( + { + caseId: theCase.id, + customFieldId: 'first_key', + request: { + caseVersion: mockCases[0].version ?? '', + value: 'Updated text field value', + }, + }, + clientArgs, + casesClient + ) + ).resolves.not.toThrow(); + + expect(clientArgs.services.caseService.patchCase).toHaveBeenCalledWith( + expect.objectContaining({ + caseId: theCase.id, + version: theCase.version, + originalCase: { + ...theCase, + }, + updatedAttributes: { + customFields: [ + { + key: 'first_key', + type: CustomFieldTypes.TEXT as const, + value: 'Updated text field value', + }, + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE as const, + value: null, + }, + ], + updated_at: expect.any(String), + updated_by: expect.any(Object), + }, + refresh: false, + }) + ); + }); + + it('can replace toggle customField', async () => { + clientArgs.services.caseService.patchCase.mockResolvedValue({ + ...theCase, + }); + + await expect( + replaceCustomField( + { + caseId: theCase.id, + customFieldId: 'second_key', + request: { + caseVersion: mockCases[0].version ?? '', + value: true, + }, + }, + clientArgs, + casesClient + ) + ).resolves.not.toThrow(); + + expect(clientArgs.services.caseService.patchCase).toHaveBeenCalledWith( + expect.objectContaining({ + caseId: theCase.id, + version: theCase.version, + originalCase: { + ...theCase, + }, + updatedAttributes: { + customFields: [ + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE as const, + value: true, + }, + { + key: 'first_key', + type: CustomFieldTypes.TEXT as const, + value: 'this is a text field value', + }, + ], + updated_at: expect.any(String), + updated_by: expect.any(Object), + }, + refresh: false, + }) + ); + }); + + it('does not throw error when customField value is null and the custom field is not required', async () => { + await expect( + replaceCustomField( + { + caseId: mockCases[0].id, + customFieldId: 'second_key', + request: { + caseVersion: mockCases[0].version ?? '', + value: null, + }, + }, + clientArgs, + casesClient + ) + ).resolves.not.toThrow(); + }); + + it('throws error when request is invalid', async () => { + await expect( + replaceCustomField( + { + caseId: mockCases[0].id, + customFieldId: 'first_key', + request: { + caseVersion: mockCases[0].version ?? '', + // @ts-expect-error check for invalid attribute + foo: 'bar', + }, + }, + clientArgs, + casesClient + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\"undefined\\" supplied to \\"value\\""` + ); + }); + + it('throws error when case version does not match', async () => { + await expect( + replaceCustomField( + { + caseId: mockCases[0].id, + customFieldId: 'first_key', + request: { + caseVersion: 'random-version', + value: 'test', + }, + }, + clientArgs, + casesClient + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to replace customField, id: first_key of case: mock-id-1 version:random-version : Error: This case mock-id-1 has been updated. Please refresh before saving additional updates."` + ); + }); + + it('throws error when customField value is null and the custom field is required', async () => { + await expect( + replaceCustomField( + { + caseId: mockCases[0].id, + customFieldId: 'first_key', + request: { + caseVersion: mockCases[0].version ?? '', + value: null, + }, + }, + clientArgs, + casesClient + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Custom field value cannot be null or undefined."` + ); + }); + + it('throws error when required customField of type text has value as empty string', async () => { + await expect( + replaceCustomField( + { + caseId: mockCases[0].id, + customFieldId: 'first_key', + request: { + caseVersion: mockCases[0].version ?? '', + value: ' ', + }, + }, + clientArgs, + casesClient + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\" \\" supplied to \\"value\\",The value field cannot be an empty string."` + ); + }); + + it('throws error when customField value is undefined and the custom field is required', async () => { + await expect( + replaceCustomField( + { + caseId: mockCases[0].id, + customFieldId: 'first_key', + request: { + caseVersion: mockCases[0].version ?? '', + // @ts-expect-error: undefined value + value: undefined, + }, + }, + clientArgs, + casesClient + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\"undefined\\" supplied to \\"value\\""` + ); + }); + + it('throws error when customField key is not present in configuration', async () => { + clientArgs.services.caseService.getCase.mockResolvedValue(mockCases[0]); + + await expect( + replaceCustomField( + { + caseId: mockCases[0].id, + customFieldId: 'missing_key', + request: { + caseVersion: mockCases[0].version ?? '', + value: 'updated', + }, + }, + clientArgs, + casesClient + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to replace customField, id: missing_key of case: mock-id-1 version:WzAsMV0= : Error: cannot find custom field"` + ); + }); + + it('throws error when the customField type does not match the configuration', async () => { + await expect( + replaceCustomField( + { + caseId: mockCases[0].id, + customFieldId: 'second_key', + request: { + caseVersion: mockCases[0].version ?? '', + value: 'foobar', + }, + }, + clientArgs, + casesClient + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to replace customField, id: second_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\"foobar\\" supplied to \\"value\\""` + ); + }); + + it('throws error when the customField not found after update', async () => { + clientArgs.services.caseService.patchCase.mockResolvedValue({ + ...theCase, + attributes: { + ...theCase.attributes, + customFields: [], + }, + }); + + await expect( + replaceCustomField( + { + caseId: mockCases[0].id, + customFieldId: 'second_key', + request: { + caseVersion: mockCases[0].version ?? '', + value: false, + }, + }, + clientArgs, + casesClient + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to replace customField, id: second_key of case: mock-id-1 version:WzAsMV0= : Error: Cannot find updated custom field."` + ); + }); + + describe('Validate max user actions', () => { + it('passes validation if max user actions per case is not reached', async () => { + clientArgs.services.userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({ + [mockCases[0].id]: MAX_USER_ACTIONS_PER_CASE - 1, + }); + + // @ts-ignore: only the array length matters here + clientArgs.services.userActionService.creator.buildUserActions.mockReturnValue({ + [mockCases[0].id]: [1], + }); + + clientArgs.services.caseService.patchCase.mockResolvedValue(theCase); + + await expect( + replaceCustomField( + { + caseId: mockCases[0].id, + customFieldId: 'first_key', + request: { + caseVersion: mockCases[0].version ?? '', + value: 'foobar', + }, + }, + clientArgs, + casesClient + ) + ).resolves.not.toThrow(); + }); + + it(`throws an error when the user actions to be created will reach ${MAX_USER_ACTIONS_PER_CASE}`, async () => { + clientArgs.services.userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({ + [mockCases[0].id]: MAX_USER_ACTIONS_PER_CASE, + }); + + // @ts-ignore: only the array length matters here + clientArgs.services.userActionService.creator.buildUserActions.mockReturnValue({ + [mockCases[0].id]: [1, 2, 3], + }); + + await expect( + replaceCustomField( + { + caseId: mockCases[0].id, + customFieldId: 'first_key', + request: { + caseVersion: mockCases[0].version ?? '', + value: 'foobar', + }, + }, + clientArgs, + casesClient + ) + ).rejects.toThrow( + `Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: The case with id mock-id-1 has reached the limit of ${MAX_USER_ACTIONS_PER_CASE} user actions.` + ); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/client/cases/replace_custom_field.ts b/x-pack/plugins/cases/server/client/cases/replace_custom_field.ts new file mode 100644 index 0000000000000..5a87c2acc1bf9 --- /dev/null +++ b/x-pack/plugins/cases/server/client/cases/replace_custom_field.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; + +import type { CasesClient, CasesClientArgs } from '..'; + +import type { CustomFieldPutRequest } from '../../../common/types/api'; +import { CustomFieldPutRequestRt, CaseRequestCustomFieldsRt } from '../../../common/types/api'; +import { Operations } from '../../authorization'; +import { createCaseError } from '../../common/error'; +import { validateMaxUserActions } from '../../../common/utils/validators'; +import { decodeOrThrow } from '../../../common/api/runtime_types'; +import type { CaseCustomField } from '../../../common/types/domain'; +import { CaseCustomFieldRt } from '../../../common/types/domain'; +import { decodeWithExcessOrThrow } from '../../../common/api'; +import { validateCustomFieldTypesInRequest } from './validators'; +import type { UserActionEvent } from '../../services/user_actions/types'; + +export interface ReplaceCustomFieldArgs { + /** + * The ID of a case + */ + caseId: string; + /** + * The ID of a custom field to be updated + */ + customFieldId: string; + /** + * value of custom field to update, case version + */ + request: CustomFieldPutRequest; +} + +/** + * Updates the specified cases with new values + * + * @ignore + */ +export const replaceCustomField = async ( + { caseId, customFieldId, request }: ReplaceCustomFieldArgs, + clientArgs: CasesClientArgs, + casesClient: CasesClient +): Promise => { + const { + services: { caseService, userActionService }, + user, + logger, + authorization, + } = clientArgs; + + try { + const { value, caseVersion } = request; + + decodeWithExcessOrThrow(CustomFieldPutRequestRt)(request); + + const caseToUpdate = await caseService.getCase({ + id: caseId, + }); + + if (caseToUpdate.version !== caseVersion) { + throw Boom.conflict( + `This case ${caseToUpdate.id} has been updated. Please refresh before saving additional updates.` + ); + } + + const configurations = await casesClient.configure.get({ + owner: caseToUpdate.attributes.owner, + }); + + await authorization.ensureAuthorized({ + entities: [{ owner: caseToUpdate.attributes.owner, id: caseToUpdate.id }], + operation: Operations.updateCase, + }); + + const foundCustomField = configurations[0]?.customFields.find( + (item) => item.key === customFieldId + ); + + if (!foundCustomField) { + throw Boom.badRequest('cannot find custom field'); + } + + validateCustomFieldTypesInRequest({ + requestCustomFields: [ + { + value, + type: foundCustomField.type, + key: customFieldId, + } as CaseCustomField, + ], + customFieldsConfiguration: configurations[0].customFields, + }); + + if (value == null && foundCustomField.required) { + throw Boom.badRequest('Custom field value cannot be null or undefined.'); + } + + const customFieldsToUpdate = [ + { + value, + type: foundCustomField.type, + key: customFieldId, + }, + ...caseToUpdate.attributes.customFields.filter((field) => field.key !== customFieldId), + ]; + + const decodedCustomFields = + decodeWithExcessOrThrow(CaseRequestCustomFieldsRt)(customFieldsToUpdate); + + const updatedAt = new Date().toISOString(); + + const patchCasesPayload = { + caseId, + originalCase: caseToUpdate, + updatedAttributes: { + customFields: decodedCustomFields, + updated_at: updatedAt, + updated_by: user, + }, + version: caseVersion, + }; + + const userActionsDict = userActionService.creator.buildUserActions({ + updatedCases: { + cases: [patchCasesPayload], + }, + user, + }); + + await validateMaxUserActions({ caseId, userActionService, userActionsToAdd: 1 }); + + const updatedCase = await caseService.patchCase({ + ...patchCasesPayload, + refresh: false, + }); + + const updatedCustomField = updatedCase.attributes.customFields?.find( + (cf) => cf.key === customFieldId + ); + + if (!updatedCustomField) { + throw new Error('Cannot find updated custom field.'); + } + + const builtUserActions = + userActionsDict != null + ? Object.keys(userActionsDict).reduce((acc, key) => { + return [...acc, ...userActionsDict[key]]; + }, []) + : []; + + await userActionService.creator.bulkCreateUpdateCase({ + builtUserActions, + }); + + return decodeOrThrow(CaseCustomFieldRt)(updatedCustomField); + } catch (error) { + throw createCaseError({ + message: `Failed to replace customField, id: ${customFieldId} of case: ${caseId} version:${request.caseVersion} : ${error}`, + error, + logger, + }); + } +}; diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index a1c070eb08d1f..c6c9f1063df60 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -1418,7 +1418,7 @@ describe('utils', () => { ).toEqual(customFields); }); - it('does not use default value for optional custom fields', () => { + it('uses the default value for optional custom fields', () => { expect( fillMissingCustomFields({ customFields: [], @@ -1430,8 +1430,8 @@ describe('utils', () => { ], }) ).toEqual([ - { ...customFields[0], value: null }, - { ...customFields[1], value: null }, + { ...customFields[0], value: 'default value' }, + { ...customFields[1], value: true }, ]); }); diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index b304159b928aa..a7f596a0f9c9e 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -473,11 +473,7 @@ export const fillMissingCustomFields = ({ // only populate with the default value required custom fields missing from the request for (const confCustomField of customFieldsConfiguration) { if (!customFieldsKeys.has(confCustomField.key)) { - if ( - confCustomField.required && - confCustomField?.defaultValue !== null && - confCustomField?.defaultValue !== undefined - ) { + if (confCustomField?.defaultValue !== null && confCustomField?.defaultValue !== undefined) { missingCustomFields.push({ key: confCustomField.key, type: confCustomField.type, diff --git a/x-pack/plugins/cases/server/client/configure/client.test.ts b/x-pack/plugins/cases/server/client/configure/client.test.ts index 2e3c2a6899f91..b5958c44de080 100644 --- a/x-pack/plugins/cases/server/client/configure/client.test.ts +++ b/x-pack/plugins/cases/server/client/configure/client.test.ts @@ -346,30 +346,6 @@ describe('client', () => { 'Failed to get patch configure in route: Error: Invalid custom field types in request for the following labels: "text label"' ); }); - - it('throws when an optional custom field has a default value', async () => { - await expect( - update( - 'test-id', - { - version: 'test-version', - customFields: [ - { - key: 'extra_default', - label: 'text label', - type: CustomFieldTypes.TEXT, - required: false, - defaultValue: 'foobar', - }, - ], - }, - clientArgs, - casesClientInternal - ) - ).rejects.toThrow( - 'Failed to get patch configure in route: Error: The following optional custom fields try to define a default value: "text label"' - ); - }); }); describe('create', () => { @@ -431,28 +407,5 @@ describe('client', () => { 'Failed to create case configuration: Error: Invalid duplicated custom field keys in request: duplicated_key' ); }); - - it('throws when an optional custom field has a default value', async () => { - await expect( - create( - { - ...baseRequest, - customFields: [ - { - key: 'extra_default', - label: 'text label', - type: CustomFieldTypes.TEXT, - required: false, - defaultValue: 'foobar', - }, - ], - }, - clientArgs, - casesClientInternal - ) - ).rejects.toThrow( - 'Failed to create case configuration: Error: The following optional custom fields try to define a default value: "text label"' - ); - }); }); }); diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 41a9dd9326c24..1261f1061a371 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -49,10 +49,7 @@ import { updateMappings } from './update_mappings'; import { decodeOrThrow } from '../../../common/api/runtime_types'; import { ConfigurationRt, ConfigurationsRt } from '../../../common/types/domain'; import { validateDuplicatedCustomFieldKeysInRequest } from '../validators'; -import { - validateCustomFieldTypesInRequest, - validateOptionalCustomFieldsInRequest, -} from './validators'; +import { validateCustomFieldTypesInRequest } from './validators'; /** * Defines the internal helper functions. @@ -256,7 +253,6 @@ export async function update( const request = decodeWithExcessOrThrow(ConfigurationPatchRequestRt)(req); validateDuplicatedCustomFieldKeysInRequest({ requestCustomFields: request.customFields }); - validateOptionalCustomFieldsInRequest({ requestCustomFields: request.customFields }); const { version, ...queryWithoutVersion } = request; @@ -372,9 +368,6 @@ export async function create( validateDuplicatedCustomFieldKeysInRequest({ requestCustomFields: validatedConfigurationRequest.customFields, }); - validateOptionalCustomFieldsInRequest({ - requestCustomFields: validatedConfigurationRequest.customFields, - }); let error = null; diff --git a/x-pack/plugins/cases/server/client/configure/validators.test.ts b/x-pack/plugins/cases/server/client/configure/validators.test.ts index d1d41bfe1bb62..0f8e20505fb39 100644 --- a/x-pack/plugins/cases/server/client/configure/validators.test.ts +++ b/x-pack/plugins/cases/server/client/configure/validators.test.ts @@ -6,10 +6,7 @@ */ import { CustomFieldTypes } from '../../../common/types/domain'; -import { - validateCustomFieldTypesInRequest, - validateOptionalCustomFieldsInRequest, -} from './validators'; +import { validateCustomFieldTypesInRequest } from './validators'; describe('validators', () => { describe('validateCustomFieldTypesInRequest', () => { @@ -72,73 +69,4 @@ describe('validators', () => { ).not.toThrow(); }); }); - - describe('validateOptionalCustomFieldsInRequest', () => { - it('does not throw an error for properly constructed optional custom fields', () => { - expect(() => - validateOptionalCustomFieldsInRequest({ - requestCustomFields: [ - { key: '1', required: false, label: 'label 1' }, - { key: '2', required: false, label: 'label 2' }, - ], - }) - ).not.toThrow(); - }); - - it('does not throw an error for required custom fields with default values', () => { - expect(() => - validateOptionalCustomFieldsInRequest({ - requestCustomFields: [ - { key: '1', required: true, defaultValue: false, label: 'label 1' }, - { key: '2', required: true, defaultValue: 'foobar', label: 'label 2' }, - ], - }) - ).not.toThrow(); - }); - - it('throws an error even if the default value has the correct type', () => { - expect(() => - validateOptionalCustomFieldsInRequest({ - requestCustomFields: [ - { key: '1', required: false, defaultValue: false, label: 'label 1' }, - { key: '2', required: false, defaultValue: 'foobar', label: 'label 2' }, - ], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"The following optional custom fields try to define a default value: \\"label 1\\", \\"label 2\\""` - ); - }); - - it('throws an error for other falsy defaultValues (null)', () => { - expect(() => - validateOptionalCustomFieldsInRequest({ - requestCustomFields: [ - { key: '1', required: false, defaultValue: null, label: 'label 1' }, - ], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"The following optional custom fields try to define a default value: \\"label 1\\""` - ); - }); - - it('throws an error for other falsy defaultValues (0)', () => { - expect(() => - validateOptionalCustomFieldsInRequest({ - requestCustomFields: [{ key: '1', required: false, defaultValue: 0, label: 'label 1' }], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"The following optional custom fields try to define a default value: \\"label 1\\""` - ); - }); - - it('throws an error for other falsy defaultValues (empty string)', () => { - expect(() => - validateOptionalCustomFieldsInRequest({ - requestCustomFields: [{ key: '1', required: false, defaultValue: '', label: 'label 1' }], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"The following optional custom fields try to define a default value: \\"label 1\\""` - ); - }); - }); }); diff --git a/x-pack/plugins/cases/server/client/configure/validators.ts b/x-pack/plugins/cases/server/client/configure/validators.ts index ca3a175e40579..c5929065c631b 100644 --- a/x-pack/plugins/cases/server/client/configure/validators.ts +++ b/x-pack/plugins/cases/server/client/configure/validators.ts @@ -38,37 +38,3 @@ export const validateCustomFieldTypesInRequest = ({ ); } }; - -/** - * Throws an error if any optional custom field defines a default value. - */ -export const validateOptionalCustomFieldsInRequest = ({ - requestCustomFields, -}: { - requestCustomFields?: Array<{ - key: string; - required: boolean; - defaultValue?: unknown; - label: string; - }>; -}) => { - if (!Array.isArray(requestCustomFields)) { - return; - } - - const invalidFields: string[] = []; - - requestCustomFields.forEach((requestField) => { - if (!requestField.required && requestField.defaultValue !== undefined) { - invalidFields.push(`"${requestField.label}"`); - } - }); - - if (invalidFields.length > 0) { - throw Boom.badRequest( - `The following optional custom fields try to define a default value: ${invalidFields.join( - ', ' - )}` - ); - } -}; diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index f9d02c094e425..c558fc258d3a2 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -59,6 +59,7 @@ const createCasesSubClientMock = (): CasesSubClientMock => { getReporters: jest.fn(), getCasesByAlertID: jest.fn(), getCategories: jest.fn(), + replaceCustomField: jest.fn(), }; }; diff --git a/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts index e6c5793064545..79e5189c02f57 100644 --- a/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts +++ b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts @@ -19,6 +19,7 @@ import { getCategoriesRoute } from './cases/categories/get_categories'; import { getCaseMetricRoute } from './internal/get_case_metrics'; import { getCasesMetricRoute } from './internal/get_cases_metrics'; import { searchCasesRoute } from './internal/search_cases'; +import { replaceCustomFieldRoute } from './internal/replace_custom_field'; export const getInternalRoutes = (userProfileService: UserProfileService) => [ @@ -34,4 +35,5 @@ export const getInternalRoutes = (userProfileService: UserProfileService) => getCaseMetricRoute, getCasesMetricRoute, searchCasesRoute, + replaceCustomFieldRoute, ] as CaseRoute[]; diff --git a/x-pack/plugins/cases/server/routes/api/internal/replace_custom_field.ts b/x-pack/plugins/cases/server/routes/api/internal/replace_custom_field.ts new file mode 100644 index 0000000000000..c243c10064c24 --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/internal/replace_custom_field.ts @@ -0,0 +1,48 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { INTERNAL_PUT_CUSTOM_FIELDS_URL } from '../../../../common/constants'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; +import type { customFieldsApiV1 } from '../../../../common/types/api'; +import type { customFieldDomainV1 } from '../../../../common/types/domain'; + +export const replaceCustomFieldRoute = createCasesRoute({ + method: 'put', + path: INTERNAL_PUT_CUSTOM_FIELDS_URL, + params: { + params: schema.object({ + case_id: schema.string(), + custom_field_id: schema.string(), + }), + }, + handler: async ({ context, request, response }) => { + try { + const caseContext = await context.cases; + const casesClient = await caseContext.getCasesClient(); + const caseId = request.params.case_id; + const customFieldId = request.params.custom_field_id; + const details = request.body as customFieldsApiV1.CustomFieldPutRequest; + + const res: customFieldDomainV1.CaseCustomField = await casesClient.cases.replaceCustomField({ + caseId, + customFieldId, + request: details, + }); + + return response.ok({ + body: res, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to replace customField in route: ${error}`, + error, + }); + } + }, +}); diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index f231a8d69b548..ec596cf815833 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -71,6 +71,7 @@ "@kbn/alerting-plugin", "@kbn/content-management-plugin", "@kbn/index-management-plugin", + "@kbn/rison", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts index cbeb334e2aa61..6222b457e7ad6 100644 --- a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts +++ b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts @@ -196,6 +196,15 @@ const CLOUD_PROVIDER_NAMES = { GCP: 'Google Cloud Platform', }; +export const CLOUD_PROVIDERS = { + AWS: 'aws', + AZURE: 'azure', + GCP: 'gcp', +}; + +/** + * Returns the cloud provider name or benchmark applicable name for the given benchmark id + */ export const getBenchmarkApplicableTo = (benchmarkId: BenchmarksCisId) => { switch (benchmarkId) { case 'cis_k8s': @@ -205,7 +214,7 @@ export const getBenchmarkApplicableTo = (benchmarkId: BenchmarksCisId) => { case 'cis_aws': return CLOUD_PROVIDER_NAMES.AWS; case 'cis_eks': - return 'Amazon Elastic Kubernetes Service'; + return 'Amazon Elastic Kubernetes Service (EKS)'; case 'cis_gcp': return CLOUD_PROVIDER_NAMES.GCP; } diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index bd266c98b8015..735a28a8ed0e1 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -230,3 +230,10 @@ export const DETECTION_ENGINE_RULES_KEY = 'detection_engine_rules'; export const DETECTION_ENGINE_ALERTS_KEY = 'detection_engine_alerts'; export const DEFAULT_GROUPING_TABLE_HEIGHT = 512; + +export const FINDINGS_GROUPING_OPTIONS = { + RESOURCE_NAME: 'resource.name', + RULE_NAME: 'rule.name', + CLOUD_ACCOUNT_NAME: 'cloud.account.name', + ORCHESTRATOR_CLUSTER_NAME: 'orchestrator.cluster.name', +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts index fbeeeb32a0c2e..4b98057fc10c4 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts @@ -57,7 +57,7 @@ const useNavigate = (pathname: string, dataViewId = SECURITY_DEFAULT_DATA_VIEW_I const { services } = useKibana(); return useCallback( - (filterParams: NavFilter = {}) => { + (filterParams: NavFilter = {}, groupBy?: string[]) => { const filters = Object.entries(filterParams).map(([key, filterValue]) => createFilter(key, filterValue, dataViewId) ); @@ -68,10 +68,11 @@ const useNavigate = (pathname: string, dataViewId = SECURITY_DEFAULT_DATA_VIEW_I // Set query language from user's preference query: services.data.query.queryString.getDefaultQuery(), filters, + ...(groupBy && { groupBy }), }), }); }, - [pathname, history, services.data.query.queryString, dataViewId] + [history, pathname, services.data.query.queryString, dataViewId] ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx new file mode 100644 index 0000000000000..1973620c8d9d8 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx @@ -0,0 +1,69 @@ +/* + * 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, fireEvent } from '@testing-library/react'; +import { AccountsEvaluatedWidget } from './accounts_evaluated_widget'; +import { BenchmarkData } from '../../common/types_old'; +import { TestProvider } from '../test/test_provider'; + +const mockNavToFindings = jest.fn(); +jest.mock('../common/hooks/use_navigate_findings', () => ({ + useNavigateFindings: () => mockNavToFindings, +})); + +describe('AccountsEvaluatedWidget', () => { + const benchmarkAssets = [ + { meta: { benchmarkId: 'cis_aws', assetCount: 10 } }, + { meta: { benchmarkId: 'cis_k8s', assetCount: 20 } }, + ] as BenchmarkData[]; + + it('renders the component with benchmark data correctly', () => { + const { getByText } = render( + + + + ); + + expect(getByText('10')).toBeInTheDocument(); + expect(getByText('20')).toBeInTheDocument(); + }); + + it('calls navToFindingsByCloudProvider when a benchmark with provider is clicked', () => { + const { getByText } = render( + + + + ); + + fireEvent.click(getByText('10')); + + expect(mockNavToFindings).toHaveBeenCalledWith( + { + 'cloud.provider': 'aws', + }, + ['cloud.account.name'] + ); + }); + + it('calls navToFindingsByCisBenchmark when a benchmark with benchmarkId is clicked', () => { + const { getByText } = render( + + + + ); + + fireEvent.click(getByText('20')); + + expect(mockNavToFindings).toHaveBeenCalledWith( + { + 'rule.benchmark.id': 'cis_k8s', + }, + ['orchestrator.cluster.name'] + ); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx index a9b118be899ab..aeb734005a65b 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx @@ -7,38 +7,40 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; +import { CLOUD_PROVIDERS, getBenchmarkApplicableTo } from '../../common/utils/helpers'; import { CIS_AWS, CIS_GCP, CIS_AZURE, CIS_K8S, CIS_EKS } from '../../common/constants'; import { CISBenchmarkIcon } from './cis_benchmark_icon'; import { CompactFormattedNumber } from './compact_formatted_number'; import { useNavigateFindings } from '../common/hooks/use_navigate_findings'; import { BenchmarkData } from '../../common/types_old'; +import { FINDINGS_GROUPING_OPTIONS } from '../common/constants'; // order in array will determine order of appearance in the dashboard const benchmarks = [ { type: CIS_AWS, - name: 'Amazon Web Services (AWS)', - provider: 'aws', + name: getBenchmarkApplicableTo(CIS_AWS), + provider: CLOUD_PROVIDERS.AWS, }, { type: CIS_GCP, - name: 'Google Cloud Platform (GCP)', - provider: 'gcp', + name: getBenchmarkApplicableTo(CIS_GCP), + provider: CLOUD_PROVIDERS.GCP, }, { type: CIS_AZURE, - name: 'Azure', - provider: 'azure', + name: getBenchmarkApplicableTo(CIS_AZURE), + provider: CLOUD_PROVIDERS.AZURE, }, { type: CIS_K8S, - name: 'Kubernetes', - benchmarkId: 'cis_k8s', + name: getBenchmarkApplicableTo(CIS_K8S), + benchmarkId: CIS_K8S, }, { type: CIS_EKS, - name: 'Amazon Elastic Kubernetes Service (EKS)', - benchmarkId: 'cis_eks', + name: getBenchmarkApplicableTo(CIS_EKS), + benchmarkId: CIS_EKS, }, ]; @@ -59,11 +61,13 @@ export const AccountsEvaluatedWidget = ({ const navToFindings = useNavigateFindings(); const navToFindingsByCloudProvider = (provider: string) => { - navToFindings({ 'cloud.provider': provider }); + navToFindings({ 'cloud.provider': provider }, [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME]); }; const navToFindingsByCisBenchmark = (cisBenchmark: string) => { - navToFindings({ 'rule.benchmark.id': cisBenchmark }); + navToFindings({ 'rule.benchmark.id': cisBenchmark }, [ + FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME, + ]); }; const benchmarkElements = benchmarks.map((benchmark) => { @@ -75,10 +79,10 @@ export const AccountsEvaluatedWidget = ({ key={benchmark.type} onClick={() => { if (benchmark.provider) { - navToFindingsByCloudProvider(benchmark.provider); + return navToFindingsByCloudProvider(benchmark.provider); } if (benchmark.benchmarkId) { - navToFindingsByCisBenchmark(benchmark.benchmarkId); + return navToFindingsByCisBenchmark(benchmark.benchmarkId); } }} css={css` diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx index dc140fef121b3..ac9b48ddfdbfe 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmark_details_box.tsx @@ -17,23 +17,32 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import { i18n } from '@kbn/i18n'; +import { FINDINGS_GROUPING_OPTIONS } from '../../../common/constants'; import { getBenchmarkIdQuery } from './benchmarks_section'; import { BenchmarkData } from '../../../../common/types_old'; import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; import cisLogoIcon from '../../../assets/icons/cis_logo.svg'; + +interface BenchmarkInfo { + name: string; + assetType: string; + handleClick: () => void; +} + export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData }) => { const navToFindings = useNavigateFindings(); - const handleBenchmarkClick = () => { - return navToFindings(getBenchmarkIdQuery(benchmark)); - }; + const handleClickCloudProvider = () => + navToFindings(getBenchmarkIdQuery(benchmark), [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME]); + + const handleClickCluster = () => + navToFindings(getBenchmarkIdQuery(benchmark), [ + FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME, + ]); - const getBenchmarkInfo = ( - benchmarkId: string, - cloudAssetCount: number - ): { name: string; assetType: string } => { - const benchmarks: Record = { + const getBenchmarkInfo = (benchmarkId: string, cloudAssetCount: number): BenchmarkInfo => { + const benchmarks: Record = { cis_gcp: { name: i18n.translate( 'xpack.csp.dashboard.benchmarkSection.benchmarkName.cisGcpBenchmarkName', @@ -48,6 +57,7 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData }) values: { count: cloudAssetCount }, } ), + handleClick: handleClickCloudProvider, }, cis_aws: { name: i18n.translate( @@ -63,6 +73,7 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData }) values: { count: cloudAssetCount }, } ), + handleClick: handleClickCloudProvider, }, cis_azure: { name: i18n.translate( @@ -78,6 +89,7 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData }) values: { count: cloudAssetCount }, } ), + handleClick: handleClickCloudProvider, }, cis_k8s: { name: i18n.translate( @@ -93,6 +105,7 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData }) values: { count: cloudAssetCount }, } ), + handleClick: handleClickCluster, }, cis_eks: { name: i18n.translate( @@ -108,6 +121,7 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData }) values: { count: cloudAssetCount }, } ), + handleClick: handleClickCluster, }, }; return benchmarks[benchmarkId]; @@ -149,14 +163,14 @@ export const BenchmarkDetailsBox = ({ benchmark }: { benchmark: BenchmarkData }) } > - +
{benchmarkInfo.name}
- + {benchmarkInfo.assetType} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts index 3d8200a144bd5..1292da8601357 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { GroupOption } from '@kbn/securitysolution-grouping'; +import { FINDINGS_GROUPING_OPTIONS } from '../../../common/constants'; import { FindingsBaseURLQuery } from '../../../common/types'; import { CloudSecurityDefaultColumn } from '../../../components/cloud_security_data_table'; @@ -16,13 +17,6 @@ export const FINDINGS_UNIT = (totalCount: number) => defaultMessage: `{totalCount, plural, =1 {finding} other {findings}}`, }); -export const GROUPING_OPTIONS = { - RESOURCE_NAME: 'resource.name', - RULE_NAME: 'rule.name', - CLOUD_ACCOUNT_NAME: 'cloud.account.name', - ORCHESTRATOR_CLUSTER_NAME: 'orchestrator.cluster.name', -}; - export const NULL_GROUPING_UNIT = i18n.translate('xpack.csp.findings.grouping.nullGroupUnit', { defaultMessage: 'findings', }); @@ -51,25 +45,25 @@ export const defaultGroupingOptions: GroupOption[] = [ label: i18n.translate('xpack.csp.findings.latestFindings.groupByResource', { defaultMessage: 'Resource', }), - key: GROUPING_OPTIONS.RESOURCE_NAME, + key: FINDINGS_GROUPING_OPTIONS.RESOURCE_NAME, }, { label: i18n.translate('xpack.csp.findings.latestFindings.groupByRuleName', { defaultMessage: 'Rule name', }), - key: GROUPING_OPTIONS.RULE_NAME, + key: FINDINGS_GROUPING_OPTIONS.RULE_NAME, }, { label: i18n.translate('xpack.csp.findings.latestFindings.groupByCloudAccount', { defaultMessage: 'Cloud account', }), - key: GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME, + key: FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME, }, { label: i18n.translate('xpack.csp.findings.latestFindings.groupByKubernetesCluster', { defaultMessage: 'Kubernetes cluster', }), - key: GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME, + key: FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME, }, ]; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx index fe8536eaf0f69..6dbc78cbf0857 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx @@ -17,6 +17,7 @@ import { css } from '@emotion/react'; import { GroupPanelRenderer, RawBucket, StatRenderer } from '@kbn/securitysolution-grouping/src'; import React from 'react'; import { i18n } from '@kbn/i18n'; +import { FINDINGS_GROUPING_OPTIONS } from '../../../common/constants'; import { NullGroup, LoadingGroup, @@ -26,7 +27,7 @@ import { getAbbreviatedNumber } from '../../../common/utils/get_abbreviated_numb import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; import { ComplianceScoreBar } from '../../../components/compliance_score_bar'; import { FindingsGroupingAggregation } from './use_grouped_findings'; -import { GROUPING_OPTIONS, NULL_GROUPING_MESSAGES, NULL_GROUPING_UNIT } from './constants'; +import { NULL_GROUPING_MESSAGES, NULL_GROUPING_UNIT } from './constants'; import { FINDINGS_GROUPING_COUNTER } from '../test_subjects'; export const groupPanelRenderer: GroupPanelRenderer = ( @@ -45,7 +46,7 @@ export const groupPanelRenderer: GroupPanelRenderer ); switch (selectedGroup) { - case GROUPING_OPTIONS.RESOURCE_NAME: + case FINDINGS_GROUPING_OPTIONS.RESOURCE_NAME: return nullGroupMessage ? ( renderNullGroup(NULL_GROUPING_MESSAGES.RESOURCE_NAME) ) : ( @@ -78,7 +79,7 @@ export const groupPanelRenderer: GroupPanelRenderer ); - case GROUPING_OPTIONS.RULE_NAME: + case FINDINGS_GROUPING_OPTIONS.RULE_NAME: return nullGroupMessage ? ( renderNullGroup(NULL_GROUPING_MESSAGES.RULE_NAME) ) : ( @@ -100,7 +101,7 @@ export const groupPanelRenderer: GroupPanelRenderer ); - case GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: + case FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: return nullGroupMessage ? ( renderNullGroup(NULL_GROUPING_MESSAGES.CLOUD_ACCOUNT_NAME) ) : ( @@ -129,7 +130,7 @@ export const groupPanelRenderer: GroupPanelRenderer ); - case GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME: + case FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME: return nullGroupMessage ? ( renderNullGroup(NULL_GROUPING_MESSAGES.ORCHESTRATOR_CLUSTER_NAME) ) : ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index 74efd68b5378b..81df07731c5dd 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -15,7 +15,10 @@ import { } from '@kbn/securitysolution-grouping/src'; import { useMemo } from 'react'; import { buildEsQuery, Filter } from '@kbn/es-query'; -import { LOCAL_STORAGE_FINDINGS_GROUPING_KEY } from '../../../common/constants'; +import { + FINDINGS_GROUPING_OPTIONS, + LOCAL_STORAGE_FINDINGS_GROUPING_KEY, +} from '../../../common/constants'; import { useDataViewContext } from '../../../common/contexts/data_view_context'; import { Evaluation } from '../../../../common/types_old'; import { LATEST_FINDINGS_RETENTION_POLICY } from '../../../../common/constants'; @@ -24,13 +27,7 @@ import { FindingsRootGroupingAggregation, useGroupedFindings, } from './use_grouped_findings'; -import { - FINDINGS_UNIT, - groupingTitle, - defaultGroupingOptions, - getDefaultQuery, - GROUPING_OPTIONS, -} from './constants'; +import { FINDINGS_UNIT, groupingTitle, defaultGroupingOptions, getDefaultQuery } from './constants'; import { useCloudSecurityGrouping } from '../../../components/cloud_security_grouping'; import { getFilters } from '../utils/get_filters'; import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api'; @@ -80,26 +77,26 @@ const getAggregationsByGroupField = (field: string): NamedAggregation[] => { ]; switch (field) { - case GROUPING_OPTIONS.RESOURCE_NAME: + case FINDINGS_GROUPING_OPTIONS.RESOURCE_NAME: return [ ...aggMetrics, getTermAggregation('resourceName', 'resource.id'), getTermAggregation('resourceSubType', 'resource.sub_type'), getTermAggregation('resourceType', 'resource.type'), ]; - case GROUPING_OPTIONS.RULE_NAME: + case FINDINGS_GROUPING_OPTIONS.RULE_NAME: return [ ...aggMetrics, getTermAggregation('benchmarkName', 'rule.benchmark.name'), getTermAggregation('benchmarkVersion', 'rule.benchmark.version'), ]; - case GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: + case FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: return [ ...aggMetrics, getTermAggregation('benchmarkName', 'rule.benchmark.name'), getTermAggregation('benchmarkId', 'rule.benchmark.id'), ]; - case GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME: + case FINDINGS_GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME: return [ ...aggMetrics, getTermAggregation('benchmarkName', 'rule.benchmark.name'), diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx index f70da5fa27740..b83844416c076 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx @@ -14,6 +14,7 @@ import { parseGroupingQuery, } from '@kbn/securitysolution-grouping/src'; import { useMemo } from 'react'; +import { buildEsQuery, Filter } from '@kbn/es-query'; import { LOCAL_STORAGE_VULNERABILITIES_GROUPING_KEY } from '../../../common/constants'; import { useDataViewContext } from '../../../common/contexts/data_view_context'; import { @@ -110,9 +111,15 @@ export const isVulnerabilitiesRootGroupingAggregation = ( export const useLatestVulnerabilitiesGrouping = ({ groupPanelRenderer, groupStatsRenderer, + groupingLevel = 0, + groupFilters = [], + selectedGroup, }: { groupPanelRenderer?: GroupPanelRenderer; groupStatsRenderer?: GroupStatsRenderer; + groupingLevel?: number; + groupFilters?: Filter[]; + selectedGroup?: string; }) => { const { dataView } = useDataViewContext(); @@ -121,7 +128,6 @@ export const useLatestVulnerabilitiesGrouping = ({ grouping, pageSize, query, - selectedGroup, onChangeGroupsItemsPerPage, onChangeGroupsPage, setUrlQuery, @@ -130,6 +136,7 @@ export const useLatestVulnerabilitiesGrouping = ({ onResetFilters, error, filters, + setActivePageIndex, } = useCloudSecurityGrouping({ dataView, groupingTitle, @@ -139,19 +146,22 @@ export const useLatestVulnerabilitiesGrouping = ({ groupPanelRenderer, groupStatsRenderer, groupingLocalStorageKey: LOCAL_STORAGE_VULNERABILITIES_GROUPING_KEY, - maxGroupingLevels: 1, + groupingLevel, }); + const additionalFilters = buildEsQuery(dataView, [], groupFilters); + const currentSelectedGroup = selectedGroup || grouping.selectedGroups[0]; + const groupingQuery = getGroupingQuery({ - additionalFilters: query ? [query] : [], - groupByField: selectedGroup, + additionalFilters: query ? [query, additionalFilters] : [additionalFilters], + groupByField: currentSelectedGroup, uniqueValue, from: `now-${LATEST_VULNERABILITIES_RETENTION_POLICY}`, to: 'now', pageNumber: activePageIndex * pageSize, size: pageSize, sort: [{ groupByField: { order: 'desc' } }], - statsAggregations: getAggregationsByGroupField(selectedGroup), + statsAggregations: getAggregationsByGroupField(currentSelectedGroup), }); const { data, isFetching } = useGroupedVulnerabilities({ @@ -162,11 +172,11 @@ export const useLatestVulnerabilitiesGrouping = ({ const groupData = useMemo( () => parseGroupingQuery( - selectedGroup, + currentSelectedGroup, uniqueValue, data as GroupingAggregation ), - [data, selectedGroup, uniqueValue] + [data, currentSelectedGroup, uniqueValue] ); const isEmptyResults = @@ -179,6 +189,7 @@ export const useLatestVulnerabilitiesGrouping = ({ grouping, isFetching, activePageIndex, + setActivePageIndex, pageSize, selectedGroup, onChangeGroupsItemsPerPage, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_container.tsx index 8ba50c3aac4f1..6af307b79ad85 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_container.tsx @@ -5,7 +5,7 @@ * 2.0. */ import { Filter } from '@kbn/es-query'; -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { useLatestVulnerabilitiesGrouping } from './hooks/use_latest_vulnerabilities_grouping'; import { LatestVulnerabilitiesTable } from './latest_vulnerabilities_table'; @@ -17,31 +17,129 @@ import { CloudSecurityGrouping } from '../../components/cloud_security_grouping' import { DEFAULT_GROUPING_TABLE_HEIGHT } from '../../common/constants'; export const LatestVulnerabilitiesContainer = () => { - const renderChildComponent = (groupFilters: Filter[]) => { + const SubGrouping = ({ + renderChildComponent, + groupingLevel, + parentGroupFilters, + selectedGroup, + groupSelectorComponent, + }: { + renderChildComponent: (groupFilters: Filter[]) => JSX.Element; + groupingLevel: number; + parentGroupFilters?: string; + selectedGroup: string; + groupSelectorComponent?: JSX.Element; + }) => { + const { + groupData, + grouping, + isFetching, + activePageIndex, + pageSize, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + isGroupLoading, + setActivePageIndex, + } = useLatestVulnerabilitiesGrouping({ + groupPanelRenderer, + groupStatsRenderer, + groupingLevel, + selectedGroup, + groupFilters: parentGroupFilters ? JSON.parse(parentGroupFilters) : [], + }); + + /** + * This is used to reset the active page index when the selected group changes + * It is needed because the grouping number of pages can change according to the selected group + */ + useEffect(() => { + setActivePageIndex(0); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedGroup]); + return ( - ); }; - const { - isGroupSelected, - groupData, - grouping, - isFetching, - activePageIndex, - pageSize, - selectedGroup, - onChangeGroupsItemsPerPage, - onChangeGroupsPage, - setUrlQuery, - isGroupLoading, - onResetFilters, - error, - isEmptyResults, - } = useLatestVulnerabilitiesGrouping({ groupPanelRenderer, groupStatsRenderer }); + const renderChildComponent = ({ + level, + currentSelectedGroup, + selectedGroupOptions, + parentGroupFilters, + groupSelectorComponent, + }: { + level: number; + currentSelectedGroup: string; + selectedGroupOptions: string[]; + parentGroupFilters?: string; + groupSelectorComponent?: JSX.Element; + }) => { + let getChildComponent; + + if (currentSelectedGroup === 'none') { + return ( + + ); + } + + if (level < selectedGroupOptions.length - 1 && !selectedGroupOptions.includes('none')) { + getChildComponent = (currentGroupFilters: Filter[]) => { + const nextGroupingLevel = level + 1; + return renderChildComponent({ + level: nextGroupingLevel, + currentSelectedGroup: selectedGroupOptions[nextGroupingLevel], + selectedGroupOptions, + parentGroupFilters: JSON.stringify([ + ...currentGroupFilters, + ...(parentGroupFilters ? JSON.parse(parentGroupFilters) : []), + ]), + groupSelectorComponent, + }); + }; + } else { + getChildComponent = (currentGroupFilters: Filter[]) => { + return ( + + ); + }; + } + return ( + + ); + }; + + const { grouping, isFetching, setUrlQuery, onResetFilters, error, isEmptyResults } = + useLatestVulnerabilitiesGrouping({ groupPanelRenderer, groupStatsRenderer }); if (error || isEmptyResults) { return ( @@ -53,34 +151,18 @@ export const LatestVulnerabilitiesContainer = () => { ); } - if (isGroupSelected) { - return ( - <> - -
- - -
- - ); - } - return ( <> - +
+ {renderChildComponent({ + level: 0, + currentSelectedGroup: grouping.selectedGroups[0], + selectedGroupOptions: grouping.selectedGroups, + groupSelectorComponent: grouping.groupSelector, + })} +
); }; diff --git a/x-pack/plugins/data_visualizer/common/types/test_grok_pattern.ts b/x-pack/plugins/data_visualizer/common/types/test_grok_pattern.ts new file mode 100644 index 0000000000000..65ae4a89988de --- /dev/null +++ b/x-pack/plugins/data_visualizer/common/types/test_grok_pattern.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 interface TestGrokPatternResponse { + matches: Array<{ + matched: boolean; + fields: Record>; + }>; +} 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 111b0a2113403..9ea517d45eea1 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 @@ -116,10 +116,10 @@ export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed, > {Array.isArray(topValues) ? topValues.map((value) => { - const fieldValue = - value.key_as_string ?? (value.key ? value.key.toString() : EMPTY_EXAMPLE); + const fieldValue = value.key_as_string ?? (value.key ? value.key.toString() : ''); + const displayValue = fieldValue ?? EMPTY_EXAMPLE; return ( - + = ({ stats, fieldFormat, barColor, compressed, /> {fieldName !== undefined && - fieldValue !== undefined && + displayValue !== undefined && onAddFilter !== undefined ? (
= ({ stats, fieldFormat, barColor, compressed, 'xpack.dataVisualizer.dataGrid.field.addFilterAriaLabel', { defaultMessage: 'Filter for {fieldName}: "{value}"', - values: { fieldName, value: fieldValue }, + values: { fieldName, value: displayValue }, } )} - data-test-subj={`dvFieldDataTopValuesAddFilterButton-${fieldName}-${fieldValue}`} + data-test-subj={`dvFieldDataTopValuesAddFilterButton-${fieldName}-${displayValue}`} style={{ minHeight: 'auto', minWidth: 'auto', @@ -172,10 +172,10 @@ export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed, 'xpack.dataVisualizer.dataGrid.field.removeFilterAriaLabel', { defaultMessage: 'Filter out {fieldName}: "{value}"', - values: { fieldName, value: fieldValue }, + values: { fieldName, value: displayValue }, } )} - data-test-subj={`dvFieldDataTopValuesExcludeFilterButton-${fieldName}-${fieldValue}`} + data-test-subj={`dvFieldDataTopValuesExcludeFilterButton-${fieldName}-${displayValue}`} style={{ minHeight: 'auto', minWidth: 'auto', diff --git a/x-pack/plugins/data_visualizer/public/application/common/hooks/use_data.ts b/x-pack/plugins/data_visualizer/public/application/common/hooks/use_data.ts index 65c882ba551d9..9b85720fc1df4 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/hooks/use_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/hooks/use_data.ts @@ -14,9 +14,9 @@ import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker'; import { merge } from 'rxjs'; import { RandomSampler } from '@kbn/ml-random-sampler-utils'; import { mapAndFlattenFilters } from '@kbn/data-plugin/public'; -import { Query } from '@kbn/es-query'; +import { buildEsQuery, Query } from '@kbn/es-query'; import { SearchQueryLanguage } from '@kbn/ml-query-utils'; -import { createMergedEsQuery } from '../../index_data_visualizer/utils/saved_search_utils'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; import { useDataDriftStateManagerContext } from '../../data_drift/use_state_manager'; import type { InitialSettings } from '../../data_drift/use_data_drift_result'; import { @@ -74,7 +74,7 @@ export const useData = ( () => { const searchQuery = searchString !== undefined && searchQueryLanguage !== undefined - ? { query: searchString, language: searchQueryLanguage } + ? ({ query: searchString, language: searchQueryLanguage } as Query) : undefined; const timefilterActiveBounds = timeRange ?? timefilter.getActiveBounds(); @@ -90,24 +90,24 @@ export const useData = ( runtimeFieldMap: selectedDataView.getRuntimeMappings(), }; - const refQuery = createMergedEsQuery( - searchQuery, + const refQuery = buildEsQuery( + selectedDataView, + searchQuery ?? [], mapAndFlattenFilters([ ...queryManager.filterManager.getFilters(), ...(referenceStateManager.filters ?? []), ]), - selectedDataView, - uiSettings + uiSettings ? getEsQueryConfig(uiSettings) : undefined ); - const compQuery = createMergedEsQuery( - searchQuery, + const compQuery = buildEsQuery( + selectedDataView, + searchQuery ?? [], mapAndFlattenFilters([ ...queryManager.filterManager.getFilters(), ...(comparisonStateManager.filters ?? []), ]), - selectedDataView, - uiSettings + uiSettings ? getEsQueryConfig(uiSettings) : undefined ); return { 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 07b74677e8ea9..05f24bdcb7b68 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 @@ -8,6 +8,7 @@ import { chunk, cloneDeep, flatten } from 'lodash'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { lastValueFrom } from 'rxjs'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { @@ -30,7 +31,7 @@ import { computeChi2PValue, type Histogram } from '@kbn/ml-chi2test'; import { mapAndFlattenFilters } from '@kbn/data-plugin/public'; import type { AggregationsMultiTermsBucketKeys } from '@elastic/elasticsearch/lib/api/types'; -import { createMergedEsQuery } from '../index_data_visualizer/utils/saved_search_utils'; +import { buildEsQuery } from '@kbn/es-query'; import { useDataVisualizerKibana } from '../kibana_context'; import { useDataDriftStateManagerContext } from './use_state_manager'; @@ -758,18 +759,18 @@ export const useFetchDataComparisonResult = ( const kqlQuery = searchString !== undefined && searchQueryLanguage !== undefined - ? { query: searchString, language: searchQueryLanguage } + ? ({ query: searchString, language: searchQueryLanguage } as Query) : undefined; const refDataQuery = getDataComparisonQuery({ - searchQuery: createMergedEsQuery( - kqlQuery, + searchQuery: buildEsQuery( + currentDataView, + kqlQuery ?? [], mapAndFlattenFilters([ ...queryManager.filterManager.getFilters(), ...(referenceStateManager.filters ?? []), ]), - currentDataView, - uiSettings + uiSettings ? getEsQueryConfig(uiSettings) : undefined ), datetimeField: currentDataView?.timeFieldName, runtimeFields, @@ -827,14 +828,14 @@ export const useFetchDataComparisonResult = ( setLoaded(0.25); const prodDataQuery = getDataComparisonQuery({ - searchQuery: createMergedEsQuery( - kqlQuery, + searchQuery: buildEsQuery( + currentDataView, + kqlQuery ?? [], mapAndFlattenFilters([ ...queryManager.filterManager.getFilters(), ...(comparisonStateManager.filters ?? []), ]), - currentDataView, - uiSettings + uiSettings ? getEsQueryConfig(uiSettings) : undefined ), datetimeField: currentDataView?.timeFieldName, runtimeFields, diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/field_badge.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/field_badge.tsx new file mode 100644 index 0000000000000..981b2195c3065 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/field_badge.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { FieldIcon } from '@kbn/react-field'; +import { i18n } from '@kbn/i18n'; +import { getSupportedFieldType } from '../../../common/components/fields_stats_grid/get_field_names'; +import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme'; + +interface Props { + type: string | undefined; + value: string; + name: string; +} + +export const FieldBadge: FC = ({ type, value, name }) => { + const { euiColorLightestShade, euiColorLightShade } = useCurrentEuiTheme(); + const supportedType = getSupportedFieldType(type ?? 'unknown'); + const tooltip = type + ? i18n.translate('xpack.dataVisualizer.file.fileContents.fieldBadge.tooltip', { + defaultMessage: 'Type: {type}', + values: { type: supportedType }, + }) + : undefined; + return ( + + + + + + + {value} + + + + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx index 21c50a7f293b6..789d73888bf35 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/file_contents.tsx @@ -5,52 +5,150 @@ * 2.0. */ +import React, { FC, useEffect, useState, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { FC } from 'react'; -import { EuiTitle, EuiSpacer } from '@elastic/eui'; +import { + EuiTitle, + EuiSpacer, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, +} from '@elastic/eui'; -import { JsonEditor, EDITOR_MODE } from '../json_editor'; +import type { FindFileStructureResponse } from '@kbn/file-upload-plugin/common'; +import useMountedState from 'react-use/lib/useMountedState'; +import { i18n } from '@kbn/i18n'; +import { EDITOR_MODE, JsonEditor } from '../json_editor'; +import { useGrokHighlighter } from './use_text_parser'; +import { LINE_LIMIT } from './grok_highlighter'; interface Props { data: string; format: string; numberOfLines: number; + semiStructureTextData: SemiStructureTextData | null; } -export const FileContents: FC = ({ data, format, numberOfLines }) => { +interface SemiStructureTextData { + grokPattern?: string; + multilineStartPattern?: string; + excludeLinesPattern?: string; + sampleStart: string; + mappings: FindFileStructureResponse['mappings']; + ecsCompatibility?: string; +} + +function semiStructureTextDataGuard( + semiStructureTextData: SemiStructureTextData | null +): semiStructureTextData is SemiStructureTextData { + return ( + semiStructureTextData !== null && + semiStructureTextData.grokPattern !== undefined && + semiStructureTextData.multilineStartPattern !== undefined + ); +} + +export const FileContents: FC = ({ data, format, numberOfLines, semiStructureTextData }) => { let mode = EDITOR_MODE.TEXT; if (format === EDITOR_MODE.JSON) { mode = EDITOR_MODE.JSON; } + const isMounted = useMountedState(); + const grokHighlighter = useGrokHighlighter(); + + const [isSemiStructureTextData, setIsSemiStructureTextData] = useState( + semiStructureTextDataGuard(semiStructureTextData) + ); + const formattedData = useMemo( + () => limitByNumberOfLines(data, numberOfLines), + [data, numberOfLines] + ); + + const [highlightedLines, setHighlightedLines] = useState(null); + const [showHighlights, setShowHighlights] = useState(isSemiStructureTextData); + + useEffect(() => { + if (isSemiStructureTextData === false) { + return; + } + const { grokPattern, multilineStartPattern, excludeLinesPattern, mappings, ecsCompatibility } = + semiStructureTextData!; - const formattedData = limitByNumberOfLines(data, numberOfLines); + grokHighlighter( + data, + grokPattern!, + mappings, + ecsCompatibility, + multilineStartPattern!, + excludeLinesPattern + ) + .then((docs) => { + if (isMounted()) { + setHighlightedLines(docs); + } + }) + .catch((e) => { + if (isMounted()) { + setHighlightedLines(null); + setIsSemiStructureTextData(false); + } + }); + }, [data, semiStructureTextData, grokHighlighter, isSemiStructureTextData, isMounted]); return ( - - -

- -

-
- -
- -
+ <> + + + +

+ +

+
+
+ {isSemiStructureTextData ? ( + + setShowHighlights(!showHighlights)} + /> + + ) : null} +
+ + + + - -
+ {highlightedLines === null || showHighlights === false ? ( + + ) : ( + <> + {highlightedLines.map((line, i) => ( + <> + {line} + {i === highlightedLines.length - 1 ? null : } + + ))} + + )} + ); }; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/grok_highlighter.ts b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/grok_highlighter.ts new file mode 100644 index 0000000000000..7be566a5a91b0 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/grok_highlighter.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MessageImporter } from '@kbn/file-upload-plugin/public'; +import type { HttpSetup } from '@kbn/core/public'; +import type { ImportFactoryOptions } from '@kbn/file-upload-plugin/public/importer'; +import type { FindFileStructureResponse } from '@kbn/file-upload-plugin/common'; +import type { TestGrokPatternResponse } from '../../../../../common/types/test_grok_pattern'; + +export const LINE_LIMIT = 5; + +type HighlightedLine = Array<{ + word: string; + field?: { + type: string; + name: string; + }; +}>; + +export class GrokHighlighter extends MessageImporter { + constructor(options: ImportFactoryOptions, private http: HttpSetup) { + super(options); + } + + public async createLines( + text: string, + grokPattern: string, + mappings: FindFileStructureResponse['mappings'], + ecsCompatibility: string | undefined + ): Promise { + const docs = this._createDocs(text, false, LINE_LIMIT); + const lines = docs.docs.map((doc) => doc.message); + const matches = await this.testGrokPattern(lines, grokPattern, ecsCompatibility); + + return lines.map((line, index) => { + const { matched, fields } = matches[index]; + if (matched === false) { + return [ + { + word: line, + }, + ]; + } + const sortedFields = Object.entries(fields) + .map(([fieldName, [{ match, offset, length }]]) => { + let type = mappings.properties[fieldName]?.type; + if (type === undefined && fieldName === 'timestamp') { + // it's possible that the timestamp field is not mapped as `timestamp` + // but instead as `@timestamp` + type = mappings.properties['@timestamp']?.type; + } + return { + name: fieldName, + match, + offset, + length, + type, + }; + }) + .sort((a, b) => a.offset - b.offset); + + let offset = 0; + const highlightedLine: HighlightedLine = []; + for (const field of sortedFields) { + highlightedLine.push({ word: line.substring(offset, field.offset) }); + highlightedLine.push({ + word: field.match, + field: { + type: field.type, + name: field.name, + }, + }); + offset = field.offset + field.length; + } + highlightedLine.push({ word: line.substring(offset) }); + return highlightedLine; + }); + } + + private async testGrokPattern( + lines: string[], + grokPattern: string, + ecsCompatibility: string | undefined + ) { + const { matches } = await this.http.fetch( + '/internal/data_visualizer/test_grok_pattern', + { + method: 'POST', + version: '1', + body: JSON.stringify({ + grokPattern, + text: lines, + ecsCompatibility, + }), + } + ); + return matches; + } +} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/use_text_parser.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/use_text_parser.tsx new file mode 100644 index 0000000000000..183f3ca727d3a --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_contents/use_text_parser.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiText } from '@elastic/eui'; +import type { FindFileStructureResponse } from '@kbn/file-upload-plugin/common'; +import { FieldBadge } from './field_badge'; +import { useDataVisualizerKibana } from '../../../kibana_context'; +import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme'; +import { GrokHighlighter } from './grok_highlighter'; + +export function useGrokHighlighter() { + const { + services: { http }, + } = useDataVisualizerKibana(); + const { euiSizeL } = useCurrentEuiTheme(); + + const createLines = useMemo( + () => + async ( + text: string, + grokPattern: string, + mappings: FindFileStructureResponse['mappings'], + ecsCompatibility: string | undefined, + multilineStartPattern: string, + excludeLinesPattern: string | undefined + ) => { + const grokHighlighter = new GrokHighlighter( + { multilineStartPattern, excludeLinesPattern }, + http + ); + const lines = await grokHighlighter.createLines( + text, + grokPattern, + mappings, + ecsCompatibility + ); + + return lines.map((line) => { + const formattedWords: JSX.Element[] = []; + for (const { word, field } of line) { + if (field) { + formattedWords.push(); + } else { + formattedWords.push({word}); + } + } + return ( + + {formattedWords} + + ); + }); + }, + [euiSizeL, http] + ); + + return createLines; +} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx index 26a727a7a922e..855df5855536a 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/results_view/results_view.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/eui'; import { FindFileStructureResponse } from '@kbn/file-upload-plugin/common'; +import { FILE_FORMATS } from '../../../../../common/constants'; import { FileContents } from '../file_contents'; import { AnalysisSummary } from '../analysis_summary'; import { FieldsStatsGrid } from '../../../common/components/fields_stats_grid'; @@ -48,6 +49,17 @@ export const ResultsView: FC = ({ onCancel, disableImport, }) => { + const semiStructureTextData = + results.format === FILE_FORMATS.SEMI_STRUCTURED_TEXT + ? { + grokPattern: results.grok_pattern, + multilineStartPattern: results.multiline_start_pattern, + sampleStart: results.sample_start, + excludeLinesPattern: results.exclude_lines_pattern, + mappings: results.mappings, + ecsCompatibility: results.ecs_compatibility, + } + : null; return ( @@ -77,6 +89,7 @@ export const ResultsView: FC = ({ data={data} format={results.format} numberOfLines={results.num_lines_analyzed} + semiStructureTextData={semiStructureTextData} /> 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 index 0faa236e30c6f..2f91dce01b456 100644 --- 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 @@ -675,7 +675,7 @@ export const IndexDataVisualizerESQL: FC = (dataVi // Query that has been typed, but has not submitted with cmd + enter const [localQuery, setLocalQuery] = useState({ esql: '' }); - const onQueryUpdate = (q?: AggregateQuery) => { + const onQueryUpdate = async (q?: AggregateQuery) => { // When user submits a new query // resets all current requests and other data if (cancelOverallStatsRequest) { 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 cc17387886071..5d8ebe9e44d57 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 @@ -8,6 +8,7 @@ import { css } from '@emotion/react'; import React, { FC, useEffect, useMemo, useState, useCallback, useRef } from 'react'; import type { Required } from 'utility-types'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; import { useEuiBreakpoint, @@ -21,7 +22,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { type Filter, FilterStateStore, type Query } from '@kbn/es-query'; +import { type Filter, FilterStateStore, type Query, buildEsQuery } from '@kbn/es-query'; import { generateFilters } from '@kbn/data-plugin/public'; import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { usePageUrlState, useUrlState } from '@kbn/ml-url-state'; @@ -62,7 +63,6 @@ import { DocumentCountContent } from '../../../common/components/document_count_ import { OMIT_FIELDS } from '../../../../../common/constants'; import { SearchPanel } from '../search_panel'; import { ActionsPanel } from '../actions_panel'; -import { createMergedEsQuery } from '../../utils/saved_search_utils'; import { DataVisualizerDataViewManagement } from '../data_view_management'; import type { GetAdditionalLinks } from '../../../common/components/results_links'; import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data'; @@ -389,14 +389,14 @@ export const IndexDataVisualizerView: FC = (dataVi language: searchQueryLanguage, }; - const combinedQuery = createMergedEsQuery( + const combinedQuery = buildEsQuery( + currentDataView, { query: searchString || '', language: searchQueryLanguage, }, data.query.filterManager.getFilters() ?? [], - currentDataView, - uiSettings + uiSettings ? getEsQueryConfig(uiSettings) : undefined ); setSearchParams({ diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_bar.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_bar.tsx index 3ad691bbe11ce..d0f6812c4e253 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_bar.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_bar.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import type { Filter, Query, TimeRange } from '@kbn/es-query'; +import { buildEsQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { isDefined } from '@kbn/ml-is-defined'; import { DataView } from '@kbn/data-views-plugin/common'; import type { SearchQueryLanguage } from '@kbn/ml-query-utils'; -import { createMergedEsQuery } from '../../utils/saved_search_utils'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; import { useDataVisualizerKibana } from '../../../kibana_context'; export const SearchPanelContent = ({ @@ -63,16 +63,17 @@ export const SearchPanelContent = ({ const searchHandler = ({ query, filters }: { query?: Query; filters?: Filter[] }) => { const mergedQuery = isDefined(query) ? query : searchInput; const mergedFilters = isDefined(filters) ? filters : queryManager.filterManager.getFilters(); + try { if (mergedFilters) { queryManager.filterManager.setFilters(mergedFilters); } - const combinedQuery = createMergedEsQuery( - mergedQuery, - queryManager.filterManager.getFilters() ?? [], + const combinedQuery = buildEsQuery( dataView, - uiSettings + mergedQuery ? [mergedQuery] : [], + queryManager.filterManager.getFilters() ?? [], + uiSettings ? getEsQueryConfig(uiSettings) : undefined ); setSearchParams({ 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 b012d049ae04f..4570a2019af26 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 @@ -137,10 +137,11 @@ export const useDataVisualizerGridData = ( }); if (searchData === undefined || dataVisualizerListState.searchString !== '') { - if (dataVisualizerListState.filters) { + if (filterManager) { const globalFilters = filterManager?.getGlobalFilters(); - if (filterManager) filterManager.setFilters(dataVisualizerListState.filters); + if (dataVisualizerListState.filters) + filterManager.setFilters(dataVisualizerListState.filters); if (globalFilters) filterManager?.addFilters(globalFilters); } return { @@ -169,6 +170,7 @@ export const useDataVisualizerGridData = ( currentFilters, }), lastRefresh, + data.query.filterManager, ]); const _timeBuckets = useTimeBuckets(); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts index c43483a34e34c..2b25a5e8d2b8c 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { - getQueryFromSavedSearchObject, - createMergedEsQuery, - getEsQueryFromSavedSearch, -} from './saved_search_utils'; +import { getQueryFromSavedSearchObject, getEsQueryFromSavedSearch } from './saved_search_utils'; import type { SavedSearchSavedObject } from '../../../../common/types'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; -import { type Filter, FilterStateStore } from '@kbn/es-query'; +import { FilterStateStore } from '@kbn/es-query'; import { stubbedSavedObjectIndexPattern } from '@kbn/data-views-plugin/common/data_view.stub'; import { DataView } from '@kbn/data-views-plugin/public'; import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; @@ -217,80 +213,6 @@ describe('getQueryFromSavedSearchObject()', () => { }); }); -describe('createMergedEsQuery()', () => { - const luceneQuery = { - query: 'responsetime:>50', - language: 'lucene', - }; - const kqlQuery = { - query: 'responsetime > 49', - language: 'kuery', - }; - const mockFilters: Filter[] = [ - { - meta: { - index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0', - negate: false, - disabled: false, - alias: null, - type: 'phrase', - key: 'airline', - params: { - query: 'ASA', - }, - }, - query: { - match: { - airline: { - query: 'ASA', - type: 'phrase', - }, - }, - }, - $state: { - store: 'appState' as FilterStateStore, - }, - }, - ]; - - it('return formatted ES bool query with both the original query and filters combined', () => { - expect(createMergedEsQuery(luceneQuery, mockFilters)).toEqual({ - bool: { - filter: [{ match_phrase: { airline: { query: 'ASA' } } }], - must: [{ query_string: { query: 'responsetime:>50' } }], - must_not: [], - should: [], - }, - }); - expect(createMergedEsQuery(kqlQuery, mockFilters)).toEqual({ - bool: { - filter: [{ match_phrase: { airline: { query: 'ASA' } } }], - minimum_should_match: 1, - must_not: [], - should: [{ range: { responsetime: { gt: '49' } } }], - }, - }); - }); - it('return formatted ES bool query without filters ', () => { - expect(createMergedEsQuery(luceneQuery)).toEqual({ - bool: { - filter: [], - must: [{ query_string: { query: 'responsetime:>50' } }], - must_not: [], - should: [], - }, - }); - expect(createMergedEsQuery(kqlQuery)).toEqual({ - bool: { - filter: [], - minimum_should_match: 1, - must_not: [], - should: [{ range: { responsetime: { gt: '49' } } }], - }, - }); - }); -}); - describe('getEsQueryFromSavedSearch()', () => { it('return undefined if saved search is not provided', () => { expect( diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts index 04bc52bf08057..3ecc8a3a7a3d8 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts @@ -9,22 +9,15 @@ // `x-pack/plugins/apm/public/components/app/correlations/progress_controls.tsx` import { cloneDeep } from 'lodash'; import { IUiSettingsClient } from '@kbn/core/public'; -import { - fromKueryExpression, - toElasticsearchQuery, - buildQueryFromFilters, - buildEsQuery, - Query, - Filter, - AggregateQuery, -} from '@kbn/es-query'; +import { buildEsQuery, Query, Filter } from '@kbn/es-query'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { DataView } from '@kbn/data-views-plugin/public'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; -import { getEsQueryConfig, isQuery, SearchSource } from '@kbn/data-plugin/common'; +import { getEsQueryConfig, SearchSource } from '@kbn/data-plugin/common'; import { FilterManager, mapAndFlattenFilters } from '@kbn/data-plugin/public'; import { getDefaultDSLQuery } from '@kbn/ml-query-utils'; -import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '@kbn/ml-query-utils'; +import { SearchQueryLanguage } from '@kbn/ml-query-utils'; +import { isDefined } from '@kbn/ml-is-defined'; import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types'; /** @@ -59,53 +52,8 @@ export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObjec return parsed; } -/** - * Create an Elasticsearch query that combines both lucene/kql query string and filters - * Should also form a valid query if only the query or filters is provided - */ -export function createMergedEsQuery( - query?: Query | AggregateQuery | undefined, - filters?: Filter[], - dataView?: DataView, - uiSettings?: IUiSettingsClient -) { - let combinedQuery = getDefaultDSLQuery() as QueryDslQueryContainer; - - if (isQuery(query) && query.language === SEARCH_QUERY_LANGUAGE.KUERY) { - const ast = fromKueryExpression(query.query); - if (query.query !== '') { - combinedQuery = toElasticsearchQuery(ast, dataView); - } - if (combinedQuery.bool !== undefined) { - const filterQuery = buildQueryFromFilters(filters, dataView); - - if (!Array.isArray(combinedQuery.bool.filter)) { - combinedQuery.bool.filter = - combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter]; - } - - if (!Array.isArray(combinedQuery.bool.must_not)) { - combinedQuery.bool.must_not = - combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not]; - } - - combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter]; - combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not]; - } - } else { - combinedQuery = buildEsQuery( - dataView, - query ? [query] : [], - filters ? filters : [], - uiSettings ? getEsQueryConfig(uiSettings) : undefined - ); - } - - return combinedQuery; -} - -function getSavedSearchSource(savedSearch: SavedSearch) { - return savedSearch && +function getSavedSearchSource(savedSearch?: SavedSearch | null) { + return isDefined(savedSearch) && 'searchSource' in savedSearch && savedSearch?.searchSource instanceof SearchSource ? savedSearch.searchSource @@ -131,11 +79,15 @@ export function getEsQueryFromSavedSearch({ filters?: Filter[]; filterManager?: FilterManager; }) { - if (!dataView || !savedSearch) return; + if (!dataView && !savedSearch) return; const userQuery = query; const userFilters = filters; + if (filterManager && userFilters) { + filterManager.addFilters(userFilters); + } + const savedSearchSource = getSavedSearchSource(savedSearch); // If saved search has a search source with nested parent @@ -146,8 +98,8 @@ export function getEsQueryFromSavedSearch({ // Flattened query from search source may contain a clause that narrows the time range // which might interfere with global time pickers so we need to remove const savedQuery = - cloneDeep(savedSearch.searchSource.getSearchRequestBody()?.query) ?? getDefaultDSLQuery(); - const timeField = savedSearch.searchSource.getField('index')?.timeFieldName; + cloneDeep(savedSearchSource.getSearchRequestBody()?.query) ?? getDefaultDSLQuery(); + const timeField = savedSearchSource.getField('index')?.timeFieldName; if (Array.isArray(savedQuery.bool.filter) && timeField !== undefined) { savedQuery.bool.filter = savedQuery.bool.filter.filter( @@ -155,6 +107,7 @@ export function getEsQueryFromSavedSearch({ !(c.hasOwnProperty('range') && c.range?.hasOwnProperty(timeField)) ); } + return { searchQuery: savedQuery, searchString: userQuery.query, @@ -163,39 +116,38 @@ export function getEsQueryFromSavedSearch({ } // If no saved search available, use user's query and filters - if (!savedSearch && userQuery) { - if (filterManager && userFilters) filterManager.addFilters(userFilters); - - const combinedQuery = createMergedEsQuery( - userQuery, - Array.isArray(userFilters) ? userFilters : [], + if ( + !savedSearch && + (userQuery || userFilters || (filterManager && filterManager.getGlobalFilters()?.length > 0)) + ) { + const combinedQuery = buildEsQuery( dataView, - uiSettings + userQuery ?? [], + filterManager?.getFilters() ?? [], + uiSettings ? getEsQueryConfig(uiSettings) : undefined ); return { searchQuery: combinedQuery, - searchString: userQuery.query, - queryLanguage: userQuery.language as SearchQueryLanguage, + searchString: userQuery?.query ?? '', + queryLanguage: (userQuery?.language ?? 'kuery') as SearchQueryLanguage, }; } // If saved search available, merge saved search with the latest user query or filters // which might differ from extracted saved search data if (savedSearchSource) { - const globalFilters = filterManager?.getGlobalFilters(); // FIXME: Add support for AggregateQuery type #150091 const currentQuery = userQuery ?? (savedSearchSource.getField('query') as Query); const currentFilters = userFilters ?? mapAndFlattenFilters(savedSearchSource.getField('filter') as Filter[]); - if (filterManager) filterManager.setFilters(currentFilters); - if (globalFilters) filterManager?.addFilters(globalFilters); + if (filterManager) filterManager.addFilters(currentFilters); - const combinedQuery = createMergedEsQuery( + const combinedQuery = buildEsQuery( + dataView, currentQuery, filterManager ? filterManager?.getFilters() : currentFilters, - dataView, - uiSettings + uiSettings ? getEsQueryConfig(uiSettings) : undefined ); return { diff --git a/x-pack/plugins/data_visualizer/server/index.ts b/x-pack/plugins/data_visualizer/server/index.ts index 1f15b498f8777..17db3c1abb603 100644 --- a/x-pack/plugins/data_visualizer/server/index.ts +++ b/x-pack/plugins/data_visualizer/server/index.ts @@ -9,5 +9,5 @@ import { PluginInitializerContext } from '@kbn/core/server'; export const plugin = async (initializerContext: PluginInitializerContext) => { const { DataVisualizerPlugin } = await import('./plugin'); - return new DataVisualizerPlugin(); + return new DataVisualizerPlugin(initializerContext); }; diff --git a/x-pack/plugins/data_visualizer/server/plugin.ts b/x-pack/plugins/data_visualizer/server/plugin.ts index 5f16df8b5ffb7..0e70f756f9b21 100644 --- a/x-pack/plugins/data_visualizer/server/plugin.ts +++ b/x-pack/plugins/data_visualizer/server/plugin.ts @@ -5,18 +5,30 @@ * 2.0. */ -import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; -import { StartDeps, SetupDeps } from './types'; +import type { + CoreSetup, + CoreStart, + Plugin, + Logger, + PluginInitializerContext, +} from '@kbn/core/server'; +import type { StartDeps, SetupDeps } from './types'; import { registerWithCustomIntegrations } from './register_custom_integration'; +import { routes } from './routes'; export class DataVisualizerPlugin implements Plugin { - constructor() {} + private readonly _logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this._logger = initializerContext.logger.get(); + } setup(coreSetup: CoreSetup, plugins: SetupDeps) { // home-plugin required if (plugins.home && plugins.customIntegrations) { registerWithCustomIntegrations(plugins.customIntegrations); } + routes(coreSetup, this._logger); } start(core: CoreStart) {} diff --git a/x-pack/plugins/data_visualizer/server/routes.ts b/x-pack/plugins/data_visualizer/server/routes.ts new file mode 100644 index 0000000000000..c4e286f9671d1 --- /dev/null +++ b/x-pack/plugins/data_visualizer/server/routes.ts @@ -0,0 +1,69 @@ +/* + * 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 { CoreSetup, Logger } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; +import type { StartDeps } from './types'; +import { wrapError } from './utils/error_wrapper'; +import type { TestGrokPatternResponse } from '../common/types/test_grok_pattern'; + +/** + * @apiGroup DataVisualizer + * + * @api {post} /internal/data_visualizer/test_grok_pattern Tests a grok pattern against a sample of text + * @apiName testGrokPattern + * @apiDescription Tests a grok pattern against a sample of text and return the positions of the fields + */ +export function routes(coreSetup: CoreSetup, logger: Logger) { + const router = coreSetup.http.createRouter(); + + router.versioned + .post({ + path: '/internal/data_visualizer/test_grok_pattern', + access: 'internal', + options: { + tags: ['access:fileUpload:analyzeFile'], + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: schema.object({ + grokPattern: schema.string(), + text: schema.arrayOf(schema.string()), + ecsCompatibility: schema.maybe(schema.string()), + }), + }, + }, + }, + async (context, request, response) => { + try { + const esClient = (await context.core).elasticsearch.client; + const body = await esClient.asInternalUser.transport.request({ + method: 'GET', + path: `/_text_structure/test_grok_pattern`, + body: { + grok_pattern: request.body.grokPattern, + text: request.body.text, + }, + ...(request.body.ecsCompatibility + ? { + querystring: { ecs_compatibility: request.body.ecsCompatibility }, + } + : {}), + }); + + return response.ok({ body }); + } catch (e) { + logger.warn(`Unable to test grok pattern ${e.message}`); + return response.customError(wrapError(e)); + } + } + ); +} diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index af962ac08b6e8..aad960d856da3 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -17,20 +17,28 @@ "@kbn/aiops-utils", "@kbn/charts-plugin", "@kbn/cloud-plugin", + "@kbn/code-editor", + "@kbn/config-schema", "@kbn/core-execution-context-common", + "@kbn/core-notifications-browser", "@kbn/core", "@kbn/custom-integrations-plugin", "@kbn/data-plugin", + "@kbn/data-service", "@kbn/data-view-field-editor-plugin", "@kbn/data-views-plugin", "@kbn/datemath", "@kbn/discover-plugin", + "@kbn/ebt-tools", "@kbn/embeddable-plugin", "@kbn/embeddable-plugin", "@kbn/es-query", + "@kbn/es-types", "@kbn/es-ui-shared-plugin", + "@kbn/esql-utils", "@kbn/field-formats-plugin", "@kbn/field-types", + "@kbn/field-utils", "@kbn/file-upload-plugin", "@kbn/home-plugin", "@kbn/i18n-react", @@ -41,41 +49,34 @@ "@kbn/maps-plugin", "@kbn/ml-agg-utils", "@kbn/ml-cancellable-search", + "@kbn/ml-chi2test", + "@kbn/ml-data-grid", "@kbn/ml-date-picker", + "@kbn/ml-error-utils", + "@kbn/ml-in-memory-table", "@kbn/ml-is-defined", "@kbn/ml-is-populated-object", + "@kbn/ml-kibana-theme", "@kbn/ml-local-storage", "@kbn/ml-nested-property", "@kbn/ml-number-utils", "@kbn/ml-query-utils", + "@kbn/ml-random-sampler-utils", + "@kbn/ml-string-hash", "@kbn/ml-url-state", - "@kbn/ml-data-grid", - "@kbn/ml-error-utils", - "@kbn/ml-kibana-theme", - "@kbn/ml-in-memory-table", "@kbn/react-field", "@kbn/rison", "@kbn/saved-search-plugin", "@kbn/security-plugin", "@kbn/share-plugin", "@kbn/test-jest-helpers", + "@kbn/text-based-languages", "@kbn/ui-actions-plugin", + "@kbn/ui-theme", "@kbn/unified-search-plugin", "@kbn/usage-collection-plugin", "@kbn/utility-types", - "@kbn/ml-string-hash", - "@kbn/ml-random-sampler-utils", - "@kbn/data-service", - "@kbn/core-notifications-browser", - "@kbn/ebt-tools", - "@kbn/ml-chi2test", - "@kbn/field-utils", - "@kbn/visualization-utils", - "@kbn/text-based-languages", - "@kbn/code-editor", - "@kbn/es-types", - "@kbn/ui-theme", - "@kbn/esql-utils" + "@kbn/visualization-utils" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.test.ts b/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.test.ts index 78af0a862d302..93f8152efa19b 100644 --- a/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.test.ts +++ b/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.test.ts @@ -8,6 +8,7 @@ import { MlTrainedModelConfig, MlTrainedModelStats } from '@elastic/elasticsearch/lib/api/types'; import { BUILT_IN_MODEL_TAG, TRAINED_MODEL_TYPE } from '@kbn/ml-trained-models-utils'; +import { MlModel, MlModelDeploymentState } from '../types/ml'; import { MlInferencePipeline, TrainedModelState } from '../types/pipelines'; import { @@ -19,7 +20,7 @@ import { parseModelStateReasonFromStats, } from '.'; -const mockModel: MlTrainedModelConfig = { +const mockTrainedModel: MlTrainedModelConfig = { inference_config: { ner: {}, }, @@ -32,8 +33,27 @@ const mockModel: MlTrainedModelConfig = { version: '1', }; +const mockModel: MlModel = { + modelId: 'model_1', + type: 'ner', + title: 'Model 1', + description: 'Model 1 description', + licenseType: 'elastic', + modelDetailsPageUrl: 'https://my-model.ai', + deploymentState: MlModelDeploymentState.NotDeployed, + startTime: 0, + targetAllocationCount: 0, + nodeAllocationCount: 0, + threadsPerAllocation: 0, + isPlaceholder: false, + hasStats: false, + types: ['pytorch', 'ner'], + inputFieldNames: ['title'], + version: '1', +}; + describe('getMlModelTypesForModelConfig lib function', () => { - const builtInMockModel: MlTrainedModelConfig = { + const builtInMockTrainedModel: MlTrainedModelConfig = { inference_config: { text_classification: {}, }, @@ -47,13 +67,13 @@ describe('getMlModelTypesForModelConfig lib function', () => { it('should return the model type and inference config type', () => { const expected = ['pytorch', 'ner']; - const response = getMlModelTypesForModelConfig(mockModel); + const response = getMlModelTypesForModelConfig(mockTrainedModel); expect(response.sort()).toEqual(expected.sort()); }); it('should include the built in type', () => { const expected = ['lang_ident', 'text_classification', BUILT_IN_MODEL_TAG]; - const response = getMlModelTypesForModelConfig(builtInMockModel); + const response = getMlModelTypesForModelConfig(builtInMockTrainedModel); expect(response.sort()).toEqual(expected.sort()); }); }); @@ -71,9 +91,9 @@ describe('generateMlInferencePipelineBody lib function', () => { { inference: { field_map: { - 'my-source-field': 'MODEL_INPUT_FIELD', + 'my-source-field': 'title', }, - model_id: 'test_id', + model_id: 'model_1', on_failure: [ { append: { @@ -154,21 +174,21 @@ describe('generateMlInferencePipelineBody lib function', () => { { inference: expect.objectContaining({ field_map: { - 'my-source-field1': 'MODEL_INPUT_FIELD', + 'my-source-field1': 'title', }, }), }, { inference: expect.objectContaining({ field_map: { - 'my-source-field2': 'MODEL_INPUT_FIELD', + 'my-source-field2': 'title', }, }), }, { inference: expect.objectContaining({ field_map: { - 'my-source-field3': 'MODEL_INPUT_FIELD', + 'my-source-field3': 'title', }, }), }, diff --git a/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.ts b/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.ts index 5f56c1105b297..fa16dd29f83b1 100644 --- a/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.ts +++ b/x-pack/plugins/enterprise_search/common/ml_inference_pipeline/index.ts @@ -18,6 +18,8 @@ import { BUILT_IN_MODEL_TAG, } from '@kbn/ml-trained-models-utils'; +import { MlModel } from '../types/ml'; + import { MlInferencePipeline, CreateMLInferencePipeline, @@ -33,7 +35,7 @@ export interface MlInferencePipelineParams { description?: string; fieldMappings: FieldMapping[]; inferenceConfig?: InferencePipelineInferenceConfig; - model: MlTrainedModelConfig; + model: MlModel; pipelineName: string; } @@ -90,7 +92,7 @@ export const generateMlInferencePipelineBody = ({ model_version: model.version, pipeline: pipelineName, processed_timestamp: '{{{ _ingest.timestamp }}}', - types: getMlModelTypesForModelConfig(model), + types: model.types, }, ], }, @@ -104,19 +106,19 @@ export const getInferenceProcessor = ( sourceField: string, targetField: string, inferenceConfig: InferencePipelineInferenceConfig | undefined, - model: MlTrainedModelConfig, + model: MlModel, pipelineName: string ): IngestInferenceProcessor => { // If model returned no input field, insert a placeholder const modelInputField = - model.input?.field_names?.length > 0 ? model.input.field_names[0] : 'MODEL_INPUT_FIELD'; + model.inputFieldNames.length > 0 ? model.inputFieldNames[0] : 'MODEL_INPUT_FIELD'; return { field_map: { [sourceField]: modelInputField, }, inference_config: inferenceConfig, - model_id: model.model_id, + model_id: model.modelId, on_failure: [ { append: { diff --git a/x-pack/plugins/enterprise_search/common/types/kibana_deps.ts b/x-pack/plugins/enterprise_search/common/types/kibana_deps.ts index 2379692abb736..4ab0cfae0932d 100644 --- a/x-pack/plugins/enterprise_search/common/types/kibana_deps.ts +++ b/x-pack/plugins/enterprise_search/common/types/kibana_deps.ts @@ -7,6 +7,7 @@ import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; import type { CloudStart } from '@kbn/cloud-plugin/public'; +import type { ConsolePluginStart } from '@kbn/console-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DiscoverStart } from '@kbn/discover-plugin/public'; import type { FeaturesPluginStart } from '@kbn/features-plugin/public'; @@ -19,6 +20,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; export interface KibanaDeps { charts: ChartsPluginStart; cloud: CloudStart; + console?: ConsolePluginStart; data: DataPublicPluginStart; discover: DiscoverStart; features: FeaturesPluginStart; diff --git a/x-pack/plugins/enterprise_search/common/types/ml.ts b/x-pack/plugins/enterprise_search/common/types/ml.ts index 894ffa6f0726b..2f40475535107 100644 --- a/x-pack/plugins/enterprise_search/common/types/ml.ts +++ b/x-pack/plugins/enterprise_search/common/types/ml.ts @@ -27,6 +27,10 @@ export interface MlModel { modelId: string; /** Model inference type, e.g. ner, text_classification */ type: string; + /** Type-related tags: model type (e.g. pytorch), inference type, built-in tag */ + types: string[]; + /** Field names in inference input configuration */ + inputFieldNames: string[]; title: string; description?: string; licenseType?: string; @@ -44,4 +48,5 @@ export interface MlModel { isPlaceholder: boolean; /** Does this model have deployment stats? */ hasStats: boolean; + version?: string; } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_api_key_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_api_key_api_logic.test.ts index 524bc70e9b279..55161f5912cfe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_api_key_api_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_api_key_api_logic.test.ts @@ -11,19 +11,43 @@ import { nextTick } from '@kbn/test-jest-helpers'; import { generateApiKey } from './generate_connector_api_key_api_logic'; +jest.mock('@kbn/search-connectors', () => ({ + createConnectorSecret: jest.fn(), + updateConnectorSecret: jest.fn(), +})); + describe('generateConnectorApiKeyApiLogic', () => { const { http } = mockHttpValues; beforeEach(() => { jest.clearAllMocks(); }); - describe('generateApiKey', () => { + describe('generateApiKey for connector clients', () => { + it('calls correct api', async () => { + const promise = Promise.resolve('result'); + http.post.mockReturnValue(promise); + const result = generateApiKey({ indexName: 'indexName', isNative: false, secretId: null }); + await nextTick(); + expect(http.post).toHaveBeenCalledWith( + '/internal/enterprise_search/indices/indexName/api_key', + { + body: '{"is_native":false,"secret_id":null}', + } + ); + await expect(result).resolves.toEqual('result'); + }); + }); + + describe('generateApiKey for native connectors', () => { it('calls correct api', async () => { const promise = Promise.resolve('result'); http.post.mockReturnValue(promise); - const result = generateApiKey({ indexName: 'indexName' }); + const result = generateApiKey({ indexName: 'indexName', isNative: true, secretId: '1234' }); await nextTick(); expect(http.post).toHaveBeenCalledWith( - '/internal/enterprise_search/indices/indexName/api_key' + '/internal/enterprise_search/indices/indexName/api_key', + { + body: '{"is_native":true,"secret_id":"1234"}', + } ); await expect(result).resolves.toEqual('result'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_api_key_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_api_key_api_logic.ts index ace963d9208be..ebca9c99add0d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_api_key_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_api_key_api_logic.ts @@ -15,9 +15,23 @@ export interface ApiKey { name: string; } -export const generateApiKey = async ({ indexName }: { indexName: string }) => { +export const generateApiKey = async ({ + indexName, + isNative, + secretId, +}: { + indexName: string; + isNative: boolean; + secretId: string | null; +}) => { const route = `/internal/enterprise_search/indices/${indexName}/api_key`; - return await HttpLogic.values.http.post(route); + const params = { + is_native: isNative, + secret_id: secretId, + }; + return await HttpLogic.values.http.post(route, { + body: JSON.stringify(params), + }); }; export const GenerateConnectorApiKeyApiLogic = createApiLogic( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.test.ts index 6d66ed5704721..869bd9273ac09 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.test.ts @@ -30,8 +30,11 @@ const DEFAULT_VALUES: CachedFetchModelsApiLogicValues = { const FETCH_MODELS_API_DATA_RESPONSE: MlModel[] = [ { modelId: 'model_1', - title: 'Model 1', type: 'ner', + title: 'Model 1', + description: 'Model 1 description', + licenseType: 'elastic', + modelDetailsPageUrl: 'https://my-model.ai', deploymentState: MlModelDeploymentState.NotDeployed, startTime: 0, targetAllocationCount: 0, @@ -39,6 +42,9 @@ const FETCH_MODELS_API_DATA_RESPONSE: MlModel[] = [ threadsPerAllocation: 0, isPlaceholder: false, hasStats: false, + types: ['pytorch', 'ner'], + inputFieldNames: ['title'], + version: '1', }, ]; const FETCH_MODELS_API_ERROR_RESPONSE = { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.ts index d65af6ec2fcf4..a26dbada96c08 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/cached_fetch_models_api_logic.ts @@ -18,6 +18,8 @@ import { FetchModelsApiLogic, FetchModelsApiResponse } from './fetch_models_api_ const FETCH_MODELS_POLLING_DURATION = 5000; // 5 seconds const FETCH_MODELS_POLLING_DURATION_ON_FAILURE = 30000; // 30 seconds +export type { FetchModelsApiResponse } from './fetch_models_api_logic'; + export interface CachedFetchModlesApiLogicActions { apiError: Actions<{}, FetchModelsApiResponse>['apiError']; apiReset: Actions<{}, FetchModelsApiResponse>['apiReset']; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.test.ts deleted file mode 100644 index 4bcea4ac4e83a..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.test.ts +++ /dev/null @@ -1,25 +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 { mockHttpValues } from '../../../__mocks__/kea_logic'; -import { mlModelStats } from '../../__mocks__/ml_models.mock'; - -import { getMLModelsStats } from './ml_model_stats_logic'; - -describe('MLModelsApiLogic', () => { - const { http } = mockHttpValues; - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('getMLModelsStats', () => { - it('calls the ml api', async () => { - http.get.mockResolvedValue(mlModelStats); - const result = await getMLModelsStats(); - expect(http.get).toHaveBeenCalledWith('/internal/ml/trained_models/_stats', { version: '1' }); - expect(result).toEqual(mlModelStats); - }); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.ts deleted file mode 100644 index d8bc341fcb6c3..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_model_stats_logic.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { MlTrainedModelStats } from '@elastic/elasticsearch/lib/api/types'; - -import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic'; -import { HttpLogic } from '../../../shared/http'; - -export type GetMlModelsStatsArgs = undefined; - -export interface GetMlModelsStatsResponse { - count: number; - trained_model_stats: MlTrainedModelStats[]; -} - -export const getMLModelsStats = async () => { - return await HttpLogic.values.http.get( - '/internal/ml/trained_models/_stats', - { version: '1' } - ); -}; - -export const MLModelsStatsApiLogic = createApiLogic( - ['ml_models_stats_api_logic'], - getMLModelsStats, - { - clearFlashMessagesOnMakeRequest: false, - showErrorFlash: false, - } -); - -export type MLModelsStatsApiLogicActions = Actions; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.test.ts deleted file mode 100644 index cffdc08dfd2ef..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { mockHttpValues } from '../../../__mocks__/kea_logic'; -import { mlModels } from '../../__mocks__/ml_models.mock'; - -import { getMLModels } from './ml_models_logic'; - -describe('MLModelsApiLogic', () => { - const { http } = mockHttpValues; - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('getMLModels', () => { - it('calls the ml api', async () => { - http.get.mockResolvedValue(mlModels); - const result = await getMLModels(); - expect(http.get).toHaveBeenCalledWith('/internal/ml/trained_models', { - query: { size: 1000, with_pipelines: true }, - version: '1', - }); - expect(result).toEqual(mlModels); - }); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.ts deleted file mode 100644 index 9cec020b8d782..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_models_logic.ts +++ /dev/null @@ -1,31 +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 { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models'; - -import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic'; -import { HttpLogic } from '../../../shared/http'; - -export type GetMlModelsArgs = number | undefined; - -export type GetMlModelsResponse = TrainedModelConfigResponse[]; - -export const getMLModels = async (size: GetMlModelsArgs = 1000) => { - return await HttpLogic.values.http.get( - '/internal/ml/trained_models', - { - query: { size, with_pipelines: true }, - version: '1', - } - ); -}; - -export const MLModelsApiLogic = createApiLogic(['ml_models_api_logic'], getMLModels, { - clearFlashMessagesOnMakeRequest: false, - showErrorFlash: false, -}); - -export type MLModelsApiLogicActions = Actions; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.test.ts deleted file mode 100644 index 6a1a4e0e512df..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.test.ts +++ /dev/null @@ -1,177 +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 { LogicMounter } from '../../../__mocks__/kea_logic'; -import { mlModels, mlModelStats } from '../../__mocks__/ml_models.mock'; - -import { HttpError, Status } from '../../../../../common/types/api'; - -import { MLModelsStatsApiLogic } from './ml_model_stats_logic'; -import { MLModelsApiLogic } from './ml_models_logic'; -import { TrainedModelsApiLogic, TrainedModelsApiLogicValues } from './ml_trained_models_logic'; - -const DEFAULT_VALUES: TrainedModelsApiLogicValues = { - error: null, - status: Status.IDLE, - data: null, - // models - modelsApiStatus: { - status: Status.IDLE, - }, - modelsData: undefined, - modelsApiError: undefined, - modelsStatus: Status.IDLE, - // stats - modelStatsApiStatus: { - status: Status.IDLE, - }, - modelStatsData: undefined, - modelsStatsApiError: undefined, - modelStatsStatus: Status.IDLE, -}; - -describe('TrainedModelsApiLogic', () => { - const { mount } = new LogicMounter(TrainedModelsApiLogic); - const { mount: mountMLModelsApiLogic } = new LogicMounter(MLModelsApiLogic); - const { mount: mountMLModelsStatsApiLogic } = new LogicMounter(MLModelsStatsApiLogic); - - beforeEach(() => { - jest.clearAllMocks(); - - mountMLModelsApiLogic(); - mountMLModelsStatsApiLogic(); - mount(); - }); - - it('has default values', () => { - expect(TrainedModelsApiLogic.values).toEqual(DEFAULT_VALUES); - }); - describe('selectors', () => { - describe('data', () => { - it('returns combined trained models', () => { - MLModelsApiLogic.actions.apiSuccess(mlModels); - MLModelsStatsApiLogic.actions.apiSuccess(mlModelStats); - - expect(TrainedModelsApiLogic.values.data).toEqual([ - { - ...mlModels[0], - ...mlModelStats.trained_model_stats[0], - }, - { - ...mlModels[1], - ...mlModelStats.trained_model_stats[1], - }, - { - ...mlModels[2], - ...mlModelStats.trained_model_stats[2], - }, - ]); - }); - it('returns just models if stats not available', () => { - MLModelsApiLogic.actions.apiSuccess(mlModels); - - expect(TrainedModelsApiLogic.values.data).toEqual(mlModels); - }); - it('returns null trained models even with stats if models missing', () => { - MLModelsStatsApiLogic.actions.apiSuccess(mlModelStats); - - expect(TrainedModelsApiLogic.values.data).toEqual(null); - }); - }); - describe('error', () => { - const modelError: HttpError = { - body: { - error: 'Model Error', - statusCode: 400, - }, - fetchOptions: {}, - request: {}, - } as HttpError; - const statsError: HttpError = { - body: { - error: 'Stats Error', - statusCode: 500, - }, - fetchOptions: {}, - request: {}, - } as HttpError; - - it('returns null with no errors', () => { - MLModelsApiLogic.actions.apiSuccess(mlModels); - MLModelsStatsApiLogic.actions.apiSuccess(mlModelStats); - - expect(TrainedModelsApiLogic.values.error).toBeNull(); - }); - it('models error', () => { - MLModelsApiLogic.actions.apiError(modelError); - - expect(TrainedModelsApiLogic.values.error).toBe(modelError); - }); - it('stats error', () => { - MLModelsStatsApiLogic.actions.apiError(statsError); - - expect(TrainedModelsApiLogic.values.error).toBe(statsError); - }); - it('prefers models error if both api calls fail', () => { - MLModelsApiLogic.actions.apiError(modelError); - MLModelsStatsApiLogic.actions.apiError(statsError); - - expect(TrainedModelsApiLogic.values.error).toBe(modelError); - }); - }); - describe('status', () => { - it('returns matching status for both calls', () => { - MLModelsApiLogic.actions.apiSuccess(mlModels); - MLModelsStatsApiLogic.actions.apiSuccess(mlModelStats); - - expect(TrainedModelsApiLogic.values.status).toEqual(Status.SUCCESS); - }); - it('returns models status when its lower', () => { - MLModelsStatsApiLogic.actions.apiSuccess(mlModelStats); - - expect(TrainedModelsApiLogic.values.status).toEqual(Status.IDLE); - }); - it('returns stats status when its lower', () => { - MLModelsApiLogic.actions.apiSuccess(mlModels); - - expect(TrainedModelsApiLogic.values.status).toEqual(Status.IDLE); - }); - it('returns error status if one api call fails', () => { - MLModelsApiLogic.actions.apiSuccess(mlModels); - MLModelsStatsApiLogic.actions.apiError({ - body: { - error: 'Stats Error', - statusCode: 500, - }, - fetchOptions: {}, - request: {}, - } as HttpError); - - expect(TrainedModelsApiLogic.values.status).toEqual(Status.ERROR); - }); - }); - }); - describe('actions', () => { - it('makeRequest fetches models and stats', () => { - jest.spyOn(TrainedModelsApiLogic.actions, 'makeGetModelsRequest'); - jest.spyOn(TrainedModelsApiLogic.actions, 'makeGetModelsStatsRequest'); - - TrainedModelsApiLogic.actions.makeRequest(undefined); - - expect(TrainedModelsApiLogic.actions.makeGetModelsRequest).toHaveBeenCalledTimes(1); - expect(TrainedModelsApiLogic.actions.makeGetModelsStatsRequest).toHaveBeenCalledTimes(1); - }); - it('apiReset resets both api logics', () => { - jest.spyOn(TrainedModelsApiLogic.actions, 'getModelsApiReset'); - jest.spyOn(TrainedModelsApiLogic.actions, 'getModelsStatsApiReset'); - - TrainedModelsApiLogic.actions.apiReset(); - - expect(TrainedModelsApiLogic.actions.getModelsApiReset).toHaveBeenCalledTimes(1); - expect(TrainedModelsApiLogic.actions.getModelsStatsApiReset).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.ts deleted file mode 100644 index d36a80df6af6a..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/ml_trained_models_logic.ts +++ /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 { kea, MakeLogicType } from 'kea'; - -import { MlTrainedModelStats } from '@elastic/elasticsearch/lib/api/types'; -import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models'; - -import { ApiStatus, Status, HttpError } from '../../../../../common/types/api'; -import { Actions } from '../../../shared/api_logic/create_api_logic'; - -import { - GetMlModelsStatsResponse, - MLModelsStatsApiLogic, - MLModelsStatsApiLogicActions, -} from './ml_model_stats_logic'; -import { GetMlModelsResponse, MLModelsApiLogic, MLModelsApiLogicActions } from './ml_models_logic'; - -export type TrainedModel = TrainedModelConfigResponse & Partial; - -export type TrainedModelsApiLogicActions = Actions & { - getModelsApiError: MLModelsApiLogicActions['apiError']; - getModelsApiReset: MLModelsApiLogicActions['apiReset']; - getModelsApiSuccess: MLModelsApiLogicActions['apiSuccess']; - getModelsStatsApiError: MLModelsStatsApiLogicActions['apiError']; - getModelsStatsApiReset: MLModelsStatsApiLogicActions['apiReset']; - getModelsStatsApiSuccess: MLModelsStatsApiLogicActions['apiSuccess']; - makeGetModelsRequest: MLModelsApiLogicActions['makeRequest']; - makeGetModelsStatsRequest: MLModelsStatsApiLogicActions['makeRequest']; -}; -export interface TrainedModelsApiLogicValues { - error: HttpError | null; - status: Status; - data: TrainedModel[] | null; - // models - modelsApiStatus: ApiStatus; - modelsData: GetMlModelsResponse | undefined; - modelsApiError?: HttpError; - modelsStatus: Status; - // stats - modelStatsApiStatus: ApiStatus; - modelStatsData: GetMlModelsStatsResponse | undefined; - modelsStatsApiError?: HttpError; - modelStatsStatus: Status; -} - -export const TrainedModelsApiLogic = kea< - MakeLogicType ->({ - actions: { - apiError: (error) => error, - apiReset: true, - apiSuccess: (result) => result, - makeRequest: () => undefined, - }, - connect: { - actions: [ - MLModelsApiLogic, - [ - 'apiError as getModelsApiError', - 'apiReset as getModelsApiReset', - 'apiSuccess as getModelsApiSuccess', - 'makeRequest as makeGetModelsRequest', - ], - MLModelsStatsApiLogic, - [ - 'apiError as getModelsStatsApiError', - 'apiReset as getModelsStatsApiReset', - 'apiSuccess as getModelsStatsApiSuccess', - 'makeRequest as makeGetModelsStatsRequest', - ], - ], - values: [ - MLModelsApiLogic, - [ - 'apiStatus as modelsApiStatus', - 'error as modelsApiError', - 'status as modelsStatus', - 'data as modelsData', - ], - MLModelsStatsApiLogic, - [ - 'apiStatus as modelStatsApiStatus', - 'error as modelsStatsApiError', - 'status as modelStatsStatus', - 'data as modelStatsData', - ], - ], - }, - listeners: ({ actions, values }) => ({ - getModelsApiError: (error) => { - actions.apiError(error); - }, - getModelsApiSuccess: () => { - if (!values.data) return; - actions.apiSuccess(values.data); - }, - getModelsStatsApiError: (error) => { - if (values.modelsApiError) return; - actions.apiError(error); - }, - getModelsStatsApiSuccess: () => { - if (!values.data) return; - actions.apiSuccess(values.data); - }, - apiReset: () => { - actions.getModelsApiReset(); - actions.getModelsStatsApiReset(); - }, - makeRequest: () => { - actions.makeGetModelsRequest(undefined); - actions.makeGetModelsStatsRequest(undefined); - }, - }), - path: ['enterprise_search', 'api', 'ml_trained_models_api_logic'], - selectors: ({ selectors }) => ({ - data: [ - () => [selectors.modelsData, selectors.modelStatsData], - ( - modelsData: TrainedModelsApiLogicValues['modelsData'], - modelStatsData: TrainedModelsApiLogicValues['modelStatsData'] - ): TrainedModel[] | null => { - if (!modelsData) return null; - if (!modelStatsData) return modelsData; - const statsMap: Record = - modelStatsData.trained_model_stats.reduce((map, value) => { - if (value.model_id) { - map[value.model_id] = value; - } - return map; - }, {} as Record); - return modelsData.map((modelConfig) => { - const modelStats = statsMap[modelConfig.model_id]; - return { - ...modelConfig, - ...(modelStats ?? {}), - }; - }); - }, - ], - error: [ - () => [selectors.modelsApiStatus, selectors.modelStatsApiStatus], - ( - modelsApiStatus: TrainedModelsApiLogicValues['modelsApiStatus'], - modelStatsApiStatus: TrainedModelsApiLogicValues['modelStatsApiStatus'] - ) => { - if (modelsApiStatus.error) return modelsApiStatus.error; - if (modelStatsApiStatus.error) return modelStatsApiStatus.error; - return null; - }, - ], - status: [ - () => [selectors.modelsApiStatus, selectors.modelStatsApiStatus], - ( - modelsApiStatus: TrainedModelsApiLogicValues['modelsApiStatus'], - modelStatsApiStatus: TrainedModelsApiLogicValues['modelStatsApiStatus'] - ) => { - if (modelsApiStatus.status === modelStatsApiStatus.status) return modelsApiStatus.status; - if (modelsApiStatus.status === Status.ERROR || modelStatsApiStatus.status === Status.ERROR) - return Status.ERROR; - if (modelsApiStatus.status < modelStatsApiStatus.status) return modelsApiStatus.status; - return modelStatsApiStatus.status; - }, - ], - }), -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/api_key_configuration.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/api_key_configuration.tsx index fdc36863f4925..b2233bb4c9ee1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/api_key_configuration.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/api_key_configuration.tsx @@ -61,10 +61,12 @@ const ConfirmModal: React.FC<{ ); -export const ApiKeyConfig: React.FC<{ hasApiKey: boolean; indexName: string }> = ({ - hasApiKey, - indexName, -}) => { +export const ApiKeyConfig: React.FC<{ + hasApiKey: boolean; + indexName: string; + isNative: boolean; + secretId: string | null; +}> = ({ hasApiKey, indexName, isNative, secretId }) => { const { makeRequest, apiReset } = useActions(GenerateConnectorApiKeyApiLogic); const { data, status } = useValues(GenerateConnectorApiKeyApiLogic); useEffect(() => { @@ -76,7 +78,7 @@ export const ApiKeyConfig: React.FC<{ hasApiKey: boolean; indexName: string }> = if (hasApiKey || data) { setIsModalVisible(true); } else { - makeRequest({ indexName }); + makeRequest({ indexName, isNative, secretId }); } }; @@ -87,7 +89,7 @@ export const ApiKeyConfig: React.FC<{ hasApiKey: boolean; indexName: string }> = }; const onConfirm = () => { - makeRequest({ indexName }); + makeRequest({ indexName, isNative, secretId }); setIsModalVisible(false); }; @@ -96,17 +98,28 @@ export const ApiKeyConfig: React.FC<{ hasApiKey: boolean; indexName: string }> = {isModalVisible && } - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.description', - { - defaultMessage: - 'First, generate an Elasticsearch API key. This {apiKeyName} key will enable read and write permissions for the connector to index documents to the created {indexName} index. Save the key in a safe place, as you will need it to configure your connector.', - values: { - apiKeyName: `${indexName}-connector`, - indexName, - }, - } - )} + {isNative + ? i18n.translate( + 'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.apiKey.description', + { + defaultMessage: `This native connector's API key {apiKeyName} is managed internally by Elasticsearch. The connector uses this API key to index documents into the {indexName} index. To rollover your API key, click "Generate API key".`, + values: { + apiKeyName: `${indexName}-connector`, + indexName, + }, + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.description', + { + defaultMessage: + 'First, generate an Elasticsearch API key. This {apiKeyName} key will enable read and write permissions for the connector to index documents to the created {indexName} index. Save the key in a safe place, as you will need it to configure your connector.', + values: { + apiKeyName: `${indexName}-connector`, + indexName, + }, + } + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration.tsx index e338b7d1f193b..748815d4421b6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration.tsx @@ -95,7 +95,12 @@ export const ConnectorConfiguration: React.FC = () => { steps={[ { children: ( - + ), status: hasApiKey ? 'complete' : 'incomplete', title: i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/native_connector_configuration/native_connector_configuration.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/native_connector_configuration/native_connector_configuration.tsx index fce32710a580d..3457e3c709fca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/native_connector_configuration/native_connector_configuration.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/native_connector_configuration/native_connector_configuration.tsx @@ -30,9 +30,11 @@ import { HttpLogic } from '../../../../../shared/http'; import { CONNECTOR_ICONS } from '../../../../../shared/icons/connector_icons'; import { KibanaLogic } from '../../../../../shared/kibana'; +import { GenerateConnectorApiKeyApiLogic } from '../../../../api/connector/generate_connector_api_key_api_logic'; import { hasConfiguredConfiguration } from '../../../../utils/has_configured_configuration'; import { isConnectorIndex } from '../../../../utils/indices'; import { IndexViewLogic } from '../../index_view_logic'; +import { ApiKeyConfig } from '../api_key_configuration'; import { ConnectorNameAndDescription } from '../connector_name_and_description/connector_name_and_description'; import { BETA_CONNECTORS, NATIVE_CONNECTORS } from '../constants'; @@ -45,6 +47,7 @@ export const NativeConnectorConfiguration: React.FC = () => { const { index } = useValues(IndexViewLogic); const { config } = useValues(KibanaLogic); const { errorConnectingMessage } = useValues(HttpLogic); + const { data: apiKeyData } = useValues(GenerateConnectorApiKeyApiLogic); if (!isConnectorIndex(index)) { return <>; @@ -74,6 +77,8 @@ export const NativeConnectorConfiguration: React.FC = () => { const hasResearched = hasDescription || hasConfigured || hasConfiguredAdvanced; const icon = nativeConnector.icon; + const hasApiKey = !!(index.connector.api_key_id ?? apiKeyData); + // TODO service_type === "" is considered unknown/custom connector multipleplaces replace all of them with a better solution const isBeta = !index.connector.service_type || @@ -140,6 +145,24 @@ export const NativeConnectorConfiguration: React.FC = () => { ), titleSize: 'xs', }, + { + children: ( + + ), + status: hasApiKey ? 'complete' : 'incomplete', + title: i18n.translate( + 'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.steps.regenerateApiKeyTitle', + { + defaultMessage: 'Regenerate API key', + } + ), + titleSize: 'xs', + }, { children: , status: hasDescription ? 'complete' : 'incomplete', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.test.tsx index 68bf7fc48e7dd..09789e34c963b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.test.tsx @@ -29,7 +29,6 @@ import { import { ConfigureFields } from './configure_fields'; import { ConfigurePipeline } from './configure_pipeline'; import { EMPTY_PIPELINE_CONFIGURATION } from './ml_inference_logic'; -import { NoModelsPanel } from './no_models'; import { ReviewPipeline } from './review_pipeline'; import { TestPipeline } from './test_pipeline'; import { AddInferencePipelineSteps } from './types'; @@ -82,11 +81,6 @@ describe('AddInferencePipelineFlyout', () => { const wrapper = shallow(); expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); }); - it('renders no models panel when there are no models', () => { - setMockValues({ ...DEFAULT_VALUES, supportedMLModels: [] }); - const wrapper = shallow(); - expect(wrapper.find(NoModelsPanel)).toHaveLength(1); - }); it('renders AddInferencePipelineHorizontalSteps', () => { const wrapper = shallow(); expect(wrapper.find(AddInferencePipelineHorizontalSteps)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.tsx index 654cdafad35eb..57aa7ab467488 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/add_inference_pipeline_flyout.tsx @@ -41,7 +41,6 @@ import { IndexViewLogic } from '../../index_view_logic'; import { ConfigureFields } from './configure_fields'; import { ConfigurePipeline } from './configure_pipeline'; import { MLInferenceLogic } from './ml_inference_logic'; -import { NoModelsPanel } from './no_models'; import { ReviewPipeline } from './review_pipeline'; import { TestPipeline } from './test_pipeline'; import { AddInferencePipelineSteps } from './types'; @@ -54,9 +53,15 @@ export interface AddInferencePipelineFlyoutProps { export const AddInferencePipelineFlyout = (props: AddInferencePipelineFlyoutProps) => { const { indexName } = useValues(IndexNameLogic); - const { setIndexName } = useActions(MLInferenceLogic); + const { setIndexName, makeMlInferencePipelinesRequest, startPollingModels, makeMappingRequest } = + useActions(MLInferenceLogic); useEffect(() => { setIndexName(indexName); + + // Trigger fetching of initial data: existing ML pipelines, available models, index mapping + makeMlInferencePipelinesRequest(undefined); + startPollingModels(); + makeMappingRequest({ indexName }); }, [indexName]); return ( @@ -82,7 +87,6 @@ export const AddInferencePipelineContent = ({ onClose }: AddInferencePipelineFly const { ingestionMethod } = useValues(IndexViewLogic); const { createErrors, - supportedMLModels, isLoading, addInferencePipelineModal: { step }, } = useValues(MLInferenceLogic); @@ -103,9 +107,6 @@ export const AddInferencePipelineContent = ({ onClose }: AddInferencePipelineFly ); } - if (supportedMLModels.length === 0) { - return ; - } return ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/inference_config.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/inference_config.tsx index 7b3fdf4353d99..244f7a3e8a200 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/inference_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/inference_config.tsx @@ -14,7 +14,6 @@ import { i18n } from '@kbn/i18n'; import { SUPPORTED_PYTORCH_TASKS } from '@kbn/ml-trained-models-utils'; -import { getMlModelTypesForModelConfig } from '../../../../../../../common/ml_inference_pipeline'; import { getMLType } from '../../../shared/ml_inference/utils'; import { MLInferenceLogic } from './ml_inference_logic'; @@ -23,10 +22,10 @@ import { ZeroShotClassificationInferenceConfiguration } from './zero_shot_infere export const InferenceConfiguration: React.FC = () => { const { addInferencePipelineModal: { configuration }, - selectedMLModel, + selectedModel, } = useValues(MLInferenceLogic); - if (!selectedMLModel || configuration.existingPipeline) return null; - const modelType = getMLType(getMlModelTypesForModelConfig(selectedMLModel)); + if (!selectedModel || configuration.existingPipeline) return null; + const modelType = getMLType(selectedModel.types); switch (modelType) { case SUPPORTED_PYTORCH_TASKS.ZERO_SHOT_CLASSIFICATION: return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts index e12366f42f3ef..e067b89a7887e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts @@ -6,16 +6,15 @@ */ import { LogicMounter } from '../../../../../__mocks__/kea_logic'; -import { nerModel, textExpansionModel } from '../../../../__mocks__/ml_models.mock'; import { HttpResponse } from '@kbn/core/public'; -import { ErrorResponse } from '../../../../../../../common/types/api'; -import { TrainedModelState } from '../../../../../../../common/types/pipelines'; +import { ErrorResponse, Status } from '../../../../../../../common/types/api'; +import { MlModel, MlModelDeploymentState } from '../../../../../../../common/types/ml'; import { GetDocumentsApiLogic } from '../../../../api/documents/get_document_logic'; import { MappingsApiLogic } from '../../../../api/mappings/mappings_logic'; -import { MLModelsApiLogic } from '../../../../api/ml_models/ml_models_logic'; +import { CachedFetchModelsApiLogic } from '../../../../api/ml_models/cached_fetch_models_api_logic'; import { StartTextExpansionModelApiLogic } from '../../../../api/ml_models/text_expansion/start_text_expansion_model_api_logic'; import { AttachMlInferencePipelineApiLogic } from '../../../../api/pipelines/attach_ml_inference_pipeline'; import { CreateMlInferencePipelineApiLogic } from '../../../../api/pipelines/create_ml_inference_pipeline'; @@ -41,7 +40,6 @@ const DEFAULT_VALUES: MLInferenceProcessorsValues = { }, createErrors: [], existingPipeline: undefined, - existingInferencePipelines: [], formErrors: { fieldMappings: 'Field is required.', modelID: 'Field is required.', @@ -50,6 +48,7 @@ const DEFAULT_VALUES: MLInferenceProcessorsValues = { index: null, isConfigureStepValid: false, isLoading: true, + isModelsInitialLoading: false, isPipelineDataValid: false, isTextExpansionModelSelected: false, mappingData: undefined, @@ -57,17 +56,38 @@ const DEFAULT_VALUES: MLInferenceProcessorsValues = { mlInferencePipeline: undefined, mlInferencePipelineProcessors: undefined, mlInferencePipelinesData: undefined, - mlModelsData: null, - mlModelsStatus: 0, - selectedMLModel: null, + modelsData: undefined, + modelsStatus: 0, + selectableModels: [], + selectedModel: undefined, sourceFields: undefined, - supportedMLModels: [], }; +const MODELS: MlModel[] = [ + { + modelId: 'model_1', + type: 'ner', + title: 'Model 1', + description: 'Model 1 description', + licenseType: 'elastic', + modelDetailsPageUrl: 'https://my-model.ai', + deploymentState: MlModelDeploymentState.NotDeployed, + startTime: 0, + targetAllocationCount: 0, + nodeAllocationCount: 0, + threadsPerAllocation: 0, + isPlaceholder: false, + hasStats: false, + types: ['pytorch', 'ner'], + inputFieldNames: ['title'], + version: '1', + }, +]; + describe('MlInferenceLogic', () => { const { mount } = new LogicMounter(MLInferenceLogic); const { mount: mountMappingApiLogic } = new LogicMounter(MappingsApiLogic); - const { mount: mountMLModelsApiLogic } = new LogicMounter(MLModelsApiLogic); + const { mount: mountCachedFetchModelsApiLogic } = new LogicMounter(CachedFetchModelsApiLogic); const { mount: mountSimulateExistingMlInterfacePipelineApiLogic } = new LogicMounter( SimulateExistingMlInterfacePipelineApiLogic ); @@ -92,7 +112,7 @@ describe('MlInferenceLogic', () => { beforeEach(() => { jest.clearAllMocks(); mountMappingApiLogic(); - mountMLModelsApiLogic(); + mountCachedFetchModelsApiLogic(); mountFetchMlInferencePipelineProcessorsApiLogic(); mountFetchMlInferencePipelinesApiLogic(); mountSimulateExistingMlInterfacePipelineApiLogic(); @@ -105,7 +125,13 @@ describe('MlInferenceLogic', () => { }); it('has expected default values', () => { - expect(MLInferenceLogic.values).toEqual(DEFAULT_VALUES); + CachedFetchModelsApiLogic.actions.apiSuccess(MODELS); + expect(MLInferenceLogic.values).toEqual({ + ...DEFAULT_VALUES, + modelsData: MODELS, // Populated by afterMount hook + modelsStatus: Status.SUCCESS, + selectableModels: MODELS, + }); }); describe('actions', () => { @@ -182,316 +208,6 @@ describe('MlInferenceLogic', () => { }); describe('selectors', () => { - describe('existingInferencePipelines', () => { - beforeEach(() => { - MappingsApiLogic.actions.apiSuccess({ - mappings: { - properties: { - body: { - type: 'text', - }, - }, - }, - }); - }); - it('returns empty list when there is not existing pipelines available', () => { - expect(MLInferenceLogic.values.existingInferencePipelines).toEqual([]); - }); - it('returns existing pipeline option', () => { - FetchMlInferencePipelinesApiLogic.actions.apiSuccess({ - 'unit-test': { - processors: [ - { - inference: { - field_map: { - body: 'text_field', - }, - model_id: 'test-model', - target_field: 'ml.inference.test-field', - }, - }, - ], - version: 1, - }, - }); - - expect(MLInferenceLogic.values.existingInferencePipelines).toEqual([ - { - disabled: false, - modelId: 'test-model', - modelType: '', - pipelineName: 'unit-test', - sourceFields: ['body'], - indexFields: ['body'], - }, - ]); - }); - it('returns disabled pipeline option if missing source fields', () => { - FetchMlInferencePipelinesApiLogic.actions.apiSuccess({ - 'unit-test': { - processors: [ - { - inference: { - field_map: { - title: 'text_field', // Does not exist in index - }, - model_id: 'test-model', - target_field: 'ml.inference.title', - }, - }, - { - inference: { - field_map: { - body: 'text_field', // Exists in index - }, - model_id: 'test-model', - target_field: 'ml.inference.body', - }, - }, - { - inference: { - field_map: { - body_content: 'text_field', // Does not exist in index - }, - model_id: 'test-model', - target_field: 'ml.inference.body_content', - }, - }, - ], - version: 1, - }, - }); - - expect(MLInferenceLogic.values.existingInferencePipelines).toEqual([ - { - disabled: true, - disabledReason: expect.stringContaining('title, body_content'), - modelId: 'test-model', - modelType: '', - pipelineName: 'unit-test', - sourceFields: ['title', 'body', 'body_content'], - indexFields: ['body'], - }, - ]); - }); - it('returns enabled pipeline option if model is redacted', () => { - FetchMlInferencePipelinesApiLogic.actions.apiSuccess({ - 'unit-test': { - processors: [ - { - inference: { - field_map: { - body: 'text_field', - }, - model_id: '', - target_field: 'ml.inference.test-field', - }, - }, - ], - version: 1, - }, - }); - - expect(MLInferenceLogic.values.existingInferencePipelines).toEqual([ - { - disabled: false, - pipelineName: 'unit-test', - modelType: '', - modelId: '', - sourceFields: ['body'], - indexFields: ['body'], - }, - ]); - }); - it('filters pipeline if pipeline already attached', () => { - FetchMlInferencePipelineProcessorsApiLogic.actions.apiSuccess([ - { - modelId: 'test-model', - modelState: TrainedModelState.Started, - pipelineName: 'unit-test', - pipelineReferences: ['test@ml-inference'], - types: ['ner', 'pytorch'], - }, - ]); - FetchMlInferencePipelinesApiLogic.actions.apiSuccess({ - 'unit-test': { - processors: [ - { - inference: { - field_map: { - body: 'text_field', - }, - model_id: 'test-model', - target_field: 'ml.inference.test-field', - }, - }, - ], - version: 1, - }, - }); - - expect(MLInferenceLogic.values.existingInferencePipelines).toEqual([]); - }); - }); - describe('mlInferencePipeline', () => { - it('returns undefined when configuration is invalid', () => { - MLInferenceLogic.actions.setInferencePipelineConfiguration({ - modelID: '', - pipelineName: '', // Invalid - fieldMappings: [], // Invalid - targetField: '', - }); - - expect(MLInferenceLogic.values.mlInferencePipeline).toBeUndefined(); - }); - it('generates inference pipeline', () => { - MLModelsApiLogic.actions.apiSuccess([nerModel]); - MLInferenceLogic.actions.setInferencePipelineConfiguration({ - modelID: nerModel.model_id, - pipelineName: 'unit-test', - fieldMappings: [ - { - sourceField: 'body', - targetField: 'ml.inference.body', - }, - ], - targetField: '', - }); - - expect(MLInferenceLogic.values.mlInferencePipeline).not.toBeUndefined(); - }); - it('returns undefined when existing pipeline not yet selected', () => { - MLInferenceLogic.actions.setInferencePipelineConfiguration({ - existingPipeline: true, - modelID: '', - pipelineName: '', - fieldMappings: [], - targetField: '', - }); - expect(MLInferenceLogic.values.mlInferencePipeline).toBeUndefined(); - }); - it('return existing pipeline when selected', () => { - const existingPipeline = { - description: 'this is a test', - processors: [], - version: 1, - }; - FetchMlInferencePipelinesApiLogic.actions.apiSuccess({ - 'unit-test': existingPipeline, - }); - MLInferenceLogic.actions.setInferencePipelineConfiguration({ - existingPipeline: true, - modelID: '', - pipelineName: 'unit-test', - fieldMappings: [ - { - sourceField: 'body', - targetField: 'ml.inference.body', - }, - ], - targetField: '', - }); - expect(MLInferenceLogic.values.mlInferencePipeline).not.toBeUndefined(); - expect(MLInferenceLogic.values.mlInferencePipeline).toEqual(existingPipeline); - }); - }); - describe('supportedMLModels', () => { - it('filters unsupported ML models', () => { - MLModelsApiLogic.actions.apiSuccess([ - { - inference_config: { - ner: {}, - }, - input: { - field_names: ['text_field'], - }, - model_id: 'ner-mocked-model', - model_type: 'pytorch', - tags: [], - version: '1', - }, - { - inference_config: { - some_unsupported_task_type: {}, - }, - input: { - field_names: ['text_field'], - }, - model_id: 'unsupported-mocked-model', - model_type: 'pytorch', - tags: [], - version: '1', - }, - ]); - - expect(MLInferenceLogic.values.supportedMLModels).toEqual([ - expect.objectContaining({ - inference_config: { - ner: {}, - }, - }), - ]); - }); - - it('promotes text_expansion ML models and sorts others by ID', () => { - MLModelsApiLogic.actions.apiSuccess([ - { - inference_config: { - ner: {}, - }, - input: { - field_names: ['text_field'], - }, - model_id: 'ner-mocked-model', - model_type: 'pytorch', - tags: [], - version: '1', - }, - { - inference_config: { - text_expansion: {}, - }, - input: { - field_names: ['text_field'], - }, - model_id: 'text-expansion-mocked-model', - model_type: 'pytorch', - tags: [], - version: '1', - }, - { - inference_config: { - text_embedding: {}, - }, - input: { - field_names: ['text_field'], - }, - model_id: 'text-embedding-mocked-model', - model_type: 'pytorch', - tags: [], - version: '1', - }, - ]); - - expect(MLInferenceLogic.values.supportedMLModels).toEqual([ - expect.objectContaining({ - inference_config: { - text_expansion: {}, - }, - }), - expect.objectContaining({ - inference_config: { - ner: {}, - }, - }), - expect.objectContaining({ - inference_config: { - text_embedding: {}, - }, - }), - ]); - }); - }); describe('formErrors', () => { it('has errors when configuration is empty', () => { expect(MLInferenceLogic.values.formErrors).toEqual({ @@ -570,6 +286,75 @@ describe('MlInferenceLogic', () => { }); }); }); + describe('mlInferencePipeline', () => { + it('returns undefined when configuration is invalid', () => { + MLInferenceLogic.actions.setInferencePipelineConfiguration({ + modelID: '', + pipelineName: '', // Invalid + fieldMappings: [], // Invalid + targetField: '', + }); + + expect(MLInferenceLogic.values.mlInferencePipeline).toBeUndefined(); + }); + it('generates inference pipeline', () => { + CachedFetchModelsApiLogic.actions.apiSuccess(MODELS); + MLInferenceLogic.actions.setInferencePipelineConfiguration({ + modelID: MODELS[0].modelId, + pipelineName: 'unit-test', + fieldMappings: [ + { + sourceField: 'body', + targetField: 'ml.inference.body', + }, + ], + targetField: '', + }); + + expect(MLInferenceLogic.values.mlInferencePipeline).not.toBeUndefined(); + }); + it('returns undefined when existing pipeline not yet selected', () => { + MLInferenceLogic.actions.setInferencePipelineConfiguration({ + existingPipeline: true, + modelID: '', + pipelineName: '', + fieldMappings: [], + targetField: '', + }); + expect(MLInferenceLogic.values.mlInferencePipeline).toBeUndefined(); + }); + it('return existing pipeline when selected', () => { + const existingPipeline = { + description: 'this is a test', + processors: [], + version: 1, + }; + FetchMlInferencePipelinesApiLogic.actions.apiSuccess({ + 'unit-test': existingPipeline, + }); + MLInferenceLogic.actions.setInferencePipelineConfiguration({ + existingPipeline: true, + modelID: '', + pipelineName: 'unit-test', + fieldMappings: [ + { + sourceField: 'body', + targetField: 'ml.inference.body', + }, + ], + targetField: '', + }); + expect(MLInferenceLogic.values.mlInferencePipeline).not.toBeUndefined(); + expect(MLInferenceLogic.values.mlInferencePipeline).toEqual(existingPipeline); + }); + }); + describe('selectableModels', () => { + it('makes fetch models request', () => { + MLInferenceLogic.actions.fetchModelsApiSuccess(MODELS); + + expect(MLInferenceLogic.values.selectableModels).toBe(MODELS); + }); + }); }); describe('listeners', () => { @@ -615,14 +400,14 @@ describe('MlInferenceLogic', () => { ...mockModelConfiguration, configuration: { ...mockModelConfiguration.configuration, - modelID: textExpansionModel.model_id, + modelID: MODELS[0].modelId, fieldMappings: [], }, }, }); jest.spyOn(MLInferenceLogic.actions, 'makeCreatePipelineRequest'); - MLModelsApiLogic.actions.apiSuccess([textExpansionModel]); + CachedFetchModelsApiLogic.actions.apiSuccess(MODELS); MLInferenceLogic.actions.selectFields(['my_source_field1', 'my_source_field2']); MLInferenceLogic.actions.addSelectedFieldsToMapping(true); MLInferenceLogic.actions.createPipeline(); @@ -630,7 +415,7 @@ describe('MlInferenceLogic', () => { expect(MLInferenceLogic.actions.makeCreatePipelineRequest).toHaveBeenCalledWith({ indexName: mockModelConfiguration.indexName, inferenceConfig: undefined, - modelId: textExpansionModel.model_id, + modelId: MODELS[0].modelId, fieldMappings: [ { sourceField: 'my_source_field1', @@ -648,13 +433,13 @@ describe('MlInferenceLogic', () => { }); describe('startTextExpansionModelSuccess', () => { it('fetches ml models', () => { - jest.spyOn(MLInferenceLogic.actions, 'makeMLModelsRequest'); + jest.spyOn(MLInferenceLogic.actions, 'startPollingModels'); StartTextExpansionModelApiLogic.actions.apiSuccess({ deploymentState: 'started', modelId: 'foo', }); - expect(MLInferenceLogic.actions.makeMLModelsRequest).toHaveBeenCalledWith(undefined); + expect(MLInferenceLogic.actions.startPollingModels).toHaveBeenCalled(); }); }); describe('onAddInferencePipelineStepChange', () => { @@ -673,12 +458,12 @@ describe('MlInferenceLogic', () => { existingPipeline: false, }); jest.spyOn(MLInferenceLogic.actions, 'fetchPipelineByName'); - jest.spyOn(MLInferenceLogic.actions, 'makeMLModelsRequest'); + jest.spyOn(MLInferenceLogic.actions, 'startPollingModels'); MLInferenceLogic.actions.onAddInferencePipelineStepChange(AddInferencePipelineSteps.Fields); expect(MLInferenceLogic.actions.fetchPipelineByName).toHaveBeenCalledWith({ pipelineName: 'ml-inference-unit-test-pipeline', }); - expect(MLInferenceLogic.actions.makeMLModelsRequest).toHaveBeenCalledWith(undefined); + expect(MLInferenceLogic.actions.startPollingModels).toHaveBeenCalled(); }); it('does not trigger pipeline and model fetch existing pipeline is selected', () => { MLInferenceLogic.actions.setInferencePipelineConfiguration({ @@ -688,10 +473,10 @@ describe('MlInferenceLogic', () => { existingPipeline: true, }); jest.spyOn(MLInferenceLogic.actions, 'fetchPipelineByName'); - jest.spyOn(MLInferenceLogic.actions, 'makeMLModelsRequest'); + jest.spyOn(MLInferenceLogic.actions, 'startPollingModels'); MLInferenceLogic.actions.onAddInferencePipelineStepChange(AddInferencePipelineSteps.Fields); expect(MLInferenceLogic.actions.fetchPipelineByName).not.toHaveBeenCalled(); - expect(MLInferenceLogic.actions.makeMLModelsRequest).not.toHaveBeenCalled(); + expect(MLInferenceLogic.actions.startPollingModels).not.toHaveBeenCalled(); }); }); describe('fetchPipelineSuccess', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts index bdcf23d71a743..478b51dc1d7b1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts @@ -14,11 +14,10 @@ import { formatPipelineName, generateMlInferencePipelineBody, getMlInferencePrefixedFieldName, - getMlModelTypesForModelConfig, ML_INFERENCE_PREFIX, - parseMlInferenceParametersFromPipeline, } from '../../../../../../../common/ml_inference_pipeline'; import { Status } from '../../../../../../../common/types/api'; +import { MlModel } from '../../../../../../../common/types/ml'; import { MlInferencePipeline } from '../../../../../../../common/types/pipelines'; import { Actions } from '../../../../../shared/api_logic/create_api_logic'; @@ -34,10 +33,10 @@ import { MappingsApiLogic, } from '../../../../api/mappings/mappings_logic'; import { - TrainedModel, - TrainedModelsApiLogicActions, - TrainedModelsApiLogic, -} from '../../../../api/ml_models/ml_trained_models_logic'; + CachedFetchModelsApiLogic, + CachedFetchModlesApiLogicActions, + FetchModelsApiResponse, +} from '../../../../api/ml_models/cached_fetch_models_api_logic'; import { StartTextExpansionModelApiLogic, StartTextExpansionModelApiLogicActions, @@ -68,12 +67,7 @@ import { } from '../../../../api/pipelines/fetch_pipeline'; import { isConnectorIndex } from '../../../../utils/indices'; -import { - getMLType, - isSupportedMLModel, - sortModels, - sortSourceFields, -} from '../../../shared/ml_inference/utils'; +import { sortSourceFields } from '../../../shared/ml_inference/utils'; import { PipelinesLogic } from '../pipelines_logic'; import { @@ -83,7 +77,6 @@ import { } from './types'; import { - EXISTING_PIPELINE_DISABLED_MISSING_SOURCE_FIELDS, validateInferencePipelineConfiguration, validateInferencePipelineFields, validatePipelineNameIsAvailable, @@ -109,16 +102,6 @@ const getFullTargetFieldName = ( return getMlInferencePrefixedFieldName(suffixedTargetField); }; -export interface MLInferencePipelineOption { - disabled: boolean; - disabledReason?: string; - modelId: string; - modelType: string; - pipelineName: string; - sourceFields: string[]; - indexFields: string[]; -} - export interface MLInferenceProcessorsActions { addSelectedFieldsToMapping: (isTextExpansionModelSelected: boolean) => { isTextExpansionModelSelected: boolean; @@ -143,6 +126,7 @@ export interface MLInferenceProcessorsActions { CreateMlInferencePipelineResponse >['apiSuccess']; createPipeline: () => void; + fetchModelsApiSuccess: CachedFetchModlesApiLogicActions['apiSuccess']; fetchPipelineByName: FetchPipelineApiLogicActions['makeRequest']; fetchPipelineSuccess: FetchPipelineApiLogicActions['apiSuccess']; makeAttachPipelineRequest: Actions< @@ -153,7 +137,6 @@ export interface MLInferenceProcessorsActions { CreateMlInferencePipelineApiLogicArgs, CreateMlInferencePipelineResponse >['makeRequest']; - makeMLModelsRequest: TrainedModelsApiLogicActions['makeRequest']; makeMappingRequest: Actions['makeRequest']; makeMlInferencePipelinesRequest: Actions< FetchMlInferencePipelinesArgs, @@ -164,14 +147,10 @@ export interface MLInferenceProcessorsActions { FetchMlInferencePipelinesArgs, FetchMlInferencePipelinesResponse >['apiSuccess']; - mlModelsApiError: TrainedModelsApiLogicActions['apiError']; onAddInferencePipelineStepChange: (step: AddInferencePipelineSteps) => { step: AddInferencePipelineSteps; }; removeFieldFromMapping: (fieldName: string) => { fieldName: string }; - selectExistingPipeline: (pipelineName: string) => { - pipelineName: string; - }; selectFields: (fieldNames: string[]) => { fieldNames: string[] }; setAddInferencePipelineStep: (step: AddInferencePipelineSteps) => { step: AddInferencePipelineSteps; @@ -181,6 +160,7 @@ export interface MLInferenceProcessorsActions { configuration: InferencePipelineConfiguration; }; setTargetField: (targetFieldName: string) => { targetFieldName: string }; + startPollingModels: CachedFetchModlesApiLogicActions['startPolling']; startTextExpansionModelSuccess: StartTextExpansionModelApiLogicActions['apiSuccess']; } @@ -195,11 +175,11 @@ export interface MLInferenceProcessorsValues { addInferencePipelineModal: AddInferencePipelineModal; createErrors: string[]; existingPipeline: FetchPipelineResponse | undefined; - existingInferencePipelines: MLInferencePipelineOption[]; formErrors: AddInferencePipelineFormErrors; index: CachedFetchIndexApiLogicValues['indexData']; isConfigureStepValid: boolean; isLoading: boolean; + isModelsInitialLoading: boolean; isPipelineDataValid: boolean; isTextExpansionModelSelected: boolean; mappingData: typeof MappingsApiLogic.values.data; @@ -207,11 +187,11 @@ export interface MLInferenceProcessorsValues { mlInferencePipeline: MlInferencePipeline | undefined; mlInferencePipelineProcessors: FetchMlInferencePipelineProcessorsResponse | undefined; mlInferencePipelinesData: FetchMlInferencePipelinesResponse | undefined; - mlModelsData: TrainedModel[] | null; - mlModelsStatus: Status; - selectedMLModel: TrainedModel | null; + modelsData: FetchModelsApiResponse | undefined; + modelsStatus: Status; + selectableModels: MlModel[]; + selectedModel: MlModel | undefined; sourceFields: string[] | undefined; - supportedMLModels: TrainedModel[]; } export const MLInferenceLogic = kea< @@ -227,7 +207,6 @@ export const MLInferenceLogic = kea< createPipeline: true, onAddInferencePipelineStepChange: (step: AddInferencePipelineSteps) => ({ step }), removeFieldFromMapping: (fieldName: string) => ({ fieldName }), - selectExistingPipeline: (pipelineName: string) => ({ pipelineName }), selectFields: (fieldNames: string[]) => ({ fieldNames }), setAddInferencePipelineStep: (step: AddInferencePipelineSteps) => ({ step }), setIndexName: (indexName: string) => ({ indexName }), @@ -238,6 +217,8 @@ export const MLInferenceLogic = kea< }, connect: { actions: [ + CachedFetchModelsApiLogic, + ['apiSuccess as fetchModelsApiSuccess', 'startPolling as startPollingModels'], FetchMlInferencePipelinesApiLogic, [ 'makeRequest as makeMlInferencePipelinesRequest', @@ -245,8 +226,6 @@ export const MLInferenceLogic = kea< ], MappingsApiLogic, ['makeRequest as makeMappingRequest', 'apiError as mappingsApiError'], - TrainedModelsApiLogic, - ['makeRequest as makeMLModelsRequest', 'apiError as mlModelsApiError'], CreateMlInferencePipelineApiLogic, [ 'apiError as createApiError', @@ -260,7 +239,7 @@ export const MLInferenceLogic = kea< 'makeRequest as makeAttachPipelineRequest', ], PipelinesLogic, - ['closeAddMlInferencePipelineModal as closeAddMlInferencePipelineModal'], + ['closeAddMlInferencePipelineModal'], StartTextExpansionModelApiLogic, ['apiSuccess as startTextExpansionModelSuccess'], FetchPipelineApiLogic, @@ -271,21 +250,20 @@ export const MLInferenceLogic = kea< ], ], values: [ + CachedFetchModelsApiLogic, + ['modelsData', 'status as modelsStatus', 'isInitialLoading as isModelsInitialLoading'], CachedFetchIndexApiLogic, ['indexData as index'], FetchMlInferencePipelinesApiLogic, ['data as mlInferencePipelinesData'], MappingsApiLogic, ['data as mappingData', 'status as mappingStatus'], - TrainedModelsApiLogic, - ['data as mlModelsData', 'status as mlModelsStatus'], FetchMlInferencePipelineProcessorsApiLogic, ['data as mlInferencePipelineProcessors'], FetchPipelineApiLogic, ['data as existingPipeline'], ], }, - events: {}, listeners: ({ values, actions }) => ({ attachPipeline: () => { const { @@ -327,24 +305,6 @@ export const MLInferenceLogic = kea< pipelineName: configuration.pipelineName, }); }, - selectExistingPipeline: ({ pipelineName }) => { - const pipeline = values.mlInferencePipelinesData?.[pipelineName]; - if (!pipeline) return; - const params = parseMlInferenceParametersFromPipeline(pipelineName, pipeline); - if (params === null) return; - actions.setInferencePipelineConfiguration({ - existingPipeline: true, - modelID: params.model_id, - pipelineName, - fieldMappings: params.field_mappings, - targetField: '', - }); - }, - setIndexName: ({ indexName }) => { - actions.makeMlInferencePipelinesRequest(undefined); - actions.makeMLModelsRequest(undefined); - actions.makeMappingRequest({ indexName }); - }, mlInferencePipelinesSuccess: (data) => { if ( (data?.length ?? 0) === 0 && @@ -359,7 +319,7 @@ export const MLInferenceLogic = kea< }, startTextExpansionModelSuccess: () => { // Refresh ML models list when the text expansion model is started - actions.makeMLModelsRequest(undefined); + actions.startPollingModels(); }, onAddInferencePipelineStepChange: ({ step }) => { const { @@ -377,12 +337,12 @@ export const MLInferenceLogic = kea< // back to the Configuration step if we find a pipeline with the same name // Re-fetch ML model list to include those that were deployed in this step - actions.makeMLModelsRequest(undefined); + actions.startPollingModels(); } actions.setAddInferencePipelineStep(step); }, fetchPipelineSuccess: () => { - // We found a pipeline with the name go back to configuration step + // We found a pipeline with the name, go back to configuration step actions.setAddInferencePipelineStep(AddInferencePipelineSteps.Configuration); }, }), @@ -509,30 +469,28 @@ export const MLInferenceLogic = kea< }, ], isLoading: [ - () => [selectors.mlModelsStatus, selectors.mappingStatus], - (mlModelsStatus, mappingStatus) => - !API_REQUEST_COMPLETE_STATUSES.includes(mlModelsStatus) || - !API_REQUEST_COMPLETE_STATUSES.includes(mappingStatus), + () => [selectors.mappingStatus], + (mappingStatus: Status) => !API_REQUEST_COMPLETE_STATUSES.includes(mappingStatus), ], isPipelineDataValid: [ () => [selectors.formErrors], (errors: AddInferencePipelineFormErrors) => Object.keys(errors).length === 0, ], isTextExpansionModelSelected: [ - () => [selectors.selectedMLModel], - (model: TrainedModel | null) => !!model?.inference_config?.text_expansion, + () => [selectors.selectedModel], + (model: MlModel | null) => model?.type === 'text_expansion', ], mlInferencePipeline: [ () => [ selectors.isPipelineDataValid, selectors.addInferencePipelineModal, - selectors.mlModelsData, + selectors.modelsData, selectors.mlInferencePipelinesData, ], ( isPipelineDataValid: MLInferenceProcessorsValues['isPipelineDataValid'], { configuration }: MLInferenceProcessorsValues['addInferencePipelineModal'], - models: MLInferenceProcessorsValues['mlModelsData'], + models: MLInferenceProcessorsValues['modelsData'], mlInferencePipelinesData: MLInferenceProcessorsValues['mlInferencePipelinesData'] ) => { if (configuration.existingPipeline) { @@ -546,7 +504,7 @@ export const MLInferenceLogic = kea< return pipeline as MlInferencePipeline; } if (!isPipelineDataValid) return undefined; - const model = models?.find((mlModel) => mlModel.model_id === configuration.modelID); + const model = models?.find((mlModel) => mlModel.modelId === configuration.modelID); if (!model) return undefined; return generateMlInferencePipelineBody({ @@ -581,78 +539,16 @@ export const MLInferenceLogic = kea< .sort(sortSourceFields); }, ], - supportedMLModels: [ - () => [selectors.mlModelsData], - (mlModelsData: MLInferenceProcessorsValues['mlModelsData']) => { - return (mlModelsData?.filter(isSupportedMLModel) ?? []).sort(sortModels); - }, - ], - existingInferencePipelines: [ - () => [ - selectors.mlInferencePipelinesData, - selectors.sourceFields, - selectors.supportedMLModels, - selectors.mlInferencePipelineProcessors, - ], - ( - mlInferencePipelinesData: MLInferenceProcessorsValues['mlInferencePipelinesData'], - indexFields: MLInferenceProcessorsValues['sourceFields'], - supportedMLModels: MLInferenceProcessorsValues['supportedMLModels'], - mlInferencePipelineProcessors: MLInferenceProcessorsValues['mlInferencePipelineProcessors'] - ) => { - if (!mlInferencePipelinesData) { - return []; - } - const indexProcessorNames = - mlInferencePipelineProcessors?.map((processor) => processor.pipelineName) ?? []; - - const existingPipelines: MLInferencePipelineOption[] = Object.entries( - mlInferencePipelinesData - ) - .map(([pipelineName, pipeline]): MLInferencePipelineOption | undefined => { - if (!pipeline || indexProcessorNames.includes(pipelineName)) return undefined; - - // Parse configuration from pipeline definition - const pipelineParams = parseMlInferenceParametersFromPipeline(pipelineName, pipeline); - if (!pipelineParams) return undefined; - const { model_id: modelId, field_mappings: fieldMappings } = pipelineParams; - - const sourceFields = fieldMappings?.map((m) => m.sourceField) ?? []; - const missingSourceFields = sourceFields.filter((f) => !indexFields?.includes(f)) ?? []; - const mlModel = supportedMLModels.find((model) => model.model_id === modelId); - const modelType = mlModel ? getMLType(getMlModelTypesForModelConfig(mlModel)) : ''; - const disabledReason = - missingSourceFields.length > 0 - ? EXISTING_PIPELINE_DISABLED_MISSING_SOURCE_FIELDS(missingSourceFields.join(', ')) - : undefined; - - return { - disabled: disabledReason !== undefined, - disabledReason, - modelId, - modelType, - pipelineName, - sourceFields, - indexFields: indexFields ?? [], - }; - }) - .filter((p): p is MLInferencePipelineOption => p !== undefined); - - return existingPipelines; - }, + selectableModels: [ + () => [selectors.modelsData], + (response: FetchModelsApiResponse) => response ?? [], ], - selectedMLModel: [ - () => [selectors.supportedMLModels, selectors.addInferencePipelineModal], + selectedModel: [ + () => [selectors.selectableModels, selectors.addInferencePipelineModal], ( - supportedMLModels: MLInferenceProcessorsValues['supportedMLModels'], + models: MlModel[], addInferencePipelineModal: MLInferenceProcessorsValues['addInferencePipelineModal'] - ) => { - return ( - supportedMLModels.find( - (model) => model.model_id === addInferencePipelineModal.configuration.modelID - ) ?? null - ); - }, + ) => models.find((m) => m.modelId === addInferencePipelineModal.configuration.modelID), ], }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx index c8a970751643a..b08b4697e6cfc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx @@ -52,6 +52,9 @@ const DEFAULT_MODEL: MlModel = { threadsPerAllocation: 0, isPlaceholder: false, hasStats: false, + types: ['pytorch', 'ner'], + inputFieldNames: ['title'], + version: '1', }; const MOCK_ACTIONS = { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.test.ts index b0c26aaf8be8c..6a20629de4d43 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.test.ts @@ -8,7 +8,7 @@ import { LogicMounter } from '../../../../../__mocks__/kea_logic'; import { HttpError } from '../../../../../../../common/types/api'; -import { MlModel, MlModelDeploymentState } from '../../../../../../../common/types/ml'; +import { MlModelDeploymentState } from '../../../../../../../common/types/ml'; import { CachedFetchModelsApiLogic } from '../../../../api/ml_models/cached_fetch_models_api_logic'; import { CreateModelApiLogic, @@ -22,31 +22,15 @@ const CREATE_MODEL_API_RESPONSE: CreateModelResponse = { modelId: 'model_1', deploymentState: MlModelDeploymentState.NotDeployed, }; -const FETCH_MODELS_API_DATA_RESPONSE: MlModel[] = [ - { - modelId: 'model_1', - title: 'Model 1', - type: 'ner', - deploymentState: MlModelDeploymentState.NotDeployed, - startTime: 0, - targetAllocationCount: 0, - nodeAllocationCount: 0, - threadsPerAllocation: 0, - isPlaceholder: false, - hasStats: false, - }, -]; describe('ModelSelectLogic', () => { const { mount } = new LogicMounter(ModelSelectLogic); const { mount: mountCreateModelApiLogic } = new LogicMounter(CreateModelApiLogic); - const { mount: mountCachedFetchModelsApiLogic } = new LogicMounter(CachedFetchModelsApiLogic); const { mount: mountStartModelApiLogic } = new LogicMounter(StartModelApiLogic); beforeEach(() => { jest.clearAllMocks(); mountCreateModelApiLogic(); - mountCachedFetchModelsApiLogic(); mountStartModelApiLogic(); mount(); }); @@ -72,23 +56,13 @@ describe('ModelSelectLogic', () => { expect(ModelSelectLogic.actions.startPollingModels).toHaveBeenCalled(); }); it('sets selected model as non-placeholder', () => { - jest.spyOn(ModelSelectLogic.actions, 'clearModelPlaceholderFlagFromMLInferenceLogic'); + jest.spyOn(ModelSelectLogic.actions, 'clearModelPlaceholderFlag'); ModelSelectLogic.actions.createModelSuccess(CREATE_MODEL_API_RESPONSE); - expect( - ModelSelectLogic.actions.clearModelPlaceholderFlagFromMLInferenceLogic - ).toHaveBeenCalledWith(CREATE_MODEL_API_RESPONSE.modelId); - }); - }); - - describe('fetchModels', () => { - it('makes fetch models request', () => { - jest.spyOn(ModelSelectLogic.actions, 'fetchModelsMakeRequest'); - - ModelSelectLogic.actions.fetchModels(); - - expect(ModelSelectLogic.actions.fetchModelsMakeRequest).toHaveBeenCalled(); + expect(ModelSelectLogic.actions.clearModelPlaceholderFlag).toHaveBeenCalledWith( + CREATE_MODEL_API_RESPONSE.modelId + ); }); }); @@ -150,14 +124,6 @@ describe('ModelSelectLogic', () => { }); }); - describe('selectableModels', () => { - it('gets models data from API response', () => { - CachedFetchModelsApiLogic.actions.apiSuccess(FETCH_MODELS_API_DATA_RESPONSE); - - expect(ModelSelectLogic.values.selectableModels).toEqual(FETCH_MODELS_API_DATA_RESPONSE); - }); - }); - describe('isLoading', () => { it('is set to true if the fetch API is loading the first time', () => { CachedFetchModelsApiLogic.actions.apiReset(); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts index 4074ffac92f6b..a6be0e921b358 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_logic.ts @@ -8,17 +8,11 @@ import { kea, MakeLogicType } from 'kea'; import { HttpError, Status } from '../../../../../../../common/types/api'; -import { MlModel } from '../../../../../../../common/types/ml'; import { getErrorsFromHttpResponse } from '../../../../../shared/flash_messages/handle_api_errors'; -import { - CachedFetchModelsApiLogic, - CachedFetchModlesApiLogicActions, -} from '../../../../api/ml_models/cached_fetch_models_api_logic'; import { CreateModelApiLogic, CreateModelApiLogicActions, } from '../../../../api/ml_models/create_model_api_logic'; -import { FetchModelsApiResponse } from '../../../../api/ml_models/fetch_models_api_logic'; import { StartModelApiLogic, StartModelApiLogicActions, @@ -32,39 +26,29 @@ import { } from './ml_inference_logic'; export interface ModelSelectActions { - clearModelPlaceholderFlagFromMLInferenceLogic: MLInferenceProcessorsActions['clearModelPlaceholderFlag']; + clearModelPlaceholderFlag: MLInferenceProcessorsActions['clearModelPlaceholderFlag']; createModel: (modelId: string) => { modelId: string }; createModelError: CreateModelApiLogicActions['apiError']; createModelMakeRequest: CreateModelApiLogicActions['makeRequest']; createModelSuccess: CreateModelApiLogicActions['apiSuccess']; - fetchModels: () => void; - fetchModelsError: CachedFetchModlesApiLogicActions['apiError']; - fetchModelsMakeRequest: CachedFetchModlesApiLogicActions['makeRequest']; - fetchModelsSuccess: CachedFetchModlesApiLogicActions['apiSuccess']; setInferencePipelineConfiguration: MLInferenceProcessorsActions['setInferencePipelineConfiguration']; - setInferencePipelineConfigurationFromMLInferenceLogic: MLInferenceProcessorsActions['setInferencePipelineConfiguration']; startModel: (modelId: string) => { modelId: string }; startModelError: CreateModelApiLogicActions['apiError']; startModelMakeRequest: StartModelApiLogicActions['makeRequest']; startModelSuccess: StartModelApiLogicActions['apiSuccess']; - startPollingModels: CachedFetchModlesApiLogicActions['startPolling']; + startPollingModels: MLInferenceProcessorsActions['startPollingModels']; } export interface ModelSelectValues { addInferencePipelineModal: MLInferenceProcessorsValues['addInferencePipelineModal']; - addInferencePipelineModalFromMLInferenceLogic: MLInferenceProcessorsValues['addInferencePipelineModal']; areActionButtonsDisabled: boolean; createModelError: HttpError | undefined; createModelStatus: Status; ingestionMethod: string; - ingestionMethodFromIndexViewLogic: string; isLoading: boolean; - isInitialLoading: boolean; modelStateChangeError: string | undefined; - modelsData: FetchModelsApiResponse | undefined; - modelsStatus: Status; - selectableModels: MlModel[]; - selectedModel: MlModel | undefined; + selectableModels: MLInferenceProcessorsValues['selectableModels']; + selectedModel: MLInferenceProcessorsValues['selectedModel']; startModelError: HttpError | undefined; startModelStatus: Status; } @@ -72,19 +56,10 @@ export interface ModelSelectValues { export const ModelSelectLogic = kea>({ actions: { createModel: (modelId: string) => ({ modelId }), - fetchModels: true, - setInferencePipelineConfiguration: (configuration) => ({ configuration }), startModel: (modelId: string) => ({ modelId }), }, connect: { actions: [ - CachedFetchModelsApiLogic, - [ - 'makeRequest as fetchModelsMakeRequest', - 'apiSuccess as fetchModelsSuccess', - 'apiError as fetchModelsError', - 'startPolling as startPollingModels', - ], CreateModelApiLogic, [ 'makeRequest as createModelMakeRequest', @@ -92,10 +67,7 @@ export const ModelSelectLogic = kea ({ - afterMount: () => { - actions.startPollingModels(); - }, - }), listeners: ({ actions }) => ({ createModel: ({ modelId }) => { actions.createModelMakeRequest({ modelId }); @@ -128,13 +98,7 @@ export const ModelSelectLogic = kea { actions.startPollingModels(); // The create action succeeded, so the model is no longer a placeholder - actions.clearModelPlaceholderFlagFromMLInferenceLogic(response.modelId); - }, - fetchModels: () => { - actions.fetchModelsMakeRequest({}); - }, - setInferencePipelineConfiguration: ({ configuration }) => { - actions.setInferencePipelineConfigurationFromMLInferenceLogic(configuration); + actions.clearModelPlaceholderFlag(response.modelId); }, startModel: ({ modelId }) => { actions.startModelMakeRequest({ modelId }); @@ -145,19 +109,11 @@ export const ModelSelectLogic = kea ({ - addInferencePipelineModal: [ - () => [selectors.addInferencePipelineModalFromMLInferenceLogic], - (modal) => modal, // Pass-through - ], areActionButtonsDisabled: [ () => [selectors.createModelStatus, selectors.startModelStatus], (createModelStatus: Status, startModelStatus: Status) => createModelStatus === Status.LOADING || startModelStatus === Status.LOADING, ], - ingestionMethod: [ - () => [selectors.ingestionMethodFromIndexViewLogic], - (ingestionMethod) => ingestionMethod, // Pass-through - ], modelStateChangeError: [ () => [selectors.createModelError, selectors.startModelError], (createModelError?: HttpError, startModelError?: HttpError) => { @@ -166,17 +122,5 @@ export const ModelSelectLogic = kea [selectors.modelsData], - (response: FetchModelsApiResponse) => response ?? [], - ], - selectedModel: [ - () => [selectors.selectableModels, selectors.addInferencePipelineModal], - ( - models: MlModel[], - addInferencePipelineModal: MLInferenceProcessorsValues['addInferencePipelineModal'] - ) => models.find((m) => m.modelId === addInferencePipelineModal.configuration.modelID), - ], - isLoading: [() => [selectors.isInitialLoading], (isInitialLoading) => isInitialLoading], }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_option.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_option.test.tsx index bcf4eb8342db1..6c4f1f4bbabb8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_option.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select_option.test.tsx @@ -34,6 +34,9 @@ const DEFAULT_PROPS: EuiSelectableOption = { threadsPerAllocation: 0, isPlaceholder: false, hasStats: false, + types: ['pytorch', 'ner'], + inputFieldNames: ['title'], + version: '1', }; describe('ModelSelectOption', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx deleted file mode 100644 index 4670b00e93927..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiEmptyPrompt, EuiImage, EuiLink, EuiText, useEuiTheme } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import noMlModelsGraphicDark from '../../../../../../assets/images/no_ml_models_dark.svg'; -import noMlModelsGraphicLight from '../../../../../../assets/images/no_ml_models_light.svg'; - -import { docLinks } from '../../../../../shared/doc_links'; - -export const NoModelsPanel: React.FC = () => { - const { colorMode } = useEuiTheme(); - - return ( - - - -

- - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.esDocs.link', - { defaultMessage: 'Learn how to add a trained model' } - )} - - ), - }} - /> -

-
- - } - /> - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select.tsx index 9295c5440a054..39b354237a5c7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select.tsx @@ -11,17 +11,17 @@ import { useActions, useValues } from 'kea'; import { EuiSelectable, useEuiTheme, useIsWithinMaxBreakpoint } from '@elastic/eui'; -import { MLInferenceLogic, MLInferencePipelineOption } from './ml_inference_logic'; +import { MLInferencePipelineOption, PipelineSelectLogic } from './pipeline_select_logic'; import { PipelineSelectOption, PipelineSelectOptionProps } from './pipeline_select_option'; export const PipelineSelect: React.FC = () => { const { - addInferencePipelineModal: { configuration }, + addInferencePipelineModal: { + configuration: { pipelineName }, + }, existingInferencePipelines, - } = useValues(MLInferenceLogic); - const { selectExistingPipeline } = useActions(MLInferenceLogic); - - const { pipelineName } = configuration; + } = useValues(PipelineSelectLogic); + const { selectExistingPipeline } = useActions(PipelineSelectLogic); const { euiTheme } = useEuiTheme(); const largeScreenRowHeight = euiTheme.base * 6; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_logic.test.ts new file mode 100644 index 0000000000000..362abeb9a6625 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_logic.test.ts @@ -0,0 +1,278 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter } from '../../../../../__mocks__/kea_logic'; + +import { MlModel, MlModelDeploymentState } from '../../../../../../../common/types/ml'; +import { TrainedModelState } from '../../../../../../../common/types/pipelines'; + +import { MappingsApiLogic } from '../../../../api/mappings/mappings_logic'; +import { CachedFetchModelsApiLogic } from '../../../../api/ml_models/cached_fetch_models_api_logic'; +import { FetchMlInferencePipelineProcessorsApiLogic } from '../../../../api/pipelines/fetch_ml_inference_pipeline_processors'; +import { + FetchMlInferencePipelinesApiLogic, + FetchMlInferencePipelinesResponse, +} from '../../../../api/pipelines/fetch_ml_inference_pipelines'; + +import { PipelineSelectLogic, PipelineSelectValues } from './pipeline_select_logic'; +import { AddInferencePipelineSteps } from './types'; + +const DEFAULT_VALUES: PipelineSelectValues = { + addInferencePipelineModal: { + configuration: { + modelID: '', + pipelineName: '', + targetField: '', + }, + indexName: '', + step: AddInferencePipelineSteps.Configuration, + }, + existingInferencePipelines: [], + mlInferencePipelineProcessors: undefined, + mlInferencePipelinesData: undefined, + selectableModels: [], + sourceFields: undefined, +}; + +const DEFAULT_MODELS: MlModel[] = [ + { + modelId: 'model_1', + type: 'ner', + title: 'Model 1', + description: 'Model 1 description', + licenseType: 'elastic', + modelDetailsPageUrl: 'https://my-model.ai', + deploymentState: MlModelDeploymentState.NotDeployed, + startTime: 0, + targetAllocationCount: 0, + nodeAllocationCount: 0, + threadsPerAllocation: 0, + isPlaceholder: false, + hasStats: false, + types: ['pytorch', 'ner'], + inputFieldNames: ['title'], + version: '1', + }, +]; + +const DEFAULT_PIPELINES: FetchMlInferencePipelinesResponse = { + 'my-pipeline': { + processors: [ + { + inference: { + field_map: { + body: 'text_field', + }, + model_id: DEFAULT_MODELS[0].modelId, + target_field: 'ml.inference.body', + }, + }, + ], + version: 1, + }, +}; + +describe('PipelineSelectLogic', () => { + const { mount } = new LogicMounter(PipelineSelectLogic); + const { mount: mountFetchMlInferencePipelineProcessorsApiLogic } = new LogicMounter( + FetchMlInferencePipelineProcessorsApiLogic + ); + const { mount: mountFetchMlInferencePipelinesApiLogic } = new LogicMounter( + FetchMlInferencePipelinesApiLogic + ); + + beforeEach(() => { + jest.clearAllMocks(); + mountFetchMlInferencePipelineProcessorsApiLogic(); + mountFetchMlInferencePipelinesApiLogic(); + mount(); + }); + + describe('actions', () => { + describe('selectExistingPipeline', () => { + it('updates inference pipeline configuration', () => { + mount(DEFAULT_VALUES); + jest.spyOn(PipelineSelectLogic.actions, 'setInferencePipelineConfiguration'); + + FetchMlInferencePipelinesApiLogic.actions.apiSuccess(DEFAULT_PIPELINES); + PipelineSelectLogic.actions.selectExistingPipeline('my-pipeline'); + + expect(PipelineSelectLogic.actions.setInferencePipelineConfiguration).toHaveBeenCalledWith({ + existingPipeline: true, + modelID: DEFAULT_MODELS[0].modelId, + pipelineName: 'my-pipeline', + fieldMappings: [ + { + sourceField: 'body', + targetField: 'ml.inference.body', + }, + ], + targetField: '', + }); + }); + it('does not update inference pipeline configuration if pipeline name is not in list of fetched pipelines', () => { + mount(DEFAULT_VALUES); + jest.spyOn(PipelineSelectLogic.actions, 'setInferencePipelineConfiguration'); + + FetchMlInferencePipelinesApiLogic.actions.apiSuccess(DEFAULT_PIPELINES); + PipelineSelectLogic.actions.selectExistingPipeline('nonexistent-pipeline'); + + expect( + PipelineSelectLogic.actions.setInferencePipelineConfiguration + ).not.toHaveBeenCalled(); + }); + it('does not update inference pipeline configuration if inference processor cannot be parsed from fetched pipeline', () => { + mount(DEFAULT_VALUES); + jest.spyOn(PipelineSelectLogic.actions, 'setInferencePipelineConfiguration'); + + FetchMlInferencePipelinesApiLogic.actions.apiSuccess({ + 'my-pipeline': { + processors: [ + { + set: { + // No inference processor + field: 'some-field', + }, + }, + ], + version: 1, + }, + }); + PipelineSelectLogic.actions.selectExistingPipeline('my-pipeline'); + + expect( + PipelineSelectLogic.actions.setInferencePipelineConfiguration + ).not.toHaveBeenCalled(); + }); + }); + }); + + describe('selectors', () => { + describe('existingInferencePipelines', () => { + beforeEach(() => { + CachedFetchModelsApiLogic.actions.apiSuccess(DEFAULT_MODELS); + MappingsApiLogic.actions.apiSuccess({ + mappings: { + properties: { + body: { + type: 'text', + }, + }, + }, + }); + }); + it('returns empty list when there are no existing pipelines available', () => { + expect(PipelineSelectLogic.values.existingInferencePipelines).toEqual([]); + }); + it('returns existing pipeline option', () => { + FetchMlInferencePipelinesApiLogic.actions.apiSuccess(DEFAULT_PIPELINES); + + expect(PipelineSelectLogic.values.existingInferencePipelines).toEqual([ + { + disabled: false, + modelId: DEFAULT_MODELS[0].modelId, + modelType: 'ner', + pipelineName: 'my-pipeline', + sourceFields: ['body'], + indexFields: ['body'], + }, + ]); + }); + it('returns disabled pipeline option if missing source fields', () => { + FetchMlInferencePipelinesApiLogic.actions.apiSuccess({ + 'my-pipeline': { + processors: [ + { + inference: { + field_map: { + title: 'text_field', // Does not exist in index + }, + model_id: DEFAULT_MODELS[0].modelId, + target_field: 'ml.inference.title', + }, + }, + { + inference: { + field_map: { + body: 'text_field', // Exists in index + }, + model_id: DEFAULT_MODELS[0].modelId, + target_field: 'ml.inference.body', + }, + }, + { + inference: { + field_map: { + body_content: 'text_field', // Does not exist in index + }, + model_id: DEFAULT_MODELS[0].modelId, + target_field: 'ml.inference.body_content', + }, + }, + ], + version: 1, + }, + }); + + expect(PipelineSelectLogic.values.existingInferencePipelines).toEqual([ + { + disabled: true, + disabledReason: expect.stringContaining('title, body_content'), + modelId: DEFAULT_MODELS[0].modelId, + modelType: 'ner', + pipelineName: 'my-pipeline', + sourceFields: ['title', 'body', 'body_content'], + indexFields: ['body'], + }, + ]); + }); + it('returns enabled pipeline option if model is redacted', () => { + FetchMlInferencePipelinesApiLogic.actions.apiSuccess({ + 'my-pipeline': { + processors: [ + { + inference: { + field_map: { + body: 'text_field', + }, + model_id: '', // Redacted + target_field: 'ml.inference.body', + }, + }, + ], + version: 1, + }, + }); + + expect(PipelineSelectLogic.values.existingInferencePipelines).toEqual([ + { + disabled: false, + pipelineName: 'my-pipeline', + modelType: '', + modelId: '', + sourceFields: ['body'], + indexFields: ['body'], + }, + ]); + }); + it('filters pipeline if pipeline already attached', () => { + FetchMlInferencePipelineProcessorsApiLogic.actions.apiSuccess([ + { + modelId: DEFAULT_MODELS[0].modelId, + modelState: TrainedModelState.Started, + pipelineName: 'my-pipeline', + pipelineReferences: ['test@ml-inference'], + types: ['ner', 'pytorch'], + }, + ]); + FetchMlInferencePipelinesApiLogic.actions.apiSuccess(DEFAULT_PIPELINES); + + expect(PipelineSelectLogic.values.existingInferencePipelines).toEqual([]); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_logic.ts new file mode 100644 index 0000000000000..1c294ab4f3aad --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_logic.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { parseMlInferenceParametersFromPipeline } from '../../../../../../../common/ml_inference_pipeline'; + +import { getMLType } from '../../../shared/ml_inference/utils'; + +import { + MLInferenceLogic, + MLInferenceProcessorsActions, + MLInferenceProcessorsValues, +} from './ml_inference_logic'; +import { EXISTING_PIPELINE_DISABLED_MISSING_SOURCE_FIELDS } from './utils'; + +export interface MLInferencePipelineOption { + disabled: boolean; + disabledReason?: string; + modelId: string; + modelType: string; + pipelineName: string; + sourceFields: string[]; + indexFields: string[]; +} + +export interface PipelineSelectActions { + selectExistingPipeline: (pipelineName: string) => { + pipelineName: string; + }; + setInferencePipelineConfiguration: MLInferenceProcessorsActions['setInferencePipelineConfiguration']; +} + +export interface PipelineSelectValues { + addInferencePipelineModal: MLInferenceProcessorsValues['addInferencePipelineModal']; + existingInferencePipelines: MLInferencePipelineOption[]; + mlInferencePipelineProcessors: MLInferenceProcessorsValues['mlInferencePipelineProcessors']; + mlInferencePipelinesData: MLInferenceProcessorsValues['mlInferencePipelinesData']; + selectableModels: MLInferenceProcessorsValues['selectableModels']; + sourceFields: MLInferenceProcessorsValues['sourceFields']; +} + +export const PipelineSelectLogic = kea>({ + actions: { + selectExistingPipeline: (pipelineName: string) => ({ pipelineName }), + }, + connect: { + actions: [MLInferenceLogic, ['setInferencePipelineConfiguration']], + values: [ + MLInferenceLogic, + [ + 'addInferencePipelineModal', + 'mlInferencePipelineProcessors', + 'mlInferencePipelinesData', + 'selectableModels', + 'selectedModel', + 'sourceFields', + ], + ], + }, + path: ['enterprise_search', 'content', 'pipeline_select_logic'], + listeners: ({ actions, values }) => ({ + selectExistingPipeline: ({ pipelineName }) => { + const pipeline = values.mlInferencePipelinesData?.[pipelineName]; + if (!pipeline) return; + const params = parseMlInferenceParametersFromPipeline(pipelineName, pipeline); + if (params === null) return; + actions.setInferencePipelineConfiguration({ + existingPipeline: true, + modelID: params.model_id, + pipelineName, + fieldMappings: params.field_mappings, + targetField: '', + }); + }, + }), + selectors: ({ selectors }) => ({ + existingInferencePipelines: [ + () => [ + selectors.mlInferencePipelinesData, + selectors.sourceFields, + selectors.selectableModels, + selectors.mlInferencePipelineProcessors, + ], + ( + mlInferencePipelinesData: MLInferenceProcessorsValues['mlInferencePipelinesData'], + indexFields: MLInferenceProcessorsValues['sourceFields'], + selectableModels: MLInferenceProcessorsValues['selectableModels'], + mlInferencePipelineProcessors: MLInferenceProcessorsValues['mlInferencePipelineProcessors'] + ) => { + if (!mlInferencePipelinesData) { + return []; + } + const indexProcessorNames = + mlInferencePipelineProcessors?.map((processor) => processor.pipelineName) ?? []; + + const existingPipelines: MLInferencePipelineOption[] = Object.entries( + mlInferencePipelinesData + ) + .map(([pipelineName, pipeline]): MLInferencePipelineOption | undefined => { + if (!pipeline || indexProcessorNames.includes(pipelineName)) return undefined; + + // Parse configuration from pipeline definition + const pipelineParams = parseMlInferenceParametersFromPipeline(pipelineName, pipeline); + if (!pipelineParams) return undefined; + const { model_id: modelId, field_mappings: fieldMappings } = pipelineParams; + + const sourceFields = fieldMappings?.map((m) => m.sourceField) ?? []; + const missingSourceFields = sourceFields.filter((f) => !indexFields?.includes(f)) ?? []; + const mlModel = selectableModels.find((model) => model.modelId === modelId); + const modelType = mlModel ? getMLType(mlModel.types) : ''; + const disabledReason = + missingSourceFields.length > 0 + ? EXISTING_PIPELINE_DISABLED_MISSING_SOURCE_FIELDS(missingSourceFields.join(', ')) + : undefined; + + return { + disabled: disabledReason !== undefined, + disabledReason, + modelId, + modelType, + pipelineName, + sourceFields, + indexFields: indexFields ?? [], + }; + }) + .filter((p): p is MLInferencePipelineOption => p !== undefined); + + return existingPipelines; + }, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.test.tsx index fdf7da391fba6..66412779c9878 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.test.tsx @@ -13,7 +13,7 @@ import { EuiText, EuiTitle } from '@elastic/eui'; import { MLModelTypeBadge } from '../ml_model_type_badge'; -import { MLInferencePipelineOption } from './ml_inference_logic'; +import { MLInferencePipelineOption } from './pipeline_select_logic'; import { PipelineSelectOption, PipelineSelectOptionDisabled } from './pipeline_select_option'; import { MODEL_REDACTED_VALUE } from './utils'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.tsx index 49e00f30c12d4..f5fec5f54ac76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/pipeline_select_option.tsx @@ -11,7 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiTextColor, EuiTitle } f import { MLModelTypeBadge } from '../ml_model_type_badge'; -import { MLInferencePipelineOption } from './ml_inference_logic'; +import { MLInferencePipelineOption } from './pipeline_select_logic'; import { EXISTING_PIPELINE_DISABLED_MISSING_SOURCE_FIELDS, MODEL_REDACTED_VALUE } from './utils'; export interface PipelineSelectOptionProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.test.ts index 4ddf5b1c4b77a..1e4eb17744517 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.test.ts @@ -4,89 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { nerModel, textClassificationModel } from '../../../__mocks__/ml_models.mock'; -import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models'; - -import { - getMLType, - getModelDisplayTitle, - isSupportedMLModel, - sortSourceFields, - NLP_CONFIG_KEYS, -} from './utils'; +import { getMLType, getModelDisplayTitle, sortSourceFields, NLP_CONFIG_KEYS } from './utils'; describe('ml inference utils', () => { - describe('isSupportedMLModel', () => { - const makeFakeModel = ( - config: Partial - ): TrainedModelConfigResponse => { - const { inference_config: _throwAway, ...base } = nerModel; - return { - inference_config: {}, - ...base, - ...config, - }; - }; - it('returns true for expected models', () => { - const models: TrainedModelConfigResponse[] = [ - nerModel, - textClassificationModel, - makeFakeModel({ - inference_config: { - text_embedding: {}, - }, - model_id: 'mock-text_embedding', - }), - makeFakeModel({ - inference_config: { - zero_shot_classification: { - classification_labels: [], - }, - }, - model_id: 'mock-zero_shot_classification', - }), - makeFakeModel({ - inference_config: { - question_answering: {}, - }, - model_id: 'mock-question_answering', - }), - makeFakeModel({ - inference_config: { - fill_mask: {}, - }, - model_id: 'mock-fill_mask', - }), - makeFakeModel({ - inference_config: { - classification: {}, - }, - model_id: 'lang_ident_model_1', - model_type: 'lang_ident', - }), - ]; - - for (const model of models) { - expect(isSupportedMLModel(model)).toBe(true); - } - }); - - it('returns false for unexpected models', () => { - const models: TrainedModelConfigResponse[] = [ - makeFakeModel({}), - makeFakeModel({ - inference_config: { - fakething: {}, - }, - }), - ]; - - for (const model of models) { - expect(isSupportedMLModel(model)).toBe(false); - } - }); - }); describe('sortSourceFields', () => { it('promotes fields', () => { let fields: string[] = ['id', 'body', 'url']; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.ts index 3d97f52c659c1..88d273fcef135 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/ml_inference/utils.ts @@ -6,12 +6,9 @@ */ import { i18n } from '@kbn/i18n'; -import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models'; import { TRAINED_MODEL_TYPE, SUPPORTED_PYTORCH_TASKS } from '@kbn/ml-trained-models-utils'; -import { TrainedModel } from '../../../api/ml_models/ml_trained_models_logic'; - export const NLP_CONFIG_KEYS: string[] = Object.values(SUPPORTED_PYTORCH_TASKS); export const RECOMMENDED_FIELDS = ['body', 'body_content', 'title']; @@ -51,13 +48,6 @@ export const NLP_DISPLAY_TITLES: Record = { ), }; -export const isSupportedMLModel = (model: TrainedModelConfigResponse): boolean => { - return ( - Object.keys(model.inference_config || {}).some((key) => NLP_CONFIG_KEYS.includes(key)) || - model.model_type === TRAINED_MODEL_TYPE.LANG_IDENT - ); -}; - export const sortSourceFields = (a: string, b: string): number => { const promoteA = RECOMMENDED_FIELDS.includes(a); const promoteB = RECOMMENDED_FIELDS.includes(b); @@ -82,16 +72,3 @@ export const getMLType = (modelTypes: string[]): string => { }; export const getModelDisplayTitle = (type: string): string | undefined => NLP_DISPLAY_TITLES[type]; - -export const isTextExpansionModel = (model: TrainedModel): boolean => - Boolean(model.inference_config?.text_expansion); - -/** - * Sort function for displaying a list of models. Promotes text_expansion models and sorts the rest by model ID. - */ -export const sortModels = (m1: TrainedModel, m2: TrainedModel) => - isTextExpansionModel(m1) - ? -1 - : isTextExpansionModel(m2) - ? 1 - : m1.model_id.localeCompare(m2.model_id); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/add_data_panel_content.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/add_data_panel_content.tsx index e834e9ff45fe5..5d2d8cecf8466 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/add_data_panel_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/add_data_panel_content.tsx @@ -43,6 +43,7 @@ export const AddDataPanelContent: React.FC = ({ setSelectedLanguage={setSelectedLanguage} assetBasePath={assetBasePath} application={services.application} + consolePlugin={services.console} sharePlugin={services.share} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/search_query_panel_content.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/search_query_panel_content.tsx index 2ce2801f033e0..d32614865b614 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/search_query_panel_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started/panels/search_query_panel_content.tsx @@ -47,6 +47,7 @@ export const SearchQueryPanelContent: React.FC = ( setSelectedLanguage={setSelectedLanguage} assetBasePath={assetBasePath} application={services.application} + consolePlugin={services.console} sharePlugin={services.share} /> ); diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts index de2d2d2db3927..61c250ebc27e5 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts @@ -11,8 +11,6 @@ import { createConnector, fetchConnectorByIndexName, deleteConnectorById, - createConnectorSecret, - updateConnectorApiKeyId, } from '@kbn/search-connectors'; import { ErrorCode } from '../../../common/types/error_codes'; @@ -27,8 +25,6 @@ jest.mock('@kbn/search-connectors', () => ({ createConnector: jest.fn(), deleteConnectorById: jest.fn(), fetchConnectorByIndexName: jest.fn(), - createConnectorSecret: jest.fn(), - updateConnectorApiKeyId: jest.fn(), })); jest.mock('../crawler/fetch_crawlers', () => ({ fetchCrawlerByIndexName: jest.fn() })); jest.mock('../indices/generate_api_key', () => ({ generateApiKey: jest.fn() })); @@ -76,10 +72,7 @@ describe('addConnector lib function', () => { (fetchConnectorByIndexName as jest.Mock).mockImplementation(() => undefined); (fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined); mockClient.asCurrentUser.indices.getMapping.mockImplementation(() => connectorsIndicesMapping); - (generateApiKey as jest.Mock).mockImplementation(() => undefined); - (createConnectorSecret as jest.Mock).mockImplementation(() => undefined); - (updateConnectorApiKeyId as jest.Mock).mockImplementation(() => undefined); await expect( addConnector(mockClient as unknown as IScopedClusterClient, { @@ -108,8 +101,6 @@ describe('addConnector lib function', () => { // non-native connector should not generate API key or update secrets storage expect(generateApiKey).toBeCalledTimes(0); - expect(createConnectorSecret).toBeCalledTimes(0); - expect(updateConnectorApiKeyId).toBeCalledTimes(0); }); it('should add a native connector', async () => { @@ -122,13 +113,10 @@ describe('addConnector lib function', () => { (fetchConnectorByIndexName as jest.Mock).mockImplementation(() => undefined); (fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined); mockClient.asCurrentUser.indices.getMapping.mockImplementation(() => connectorsIndicesMapping); - (generateApiKey as jest.Mock).mockImplementation(() => ({ id: 'api-key-id', encoded: 'encoded-api-key', })); - (createConnectorSecret as jest.Mock).mockImplementation(() => ({ id: 'connector-secret-id' })); - (updateConnectorApiKeyId as jest.Mock).mockImplementation(() => ({ acknowledged: true })); await expect( addConnector(mockClient as unknown as IScopedClusterClient, { @@ -156,14 +144,7 @@ describe('addConnector lib function', () => { }); // native connector should generate API key and update secrets storage - expect(generateApiKey).toHaveBeenCalledWith(mockClient, 'index_name'); - expect(createConnectorSecret).toHaveBeenCalledWith(mockClient.asCurrentUser, 'encoded-api-key'); - expect(updateConnectorApiKeyId).toHaveBeenCalledWith( - mockClient.asCurrentUser, - 'fakeId', - 'api-key-id', - 'connector-secret-id' - ); + expect(generateApiKey).toHaveBeenCalledWith(mockClient, 'index_name', true, null); }); it('should reject if index already exists', async () => { @@ -254,13 +235,10 @@ describe('addConnector lib function', () => { (fetchConnectorByIndexName as jest.Mock).mockImplementation(() => ({ id: 'connectorId' })); (fetchCrawlerByIndexName as jest.Mock).mockImplementation(() => undefined); mockClient.asCurrentUser.indices.getMapping.mockImplementation(() => connectorsIndicesMapping); - (generateApiKey as jest.Mock).mockImplementation(() => ({ id: 'api-key-id', encoded: 'encoded-api-key', })); - (createConnectorSecret as jest.Mock).mockImplementation(() => ({ id: 'connector-secret-id' })); - (updateConnectorApiKeyId as jest.Mock).mockImplementation(() => ({ acknowledged: true })); await expect( addConnector(mockClient as unknown as IScopedClusterClient, { diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts index 3c5265234bb9a..0f15f2767079f 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts @@ -9,11 +9,9 @@ import { IScopedClusterClient } from '@kbn/core/server'; import { createConnector, - createConnectorSecret, Connector, ConnectorStatus, deleteConnectorById, - updateConnectorApiKeyId, } from '@kbn/search-connectors'; import { fetchConnectorByIndexName, NATIVE_CONNECTOR_DEFINITIONS } from '@kbn/search-connectors'; @@ -97,14 +95,7 @@ export const addConnector = async ( input.isNative && input.serviceType !== ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE ) { - const apiKey = await generateApiKey(client, index); - const connectorSecret = await createConnectorSecret(client.asCurrentUser, apiKey.encoded); - await updateConnectorApiKeyId( - client.asCurrentUser, - connector.id, - apiKey.id, - connectorSecret.id - ); + await generateApiKey(client, index, true, null); } return connector; diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.test.ts b/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.test.ts index 92c87b354a470..541566ee2d19d 100644 --- a/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.test.ts @@ -7,11 +7,22 @@ import { IScopedClusterClient } from '@kbn/core/server'; -import { CONNECTORS_INDEX } from '@kbn/search-connectors'; +import { + CONNECTORS_INDEX, + createConnectorSecret, + updateConnectorSecret, +} from '@kbn/search-connectors'; import { generateApiKey } from './generate_api_key'; -describe('generateApiKey lib function', () => { +jest.mock('@kbn/search-connectors', () => ({ + CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX: '.search-acl-filter-', + CONNECTORS_INDEX: '.elastic-connectors', + createConnectorSecret: jest.fn(), + updateConnectorSecret: jest.fn(), +})); + +describe('generateApiKey lib function for connector clients', () => { const mockClient = { asCurrentUser: { index: jest.fn(), @@ -47,9 +58,11 @@ describe('generateApiKey lib function', () => { encoded: 'encoded', id: 'apiKeyId', })); + (createConnectorSecret as jest.Mock).mockImplementation(() => undefined); + (updateConnectorSecret as jest.Mock).mockImplementation(() => undefined); await expect( - generateApiKey(mockClient as unknown as IScopedClusterClient, 'index_name') + generateApiKey(mockClient as unknown as IScopedClusterClient, 'index_name', false, null) ).resolves.toEqual({ encoded: 'encoded', id: 'apiKeyId' }); expect(mockClient.asCurrentUser.index).not.toHaveBeenCalled(); expect(mockClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({ @@ -66,6 +79,8 @@ describe('generateApiKey lib function', () => { }, }, }); + expect(createConnectorSecret).toBeCalledTimes(0); + expect(updateConnectorSecret).toBeCalledTimes(0); }); it('should create an API key plus connector for connectors', async () => { mockClient.asCurrentUser.search.mockImplementation(() => @@ -83,9 +98,11 @@ describe('generateApiKey lib function', () => { encoded: 'encoded', id: 'apiKeyId', })); + (createConnectorSecret as jest.Mock).mockImplementation(() => undefined); + (updateConnectorSecret as jest.Mock).mockImplementation(() => undefined); await expect( - generateApiKey(mockClient as unknown as IScopedClusterClient, 'search-test') + generateApiKey(mockClient as unknown as IScopedClusterClient, 'search-test', false, null) ).resolves.toEqual({ encoded: 'encoded', id: 'apiKeyId' }); expect(mockClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({ name: 'search-test-connector', @@ -107,6 +124,8 @@ describe('generateApiKey lib function', () => { index: CONNECTORS_INDEX, }); expect(mockClient.asCurrentUser.security.invalidateApiKey).not.toHaveBeenCalled(); + expect(createConnectorSecret).toBeCalledTimes(0); + expect(updateConnectorSecret).toBeCalledTimes(0); }); it('should invalidate API key if already defined', async () => { mockClient.asCurrentUser.search.mockImplementation(() => @@ -130,9 +149,11 @@ describe('generateApiKey lib function', () => { encoded: 'encoded', id: 'apiKeyId', })); + (createConnectorSecret as jest.Mock).mockImplementation(() => undefined); + (updateConnectorSecret as jest.Mock).mockImplementation(() => undefined); await expect( - generateApiKey(mockClient as unknown as IScopedClusterClient, 'index_name') + generateApiKey(mockClient as unknown as IScopedClusterClient, 'index_name', false, null) ).resolves.toEqual({ encoded: 'encoded', id: 'apiKeyId' }); expect(mockClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({ name: 'index_name-connector', @@ -154,7 +175,173 @@ describe('generateApiKey lib function', () => { index: CONNECTORS_INDEX, }); expect(mockClient.asCurrentUser.security.invalidateApiKey).toHaveBeenCalledWith({ - id: '1', + ids: ['1'], + }); + expect(createConnectorSecret).toBeCalledTimes(0); + expect(updateConnectorSecret).toBeCalledTimes(0); + }); +}); + +describe('generateApiKey lib function for native connectors', () => { + const mockClient = { + asCurrentUser: { + index: jest.fn(), + indices: { + create: jest.fn(), + }, + search: jest.fn(), + security: { + createApiKey: jest.fn(), + invalidateApiKey: jest.fn(), + }, + }, + asInternalUser: {}, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create an API key if index does not have a connector', async () => { + mockClient.asCurrentUser.search.mockImplementation(() => + Promise.resolve({ + hits: { + hits: [], + }, + }) + ); + mockClient.asCurrentUser.index.mockImplementation(() => ({ + _id: 'connectorId', + _source: 'Document', + })); + mockClient.asCurrentUser.security.createApiKey.mockImplementation(() => ({ + encoded: 'encoded', + id: 'apiKeyId', + })); + (createConnectorSecret as jest.Mock).mockImplementation(() => undefined); + (updateConnectorSecret as jest.Mock).mockImplementation(() => undefined); + + await expect( + generateApiKey(mockClient as unknown as IScopedClusterClient, 'index_name', true, null) + ).resolves.toEqual({ encoded: 'encoded', id: 'apiKeyId' }); + expect(mockClient.asCurrentUser.index).not.toHaveBeenCalled(); + expect(mockClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({ + name: 'index_name-connector', + role_descriptors: { + ['index-name-connector-role']: { + cluster: ['monitor'], + index: [ + { + names: ['index_name', '.search-acl-filter-index_name', `${CONNECTORS_INDEX}*`], + privileges: ['all'], + }, + ], + }, + }, + }); + expect(createConnectorSecret).toBeCalledTimes(0); + expect(updateConnectorSecret).toBeCalledTimes(0); + }); + it('should create an API key plus connector for connectors', async () => { + mockClient.asCurrentUser.search.mockImplementation(() => + Promise.resolve({ + hits: { + hits: [{ _id: 'connectorId', _source: { doc: 'doc' } }], + }, + }) + ); + mockClient.asCurrentUser.index.mockImplementation(() => ({ + _id: 'connectorId', + _source: 'Document', + })); + mockClient.asCurrentUser.security.createApiKey.mockImplementation(() => ({ + encoded: 'encoded', + id: 'apiKeyId', + })); + (createConnectorSecret as jest.Mock).mockImplementation(() => ({ + id: '1234', + })); + (updateConnectorSecret as jest.Mock).mockImplementation(() => undefined); + + await expect( + generateApiKey(mockClient as unknown as IScopedClusterClient, 'search-test', true, null) + ).resolves.toEqual({ encoded: 'encoded', id: 'apiKeyId' }); + expect(mockClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({ + name: 'search-test-connector', + role_descriptors: { + ['search-test-connector-role']: { + cluster: ['monitor'], + index: [ + { + names: ['search-test', '.search-acl-filter-search-test', `${CONNECTORS_INDEX}*`], + privileges: ['all'], + }, + ], + }, + }, + }); + expect(mockClient.asCurrentUser.index).toHaveBeenCalledWith({ + document: { api_key_id: 'apiKeyId', api_key_secret_id: '1234', doc: 'doc' }, + id: 'connectorId', + index: CONNECTORS_INDEX, + }); + expect(mockClient.asCurrentUser.security.invalidateApiKey).not.toHaveBeenCalled(); + expect(createConnectorSecret).toHaveBeenCalledWith(mockClient.asCurrentUser, 'encoded'); + expect(updateConnectorSecret).toBeCalledTimes(0); + }); + it('should invalidate API key if already defined', async () => { + mockClient.asCurrentUser.search.mockImplementation(() => + Promise.resolve({ + hits: { + hits: [ + { + _id: 'connectorId', + _source: { api_key_id: '1', doc: 'doc' }, + fields: { api_key_id: '1' }, + }, + ], + }, + }) + ); + mockClient.asCurrentUser.index.mockImplementation(() => ({ + _id: 'connectorId', + _source: 'Document', + })); + mockClient.asCurrentUser.security.createApiKey.mockImplementation(() => ({ + encoded: 'encoded', + id: 'apiKeyId', + })); + (createConnectorSecret as jest.Mock).mockImplementation(() => undefined); + (updateConnectorSecret as jest.Mock).mockImplementation(() => ({ + result: 'updated', + })); + + await expect( + generateApiKey(mockClient as unknown as IScopedClusterClient, 'index_name', true, '1234') + ).resolves.toEqual({ encoded: 'encoded', id: 'apiKeyId' }); + expect(mockClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({ + name: 'index_name-connector', + role_descriptors: { + ['index-name-connector-role']: { + cluster: ['monitor'], + index: [ + { + names: ['index_name', '.search-acl-filter-index_name', `${CONNECTORS_INDEX}*`], + privileges: ['all'], + }, + ], + }, + }, + }); + expect(mockClient.asCurrentUser.index).toHaveBeenCalledWith({ + document: { api_key_id: 'apiKeyId', api_key_secret_id: '1234', doc: 'doc' }, + id: 'connectorId', + index: CONNECTORS_INDEX, + }); + expect(mockClient.asCurrentUser.security.invalidateApiKey).toHaveBeenCalledWith({ + ids: ['1'], }); + expect(createConnectorSecret).toBeCalledTimes(0); + expect(updateConnectorSecret).toHaveBeenCalledWith(mockClient.asCurrentUser, 'encoded', '1234'); }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.ts b/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.ts index fb2ddbaad9f9d..01955cb004b24 100644 --- a/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.ts +++ b/x-pack/plugins/enterprise_search/server/lib/indices/generate_api_key.ts @@ -11,11 +11,18 @@ import { ConnectorDocument, CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX, CONNECTORS_INDEX, + createConnectorSecret, + updateConnectorSecret, } from '@kbn/search-connectors'; import { toAlphanumeric } from '../../../common/utils/to_alphanumeric'; -export const generateApiKey = async (client: IScopedClusterClient, indexName: string) => { +export const generateApiKey = async ( + client: IScopedClusterClient, + indexName: string, + isNative: boolean, + secretId: string | null +) => { const aclIndexName = `${CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX}${indexName}`; const apiKeyResult = await client.asCurrentUser.security.createApiKey({ @@ -32,20 +39,47 @@ export const generateApiKey = async (client: IScopedClusterClient, indexName: st }, }, }); + const connectorResult = await client.asCurrentUser.search({ index: CONNECTORS_INDEX, query: { term: { index_name: indexName } }, }); const connector = connectorResult.hits.hits[0]; if (connector) { - if (connector.fields?.api_key_id) { - await client.asCurrentUser.security.invalidateApiKey({ id: connector.fields.api_key_id }); + const apiKeyFields = isNative + ? { + api_key_id: apiKeyResult.id, + api_key_secret_id: await storeConnectorSecret(client, apiKeyResult.encoded, secretId), + } + : { + api_key_id: apiKeyResult.id, + }; + + if (connector._source?.api_key_id) { + await client.asCurrentUser.security.invalidateApiKey({ ids: [connector._source.api_key_id] }); } await client.asCurrentUser.index({ - document: { ...connector._source, api_key_id: apiKeyResult.id }, + document: { + ...connector._source, + ...apiKeyFields, + }, id: connector._id, index: CONNECTORS_INDEX, }); } return apiKeyResult; }; + +const storeConnectorSecret = async ( + client: IScopedClusterClient, + value: string, + secretId: string | null +) => { + if (secretId === null) { + const connectorSecretResult = await createConnectorSecret(client.asCurrentUser, value); + return connectorSecretResult.id; + } + + await updateConnectorSecret(client.asCurrentUser, value, secretId); + return secretId; +}; diff --git a/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.test.ts b/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.test.ts index bd02af095fe04..b96bfe6de8e7f 100644 --- a/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.test.ts @@ -72,12 +72,18 @@ describe('fetchMlModels', () => { inference_config: { text_embedding: {}, }, + input: { + fields: ['text_field'], + }, }, { model_id: 'model_1', inference_config: { text_classification: {}, }, + input: { + fields: ['text_field'], + }, }, ], }; @@ -160,24 +166,36 @@ describe('fetchMlModels', () => { inference_config: { text_embedding: {}, }, + input: { + fields: ['text_field'], + }, }, { model_id: E5_LINUX_OPTIMIZED_MODEL_ID, inference_config: { text_embedding: {}, }, + input: { + fields: ['text_field'], + }, }, { model_id: ELSER_MODEL_ID, inference_config: { text_expansion: {}, }, + input: { + fields: ['text_field'], + }, }, { model_id: ELSER_LINUX_OPTIMIZED_MODEL_ID, inference_config: { text_expansion: {}, }, + input: { + fields: ['text_field'], + }, }, ], }; @@ -210,24 +228,36 @@ describe('fetchMlModels', () => { inference_config: { text_embedding: {}, }, + input: { + fields: ['text_field'], + }, }, { model_id: E5_LINUX_OPTIMIZED_MODEL_ID, inference_config: { text_embedding: {}, }, + input: { + fields: ['text_field'], + }, }, { model_id: ELSER_MODEL_ID, inference_config: { text_expansion: {}, }, + input: { + fields: ['text_field'], + }, }, { model_id: ELSER_LINUX_OPTIMIZED_MODEL_ID, inference_config: { text_expansion: {}, }, + input: { + fields: ['text_field'], + }, }, ], }; @@ -265,18 +295,27 @@ describe('fetchMlModels', () => { inference_config: { text_expansion: {}, }, + input: { + fields: ['text_field'], + }, }, { model_id: E5_MODEL_ID, inference_config: { text_embedding: {}, }, + input: { + fields: ['text_field'], + }, }, { model_id: 'model_1', inference_config: { ner: {}, }, + input: { + fields: ['text_field'], + }, }, ], }; @@ -337,6 +376,9 @@ describe('fetchMlModels', () => { inference_config: { text_expansion: {}, }, + input: { + fields: ['text_field'], + }, }, ], }; @@ -385,18 +427,27 @@ describe('fetchMlModels', () => { inference_config: { ner: {}, // "Named Entity Recognition" }, + input: { + fields: ['text_field'], + }, }, { model_id: 'model_2', inference_config: { text_embedding: {}, // "Dense Vector Text Embedding" }, + input: { + fields: ['text_field'], + }, }, { model_id: 'model_3', inference_config: { text_classification: {}, // "Text Classification" }, + input: { + fields: ['text_field'], + }, }, ], }; diff --git a/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.ts b/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.ts index c1af4ab69c0bc..c39b6ec146ea1 100644 --- a/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.ts +++ b/x-pack/plugins/enterprise_search/server/lib/ml/fetch_ml_models.ts @@ -6,9 +6,12 @@ */ import { MlTrainedModelConfig, MlTrainedModelStats } from '@elastic/elasticsearch/lib/api/types'; + import { i18n } from '@kbn/i18n'; import { MlTrainedModels } from '@kbn/ml-plugin/server'; +import { getMlModelTypesForModelConfig } from '../../../common/ml_inference_pipeline'; + import { MlModelDeploymentState, MlModel } from '../../../common/types/ml'; import { @@ -109,39 +112,39 @@ export const fetchCompatiblePromotedModelIds = async (trainedModelsProvider: MlT }; const getModel = (modelConfig: MlTrainedModelConfig, modelStats?: MlTrainedModelStats): MlModel => { - { - const modelId = modelConfig.model_id; - const type = modelConfig.inference_config ? Object.keys(modelConfig.inference_config)[0] : ''; - const model = { - ...BASE_MODEL, - modelId, - type, - title: getUserFriendlyTitle(modelId, type), - isPromoted: [ - ELSER_MODEL_ID, - ELSER_LINUX_OPTIMIZED_MODEL_ID, - E5_MODEL_ID, - E5_LINUX_OPTIMIZED_MODEL_ID, - ].includes(modelId), - }; - - // Enrich deployment stats - if (modelStats && modelStats.deployment_stats) { - model.hasStats = true; - model.deploymentState = getDeploymentState( - modelStats.deployment_stats.allocation_status.state - ); - model.nodeAllocationCount = modelStats.deployment_stats.allocation_status.allocation_count; - model.targetAllocationCount = - modelStats.deployment_stats.allocation_status.target_allocation_count; - model.threadsPerAllocation = modelStats.deployment_stats.threads_per_allocation; - model.startTime = modelStats.deployment_stats.start_time; - } else if (model.modelId === LANG_IDENT_MODEL_ID) { - model.deploymentState = MlModelDeploymentState.FullyAllocated; - } - - return model; + const modelId = modelConfig.model_id; + const type = modelConfig.inference_config ? Object.keys(modelConfig.inference_config)[0] : ''; + const model = { + ...BASE_MODEL, + modelId, + type, + title: getUserFriendlyTitle(modelId, type), + description: modelConfig.description, + types: getMlModelTypesForModelConfig(modelConfig), + inputFieldNames: modelConfig.input.field_names, + version: modelConfig.version, + isPromoted: [ + ELSER_MODEL_ID, + ELSER_LINUX_OPTIMIZED_MODEL_ID, + E5_MODEL_ID, + E5_LINUX_OPTIMIZED_MODEL_ID, + ].includes(modelId), + }; + + // Enrich deployment stats + if (modelStats && modelStats.deployment_stats) { + model.hasStats = true; + model.deploymentState = getDeploymentState(modelStats.deployment_stats.allocation_status.state); + model.nodeAllocationCount = modelStats.deployment_stats.allocation_status.allocation_count; + model.targetAllocationCount = + modelStats.deployment_stats.allocation_status.target_allocation_count; + model.threadsPerAllocation = modelStats.deployment_stats.threads_per_allocation; + model.startTime = modelStats.deployment_stats.start_time; + } else if (model.modelId === LANG_IDENT_MODEL_ID) { + model.deploymentState = MlModelDeploymentState.FullyAllocated; } + + return model; }; const enrichModelWithDownloadStatus = async ( diff --git a/x-pack/plugins/enterprise_search/server/lib/ml/utils.ts b/x-pack/plugins/enterprise_search/server/lib/ml/utils.ts index 19a43059d3a08..d063fd158385a 100644 --- a/x-pack/plugins/enterprise_search/server/lib/ml/utils.ts +++ b/x-pack/plugins/enterprise_search/server/lib/ml/utils.ts @@ -60,6 +60,8 @@ export const BASE_MODEL = { threadsPerAllocation: 0, isPlaceholder: false, hasStats: false, + types: [], + inputFieldNames: [], }; export const ELSER_MODEL_PLACEHOLDER: MlModel = { diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts index 39657c97c6202..4226e4326ce0a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { deleteConnectorById, + deleteConnectorSecret, fetchConnectorById, fetchConnectors, fetchSyncJobs, @@ -589,18 +590,25 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) { const { shouldDeleteIndex } = request.query; let connectorResponse; - let indexNameToDelete; try { - if (shouldDeleteIndex) { - const connector = await fetchConnectorById(client.asCurrentUser, connectorId); - indexNameToDelete = connector?.value.index_name; - } + const connector = await fetchConnectorById(client.asCurrentUser, connectorId); + const indexNameToDelete = shouldDeleteIndex ? connector?.value.index_name : null; + const apiKeyId = connector?.value.api_key_id; + const secretId = connector?.value.api_key_secret_id; + connectorResponse = await deleteConnectorById(client.asCurrentUser, connectorId); + if (indexNameToDelete) { await deleteIndexPipelines(client, indexNameToDelete); await deleteAccessControlIndex(client, indexNameToDelete); await client.asCurrentUser.indices.delete({ index: indexNameToDelete }); } + if (apiKeyId) { + await client.asCurrentUser.security.invalidateApiKey({ ids: [apiKeyId] }); + } + if (secretId) { + await deleteConnectorSecret(client.asCurrentUser, secretId); + } } catch (error) { if (isResourceNotFoundException(error)) { return createError({ diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts index d96130fb02472..9872f4d7c7a66 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts @@ -14,7 +14,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { deleteConnectorById } from '@kbn/search-connectors'; +import { deleteConnectorById, deleteConnectorSecret } from '@kbn/search-connectors'; import { fetchConnectorByIndexName, fetchConnectors, @@ -204,6 +204,12 @@ export function registerIndexRoutes({ if (connector) { await deleteConnectorById(client.asCurrentUser, connector.id); + if (connector.api_key_id) { + await client.asCurrentUser.security.invalidateApiKey({ ids: [connector.api_key_id] }); + } + if (connector.api_key_secret_id) { + await deleteConnectorSecret(client.asCurrentUser, connector.api_key_secret_id); + } } await deleteIndexPipelines(client, indexName); @@ -272,6 +278,10 @@ export function registerIndexRoutes({ { path: '/internal/enterprise_search/indices/{indexName}/api_key', validate: { + body: schema.object({ + is_native: schema.boolean(), + secret_id: schema.maybe(schema.nullable(schema.string())), + }), params: schema.object({ indexName: schema.string(), }), @@ -279,9 +289,11 @@ export function registerIndexRoutes({ }, elasticsearchErrorHandler(log, async (context, request, response) => { const indexName = decodeURIComponent(request.params.indexName); + const { is_native: isNative, secret_id: secretId } = request.body; + const { client } = (await context.core).elasticsearch; - const apiKey = await generateApiKey(client, indexName); + const apiKey = await generateApiKey(client, indexName, isNative, secretId || null); return response.ok({ body: apiKey, diff --git a/x-pack/plugins/fleet/README.md b/x-pack/plugins/fleet/README.md index 07dd4bfd67c00..4fd48aa82aff6 100644 --- a/x-pack/plugins/fleet/README.md +++ b/x-pack/plugins/fleet/README.md @@ -38,6 +38,8 @@ Note: The plugin was previously named Ingest Manager, it's possible that some va These are some additional recommendations to the steps detailed in the [Kibana Developer Guide](https://docs.elastic.dev/kibana-dev-docs/getting-started/setup-dev-env). +Note: this section details how to run Kibana in stateful mode. For serverless development, see the [Developing Kibana in serverless mode](dev_docs/developing_kibana_and_fleet_server.md) guide. + 1. Create a `config/kibana.dev.yml` file by copying the existing `config/kibana.yml` file. 2. It is recommended to explicitly set a base path for Kibana (refer to [Considerations for basepath](https://www.elastic.co/guide/en/kibana/current/development-basepath.html) for details). To do this, add the following to your `kibana.dev.yml`: @@ -93,6 +95,8 @@ Refer to the [Running Elasticsearch during development](https://www.elastic.co/g It can be useful to run Fleet Server in a container on your local machine in order to free up your actual "bare metal" machine to run Elastic Agent for testing purposes. Otherwise, you'll only be able to a single instance of Elastic Agent dedicated to Fleet Server on your local machine, and this can make testing integrations and policies difficult. +Note: if you need to do simultaneous Kibana and Fleet Server development, refer to the [Developing Kibana and Fleet Server simulatanously](dev_docs/developing_kibana_and_fleet_server.md) guide. + _The following is adapted from the Fleet Server [README](https://github.com/elastic/fleet-server#running-elastic-agent-with-fleet-server-in-container)_ 1. Add the following configuration to your `kibana.dev.yml` diff --git a/x-pack/plugins/fleet/dev_docs/developing_kibana_and_fleet_server.md b/x-pack/plugins/fleet/dev_docs/developing_kibana_and_fleet_server.md index 376d6cbb739e6..745f205c72a03 100644 --- a/x-pack/plugins/fleet/dev_docs/developing_kibana_and_fleet_server.md +++ b/x-pack/plugins/fleet/dev_docs/developing_kibana_and_fleet_server.md @@ -402,11 +402,14 @@ _To do: add specific docs for enrolling Multipass agents and link here_ ## Running in serverless mode -If you want to run your local stack in serverless mode, you'll only need to alter the commands used to start Elasticsearch and Kibana. Fleet Server does not require any changes outside of what's listed above to run in a serverless context. From your Kibana, start a serverless Elasticsearch snapshot, and then run Kibana as either a security or observability project. +If you want to run your local stack in serverless mode, you'll only need to alter the commands used to start Elasticsearch and Kibana. Fleet Server does not require any changes outside of what's listed above to run in a serverless context. From your Kibana, start a serverless Elasticsearch snapshot as either a security or observability project, and then run Kibana using the same project type. ```bash -# Start Elasticsearch in serverless mode -yarn es serverless --kill +# Start Elasticsearch in serverless mode as a security project +yarn es serverless --projectType=security --kill + +# Start Elasticsearch in serverless mode as an observability project +yarn es serverless --projectType=oblt --kill # Run kibana as a security project yarn serverless-security diff --git a/x-pack/plugins/fleet/dev_docs/developing_kibana_in_serverless.md b/x-pack/plugins/fleet/dev_docs/developing_kibana_in_serverless.md new file mode 100644 index 0000000000000..f52ec7bcec700 --- /dev/null +++ b/x-pack/plugins/fleet/dev_docs/developing_kibana_in_serverless.md @@ -0,0 +1,122 @@ +# Developing Kibana in serverless mode + +Fleet is enabled for the observability and security serverless project types. + +To run Elasticsearch and Kibana in serverless mode, the relevant commands are: + +For the observability project type: +```bash +# Start Elasticsearch in serverless mode as an observability project +yarn es serverless --projectType=oblt --kill + +# Run Kibana as an observability project +yarn serverless-oblt +``` + +and one of: + +```bash +# Start Elasticsearch in serverless mode as a security project +yarn es serverless --projectType=security --kill + +# Run Kibana as a security project +yarn serverless-security +``` + +Once running, you can login at `http://localhost:5601` with the username `elastic_serverless` or `system_indices_superuser` and the password `changeme`. + +Note: it is not possible to use a base path in serverless mode. In case of issue, make sure the `server.basePath` property is not set in the config. + +Tip: to reset Elasticsearch data, delete the following folder: + +```bash +# Run this from the kibana folder: +rm -rf .es/stateless +``` + +## Kibana config + +### Setting a project id + +Create a `config/serverless.dev.yml` config file if you don't already have one and add a project id: + +```yaml +xpack.cloud.serverless.project_id: test-123 +``` + +The project id is required for some functionality, such as the `isServerless` flag in Fleet's cloud setup. + +### Fleet config + +The `kibana.dev.yml` settings should be mostly the same as for stateful mode. There are however a few key differences. + +As noted above, the base path should not be set (`server.basePath` setting). + +To enroll agents with a standalone Fleet Server set: +```yaml +xpack.fleet.internal.fleetServerStandalone: true +``` + +Finally, it may be necessary for the Fleet config to accurately reflect the generated config for serverless projects, which is defined in [project-controller](https://github.com/elastic/project-controller/blob/69dc1e6b0761bd9c933c23c2a471f32e1b8f1d28/internal/application/kibana/fleet_config.go#L43). At the time of writing, this concerns the default Fleet Server host and default output: + +```yaml +xpack.fleet.fleetServerHosts: + - id: default-fleet-server + name: Default Fleet server + is_default: true + host_urls: ['http://localhost:8220'] + # If you want to run a Fleet Server containers via Docker, use this URL: + # host_urls: ['https://host.docker.internal:8220'] +xpack.fleet.outputs: + - id: es-default-output + name: Default output + type: elasticsearch + is_default: true + is_default_monitoring: true + hosts: ['https://localhost:9200'] + # # If you enroll agents via Docker, use this URL: + # hosts: ['https://host.docker.internal:9200'] +``` + +## Running a Fleet Server and enrolling agents + +In serverless mode, Fleet Server runs in standalone mode. Unless you are [simultaneously developing Kibana and Fleet Server](./developing_kibana_and_fleet_server.mddeveloping_), it is easier to run Fleet Server as a Docker container. + +The Kibana's dev utils package defines a hard-coded [Fleet Server service token](ttps://github.com/elastic/kibana/blob/92b6fd64cd58fd62f69898c222e86409d5f15b60/packages/kbn-dev-utils/src/dev_service_account.ts#L21-L25) and fingerprint of the ca.crt certificate. + +Running a standalone Fleet Server: + +```bash +docker run -it --rm \ + -e ELASTICSEARCH_HOSTS="http://host.docker.internal:9200" \ + -e ELASTICSEARCH_SERVICE_TOKEN="AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL2ZsZWV0LXNlcnZlci1kZXY6VVo1TWd6MnFTX3FVTWliWGNXNzlwQQ" \ + -e ELASTICSEARCH_CA_TRUSTED_FINGERPRINT="F71F73085975FD977339A1909EBFE2DF40DB255E0D5BB56FC37246BF383FFC84" \ + -p 8220:8220 \ + docker.elastic.co/observability-ci/fleet-server:latest +``` + +Containerized elastic agents can then be enrolled using: + +```bash +docker run \ + -e FLEET_URL=http://host.docker.internal:8220 \ + -e FLEET_ENROLL=1 \ + -e FLEET_ENROLLMENT_TOKEN=== \ + -e FLEET_INSECURE=1 \ + --rm docker.elastic.co/beats/elastic-agent: +``` + +## Troubleshooting + +### `Cannot read existing Message Signing Key pair` issue + +At the time of writing, there is a known issue where Fleet may fail to load due to error generating key pair for message signing. This may in particular happen after running API integration tests. The easiest solution in development is to reset Elasticsearch data: + +```bash +# Run this from the kibana folder: +rm -rf .es/stateless +``` + +## Release + +Serverless Kibana is periodically released from `main` following a deployment workflow composed of four environments (CI, QA, Staging, Production). It is therefore important to be aware of the release schedule and ensure timely communication with our QA team prior to merging. diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx index cd00c194a6c11..3871473d40bae 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx @@ -105,7 +105,7 @@ export const AgentBulkActions: React.FunctionComponent = ({ const atLeastOneActiveAgentSelected = selectionMode === 'manual' ? !!selectedAgents.find((agent) => agent.active) - : agentCount > 0 && shownAgents > inactiveShownAgents; + : shownAgents > inactiveShownAgents; const agents = selectionMode === 'manual' ? selectedAgents : selectionQuery; const [tagsPopoverButton, setTagsPopoverButton] = useState(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.test.tsx index 6e948f45ea942..5640890b3f810 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.test.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act } from '@testing-library/react-hooks'; import { useStartServices } from '../../../../hooks'; import { ExperimentalFeaturesService } from '../../../../services'; +import { createFleetTestRendererMock } from '../../../../../../mock'; import { useFetchAgentsData } from './use_fetch_agents_data'; @@ -78,7 +79,6 @@ jest.mock('../../../../hooks', () => ({ pageSizeOptions: [5, 20, 50], setPagination: jest.fn(), }), - useUrlParams: jest.fn().mockReturnValue({ urlParams: { kuery: '' } }), })); describe('useFetchAgentsData', () => { @@ -97,10 +97,9 @@ describe('useFetchAgentsData', () => { }); it('should fetch agents and agent policies data', async () => { - let result: any | undefined; - let waitForNextUpdate: any | undefined; + const renderer = createFleetTestRendererMock(); + const { result, waitForNextUpdate } = renderer.renderHook(() => useFetchAgentsData()); await act(async () => { - ({ result, waitForNextUpdate } = renderHook(() => useFetchAgentsData())); await waitForNextUpdate(); }); @@ -139,4 +138,30 @@ describe('useFetchAgentsData', () => { expect(result?.current.pagination).toEqual({ currentPage: 1, pageSize: 5 }); expect(result?.current.pageSizeOptions).toEqual([5, 20, 50]); }); + + it('sync querystring kuery with current search', async () => { + const renderer = createFleetTestRendererMock(); + const { result, waitForNextUpdate } = renderer.renderHook(() => useFetchAgentsData()); + await act(async () => { + await waitForNextUpdate(); + }); + + expect(renderer.history.location.search).toEqual(''); + + // Set search + await act(async () => { + result.current.setSearch('active:true'); + await waitForNextUpdate(); + }); + + expect(renderer.history.location.search).toEqual('?kuery=active%3Atrue'); + + // Clear search + await act(async () => { + result.current.setSearch(''); + await waitForNextUpdate(); + }); + + expect(renderer.history.location.search).toEqual(''); + }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.tsx index 711a5fb91a9ba..ae0067a6af21a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.tsx @@ -7,6 +7,7 @@ import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { isEqual } from 'lodash'; +import { useHistory } from 'react-router-dom'; import { agentStatusesToSummary } from '../../../../../../../common/services'; @@ -33,14 +34,16 @@ export function useFetchAgentsData() { const { notifications } = useStartServices(); // useBreadcrumbs('agent_list'); - const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; + const history = useHistory(); + const { urlParams, toUrlParams } = useUrlParams(); + const defaultKuery: string = (urlParams.kuery as string) || ''; // Agent data states const [showUpgradeable, setShowUpgradeable] = useState(false); // Table and search states const [draftKuery, setDraftKuery] = useState(defaultKuery); - const [search, setSearch] = useState(defaultKuery); + const [search, setSearchState] = useState(defaultKuery); const { pagination, pageSizeOptions, setPagination } = usePagination(); const [sortField, setSortField] = useState('enrolled_at'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); @@ -65,6 +68,22 @@ export function useFetchAgentsData() { return selectedStatus.some((status) => status === 'inactive' || status === 'unenrolled'); }, [selectedStatus]); + const setSearch = useCallback( + (newVal: string) => { + setSearchState(newVal); + if (newVal.trim() === '' && !urlParams.kuery) { + return; + } + + if (urlParams.kuery !== newVal) { + history.replace({ + search: toUrlParams({ ...urlParams, kuery: newVal === '' ? undefined : newVal }), + }); + } + }, + [urlParams, history, toUrlParams] + ); + // filters kuery const kuery = useMemo(() => { return getKuery({ diff --git a/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts b/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts index ac72f56946d04..f8f2734444b25 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/parse.test.ts @@ -408,7 +408,7 @@ describe('parseAndVerifyArchive', () => { it('should throw on missing manifest file', () => { expect(() => parseAndVerifyArchive(['input_only-0.1.0/test/manifest.yml'], {})).toThrowError( new PackageInvalidArchiveError( - 'Package at top-level directory input_only-0.1.0 must contain a top-level manifest.yml file.' + 'Manifest file input_only-0.1.0/manifest.yml not found in paths.' ) ); }); diff --git a/x-pack/plugins/fleet/server/services/epm/archive/parse.ts b/x-pack/plugins/fleet/server/services/epm/archive/parse.ts index b7a2b8a26400c..b0b5d8a94f06f 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/parse.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/parse.ts @@ -220,7 +220,9 @@ export function parseAndVerifyArchive( logger.debug(`Verifying archive - checking manifest file and manifest buffer`); if (!paths.includes(manifestFile) || !manifestBuffer) { throw new PackageInvalidArchiveError( - `Package at top-level directory ${toplevelDir} must contain a top-level ${MANIFEST_NAME} file.` + !paths.includes(manifestFile) + ? `Manifest file ${manifestFile} not found in paths.` + : `Manifest buffer is not found in assets map for manifest file ${manifestFile}.` ); } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index f680a0bf004a6..58bcfcca386cf 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -1673,7 +1673,14 @@ describe('EPM template', () => { }, ]); - expect(esClient.indices.rollover).toHaveBeenCalled(); + expect(esClient.transport.request).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/test.prefix1-default/_rollover', + querystring: { + lazy: true, + }, + }) + ); }); it('should skip rollover on expected error when flag is on', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index da2b801548e18..01b1792dc5e79 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -946,8 +946,12 @@ const getDataStreams = async ( const rolloverDataStream = (dataStreamName: string, esClient: ElasticsearchClient) => { try { // Do no wrap rollovers in retryTransientEsErrors since it is not idempotent - return esClient.indices.rollover({ - alias: dataStreamName, + return esClient.transport.request({ + method: 'POST', + path: `/${dataStreamName}/_rollover`, + querystring: { + lazy: true, + }, }); } catch (error) { throw new PackageESError( diff --git a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts index da5ee0bcc8e5c..74dc880477ff9 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts @@ -110,7 +110,7 @@ export async function bulkInstallPackages({ result: { assets: [...installedEs, ...installedKibana], status: 'already_installed', - installType: installedPackageResult.installType, + installType: 'unknown', } as InstallResult, }; } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts index 2a5ae1346d6b3..97817b063b730 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts @@ -26,6 +26,7 @@ import { handleInstallPackageFailure, installAssetsForInputPackagePolicy, installPackage, + isPackageVersionOrLaterInstalled, } from './install'; import * as install from './_install_package'; import { getBundledPackageByPkgKey } from './bundled_packages'; @@ -715,3 +716,110 @@ describe('handleInstallPackageFailure', () => { ); }); }); + +describe('isPackageVersionOrLaterInstalled', () => { + beforeEach(() => { + jest.mocked(getInstallationObject).mockReset(); + }); + it('should return true if package is installed in the same version as expected', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + jest.mocked(getInstallationObject).mockResolvedValueOnce({ + attributes: { name: 'test', version: '1.0.0', install_status: 'installed' }, + } as any); + const res = await isPackageVersionOrLaterInstalled({ + savedObjectsClient, + pkgName: 'test', + pkgVersion: '1.0.0', + }); + + expect(res).toEqual( + expect.objectContaining({ + package: expect.objectContaining({ + name: 'test', + version: '1.0.0', + install_status: 'installed', + }), + }) + ); + }); + + it('should return true if package is installed in an higher version as expected', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + jest.mocked(getInstallationObject).mockResolvedValueOnce({ + attributes: { name: 'test', version: '1.2.0', install_status: 'installed' }, + } as any); + const res = await isPackageVersionOrLaterInstalled({ + savedObjectsClient, + pkgName: 'test', + pkgVersion: '1.0.0', + }); + + expect(res).toEqual( + expect.objectContaining({ + package: expect.objectContaining({ + name: 'test', + version: '1.2.0', + install_status: 'installed', + }), + }) + ); + }); + + it('should return false if package is installed in an lower version as expected', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + jest.mocked(getInstallationObject).mockResolvedValueOnce({ + attributes: { name: 'test', version: '0.9.0', install_status: 'installed' }, + } as any); + const res = await isPackageVersionOrLaterInstalled({ + savedObjectsClient, + pkgName: 'test', + pkgVersion: '1.0.0', + }); + + expect(res).toEqual(false); + }); + + it('should retry if package is currently installing', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + jest.mocked(getInstallationObject).mockResolvedValueOnce({ + attributes: { name: 'test', version: '1.0.0', install_status: 'installing' }, + } as any); + jest.mocked(getInstallationObject).mockResolvedValueOnce({ + attributes: { name: 'test', version: '1.0.0', install_status: 'installing' }, + } as any); + jest.mocked(getInstallationObject).mockResolvedValueOnce({ + attributes: { name: 'test', version: '1.0.0', install_status: 'installed' }, + } as any); + + const res = await isPackageVersionOrLaterInstalled({ + savedObjectsClient, + pkgName: 'test', + pkgVersion: '1.0.0', + }); + + expect(res).toEqual( + expect.objectContaining({ + package: expect.objectContaining({ + name: 'test', + version: '1.0.0', + install_status: 'installed', + }), + }) + ); + + expect(getInstallationObject).toBeCalledTimes(3); + }); + + it('should throw on unexpected error', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + jest.mocked(getInstallationObject).mockRejectedValueOnce(new Error('test unexpected error')); + + const res = isPackageVersionOrLaterInstalled({ + savedObjectsClient, + pkgName: 'test', + pkgVersion: '1.0.0', + }); + + await expect(res).rejects.toThrowError('test unexpected error'); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 073f93be81d6c..43b0c9d68a04c 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -87,6 +87,8 @@ import { addErrorToLatestFailedAttempts } from './install_errors_helpers'; import { installIndexTemplatesAndPipelines } from './install_index_template_pipeline'; import { optimisticallyAddEsAssetReferences } from './es_assets_reference'; +const MAX_ENSURE_INSTALL_TIME = 60 * 1000; + export async function isPackageInstalled(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; @@ -95,27 +97,53 @@ export async function isPackageInstalled(options: { return installedPackage !== undefined; } +// Error used to retry in isPackageVersionOrLaterInstalled +class CurrentlyInstallingError extends Error {} + +/** + * Check if a package is currently installed, + * if the package is currently installing it will retry until MAX_ENSURE_INSTALL_TIME is reached + */ export async function isPackageVersionOrLaterInstalled(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; pkgVersion: string; -}): Promise<{ package: Installation; installType: InstallType } | false> { - const { savedObjectsClient, pkgName, pkgVersion } = options; - const installedPackageObject = await getInstallationObject({ savedObjectsClient, pkgName }); - const installedPackage = installedPackageObject?.attributes; - if ( - installedPackage && - (installedPackage.version === pkgVersion || semverLt(pkgVersion, installedPackage.version)) - ) { - let installType: InstallType; - try { - installType = getInstallType({ pkgVersion, installedPkg: installedPackageObject }); - } catch (e) { - installType = 'unknown'; +}): Promise<{ package: Installation } | false> { + return pRetry( + async () => { + const { savedObjectsClient, pkgName, pkgVersion } = options; + const installedPackageObject = await getInstallationObject({ savedObjectsClient, pkgName }); + const installedPackage = installedPackageObject?.attributes; + if ( + installedPackage && + (installedPackage.version === pkgVersion || semverLt(pkgVersion, installedPackage.version)) + ) { + if (installedPackage.install_status === 'installing') { + throw new CurrentlyInstallingError( + `Package ${pkgName}-${pkgVersion} is currently installing` + ); + } else if (installedPackage.install_status === 'install_failed') { + return false; + } + + return { package: installedPackage }; + } + return false; + }, + { + maxRetryTime: MAX_ENSURE_INSTALL_TIME, + onFailedAttempt: (error) => { + if (!(error instanceof CurrentlyInstallingError)) { + throw error; + } + }, } - return { package: installedPackage, installType }; - } - return false; + ).catch((err): false => { + if (err instanceof CurrentlyInstallingError) { + return false; + } + throw err; + }); } export async function ensureInstalledPackage(options: { @@ -147,6 +175,7 @@ export async function ensureInstalledPackage(options: { pkgName: pkgKeyProps.name, pkgVersion: pkgKeyProps.version, }); + if (installedPackageResult) { return installedPackageResult.package; } diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index 821a6efad3491..991b7d6af6259 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -351,7 +351,7 @@ export const SearchBar: FC = (opts) => { 'data-test-subj': 'nav-search-popover', panelClassName: 'navSearch__panel', repositionOnScroll: true, - buttonRef: setButtonRef, + popoverRef: setButtonRef, panelStyle: { marginTop: '6px' }, }} popoverButton={ diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap index f9e7a78b3b44c..227f904ba7b57 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap @@ -44,13 +44,13 @@ exports[`policy table shows empty state when there are no policies 1`] = ` class="emotion-euiPageSection__content-l-center" >
+

+ Create your first index lifecycle policy +

+
-

- Create your first index lifecycle policy -

-
-
-

- An index lifecycle policy helps you manage your indices as they age. -

-
-
-
+
+ -
+ color="inherit" + data-euiicon-type="plusInCircle" + /> + Create policy + +
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx index a2bad060a5893..9de959e4d5e83 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx @@ -100,6 +100,9 @@ export const NodeAllocation: FunctionComponent = ({ ) => { component.update(); }; - const clickTemplateAction = ( + const clickTemplateAction = async ( templateName: TemplateDeserialized['name'], action: 'edit' | 'clone' | 'delete' ) => { @@ -76,7 +76,7 @@ const createActions = (testBed: TestBed) => { clickActionMenu(templateName); - act(() => { + await act(async () => { component.find('button.euiContextMenuItem').at(actions.indexOf(action)).simulate('click'); }); component.update(); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts index f052317513194..615b8df18f905 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts @@ -78,6 +78,26 @@ describe('Index Templates tab', () => { expect(exists('templateTable')).toBe(true); expect(exists('legacyTemplateTable')).toBe(false); }); + + test('Components column renders a link to Component templates', async () => { + httpRequestsMockHelpers.setLoadTemplatesResponse({ + templates: [ + fixtures.getComposableTemplate({ + name: 'Test', + composedOf: ['component1', 'component2'], + }), + ], + legacyTemplates: [], + }); + + await act(async () => { + testBed = await setup(httpSetup); + }); + const { exists, component } = testBed; + component.update(); + + expect(exists('componentTemplatesLink')).toBe(true); + }); }); describe('when there are index templates', () => { @@ -168,19 +188,18 @@ describe('Index Templates tab', () => { // Test composable table content tableCellsValues.forEach((row, i) => { const indexTemplate = templates[i]; - const { name, indexPatterns, ilmPolicy, composedOf, template } = indexTemplate; + const { name, indexPatterns, composedOf, template } = indexTemplate; const hasContent = !!template?.settings || !!template?.mappings || !!template?.aliases; - const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; - const composedOfString = composedOf ? composedOf.join(',') : ''; + const composedOfCount = `${composedOf ? composedOf.length : 0}`; try { expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ '', // Checkbox to select row name, indexPatterns.join(', '), - ilmPolicyName, - composedOfString, + composedOfCount, + '', // data stream column hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges 'EditDelete', // Column of actions ]); diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts index 5ec150a85aa17..80a5514969f54 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts @@ -23,6 +23,7 @@ export function serializeTemplate(templateDeserialized: TemplateDeserialized): T indexPatterns, template, composedOf, + ignoreMissingComponentTemplates, dataStream, _meta, allowAutoCreate, @@ -35,6 +36,7 @@ export function serializeTemplate(templateDeserialized: TemplateDeserialized): T index_patterns: indexPatterns, data_stream: dataStream, composed_of: composedOf, + ignore_missing_component_templates: ignoreMissingComponentTemplates, allow_auto_create: allowAutoCreate, _meta, }; @@ -52,6 +54,7 @@ export function deserializeTemplate( priority, _meta, composed_of: composedOf, + ignore_missing_component_templates: ignoreMissingComponentTemplates, data_stream: dataStream, deprecated, allow_auto_create: allowAutoCreate, @@ -76,6 +79,7 @@ export function deserializeTemplate( template, ilmPolicy: settings?.index?.lifecycle, composedOf: composedOf ?? [], + ignoreMissingComponentTemplates: ignoreMissingComponentTemplates ?? [], dataStream, allowAutoCreate, _meta, diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index 756d631202445..9205605c70010 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -23,6 +23,7 @@ export interface TemplateSerialized { }; deprecated?: boolean; composed_of?: string[]; + ignore_missing_component_templates?: string[]; version?: number; priority?: number; _meta?: { [key: string]: any }; @@ -45,6 +46,7 @@ export interface TemplateDeserialized { }; lifecycle?: DataRetention; composedOf?: string[]; // Composable template only + ignoreMissingComponentTemplates?: string[]; version?: number; priority?: number; // Composable template only allowAutoCreate?: boolean; @@ -91,6 +93,7 @@ export interface TemplateListItem { ilmPolicy?: { name: string; }; + composedOf?: string[]; _kbnMeta: { type: TemplateType; hasDatastream: boolean; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts index c4595998bcd20..a843f9fe28597 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts @@ -173,6 +173,24 @@ describe('', () => { }); }); + describe('if filter is set, component templates are filtered', () => { + test('search value is set if url param is set', async () => { + const filter = 'usedBy=(test_index_template_1)'; + await act(async () => { + testBed = await setup(httpSetup, { filter }); + }); + + testBed.component.update(); + + const { table } = testBed; + const search = testBed.actions.getSearchValue(); + expect(search).toBe(filter); + + const { rows } = table.getMetaData('componentTemplatesTable'); + expect(rows.length).toBe(1); + }); + }); + describe('No component templates', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadComponentTemplatesResponse([]); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts index 1bd0152c86d40..8ebc905543a26 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts @@ -19,13 +19,14 @@ import { BASE_PATH } from '../../../../../../../common'; import { WithAppDependencies } from './setup_environment'; import { ComponentTemplateList } from '../../../component_template_list/component_template_list'; -const testBedConfig: AsyncTestBedConfig = { +const getTestBedConfig = (props?: any): AsyncTestBedConfig => ({ memoryRouter: { initialEntries: [`${BASE_PATH}component_templates`], componentRoutePath: `${BASE_PATH}component_templates`, }, doMountAsync: true, -}; + defaultProps: props, +}); export type ComponentTemplateListTestBed = TestBed & { actions: ReturnType; @@ -73,18 +74,26 @@ const createActions = (testBed: TestBed) => { deleteButton.simulate('click'); }; + const getSearchValue = () => { + return find('componentTemplatesSearch').prop('defaultValue'); + }; + return { clickReloadButton, clickComponentTemplateAt, clickDeleteActionAt, clickTableColumnSortButton, + getSearchValue, }; }; -export const setup = async (httpSetup: HttpSetup): Promise => { +export const setup = async ( + httpSetup: HttpSetup, + props?: any +): Promise => { const initTestBed = registerTestBed( WithAppDependencies(ComponentTemplateList, httpSetup), - testBedConfig + getTestBedConfig(props) ); const testBed = await initTestBed(); @@ -104,7 +113,8 @@ export type ComponentTemplateTestSubjects = | 'sectionLoading' | 'componentTemplatesLoadError' | 'deleteComponentTemplateButton' - | 'deprecatedComponentTemplateBadge' | 'reloadButton' + | 'componentTemplatesSearch' + | 'deprecatedComponentTemplateBadge' | 'componentTemplatesFiltersButton' | 'componentTemplates--deprecatedFilter'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index fc309751bfe8d..65e369cc8c880 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -36,6 +36,7 @@ import { useRedirectPath } from '../../../hooks/redirect_path'; interface Props { componentTemplateName?: string; history: RouteComponentProps['history']; + filter?: string; } const { useGlobalFlyout } = GlobalFlyout; @@ -43,6 +44,7 @@ const { useGlobalFlyout } = GlobalFlyout; export const ComponentTemplateList: React.FunctionComponent = ({ componentTemplateName, history, + filter, }) => { const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = useGlobalFlyout(); @@ -183,6 +185,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ { const { executionContext } = useComponentTemplatesContext(); @@ -33,10 +35,17 @@ export const ComponentTemplateListContainer: React.FunctionComponent< page: 'indexManagementComponentTemplatesTab', }); + const urlParams = qs.parse(location.search); + const filter = urlParams.filter ?? ''; + return ( - + ); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index f4d9c55407fd9..7f6eb87566410 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -51,6 +51,7 @@ const deprecatedFilterLabel = i18n.translate( export interface Props { componentTemplates: ComponentTemplateListItem[]; + defaultFilter: string; onReloadClick: () => void; onDeleteClick: (componentTemplateName: string[]) => void; onEditClick: (componentTemplateName: string) => void; @@ -60,6 +61,7 @@ export interface Props { export const ComponentTable: FunctionComponent = ({ componentTemplates, + defaultFilter, onReloadClick, onDeleteClick, onEditClick, @@ -188,6 +190,7 @@ export const ComponentTable: FunctionComponent = ({ ], box: { incremental: true, + 'data-test-subj': 'componentTemplatesSearch', }, filters: [ { @@ -220,6 +223,7 @@ export const ComponentTable: FunctionComponent = ({ }, }, ], + defaultQuery: defaultFilter, }, pagination: { initialPageSize: 10, diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx index a0b4f07e61722..4a08a93c9a0c4 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx @@ -18,8 +18,8 @@ import { UseRequestResponse, reactRouterNavigate } from '../../../../../shared_i import { useServices } from '../../../../app_context'; import { TemplateDeleteModal } from '../../../../components'; import { TemplateContentIndicator } from '../../../../components/shared'; +import { getComponentTemplatesLink, getTemplateDetailsLink } from '../../../../services/routing'; import { TemplateTypeIndicator, TemplateDeprecatedBadge } from '../components'; -import { getTemplateDetailsLink } from '../../../../services/routing'; interface Props { templates: TemplateListItem[]; @@ -85,12 +85,24 @@ export const TemplateTable: React.FunctionComponent = ({ { field: 'composedOf', name: i18n.translate('xpack.idxMgmt.templateList.table.componentsColumnTitle', { - defaultMessage: 'Components', + defaultMessage: 'Component templates', }), + width: '100px', truncateText: true, - sortable: true, - width: '20%', - render: (composedOf: string[] = []) => {composedOf.join(', ')}, + sortable: (template) => { + return template.composedOf?.length; + }, + render: (composedOf: string[] = [], item: TemplateListItem) => + composedOf.length === 0 ? ( + 0 + ) : ( + + {composedOf.length} + + ), }, { name: i18n.translate('xpack.idxMgmt.templateList.table.dataStreamColumnTitle', { diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts index a2d4a03013556..07653d2591ffc 100644 --- a/x-pack/plugins/index_management/public/application/services/routing.ts +++ b/x-pack/plugins/index_management/public/application/services/routing.ts @@ -69,3 +69,12 @@ export const getIndexDetailsLink = ( } return link; }; + +export const getComponentTemplatesLink = (usedByTemplateName?: string) => { + let url = '/component_templates'; + if (usedByTemplateName) { + const filter = `usedBy=(${usedByTemplateName})`; + url = `${url}?filter=${encodeURIComponent(filter)}`; + } + return url; +}; diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts index b9020585ed676..1f5c5e2a3b82e 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts @@ -28,6 +28,7 @@ export const templateSchema = schema.object({ }) ), composedOf: schema.maybe(schema.arrayOf(schema.string())), + ignoreMissingComponentTemplates: schema.maybe(schema.arrayOf(schema.string())), dataStream: schema.maybe( schema.object( { diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index 01f83e2d76ff5..f7db386095b07 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -23,6 +23,7 @@ export const getComposableTemplate = ({ isLegacy = false, type = 'default', allowAutoCreate = false, + composedOf = [], }: Partial< TemplateDeserialized & { isLegacy?: boolean; @@ -53,6 +54,7 @@ export const getComposableTemplate = ({ hasDatastream, isLegacy, }, + composedOf, }; return indexTemplate; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx index 1fc8cc6614a75..4b7a1907e206a 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, type EuiAccordionProps } from '@elastic/eui'; import { useSummaryTimeRange } from '@kbn/observability-plugin/public'; import type { TimeRange } from '@kbn/es-query'; import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; @@ -24,6 +24,8 @@ import { ALERT_STATUS_ALL } from '../../../../common/alerts/constants'; import { AlertsSectionTitle } from '../../components/section_titles'; import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; import { CollapsibleSection } from './section/collapsible_section'; +import { AlertsClosedContent } from './alerts_closed_content'; +import { type AlertsCount } from '../../../../hooks/use_alerts_count'; export const AlertsSummaryContent = ({ assetName, @@ -37,6 +39,9 @@ export const AlertsSummaryContent = ({ const { featureFlags } = usePluginConfig(); const [isAlertFlyoutVisible, { toggle: toggleAlertFlyout }] = useBoolean(false); const { overrides } = useAssetDetailsRenderPropsContext(); + const [collapsibleStatus, setCollapsibleStatus] = + useState('open'); + const [activeAlertsCount, setActiveAlertsCount] = useState(undefined); const alertsEsQueryByStatus = useMemo( () => @@ -48,6 +53,14 @@ export const AlertsSummaryContent = ({ [assetName, dateRange] ); + const onLoaded = (alertsCount?: AlertsCount) => { + const { activeAlertCount = 0 } = alertsCount ?? {}; + const hasActiveAlerts = activeAlertCount > 0; + + setCollapsibleStatus(hasActiveAlerts ? 'open' : 'closed'); + setActiveAlertsCount(alertsCount?.activeAlertCount); + }; + return ( <> } + initialTriggerValue={collapsibleStatus} extraAction={ {featureFlags.inventoryThresholdAlertRuleEnabled && ( @@ -72,9 +87,12 @@ export const AlertsSummaryContent = ({ } > - + - {featureFlags.inventoryThresholdAlertRuleEnabled && ( void; } const MemoAlertSummaryWidget = React.memo( - ({ alertsQuery, dateRange }: MemoAlertSummaryWidgetProps) => { + ({ alertsQuery, dateRange, onLoaded }: MemoAlertSummaryWidgetProps) => { const { services } = useKibanaContextForPlugin(); const summaryTimeRange = useSummaryTimeRange(dateRange); @@ -112,6 +131,7 @@ const MemoAlertSummaryWidget = React.memo( featureIds={infraAlertFeatureIds} filter={alertsQuery} timeRange={summaryTimeRange} + onLoaded={onLoaded} fullSize hideChart /> diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts_closed_content.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts_closed_content.tsx new file mode 100644 index 0000000000000..a08a0313230ee --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts_closed_content.tsx @@ -0,0 +1,44 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; + +export const AlertsClosedContent = ({ activeAlertCount }: { activeAlertCount?: number }) => { + const shouldRenderAlertsClosedContent = typeof activeAlertCount === 'number'; + + if (!shouldRenderAlertsClosedContent) { + return null; + } + + if (activeAlertCount > 0) { + return ( + + + {activeAlertCount} + + + ); + } + + return ( + + {i18n.translate('xpack.infra.assetDetails.noActiveAlertsContentClosedSection', { + defaultMessage: 'No active alerts', + })} + + ); +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/section/collapsible_section.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/section/collapsible_section.tsx index ec31851d89a6d..da0b993199ee1 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/section/collapsible_section.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/section/collapsible_section.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import React, { useEffect, useState } from 'react'; + import { EuiAccordion, EuiFlexGroup, @@ -12,7 +14,6 @@ import { useGeneratedHtmlId, type EuiAccordionProps, } from '@elastic/eui'; -import React, { useState } from 'react'; export const CollapsibleSection = ({ title, @@ -22,6 +23,7 @@ export const CollapsibleSection = ({ collapsible, ['data-test-subj']: dataTestSubj, id, + initialTriggerValue, }: { title: React.FunctionComponent; closedSectionContent?: React.ReactNode; @@ -31,13 +33,18 @@ export const CollapsibleSection = ({ collapsible: boolean; ['data-test-subj']: string; id: string; + initialTriggerValue?: EuiAccordionProps['forceState']; }) => { const [trigger, setTrigger] = useState('open'); + useEffect(() => { + setTrigger(initialTriggerValue ?? 'open'); + }, [initialTriggerValue]); + const Title = title; const ButtonContent = () => closedSectionContent && trigger === 'closed' ? ( - + </EuiFlexItem> diff --git a/x-pack/plugins/infra/public/hooks/use_alerts_count.ts b/x-pack/plugins/infra/public/hooks/use_alerts_count.ts index 7d05a275d6eae..5c602d09b7d23 100644 --- a/x-pack/plugins/infra/public/hooks/use_alerts_count.ts +++ b/x-pack/plugins/infra/public/hooks/use_alerts_count.ts @@ -28,7 +28,7 @@ interface FetchAlertsCountParams { signal: AbortSignal; } -interface AlertsCount { +export interface AlertsCount { activeAlertCount: number; recoveredAlertCount: number; } diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/alerts_tab_badge.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/alerts_tab_badge.tsx index 15c4e568ad8ed..c3f778299ab69 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/alerts_tab_badge.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/alerts_tab_badge.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React from 'react'; -import { EuiIcon, EuiLoadingSpinner, EuiNotificationBadge, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiLoadingSpinner, EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useAlertsCount } from '../../../../../hooks/use_alerts_count'; import { infraAlertFeatureIds } from './config'; @@ -40,12 +40,12 @@ export const AlertsTabBadge = () => { typeof alertsCount?.activeAlertCount === 'number' && alertsCount.activeAlertCount > 0; return shouldRenderBadge ? ( - <EuiNotificationBadge + <EuiBadge + color="danger" className="eui-alignCenter" - size="m" data-test-subj="hostsView-tabs-alerts-count" > {alertsCount?.activeAlertCount} - </EuiNotificationBadge> + </EuiBadge> ) : null; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx index c5524bae4eb41..ce36a396b150f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx @@ -35,6 +35,9 @@ export const SurveyKubernetes = () => { defaultMessage="Tell us what you think! (K8s)" /> } + formConfig={{ + kibanaVersionQueryParam: 'entry.184582718', + }} /> {!isToastSeen && ( <EuiGlobalToastList 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 475862664c336..803fcbf169935 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 @@ -15,7 +15,11 @@ import type { LensPluginStartDependencies } from '../../../plugin'; import type { DatasourceMap, VisualizationMap } from '../../../types'; import { suggestionsApi } from '../../../lens_suggestions_api'; -export const getQueryColumns = async (query: AggregateQuery, deps: LensPluginStartDependencies) => { +export const getQueryColumns = async ( + query: AggregateQuery, + deps: LensPluginStartDependencies, + abortController?: AbortController +) => { // Fetching only columns for ES|QL for performance reasons with limit 0 // Important note: ES doesnt return the warnings for 0 limit, // I am skipping them in favor of performance now @@ -24,7 +28,12 @@ export const getQueryColumns = async (query: AggregateQuery, deps: LensPluginSta if ('esql' in performantQuery && performantQuery.esql) { performantQuery.esql = `${performantQuery.esql} | limit 0`; } - const table = await fetchFieldsFromESQL(performantQuery, deps.expressions); + const table = await fetchFieldsFromESQL( + performantQuery, + deps.expressions, + undefined, + abortController + ); return table?.columns; }; @@ -34,7 +43,8 @@ export const getSuggestions = async ( datasourceMap: DatasourceMap, visualizationMap: VisualizationMap, adHocDataViews: DataViewSpec[], - setErrors: (errors: Error[]) => void + setErrors: (errors: Error[]) => void, + abortController?: AbortController ) => { try { let indexPattern = ''; @@ -55,7 +65,7 @@ export const getSuggestions = async ( if (dataView.fields.getByName('@timestamp')?.type === 'date' && !dataViewSpec) { dataView.timeFieldName = '@timestamp'; } - const columns = await getQueryColumns(query, deps); + const columns = await getQueryColumns(query, deps, abortController); const context = { dataViewSpec: dataView?.toSpec(), fieldName: '', diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx index 834929d4ca2a5..3e2bf4f60aa2b 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx @@ -279,14 +279,15 @@ export function LensEditConfigurationFlyout({ const adHocDataViews = Object.values(attributes.state.adHocDataViews ?? {}); const runQuery = useCallback( - async (q) => { + async (q, abortController) => { const attrs = await getSuggestions( q, startDependencies, datasourceMap, visualizationMap, adHocDataViews, - setErrors + setErrors, + abortController ); if (attrs) { setCurrentAttributes?.(attrs); @@ -442,13 +443,13 @@ export function LensEditConfigurationFlyout({ hideMinimizeButton editorIsInline hideRunQueryText - disableSubmitAction={isEqual(query, prevQuery.current)} - onTextLangQuerySubmit={(q) => { + onTextLangQuerySubmit={async (q, a) => { if (q) { - runQuery(q); + await runQuery(q, a); } }} isDisabled={false} + allowQueryCancellation={true} /> </EuiFlexItem> )} diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts index d6a6701aa658a..7998ba5bda3c0 100644 --- a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts @@ -10,35 +10,17 @@ import { ColorMapping, EUIAmsterdamColorBlindPalette, ElasticBrandPalette, - NeutralPalette, + DEFAULT_COLOR_MAPPING_CONFIG, + DEFAULT_OTHER_ASSIGNMENT_INDEX, } from '@kbn/coloring'; import faker from 'faker'; -import { DEFAULT_NEUTRAL_PALETTE_INDEX } from '@kbn/coloring/src/shared_components/color_mapping/config/default_color_mapping'; -export const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = { - assignmentMode: 'auto', - assignments: [], - specialAssignments: [ - { - rule: { - type: 'other', - }, - color: { - type: 'categorical', - paletteId: NeutralPalette.id, - colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, - }, - touched: false, - }, - ], - paletteId: EUIAmsterdamColorBlindPalette.id, - colorMode: { - type: 'categorical', - }, -}; - -const exampleAssignment = (valuesCount = 1, type = 'categorical', overrides = {}) => { - const color = +const exampleAssignment = ( + valuesCount = 1, + type = 'categorical', + overrides = {} +): ColorMapping.Config['assignments'][number] => { + const color: ColorMapping.Config['assignments'][number]['color'] = type === 'categorical' ? { type: 'categorical', @@ -58,11 +40,10 @@ const exampleAssignment = (valuesCount = 1, type = 'categorical', overrides = {} color, touched: false, ...overrides, - } as ColorMapping.Config['assignments'][0]; + }; }; const MANUAL_COLOR_MAPPING_CONFIG: ColorMapping.Config = { - assignmentMode: 'manual', assignments: [ exampleAssignment(4), exampleAssignment(), @@ -90,7 +71,7 @@ const MANUAL_COLOR_MAPPING_CONFIG: ColorMapping.Config = { const specialAssignmentsPalette: ColorMapping.Config['specialAssignments'] = [ { - ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0], + ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX], color: { type: 'categorical', paletteId: EUIAmsterdamColorBlindPalette.id, @@ -100,7 +81,7 @@ const specialAssignmentsPalette: ColorMapping.Config['specialAssignments'] = [ ]; const specialAssignmentsCustom1: ColorMapping.Config['specialAssignments'] = [ { - ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0], + ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX], color: { type: 'colorCode', colorCode: '#501a0e', @@ -109,7 +90,7 @@ const specialAssignmentsCustom1: ColorMapping.Config['specialAssignments'] = [ ]; const specialAssignmentsCustom2: ColorMapping.Config['specialAssignments'] = [ { - ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0], + ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX], color: { type: 'colorCode', colorCode: 'red', @@ -129,11 +110,10 @@ describe('color_telemetry_helpers', () => { getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG, MANUAL_COLOR_MAPPING_CONFIG) ).toEqual([]); }); - it('settings (default): auto color mapping, unassigned terms neutral, default palette returns correct events', () => { + it('settings (default): unassigned terms loop, default palette returns correct events', () => { expect(getColorMappingTelemetryEvents(DEFAULT_COLOR_MAPPING_CONFIG)).toEqual([ - 'lens_color_mapping_auto', 'lens_color_mapping_palette_eui_amsterdam_color_blind', - 'lens_color_mapping_unassigned_terms_neutral', + 'lens_color_mapping_unassigned_terms_loop', ]); }); it('gradient event when user changed colorMode to gradient', () => { @@ -158,9 +138,8 @@ describe('color_telemetry_helpers', () => { ) ).toEqual(['lens_color_mapping_gradient']); }); - it('settings: manual mode, custom palette, unassigned terms from palette, 2 colors with 5 terms in total', () => { + it('settings: custom palette, unassigned terms from palette, 2 colors with 5 terms in total', () => { expect(getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG)).toEqual([ - 'lens_color_mapping_manual', 'lens_color_mapping_palette_elastic_brand_2023', 'lens_color_mapping_unassigned_terms_palette', 'lens_color_mapping_colors_2_to_4', @@ -170,7 +149,6 @@ describe('color_telemetry_helpers', () => { expect( getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG, DEFAULT_COLOR_MAPPING_CONFIG) ).toEqual([ - 'lens_color_mapping_manual', 'lens_color_mapping_palette_elastic_brand_2023', 'lens_color_mapping_unassigned_terms_palette', 'lens_color_mapping_colors_2_to_4', @@ -254,7 +232,7 @@ describe('color_telemetry_helpers', () => { }); describe('unassigned terms', () => { - it('unassigned terms changed from neutral to palette', () => { + it('unassigned terms changed from loop to palette', () => { expect( getColorMappingTelemetryEvents( { @@ -265,15 +243,15 @@ describe('color_telemetry_helpers', () => { ) ).toEqual(['lens_color_mapping_unassigned_terms_palette']); }); - it('unassigned terms changed from palette to neutral', () => { + it('unassigned terms changed from palette to loop', () => { expect( getColorMappingTelemetryEvents(DEFAULT_COLOR_MAPPING_CONFIG, { ...DEFAULT_COLOR_MAPPING_CONFIG, specialAssignments: specialAssignmentsPalette, }) - ).toEqual(['lens_color_mapping_unassigned_terms_neutral']); + ).toEqual(['lens_color_mapping_unassigned_terms_loop']); }); - it('unassigned terms changed from neutral to another custom color', () => { + it('unassigned terms changed from loop to another custom color', () => { expect( getColorMappingTelemetryEvents( { diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts index d6b7acab55c7f..5bbfaaf290ef3 100644 --- a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { ColorMapping, NeutralPalette } from '@kbn/coloring'; -import type { - CategoricalColor, - ColorCode, - GradientColor, -} from '@kbn/coloring/src/shared_components/color_mapping/config/types'; +import { ColorMapping, NeutralPalette, DEFAULT_OTHER_ASSIGNMENT_INDEX } from '@kbn/coloring'; import { isEqual } from 'lodash'; import { nonNullable } from '../utils'; @@ -24,17 +19,14 @@ export const getColorMappingTelemetryEvents = ( return []; } - const { assignments, specialAssignments, assignmentMode, colorMode, paletteId } = colorMapping; + const { assignments, specialAssignments, colorMode, paletteId } = colorMapping; const { - assignmentMode: prevAssignmentMode, assignments: prevAssignments, specialAssignments: prevSpecialAssignments, colorMode: prevColorMode, paletteId: prevPaletteId, } = prevColorMapping || {}; - const assignmentModeData = assignmentMode !== prevAssignmentMode ? assignmentMode : undefined; - const paletteData = prevPaletteId !== paletteId ? `palette_${paletteId}` : undefined; const gradientData = @@ -42,18 +34,16 @@ export const getColorMappingTelemetryEvents = ( const unassignedTermsType = getUnassignedTermsType(specialAssignments, prevSpecialAssignments); - const diffData = [assignmentModeData, gradientData, paletteData, unassignedTermsType].filter( - nonNullable - ); + const diffData = [gradientData, paletteData, unassignedTermsType].filter(nonNullable); - if (assignmentMode === 'manual') { + if (assignments.length > 0) { const colorCount = assignments.length && !isEqual(assignments, prevAssignments) ? `colors_${getRangeText(assignments.length)}` : undefined; - const prevCustomColors = prevAssignments?.filter((a) => isCustomColor(a.color)); - const customColors = assignments.filter((a) => isCustomColor(a.color)); + const prevCustomColors = prevAssignments?.filter((a) => a.color.type === 'colorCode'); + const customColors = assignments.filter((a) => a.color.type === 'colorCode'); const customColorEvent = customColors.length && !isEqual(prevCustomColors, customColors) ? `custom_colors_${getRangeText(customColors.length, 1)}` @@ -68,10 +58,6 @@ export const getColorMappingTelemetryEvents = ( const constructName = (eventName: string) => `${COLOR_MAPPING_PREFIX}${eventName}`; -const isCustomColor = (color: CategoricalColor | ColorCode | GradientColor): color is ColorCode => { - return color.type === 'colorCode'; -}; - function getRangeText(n: number, min = 2, max = 16) { if (n >= min && (n === 1 || n === 2)) { return String(n); @@ -92,9 +78,12 @@ const getUnassignedTermsType = ( ) => { return !isEqual(prevSpecialAssignments, specialAssignments) ? `unassigned_terms_${ - isCustomColor(specialAssignments?.[0].color) + specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX]?.color.type === 'colorCode' ? 'custom' - : specialAssignments?.[0].color.paletteId === NeutralPalette.id + : specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX]?.color.type === 'loop' + ? 'loop' + : specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX]?.color.paletteId === + NeutralPalette.id ? NeutralPalette.id : 'palette' }` diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 309efeb8ea827..b7b02bdbc934b 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createAction, createReducer, current, PayloadAction } from '@reduxjs/toolkit'; +import { createAction, createReducer, current } from '@reduxjs/toolkit'; import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import { mapValues, uniq } from 'lodash'; import { Filter, Query } from '@kbn/es-query'; @@ -320,986 +320,813 @@ export const lensActions = { export const makeLensReducer = (storeDeps: LensStoreDeps) => { const { datasourceMap, visualizationMap } = storeDeps; - return createReducer<LensAppState>(initialState, { - [setState.type]: (state, { payload }: PayloadAction<Partial<LensAppState>>) => { - return { - ...state, - ...payload, - }; - }, - [setExecutionContext.type]: (state, { payload }: PayloadAction<SetExecutionContextPayload>) => { - return { - ...state, - ...payload, - }; - }, - [initExisting.type]: (state, { payload }: PayloadAction<Partial<LensAppState>>) => { - return { - ...state, - ...payload, - }; - }, - [onActiveDataChange.type]: ( - state, - { payload: { activeData } }: PayloadAction<{ activeData: TableInspectorAdapter }> - ) => { - return { - ...state, - activeData, - }; - }, - [setSaveable.type]: (state, { payload }: PayloadAction<boolean>) => { - return { - ...state, - isSaveable: payload, - }; - }, - [enableAutoApply.type]: (state) => { - state.autoApplyDisabled = false; - }, - [disableAutoApply.type]: (state) => { - state.autoApplyDisabled = true; - state.changesApplied = true; - }, - [applyChanges.type]: (state) => { - if (typeof state.applyChangesCounter === 'undefined') { - state.applyChangesCounter = 0; - } - state.applyChangesCounter!++; - }, - [setChangesApplied.type]: (state, { payload: applied }) => { - state.changesApplied = applied; - }, - [cloneLayer.type]: ( - state, - { - payload: { layerId, newLayerId }, - }: { - payload: { - layerId: string; - newLayerId: string; + return createReducer<LensAppState>(initialState, (builder) => { + builder + .addCase(setState, (state, { payload }) => { + return { + ...state, + ...payload, }; - } - ) => { - const clonedIDsMap = new Map<string, string>(); - - const getNewId = (prevId: string) => { - const inMapValue = clonedIDsMap.get(prevId); - if (!inMapValue) { - const newId = generateId(); - clonedIDsMap.set(prevId, newId); - return newId; + }) + .addCase(setExecutionContext, (state, { payload }) => { + return { + ...state, + ...payload, + }; + }) + .addCase(initExisting, (state, { payload }) => { + return { + ...state, + ...payload, + }; + }) + .addCase(onActiveDataChange, (state, { payload: { activeData } }) => { + return { + ...state, + activeData, + }; + }) + .addCase(setSaveable, (state, { payload }) => { + return { + ...state, + isSaveable: payload, + }; + }) + .addCase(enableAutoApply, (state) => { + state.autoApplyDisabled = false; + }) + .addCase(disableAutoApply, (state) => { + state.autoApplyDisabled = true; + state.changesApplied = true; + }) + .addCase(applyChanges, (state) => { + if (typeof state.applyChangesCounter === 'undefined') { + state.applyChangesCounter = 0; } - return inMapValue; - }; + state.applyChangesCounter++; + }) + .addCase(setChangesApplied, (state, { payload: applied }) => { + state.changesApplied = applied; + }) + .addCase(cloneLayer, (state, { payload: { layerId, newLayerId } }) => { + const clonedIDsMap = new Map<string, string>(); + + const getNewId = (prevId: string) => { + const inMapValue = clonedIDsMap.get(prevId); + if (!inMapValue) { + const newId = generateId(); + clonedIDsMap.set(prevId, newId); + return newId; + } + return inMapValue; + }; - if (!state.activeDatasourceId || !state.visualization.activeId) { - return state; - } + if (!state.activeDatasourceId || !state.visualization.activeId) { + return state; + } - state.datasourceStates = mapValues(state.datasourceStates, (datasourceState, datasourceId) => - datasourceId - ? { - ...datasourceState, - state: datasourceMap[datasourceId].cloneLayer( - datasourceState.state, - layerId, - newLayerId, - getNewId - ), - } - : datasourceState - ); - state.visualization.state = visualizationMap[state.visualization.activeId].cloneLayer!( - state.visualization.state, - layerId, - newLayerId, - clonedIDsMap - ); - }, - [removeOrClearLayer.type]: ( - state, - { - payload: { visualizationId, layerId, layerIds }, - }: { - payload: { - visualizationId: string; - layerId: string; - layerIds: string[]; - }; - } - ) => { - const activeVisualization = visualizationMap[visualizationId]; - const activeDataSource = datasourceMap[state.activeDatasourceId!]; - const isOnlyLayer = - getRemoveOperation( - activeVisualization, + state.datasourceStates = mapValues( + state.datasourceStates, + (datasourceState, datasourceId) => + datasourceId + ? { + ...datasourceState, + state: datasourceMap[datasourceId].cloneLayer( + datasourceState.state, + layerId, + newLayerId, + getNewId + ), + } + : datasourceState + ); + state.visualization.state = visualizationMap[state.visualization.activeId].cloneLayer!( state.visualization.state, layerId, - layerIds.length - ) === 'clear'; + newLayerId, + clonedIDsMap + ); + }) + .addCase(removeOrClearLayer, (state, { payload: { visualizationId, layerId, layerIds } }) => { + const activeVisualization = visualizationMap[visualizationId]; + const activeDataSource = datasourceMap[state.activeDatasourceId!]; + const isOnlyLayer = + getRemoveOperation( + activeVisualization, + state.visualization.state, + layerId, + layerIds.length + ) === 'clear'; - let removedLayerIds: string[] = []; + let removedLayerIds: string[] = []; - state.datasourceStates = mapValues( - state.datasourceStates, - (datasourceState, datasourceId) => { - const datasource = datasourceMap[datasourceId!]; + state.datasourceStates = mapValues( + state.datasourceStates, + (datasourceState, datasourceId) => { + const datasource = datasourceMap[datasourceId!]; - const { newState, removedLayerIds: removedLayerIdsForThisDatasource } = isOnlyLayer - ? datasource.clearLayer(datasourceState.state, layerId) - : datasource.removeLayer(datasourceState.state, layerId); + const { newState, removedLayerIds: removedLayerIdsForThisDatasource } = isOnlyLayer + ? datasource.clearLayer(datasourceState.state, layerId) + : datasource.removeLayer(datasourceState.state, layerId); - removedLayerIds = [...removedLayerIds, ...removedLayerIdsForThisDatasource]; + removedLayerIds = [...removedLayerIds, ...removedLayerIdsForThisDatasource]; - return { - ...datasourceState, - ...(datasourceId === state.activeDatasourceId && { - state: newState, - }), - }; - } - ); - state.stagedPreview = undefined; - // reuse the activeDatasource current dataView id for the moment - const currentDataViewsId = activeDataSource.getUsedDataView( - state.datasourceStates[state.activeDatasourceId!].state - ); - - if (isOnlyLayer || !activeVisualization.removeLayer) { - state.visualization.state = activeVisualization.clearLayer( - state.visualization.state, - layerId, - currentDataViewsId + return { + ...datasourceState, + ...(datasourceId === state.activeDatasourceId && { + state: newState, + }), + }; + } + ); + state.stagedPreview = undefined; + // reuse the activeDatasource current dataView id for the moment + const currentDataViewsId = activeDataSource.getUsedDataView( + state.datasourceStates[state.activeDatasourceId!].state ); - } - uniq(removedLayerIds).forEach( - (removedId) => - (state.visualization.state = activeVisualization.removeLayer?.( + if (isOnlyLayer || !activeVisualization.removeLayer) { + state.visualization.state = activeVisualization.clearLayer( state.visualization.state, - removedId - )) - ); - }, - [changeIndexPattern.type]: ( - state, - { - payload, - }: { - payload: { - visualizationIds?: string; - datasourceIds?: string; - layerId?: string; - indexPatternId: string; - dataViews: Pick<DataViewsState, 'indexPatterns'>; - }; - } - ) => { - const { visualizationIds, datasourceIds, layerId, indexPatternId, dataViews } = payload; - const newIndexPatternRefs = [...state.dataViews.indexPatternRefs]; - const availableRefs = new Set(newIndexPatternRefs.map((ref) => ref.id)); - // check for missing refs - Object.values(dataViews.indexPatterns || {}).forEach((indexPattern) => { - if (!availableRefs.has(indexPattern.id)) { - newIndexPatternRefs.push({ - id: indexPattern.id!, - name: indexPattern.name, - title: indexPattern.title, - }); - } - }); - const newState: Partial<LensAppState> = { - dataViews: { - ...state.dataViews, - indexPatterns: dataViews.indexPatterns, - indexPatternRefs: newIndexPatternRefs, - }, - }; - if (visualizationIds?.length) { - for (const visualizationId of visualizationIds) { - const activeVisualization = - visualizationId && - state.visualization.activeId === visualizationId && - visualizationMap[visualizationId]; - if (activeVisualization && layerId && activeVisualization?.onIndexPatternChange) { - newState.visualization = { - ...state.visualization, - state: activeVisualization.onIndexPatternChange( - state.visualization.state, - indexPatternId, - layerId - ), - }; - } + layerId, + currentDataViewsId + ); } - } - if (datasourceIds?.length) { - newState.datasourceStates = { ...state.datasourceStates }; - const frame = selectFramePublicAPI( - { lens: { ...current(state), dataViews: newState.dataViews! } }, - datasourceMap - ); - const datasourceLayers = frame.datasourceLayers; - for (const datasourceId of datasourceIds) { - const activeDatasource = datasourceId && datasourceMap[datasourceId]; - if (activeDatasource && activeDatasource?.onIndexPatternChange) { - newState.datasourceStates = { - ...newState.datasourceStates, - [datasourceId]: { - isLoading: false, - state: activeDatasource.onIndexPatternChange( - newState.datasourceStates[datasourceId].state, - dataViews.indexPatterns, + uniq(removedLayerIds).forEach( + (removedId) => + (state.visualization.state = activeVisualization.removeLayer?.( + state.visualization.state, + removedId + )) + ); + }) + .addCase(changeIndexPattern, (state, { payload }) => { + const { visualizationIds, datasourceIds, layerId, indexPatternId, dataViews } = payload; + if (!dataViews.indexPatterns) { + throw new Error('Invariant: indexPatterns should be defined'); + } + const newIndexPatternRefs = [...state.dataViews.indexPatternRefs]; + const availableRefs = new Set(newIndexPatternRefs.map((ref) => ref.id)); + // check for missing refs + Object.values(dataViews.indexPatterns || {}).forEach((indexPattern) => { + if (!availableRefs.has(indexPattern.id)) { + newIndexPatternRefs.push({ + id: indexPattern.id!, + name: indexPattern.name, + title: indexPattern.title, + }); + } + }); + const newState: Partial<LensAppState> = { + dataViews: { + ...state.dataViews, + indexPatterns: dataViews.indexPatterns, + indexPatternRefs: newIndexPatternRefs, + }, + }; + if (visualizationIds?.length) { + for (const visualizationId of visualizationIds) { + const activeVisualization = + visualizationId && + state.visualization.activeId === visualizationId && + visualizationMap[visualizationId]; + if (activeVisualization && layerId && activeVisualization?.onIndexPatternChange) { + newState.visualization = { + ...state.visualization, + state: activeVisualization.onIndexPatternChange( + state.visualization.state, indexPatternId, layerId ), - }, - }; - // Update the visualization columns - if (layerId && state.visualization.activeId) { - const nextPublicAPI = activeDatasource.getPublicAPI({ - state: newState.datasourceStates[datasourceId].state, - layerId, - indexPatterns: dataViews.indexPatterns, - }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const datasourcePublicAPI = datasourceLayers[layerId]; - if (datasourcePublicAPI) { - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - const activeVisualization = visualizationMap[state.visualization.activeId]; - let nextVisState = (newState.visualization || state.visualization).state; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - frame, - }); + }; + } + } + } + if (datasourceIds?.length) { + newState.datasourceStates = { ...state.datasourceStates }; + const frame = selectFramePublicAPI( + { lens: { ...current(state), dataViews: newState.dataViews! } }, + datasourceMap + ); + const datasourceLayers = frame.datasourceLayers; + + for (const datasourceId of datasourceIds) { + const activeDatasource = datasourceId && datasourceMap[datasourceId]; + if (activeDatasource && activeDatasource?.onIndexPatternChange) { + newState.datasourceStates = { + ...newState.datasourceStates, + [datasourceId]: { + isLoading: false, + state: activeDatasource.onIndexPatternChange( + newState.datasourceStates[datasourceId].state, + dataViews.indexPatterns, + indexPatternId, + layerId + ), + }, + }; + // Update the visualization columns + if (layerId && state.visualization.activeId) { + const nextPublicAPI = activeDatasource.getPublicAPI({ + state: newState.datasourceStates[datasourceId].state, + layerId, + indexPatterns: dataViews.indexPatterns, }); - newState.visualization = { - ...state.visualization, - state: nextVisState, - }; + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const datasourcePublicAPI = datasourceLayers[layerId]; + if (datasourcePublicAPI) { + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + const activeVisualization = visualizationMap[state.visualization.activeId]; + let nextVisState = (newState.visualization || state.visualization).state; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + frame, + }); + }); + newState.visualization = { + ...state.visualization, + state: nextVisState, + }; + } } } } } - } - return { ...state, ...newState }; - }, - [updateIndexPatterns.type]: (state, { payload }: { payload: Partial<DataViewsState> }) => { - return { - ...state, - dataViews: { ...state.dataViews, ...payload }, - }; - }, - [replaceIndexpattern.type]: ( - state, - { payload }: { payload: { newIndexPattern: IndexPattern; oldId: string } } - ) => { - state.dataViews.indexPatterns[payload.newIndexPattern.id] = payload.newIndexPattern; - delete state.dataViews.indexPatterns[payload.oldId]; - state.dataViews.indexPatternRefs = state.dataViews.indexPatternRefs.filter( - (r) => r.id !== payload.oldId - ); - state.dataViews.indexPatternRefs.push({ - id: payload.newIndexPattern.id, - title: payload.newIndexPattern.title, - name: payload.newIndexPattern.name, - }); - const visualization = visualizationMap[state.visualization.activeId!]; - state.visualization.state = - visualization.onIndexPatternRename?.( - state.visualization.state, - payload.oldId, - payload.newIndexPattern.id - ) ?? state.visualization.state; - - Object.entries(state.datasourceStates).forEach(([datasourceId, datasourceState]) => { - const datasource = datasourceMap[datasourceId]; - state.datasourceStates[datasourceId].state = - datasource?.onIndexPatternRename?.( - datasourceState.state, - payload.oldId, - payload.newIndexPattern.id! - ) ?? datasourceState.state; - }); - }, - [updateDatasourceState.type]: ( - state, - { - payload, - }: { - payload: { - newDatasourceState: unknown; - datasourceId: string; - clearStagedPreview?: boolean; - dontSyncLinkedDimensions: boolean; + return { ...state, ...newState }; + }) + .addCase(updateIndexPatterns, (state, { payload }) => { + return { + ...state, + dataViews: { ...state.dataViews, ...payload }, }; - } - ) => { - if (payload.clearStagedPreview) { - state.stagedPreview = undefined; - } + }) + .addCase(replaceIndexpattern, (state, { payload }) => { + state.dataViews.indexPatterns[payload.newIndexPattern.id] = payload.newIndexPattern; + delete state.dataViews.indexPatterns[payload.oldId]; + state.dataViews.indexPatternRefs = state.dataViews.indexPatternRefs.filter( + (r) => r.id !== payload.oldId + ); + state.dataViews.indexPatternRefs.push({ + id: payload.newIndexPattern.id, + title: payload.newIndexPattern.title, + name: payload.newIndexPattern.name, + }); + const visualization = visualizationMap[state.visualization.activeId!]; + state.visualization.state = + visualization.onIndexPatternRename?.( + state.visualization.state, + payload.oldId, + payload.newIndexPattern.id + ) ?? state.visualization.state; + + Object.entries(state.datasourceStates).forEach(([datasourceId, datasourceState]) => { + const datasource = datasourceMap[datasourceId]; + state.datasourceStates[datasourceId].state = + datasource?.onIndexPatternRename?.( + datasourceState.state, + payload.oldId, + payload.newIndexPattern.id! + ) ?? datasourceState.state; + }); + }) + .addCase(updateDatasourceState, (state, { payload }) => { + if (payload.clearStagedPreview) { + state.stagedPreview = undefined; + } - state.datasourceStates[payload.datasourceId] = { - state: payload.newDatasourceState, - isLoading: false, - }; + state.datasourceStates[payload.datasourceId] = { + state: payload.newDatasourceState, + isLoading: false, + }; - if (payload.dontSyncLinkedDimensions) { - return; - } + if (payload.dontSyncLinkedDimensions) { + return; + } - const currentState = current(state); + const currentState = current(state); - const { - datasourceState: syncedDatasourceState, - visualizationState: syncedVisualizationState, - } = syncLinkedDimensions(currentState, visualizationMap, datasourceMap, payload.datasourceId); + const { + datasourceState: syncedDatasourceState, + visualizationState: syncedVisualizationState, + } = syncLinkedDimensions( + currentState, + visualizationMap, + datasourceMap, + payload.datasourceId + ); - state.visualization.state = syncedVisualizationState; - state.datasourceStates[payload.datasourceId].state = syncedDatasourceState; - }, - [updateVisualizationState.type]: ( - state, - { - payload, - }: { - payload: { - visualizationId: string; - newState: unknown; - dontSyncLinkedDimensions?: boolean; - }; - } - ) => { - if (!state.visualization.activeId) { - throw new Error('Invariant: visualization state got updated without active visualization'); - } - // This is a safeguard that prevents us from accidentally updating the - // wrong visualization. This occurs in some cases due to the uncoordinated - // way we manage state across plugins. - if (state.visualization.activeId !== payload.visualizationId) { - return state; - } + state.visualization.state = syncedVisualizationState; + state.datasourceStates[payload.datasourceId].state = syncedDatasourceState; + }) + .addCase(updateVisualizationState, (state, { payload }) => { + if (!state.visualization.activeId) { + throw new Error( + 'Invariant: visualization state got updated without active visualization' + ); + } + // This is a safeguard that prevents us from accidentally updating the + // wrong visualization. This occurs in some cases due to the uncoordinated + // way we manage state across plugins. + if (state.visualization.activeId !== payload.visualizationId) { + return state; + } - state.visualization.state = payload.newState; + state.visualization.state = payload.newState; - if (!state.activeDatasourceId) { - return; - } + if (!state.activeDatasourceId) { + return; + } - if (payload.dontSyncLinkedDimensions) { - return; - } + if (payload.dontSyncLinkedDimensions) { + return; + } - // TODO - consolidate into applySyncLinkedDimensions - const { - datasourceState: syncedDatasourceState, - visualizationState: syncedVisualizationState, - } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap); + // TODO - consolidate into applySyncLinkedDimensions + const { + datasourceState: syncedDatasourceState, + visualizationState: syncedVisualizationState, + } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap); - state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState; - state.visualization.state = syncedVisualizationState; - }, + state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState; + state.visualization.state = syncedVisualizationState; + }) - [switchVisualization.type]: ( - state, - { - payload, - }: { - payload: { - suggestion: { - newVisualizationId: string; - visualizationState: unknown; - datasourceState?: unknown; - datasourceId?: string; - }; - clearStagedPreview?: boolean; - }; - } - ) => { - const { newVisualizationId, visualizationState, datasourceState, datasourceId } = - payload.suggestion; - return { - ...state, - datasourceStates: datasourceId - ? { - ...state.datasourceStates, - [datasourceId]: { - ...state.datasourceStates[datasourceId], - state: datasourceState, + .addCase(switchVisualization, (state, { payload }) => { + const { newVisualizationId, visualizationState, datasourceState, datasourceId } = + payload.suggestion; + return { + ...state, + datasourceStates: datasourceId + ? { + ...state.datasourceStates, + [datasourceId]: { + ...state.datasourceStates[datasourceId], + state: datasourceState, + }, + } + : state.datasourceStates, + visualization: { + ...state.visualization, + activeId: newVisualizationId, + state: visualizationState, + }, + stagedPreview: payload.clearStagedPreview + ? undefined + : state.stagedPreview || { + datasourceStates: state.datasourceStates, + visualization: state.visualization, + activeData: state.activeData, }, - } - : state.datasourceStates, - visualization: { - ...state.visualization, - activeId: newVisualizationId, - state: visualizationState, - }, - stagedPreview: payload.clearStagedPreview - ? undefined - : state.stagedPreview || { - datasourceStates: state.datasourceStates, - visualization: state.visualization, - activeData: state.activeData, - }, - }; - }, - [rollbackSuggestion.type]: (state) => { - return { - ...state, - ...(state.stagedPreview || {}), - stagedPreview: undefined, - }; - }, - [setToggleFullscreen.type]: (state) => { - return { ...state, isFullscreenDatasource: !state.isFullscreenDatasource }; - }, - [submitSuggestion.type]: (state) => { - return { - ...state, - stagedPreview: undefined, - }; - }, - [switchDatasource.type]: ( - state, - { - payload, - }: { - payload: { - newDatasourceId: string; }; - } - ) => { - return { - ...state, - datasourceStates: { - ...state.datasourceStates, - [payload.newDatasourceId]: state.datasourceStates[payload.newDatasourceId] || { - state: null, - isLoading: true, + }) + .addCase(rollbackSuggestion, (state) => { + return { + ...state, + ...(state.stagedPreview || {}), + stagedPreview: undefined, + }; + }) + .addCase(setToggleFullscreen, (state) => { + return { ...state, isFullscreenDatasource: !state.isFullscreenDatasource }; + }) + .addCase(submitSuggestion, (state) => { + return { + ...state, + stagedPreview: undefined, + }; + }) + .addCase(switchDatasource, (state, { payload }) => { + return { + ...state, + datasourceStates: { + ...state.datasourceStates, + [payload.newDatasourceId]: state.datasourceStates[payload.newDatasourceId] || { + state: null, + isLoading: true, + }, }, - }, - activeDatasourceId: payload.newDatasourceId, - }; - }, - [switchAndCleanDatasource.type]: ( - state, - { - payload, - }: { - payload: { - newDatasourceId: string; - visualizationId?: string; - currentIndexPatternId?: string; + activeDatasourceId: payload.newDatasourceId, }; - } - ) => { - const activeVisualization = - payload.visualizationId && visualizationMap[payload.visualizationId]; - const visualization = state.visualization; - let newVizState = visualization.state; - const ids: string[] = []; - if (activeVisualization && activeVisualization.getLayerIds) { - const layerIds = activeVisualization.getLayerIds(visualization.state); - ids.push(...Object.values(layerIds)); - newVizState = activeVisualization.initialize(() => ids[0]); - } - const currentVizId = ids[0]; + }) + .addCase(switchAndCleanDatasource, (state, { payload }) => { + const activeVisualization = + payload.visualizationId && visualizationMap[payload.visualizationId]; + const visualization = state.visualization; + let newVizState = visualization.state; + const ids: string[] = []; + if (activeVisualization && activeVisualization.getLayerIds) { + const layerIds = activeVisualization.getLayerIds(visualization.state); + ids.push(...Object.values(layerIds)); + newVizState = activeVisualization.initialize(() => ids[0]); + } + const currentVizId = ids[0]; - const datasourceState = current(state).datasourceStates[payload.newDatasourceId] - ? current(state).datasourceStates[payload.newDatasourceId]?.state - : datasourceMap[payload.newDatasourceId].createEmptyLayer( - payload.currentIndexPatternId ?? '' - ); - const updatedState = datasourceMap[payload.newDatasourceId].insertLayer( - datasourceState, - currentVizId - ); + const datasourceState = current(state).datasourceStates[payload.newDatasourceId] + ? current(state).datasourceStates[payload.newDatasourceId]?.state + : datasourceMap[payload.newDatasourceId].createEmptyLayer( + payload.currentIndexPatternId ?? '' + ); + const updatedState = datasourceMap[payload.newDatasourceId].insertLayer( + datasourceState, + currentVizId + ); - return { - ...state, - datasourceStates: { - [payload.newDatasourceId]: { - state: updatedState, - isLoading: false, - }, - }, - activeDatasourceId: payload.newDatasourceId, - visualization: { - ...visualization, - state: newVizState, - }, - }; - }, - [navigateAway.type]: (state) => state, - [loadInitial.type]: ( - state, - payload: PayloadAction<{ - initialInput?: LensEmbeddableInput; - redirectCallback?: (savedObjectId?: string) => void; - history?: History<unknown>; - inlineEditing?: boolean; - }> - ) => state, - [initEmpty.type]: ( - state, - { - payload, - }: { - payload: { - newState: Partial<LensAppState>; - initialContext: VisualizeFieldContext | VisualizeEditorContext | undefined; - layerId: string; - }; - } - ) => { - const newState = { - ...state, - ...payload.newState, - }; - const suggestion: Suggestion | undefined = getVisualizeFieldSuggestions({ - datasourceMap, - datasourceStates: newState.datasourceStates, - visualizationMap, - visualizeTriggerFieldContext: payload.initialContext, - dataViews: newState.dataViews, - }); - if (suggestion) { return { - ...newState, + ...state, datasourceStates: { - ...newState.datasourceStates, - [suggestion.datasourceId!]: { - ...newState.datasourceStates[suggestion.datasourceId!], - state: suggestion.datasourceState, + [payload.newDatasourceId]: { + state: updatedState, + isLoading: false, }, }, + activeDatasourceId: payload.newDatasourceId, visualization: { - ...newState.visualization, - activeId: suggestion.visualizationId, - state: suggestion.visualizationState, + ...visualization, + state: newVizState, }, - stagedPreview: undefined, }; - } + }) + .addCase(navigateAway, (state) => state) + .addCase(loadInitial, (state, payload) => state) + .addCase(initEmpty, (state, { payload }) => { + const newState = { + ...state, + ...payload.newState, + }; + const suggestion: Suggestion | undefined = getVisualizeFieldSuggestions({ + datasourceMap, + datasourceStates: newState.datasourceStates, + visualizationMap, + visualizeTriggerFieldContext: payload.initialContext, + dataViews: newState.dataViews, + }); + if (suggestion) { + return { + ...newState, + datasourceStates: { + ...newState.datasourceStates, + [suggestion.datasourceId!]: { + ...newState.datasourceStates[suggestion.datasourceId!], + state: suggestion.datasourceState, + }, + }, + visualization: { + ...newState.visualization, + activeId: suggestion.visualizationId, + state: suggestion.visualizationState, + }, + stagedPreview: undefined, + }; + } - const visualization = newState.visualization; + const visualization = newState.visualization; - if (!visualization.activeId) { - throw new Error('Invariant: visualization state got updated without active visualization'); - } + if (!visualization.activeId) { + throw new Error( + 'Invariant: visualization state got updated without active visualization' + ); + } - const activeVisualization = visualizationMap[visualization.activeId]; - if (visualization.state === null && activeVisualization) { - const activeDatasourceId = getInitialDatasourceId(datasourceMap)!; - const newVisState = activeVisualization.initialize(() => payload.layerId); - const activeDatasource = datasourceMap[activeDatasourceId]; + const activeVisualization = visualizationMap[visualization.activeId]; + if (visualization.state === null && activeVisualization) { + const activeDatasourceId = getInitialDatasourceId(datasourceMap)!; + const newVisState = activeVisualization.initialize(() => payload.layerId); + const activeDatasource = datasourceMap[activeDatasourceId]; + return { + ...newState, + activeDatasourceId, + datasourceStates: { + ...newState.datasourceStates, + [activeDatasourceId]: { + ...newState.datasourceStates[activeDatasourceId], + state: activeDatasource.insertLayer( + newState.datasourceStates[activeDatasourceId]?.state, + payload.layerId + ), + }, + }, + visualization: { + ...visualization, + state: newVisState, + }, + }; + } + return newState; + }) + .addCase(editVisualizationAction, (state, { payload }) => { + if (!state.visualization.activeId) { + throw new Error( + 'Invariant: visualization state got updated without active visualization' + ); + } + // This is a safeguard that prevents us from accidentally updating the + // wrong visualization. This occurs in some cases due to the uncoordinated + // way we manage state across plugins. + if (state.visualization.activeId !== payload.visualizationId) { + return state; + } + const activeVisualization = visualizationMap[payload.visualizationId]; + if (activeVisualization?.onEditAction) { + state.visualization.state = activeVisualization.onEditAction( + state.visualization.state, + payload.event + ); + } + }) + .addCase(insertLayer, (state, { payload }) => { + const updater = datasourceMap[payload.datasourceId].insertLayer; return { - ...newState, - activeDatasourceId, + ...state, datasourceStates: { - ...newState.datasourceStates, - [activeDatasourceId]: { - ...newState.datasourceStates[activeDatasourceId], - state: activeDatasource.insertLayer( - newState.datasourceStates[activeDatasourceId]?.state, + ...state.datasourceStates, + [payload.datasourceId]: { + ...state.datasourceStates[payload.datasourceId], + state: updater( + current(state).datasourceStates[payload.datasourceId].state, payload.layerId ), }, }, - visualization: { - ...visualization, - state: newVisState, - }, - }; - } - return newState; - }, - [editVisualizationAction.type]: ( - state, - { - payload, - }: { - payload: { - visualizationId: string; - event: LensEditEvent<keyof LensEditContextMapping>; }; - } - ) => { - if (!state.visualization.activeId) { - throw new Error('Invariant: visualization state got updated without active visualization'); - } - // This is a safeguard that prevents us from accidentally updating the - // wrong visualization. This occurs in some cases due to the uncoordinated - // way we manage state across plugins. - if (state.visualization.activeId !== payload.visualizationId) { - return state; - } - const activeVisualization = visualizationMap[payload.visualizationId]; - if (activeVisualization?.onEditAction) { - state.visualization.state = activeVisualization.onEditAction( - state.visualization.state, - payload.event - ); - } - }, - [insertLayer.type]: ( - state, - { - payload, - }: { - payload: { - layerId: string; - datasourceId: string; - }; - } - ) => { - const updater = datasourceMap[payload.datasourceId].insertLayer; - return { - ...state, - datasourceStates: { - ...state.datasourceStates, - [payload.datasourceId]: { - ...state.datasourceStates[payload.datasourceId], - state: updater( - current(state).datasourceStates[payload.datasourceId].state, - payload.layerId - ), - }, - }, - }; - }, - [removeLayers.type]: ( - state, - { - payload: { visualizationId, layerIds }, - }: { - payload: { - visualizationId: VisualizationState['activeId']; - layerIds: string[]; - }; - } - ) => { - if (!state.visualization.activeId) { - throw new Error('Invariant: visualization state got updated without active visualization'); - } - - const activeVisualization = visualizationId && visualizationMap[visualizationId]; - - // This is a safeguard that prevents us from accidentally updating the - // wrong visualization. This occurs in some cases due to the uncoordinated - // way we manage state across plugins. - if ( - state.visualization.activeId === visualizationId && - activeVisualization && - activeVisualization.removeLayer && - state.visualization.state - ) { - const updater = layerIds.reduce( - (acc, layerId) => - activeVisualization.removeLayer ? activeVisualization.removeLayer(acc, layerId) : acc, - state.visualization.state - ); + }) + .addCase(removeLayers, (state, { payload: { visualizationId, layerIds } }) => { + if (!state.visualization.activeId) { + throw new Error( + 'Invariant: visualization state got updated without active visualization' + ); + } - state.visualization.state = - typeof updater === 'function' ? updater(current(state.visualization.state)) : updater; - } + const activeVisualization = visualizationId && visualizationMap[visualizationId]; - layerIds.forEach((layerId) => { - const [layerDatasourceId] = - Object.entries(datasourceMap).find(([datasourceId, datasource]) => { - return ( - state.datasourceStates[datasourceId] && - datasource.getLayers(state.datasourceStates[datasourceId].state).includes(layerId) - ); - }) ?? []; - if (layerDatasourceId) { - const { newState } = datasourceMap[layerDatasourceId].removeLayer( - current(state).datasourceStates[layerDatasourceId].state, - layerId + // This is a safeguard that prevents us from accidentally updating the + // wrong visualization. This occurs in some cases due to the uncoordinated + // way we manage state across plugins. + if ( + state.visualization.activeId === visualizationId && + activeVisualization && + activeVisualization.removeLayer && + state.visualization.state + ) { + const updater = layerIds.reduce( + (acc, layerId) => + activeVisualization.removeLayer ? activeVisualization.removeLayer(acc, layerId) : acc, + state.visualization.state ); - state.datasourceStates[layerDatasourceId].state = newState; - // TODO - call removeLayer for any extra (linked) layers removed by the datasource - } - }); - }, - [addLayer.type]: ( - state, - { - payload: { layerId, layerType, extraArg, ignoreInitialValues }, - }: { - payload: { - layerId: string; - layerType: LayerType; - extraArg: unknown; - ignoreInitialValues: boolean; - }; - } - ) => { - if (!state.activeDatasourceId || !state.visualization.activeId) { - return state; - } + state.visualization.state = + typeof updater === 'function' ? updater(current(state.visualization.state)) : updater; + } - const activeVisualization = visualizationMap[state.visualization.activeId]; - const activeDatasource = datasourceMap[state.activeDatasourceId]; - // reuse the active datasource dataView id for the new layer - const currentDataViewsId = activeDatasource.getUsedDataView( - state.datasourceStates[state.activeDatasourceId!].state - ); - const visualizationState = activeVisualization.appendLayer!( - state.visualization.state, - layerId, - layerType, - currentDataViewsId, - extraArg - ); + layerIds.forEach((layerId) => { + const [layerDatasourceId] = + Object.entries(datasourceMap).find(([datasourceId, datasource]) => { + return ( + state.datasourceStates[datasourceId] && + datasource.getLayers(state.datasourceStates[datasourceId].state).includes(layerId) + ); + }) ?? []; + if (layerDatasourceId) { + const { newState } = datasourceMap[layerDatasourceId].removeLayer( + current(state).datasourceStates[layerDatasourceId].state, + layerId + ); + state.datasourceStates[layerDatasourceId].state = newState; + // TODO - call removeLayer for any extra (linked) layers removed by the datasource + } + }); + }) - const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap); + .addCase( + addLayer, + (state, { payload: { layerId, layerType, extraArg, ignoreInitialValues } }) => { + if (!state.activeDatasourceId || !state.visualization.activeId) { + return state; + } - const { noDatasource } = - activeVisualization - .getSupportedLayers(visualizationState, framePublicAPI) - .find(({ type }) => type === layerType) || {}; - - const layersToLinkTo = - activeVisualization.getLayersToLinkTo?.(visualizationState, layerId) ?? []; - - const datasourceState = - !noDatasource && activeDatasource - ? activeDatasource.insertLayer( - state.datasourceStates[state.activeDatasourceId].state, - layerId, - layersToLinkTo - ) - : state.datasourceStates[state.activeDatasourceId].state; - - const { activeDatasourceState, activeVisualizationState } = ignoreInitialValues - ? { activeDatasourceState: datasourceState, activeVisualizationState: visualizationState } - : addInitialValueIfAvailable({ - datasourceState, - visualizationState, - framePublicAPI, - activeVisualization, - activeDatasource, + const activeVisualization = visualizationMap[state.visualization.activeId]; + const activeDatasource = datasourceMap[state.activeDatasourceId]; + // reuse the active datasource dataView id for the new layer + const currentDataViewsId = activeDatasource.getUsedDataView( + state.datasourceStates[state.activeDatasourceId!].state + ); + const visualizationState = activeVisualization.appendLayer!( + state.visualization.state, layerId, layerType, - }); + currentDataViewsId, + extraArg + ); - state.visualization.state = activeVisualizationState; - state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState; - state.stagedPreview = undefined; + const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap); + + const { noDatasource } = + activeVisualization + .getSupportedLayers(visualizationState, framePublicAPI) + .find(({ type }) => type === layerType) || {}; + + const layersToLinkTo = + activeVisualization.getLayersToLinkTo?.(visualizationState, layerId) ?? []; + + const datasourceState = + !noDatasource && activeDatasource + ? activeDatasource.insertLayer( + state.datasourceStates[state.activeDatasourceId].state, + layerId, + layersToLinkTo + ) + : state.datasourceStates[state.activeDatasourceId].state; + + const { activeDatasourceState, activeVisualizationState } = ignoreInitialValues + ? { + activeDatasourceState: datasourceState, + activeVisualizationState: visualizationState, + } + : addInitialValueIfAvailable({ + datasourceState, + visualizationState, + framePublicAPI, + activeVisualization, + activeDatasource, + layerId, + layerType, + }); - const { - datasourceState: syncedDatasourceState, - visualizationState: syncedVisualizationState, - } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap); + state.visualization.state = activeVisualizationState; + state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState; + state.stagedPreview = undefined; - state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState; - state.visualization.state = syncedVisualizationState; - }, - [onDropToDimension.type]: ( - state, - { - payload: { source, target, dropType }, - }: { - payload: { - source: DragDropIdentifier; - target: DragDropOperation; - dropType: DropType; - }; - } - ) => { - if (!state.visualization.activeId) { - return state; - } + const { + datasourceState: syncedDatasourceState, + visualizationState: syncedVisualizationState, + } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap); - const activeVisualization = visualizationMap[state.visualization.activeId]; - const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap); + state.datasourceStates[state.activeDatasourceId].state = syncedDatasourceState; + state.visualization.state = syncedVisualizationState; + } + ) + .addCase(onDropToDimension, (state, { payload: { source, target, dropType } }) => { + if (!state.visualization.activeId) { + return state; + } - const { groups } = activeVisualization.getConfiguration({ - layerId: target.layerId, - frame: framePublicAPI, - state: state.visualization.state, - }); + const activeVisualization = visualizationMap[state.visualization.activeId]; + const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap); - const [layerDatasourceId, layerDatasource] = - Object.entries(datasourceMap).find( - ([datasourceId, datasource]) => - state.datasourceStates[datasourceId] && - datasource - .getLayers(state.datasourceStates[datasourceId].state) - .includes(target.layerId) - ) || []; - - let newDatasourceState; - - if (layerDatasource && layerDatasourceId) { - newDatasourceState = layerDatasource?.onDrop({ - state: state.datasourceStates[layerDatasourceId].state, - source, - target: { - ...(target as unknown as DragDropOperation), - filterOperations: - groups.find(({ groupId: gId }) => gId === target.groupId)?.filterOperations || - Boolean, - }, - targetLayerDimensionGroups: groups, - dropType, - indexPatterns: framePublicAPI.dataViews.indexPatterns, + const { groups } = activeVisualization.getConfiguration({ + layerId: target.layerId, + frame: framePublicAPI, + state: state.visualization.state, }); - if (!newDatasourceState) { - return; + + const [layerDatasourceId, layerDatasource] = + Object.entries(datasourceMap).find( + ([datasourceId, datasource]) => + state.datasourceStates[datasourceId] && + datasource + .getLayers(state.datasourceStates[datasourceId].state) + .includes(target.layerId) + ) || []; + + let newDatasourceState; + + if (layerDatasource && layerDatasourceId) { + newDatasourceState = layerDatasource?.onDrop({ + state: state.datasourceStates[layerDatasourceId].state, + source, + target: { + ...(target as unknown as DragDropOperation), + filterOperations: + groups.find(({ groupId: gId }) => gId === target.groupId)?.filterOperations || + Boolean, + }, + targetLayerDimensionGroups: groups, + dropType, + indexPatterns: framePublicAPI.dataViews.indexPatterns, + }); + if (!newDatasourceState) { + return; + } + state.datasourceStates[layerDatasourceId].state = newDatasourceState; } - state.datasourceStates[layerDatasourceId].state = newDatasourceState; - } - activeVisualization.onDrop = activeVisualization.onDrop?.bind(activeVisualization); - const newVisualizationState = (activeVisualization.onDrop || onDropForVisualization)?.( - { - prevState: state.visualization.state, - frame: framePublicAPI, - target, - source, - dropType, - group: groups.find(({ groupId: gId }) => gId === target.groupId), - }, - activeVisualization - ); - state.visualization.state = newVisualizationState; + activeVisualization.onDrop = activeVisualization.onDrop?.bind(activeVisualization); + const newVisualizationState = (activeVisualization.onDrop || onDropForVisualization)?.( + { + prevState: state.visualization.state, + frame: framePublicAPI, + target, + source, + dropType, + group: groups.find(({ groupId: gId }) => gId === target.groupId), + }, + activeVisualization + ); + state.visualization.state = newVisualizationState; - if (layerDatasourceId) { - const { - datasourceState: syncedDatasourceState, - visualizationState: syncedVisualizationState, - } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap); + if (layerDatasourceId) { + const { + datasourceState: syncedDatasourceState, + visualizationState: syncedVisualizationState, + } = syncLinkedDimensions(current(state), visualizationMap, datasourceMap); - state.datasourceStates[layerDatasourceId].state = syncedDatasourceState; - state.visualization.state = syncedVisualizationState; - } - state.stagedPreview = undefined; - }, - [setLayerDefaultDimension.type]: ( - state, - { - payload: { layerId, columnId, groupId }, - }: { - payload: { - layerId: string; - columnId: string; - groupId: string; - }; - } - ) => { - if (!state.activeDatasourceId || !state.visualization.activeId) { - return state; - } + state.datasourceStates[layerDatasourceId].state = syncedDatasourceState; + state.visualization.state = syncedVisualizationState; + } + state.stagedPreview = undefined; + }) + .addCase(setLayerDefaultDimension, (state, { payload: { layerId, columnId, groupId } }) => { + if (!state.activeDatasourceId || !state.visualization.activeId) { + return state; + } - const activeDatasource = datasourceMap[state.activeDatasourceId]; - const activeVisualization = visualizationMap[state.visualization.activeId]; - const layerType = - activeVisualization.getLayerType(layerId, state.visualization.state) || LayerTypes.DATA; - const { activeDatasourceState, activeVisualizationState } = addInitialValueIfAvailable({ - datasourceState: state.datasourceStates[state.activeDatasourceId].state, - visualizationState: state.visualization.state, - framePublicAPI: selectFramePublicAPI({ lens: current(state) }, datasourceMap), - activeVisualization, - activeDatasource, - layerId, - layerType, - columnId, - groupId, - }); + const activeDatasource = datasourceMap[state.activeDatasourceId]; + const activeVisualization = visualizationMap[state.visualization.activeId]; + const layerType = + activeVisualization.getLayerType(layerId, state.visualization.state) || LayerTypes.DATA; + const { activeDatasourceState, activeVisualizationState } = addInitialValueIfAvailable({ + datasourceState: state.datasourceStates[state.activeDatasourceId].state, + visualizationState: state.visualization.state, + framePublicAPI: selectFramePublicAPI({ lens: current(state) }, datasourceMap), + activeVisualization, + activeDatasource, + layerId, + layerType, + columnId, + groupId, + }); - state.visualization.state = activeVisualizationState; - state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState; - }, - [removeDimension.type]: ( - state, - { - payload: { layerId, columnId, datasourceId }, - }: { - payload: { - layerId: string; - columnId: string; - datasourceId?: string; - }; - } - ) => { - if (!state.visualization.activeId) { - return state; - } + state.visualization.state = activeVisualizationState; + state.datasourceStates[state.activeDatasourceId].state = activeDatasourceState; + }) + .addCase(removeDimension, (state, { payload: { layerId, columnId, datasourceId } }) => { + if (!state.visualization.activeId) { + return state; + } - const activeVisualization = visualizationMap[state.visualization.activeId]; + const activeVisualization = visualizationMap[state.visualization.activeId]; - const links = activeVisualization.getLinkedDimensions?.(state.visualization.state); + const links = activeVisualization.getLinkedDimensions?.(state.visualization.state); - const linkedDimensions = links - ?.filter(({ from: { columnId: fromId } }) => columnId === fromId) - ?.map(({ to }) => to); + const linkedDimensions = links + ?.filter(({ from: { columnId: fromId } }) => columnId === fromId) + ?.map(({ to }) => to); - const datasource = datasourceId ? datasourceMap[datasourceId] : undefined; + const datasource = datasourceId ? datasourceMap[datasourceId] : undefined; - const frame = selectFramePublicAPI({ lens: current(state) }, datasourceMap); + const frame = selectFramePublicAPI({ lens: current(state) }, datasourceMap); - const remove = (dimensionProps: { layerId: string; columnId: string }) => { - if (datasource && datasourceId) { - let datasourceState; + const remove = (dimensionProps: { layerId: string; columnId: string }) => { + if (datasource && datasourceId) { + let datasourceState; + try { + datasourceState = current(state.datasourceStates[datasourceId].state); + } catch { + datasourceState = state.datasourceStates[datasourceId].state; + } + state.datasourceStates[datasourceId].state = datasource?.removeColumn({ + layerId: dimensionProps.layerId, + columnId: dimensionProps.columnId, + prevState: datasourceState, + indexPatterns: frame.dataViews.indexPatterns, + }); + } + + let visualizationState; try { - datasourceState = current(state.datasourceStates[datasourceId].state); + visualizationState = current(state.visualization.state); } catch { - datasourceState = state.datasourceStates[datasourceId].state; + visualizationState = state.visualization.state; } - state.datasourceStates[datasourceId].state = datasource?.removeColumn({ + state.visualization.state = activeVisualization.removeDimension({ layerId: dimensionProps.layerId, columnId: dimensionProps.columnId, - prevState: datasourceState, - indexPatterns: frame.dataViews.indexPatterns, + prevState: visualizationState, + frame, }); - } - - let visualizationState; - try { - visualizationState = current(state.visualization.state); - } catch { - visualizationState = state.visualization.state; - } - state.visualization.state = activeVisualization.removeDimension({ - layerId: dimensionProps.layerId, - columnId: dimensionProps.columnId, - prevState: visualizationState, - frame, - }); - }; + }; - remove({ layerId, columnId }); + remove({ layerId, columnId }); - linkedDimensions?.forEach( - (linkedDimension) => - linkedDimension.columnId && // if there's no columnId, there's no dimension to remove - remove({ columnId: linkedDimension.columnId, layerId: linkedDimension.layerId }) - ); - }, - [registerLibraryAnnotationGroup.type]: ( - state, - { - payload: { group, id }, - }: { - payload: { group: EventAnnotationGroupConfig; id: string }; - } - ) => { - state.annotationGroups[id] = group; - }, + linkedDimensions?.forEach( + (linkedDimension) => + linkedDimension.columnId && // if there's no columnId, there's no dimension to remove + remove({ columnId: linkedDimension.columnId, layerId: linkedDimension.layerId }) + ); + }) + .addCase(registerLibraryAnnotationGroup, (state, { payload: { group, id } }) => { + state.annotationGroups[id] = group; + }) + .addDefaultCase((state) => state); }); }; diff --git a/x-pack/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts index 20822ee76a094..e20b60a11c57a 100644 --- a/x-pack/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -36,13 +36,14 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ clientOptions: { update: { overwrite: true } }, client: getLensClient, toListItem(savedObject) { - const { id, type, updatedAt, attributes } = savedObject; + const { id, type, updatedAt, attributes, managed } = savedObject; const { title, description } = attributes as { title: string; description?: string }; return { id, title, description, updatedAt, + managed, editor: { editUrl: getEditPath(id), editApp: 'lens' }, icon: 'lensApp', stage: 'production', diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx index dda07ef2c41c8..963e471e912f7 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx @@ -7,7 +7,13 @@ import { Ast } from '@kbn/interpreter'; import { buildExpression } from '@kbn/expressions-plugin/public'; -import { createMockDatasource, createMockFramePublicAPI, DatasourceMock } from '../../mocks'; +import { + createMockDatasource, + createMockFramePublicAPI, + DatasourceMock, + generateActiveData, +} from '../../mocks'; +import faker from 'faker'; import { DatatableVisualizationState, getDatatableVisualization } from './visualization'; import { Operation, @@ -15,6 +21,7 @@ import { FramePublicAPI, TableSuggestionColumn, VisualizationDimensionGroupConfig, + VisualizationConfigProps, } from '../../types'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; @@ -408,6 +415,68 @@ describe('Datatable Visualization', () => { ).toEqual([{ columnId: 'c' }, { columnId: 'b' }]); }); + describe('with palette', () => { + let params: VisualizationConfigProps<DatatableVisualizationState>; + beforeEach(() => { + const datasource = createMockDatasource('test'); + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'b', fields: [] }]); + params = { + layerId: 'a', + state: { + layerId: 'a', + layerType: LayerTypes.DATA, + columns: [ + { + columnId: 'b', + palette: { + type: 'palette' as const, + name: '', + params: { stops: [{ color: 'blue', stop: 0 }] }, + }, + }, + ], + }, + frame: { + ...mockFrame(), + activeData: generateActiveData([ + { + id: 'a', + rows: Array(3).fill({ + b: faker.random.number(), + }), + }, + ]), + datasourceLayers: { a: datasource.publicAPIMock }, + }, + }; + }); + + it('does include palette for accessor config if the values are numeric and palette exists', () => { + expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([ + { columnId: 'b', palette: ['blue'], triggerIconType: 'colorBy' }, + ]); + }); + it('does not include palette for accessor config if the values are not numeric and palette exists', () => { + params.frame.activeData = generateActiveData([ + { + id: 'a', + rows: Array(3).fill({ + b: faker.random.word(), + }), + }, + ]); + expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([ + { columnId: 'b' }, + ]); + }); + it('does not include palette for accessor config if the values are numeric but palette exists', () => { + params.state.columns[0].palette = undefined; + expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([ + { columnId: 'b' }, + ]); + }); + }); + it('should compute the groups correctly for text based languages', () => { const datasource = createMockDatasource('textBased', { isTextBasedLanguage: jest.fn(() => true), diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx index fe3e331d03714..05e05279567e5 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx @@ -14,6 +14,7 @@ import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; import { IconChartDatatable } from '@kbn/chart-icons'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/common'; +import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils'; import type { FormBasedPersistedState } from '../../datasources/form_based/types'; import type { SuggestionRequest, @@ -315,16 +316,20 @@ export const getDatatableVisualization = ({ .map((accessor) => { const columnConfig = columnMap[accessor]; const stops = columnConfig?.palette?.params?.stops; + const isNumeric = Boolean( + accessor && isNumericFieldForDatatable(frame.activeData?.[state.layerId], accessor) + ); const hasColoring = Boolean(columnConfig?.colorMode !== 'none' && stops); return { columnId: accessor, triggerIconType: columnConfig?.hidden ? 'invisible' - : hasColoring + : hasColoring && isNumeric ? 'colorBy' : undefined, - palette: hasColoring && stops ? stops.map(({ color }) => color) : undefined, + palette: + hasColoring && isNumeric && stops ? stops.map(({ color }) => color) : undefined, }; }), supportsMoreColumns: true, diff --git a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts index b8979103f50d8..ebbbfd3fde1c1 100644 --- a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts @@ -45,6 +45,7 @@ export type ESQLSourceDescriptor = AbstractSourceDescriptor & { id: string; esql: string; columns: ESQLColumn[]; + dataViewId: string; /* * Date field used to narrow ES|QL requests by global time range */ diff --git a/x-pack/plugins/maps/public/classes/layers/invalid_layer.ts b/x-pack/plugins/maps/public/classes/layers/invalid_layer.ts new file mode 100644 index 0000000000000..eed2ab37b32e0 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/invalid_layer.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable max-classes-per-file */ + +import { i18n } from '@kbn/i18n'; +import { LayerDescriptor } from '../../../common/descriptor_types'; +import { AbstractLayer } from './layer'; +import { AbstractSource } from '../sources/source'; +import { IStyle } from '../styles/style'; + +class InvalidSource extends AbstractSource { + constructor(id?: string) { + super({ + id, + type: 'INVALID', + }); + } +} + +export class InvalidLayer extends AbstractLayer { + private readonly _error: Error; + private readonly _style: IStyle; + + constructor(layerDescriptor: LayerDescriptor, error: Error) { + super({ + layerDescriptor, + source: new InvalidSource(layerDescriptor.sourceDescriptor?.id), + }); + this._error = error; + this._style = { + getType() { + return 'INVALID'; + }, + renderEditor() { + return null; + }, + }; + } + + hasErrors() { + return true; + } + + getErrors() { + return [ + { + title: i18n.translate('xpack.maps.invalidLayer.errorTitle', { + defaultMessage: `Unable to create layer`, + }), + body: this._error.message, + }, + ]; + } + + getStyleForEditing() { + return this._style; + } + + getStyle() { + return this._style; + } + + getCurrentStyle() { + return this._style; + } + + getMbLayerIds() { + return []; + } + + ownsMbLayerId() { + return false; + } + + ownsMbSourceId() { + return false; + } + + syncLayerWithMB() {} + + getLayerTypeIconName() { + return 'error'; + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 1fccaf7f6d0a5..aa39cf017eb0d 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -245,7 +245,7 @@ export class AbstractLayer implements ILayer { const sourceDisplayName = source ? await source.getDisplayName() : await this.getSource().getDisplayName(); - return sourceDisplayName || `Layer ${this._descriptor.id}`; + return sourceDisplayName || this._descriptor.id; } async getAttributions(): Promise<Attribution[]> { diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx index eba948dec895f..cb7206e5bab7c 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.test.tsx @@ -7,10 +7,12 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; +import type { DataViewSpec } from '@kbn/data-plugin/common'; import { CreateSourceEditor } from './create_source_editor'; jest.mock('../../../kibana_services', () => { - const mockDefaultDataView = { + const DEFAULT_DATA_VIEW_INDEX_PATTERN = 'logs'; + const defaultDataView = { fields: [ { name: 'location', @@ -23,10 +25,11 @@ jest.mock('../../../kibana_services', () => { ], timeFieldName: '@timestamp', getIndexPattern: () => { - return 'logs'; + return DEFAULT_DATA_VIEW_INDEX_PATTERN; }, }; - const mockDataView = { + + const otherDataView = { fields: [ { name: 'geometry', @@ -37,14 +40,21 @@ jest.mock('../../../kibana_services', () => { return 'world_countries'; }, }; + return { getIndexPatternService() { return { + create: async (spec: DataViewSpec) => { + return { + ...(spec.title === DEFAULT_DATA_VIEW_INDEX_PATTERN ? defaultDataView : otherDataView), + id: spec.id, + }; + }, get: async () => { - return mockDataView; + return otherDataView; }, getDefaultDataView: async () => { - return mockDefaultDataView; + return defaultDataView; }, }; }, @@ -63,6 +73,7 @@ describe('CreateSourceEditor', () => { type: 'geo_point', }, ], + dataViewId: '30de729e173668cbf8954aa56c4aca5b82a1005586a608b692dae478219f8c76', dateField: '@timestamp', esql: 'from logs | keep location | limit 10000', geoField: 'location', @@ -89,6 +100,7 @@ describe('CreateSourceEditor', () => { type: 'geo_shape', }, ], + dataViewId: 'c9f096614a62aa31893a2d6e8f43139bda7dcdb262b9373f79d0173cc152b4a4', dateField: undefined, esql: 'from world_countries | keep geometry | limit 10000', geoField: 'geometry', diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx index 4c75246aed567..4533c54d1a1ce 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/create_source_editor.tsx @@ -9,6 +9,7 @@ import React, { useEffect, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import type { ESQLColumn } from '@kbn/es-types'; +import { getESQLAdHocDataview } from '@kbn/esql-utils'; import { EuiFormRow, EuiPanel, @@ -32,6 +33,7 @@ interface Props { export function CreateSourceEditor(props: Props) { const [isInitialized, setIsInitialized] = useState(false); + const [adhocDataViewId, setAdhocDataViewId] = useState<string | undefined>(); const [columns, setColumns] = useState<ESQLColumn[]>([]); const [esql, setEsql] = useState(''); const [dateField, setDateField] = useState<string | undefined>(); @@ -52,17 +54,20 @@ export function CreateSourceEditor(props: Props) { } getDataView() - .then((dataView) => { + .then(async (dataView) => { + const adhocDataView = dataView + ? await getESQLAdHocDataview(dataView.getIndexPattern(), getIndexPatternService()) + : undefined; if (ignore) { return; } - if (dataView) { + if (adhocDataView) { let initialGeoField: DataViewField | undefined; const initialDateFields: string[] = []; const initialGeoFields: string[] = []; - for (let i = 0; i < dataView.fields.length; i++) { - const field = dataView.fields[i]; + for (let i = 0; i < adhocDataView.fields.length; i++) { + const field = adhocDataView.fields[i]; if ( [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE].includes( field.type as ES_GEO_FIELD_TYPE @@ -77,12 +82,13 @@ export function CreateSourceEditor(props: Props) { if (initialGeoField) { let initialDateField: string | undefined; - if (dataView.timeFieldName) { + // get default time field from default data view instead of adhoc data view + if (dataView?.timeFieldName) { initialDateField = dataView.timeFieldName; } else if (initialDateFields.length) { initialDateField = initialDateFields[0]; } - const initialEsql = `from ${dataView.getIndexPattern()} | keep ${ + const initialEsql = `from ${adhocDataView.getIndexPattern()} | keep ${ initialGeoField.name } | limit 10000`; setColumns([ @@ -94,6 +100,7 @@ export function CreateSourceEditor(props: Props) { : ESQL_GEO_POINT_TYPE, }, ]); + setAdhocDataViewId(adhocDataView.id); setDateField(initialDateField); setDateFields(initialDateFields); setGeoField(initialGeoField.name); @@ -123,9 +130,10 @@ export function CreateSourceEditor(props: Props) { useDebounce( () => { const sourceConfig = - esql && esql.length + esql && esql.length && adhocDataViewId ? { columns, + dataViewId: adhocDataViewId, dateField, geoField, esql, @@ -138,6 +146,7 @@ export function CreateSourceEditor(props: Props) { }, 0, [ + adhocDataViewId, columns, dateField, geoField, @@ -154,6 +163,7 @@ export function CreateSourceEditor(props: Props) { <ESQLEditor esql={esql} onESQLChange={(change) => { + setAdhocDataViewId(change.adhocDataViewId); setColumns(change.columns); setEsql(change.esql); setDateFields(change.dateFields); diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_editor.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_editor.tsx index 0145dc8239273..8ed04f61adb5d 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_editor.tsx @@ -17,11 +17,13 @@ import { getESQLMeta, verifyGeometryColumn } from './esql_utils'; interface Props { esql: string; onESQLChange: ({ + adhocDataViewId, columns, dateFields, geoFields, esql, }: { + adhocDataViewId: string; columns: ESQLColumn[]; dateFields: string[]; geoFields: string[]; @@ -81,12 +83,6 @@ export function ESQLEditor(props: Props) { return; } setError(err); - props.onESQLChange({ - columns: [], - dateFields: [], - geoFields: [], - esql: '', - }); } setIsLoading(false); diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts index 42446ad8cb7e4..eabaedf681b2c 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.test.ts @@ -11,6 +11,7 @@ import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; describe('getSupportedShapeTypes', () => { test('should return point for geo_point column', async () => { const descriptor = ESQLSource.createDescriptor({ + dataViewId: '1234', esql: 'from kibana_sample_data_logs | keep geo.coordinates | limit 10000', columns: [ { @@ -25,6 +26,7 @@ describe('getSupportedShapeTypes', () => { test('should return all geometry types for geo_shape column', async () => { const descriptor = ESQLSource.createDescriptor({ + dataViewId: '1234', esql: 'from world_countries | keep geometry | limit 10000', columns: [ { @@ -43,6 +45,7 @@ describe('getSupportedShapeTypes', () => { test('should fallback to point when geometry column can not be found', async () => { const descriptor = ESQLSource.createDescriptor({ + dataViewId: '1234', esql: 'from world_countries | keep geometry | limit 10000', }); const esqlSource = new ESQLSource(descriptor); diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx index d438a714beb40..53745e7426b70 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx @@ -52,12 +52,17 @@ export class ESQLSource extends AbstractVectorSource implements IVectorSource { throw new Error('Cannot create ESQLSourceDescriptor when esql is not provided'); } + if (!isValidStringConfig(descriptor.dataViewId)) { + throw new Error('Cannot create ESQLSourceDescriptor when dataViewId is not provided'); + } + return { ...descriptor, id: isValidStringConfig(descriptor.id) ? descriptor.id! : uuidv4(), type: SOURCE_TYPES.ESQL, esql: descriptor.esql!, columns: descriptor.columns ? descriptor.columns : [], + dataViewId: descriptor.dataViewId!, narrowByGlobalSearch: typeof descriptor.narrowByGlobalSearch !== 'undefined' ? descriptor.narrowByGlobalSearch @@ -316,4 +321,8 @@ export class ESQLSource extends AbstractVectorSource implements IVectorSource { narrowByGlobalTime: this._descriptor.narrowByGlobalTime, }; } + + getIndexPatternId() { + return this._descriptor.dataViewId; + } } diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts index c247170874ba3..144516f7db5d0 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_utils.ts @@ -7,7 +7,8 @@ import { i18n } from '@kbn/i18n'; import { lastValueFrom } from 'rxjs'; -import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import type { DataView } from '@kbn/data-plugin/common'; +import { getESQLAdHocDataview, getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; import type { ESQLColumn, ESQLSearchReponse } from '@kbn/es-types'; import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; import { getData, getIndexPatternService } from '../../../kibana_services'; @@ -49,10 +50,14 @@ export function verifyGeometryColumn(columns: ESQLColumn[]) { } export async function getESQLMeta(esql: string) { - const fields = await getFields(esql); + const adhocDataView = await getESQLAdHocDataview( + getIndexPatternFromESQLQuery(esql), + getIndexPatternService() + ); return { columns: await getColumns(esql), - ...fields, + adhocDataViewId: adhocDataView.id!, + ...getFields(adhocDataView), }; } @@ -105,33 +110,19 @@ async function getColumns(esql: string) { } } -export async function getFields(esql: string) { +export function getFields(dataView: DataView) { const dateFields: string[] = []; const geoFields: string[] = []; - const pattern: string = getIndexPatternFromESQLQuery(esql); - try { - // TODO pass field type filter to getFieldsForWildcard when field type filtering is supported - (await getIndexPatternService().getFieldsForWildcard({ pattern })).forEach((field) => { - if (field.type === 'date') { - dateFields.push(field.name); - } else if ( - field.type === ES_GEO_FIELD_TYPE.GEO_POINT || - field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE - ) { - geoFields.push(field.name); - } - }); - } catch (error) { - throw new Error( - i18n.translate('xpack.maps.source.esql.getFieldsErrorMsg', { - defaultMessage: `Unable to load fields from index pattern: {pattern}. {errorMessage}`, - values: { - errorMessage: error.message, - pattern, - }, - }) - ); - } + dataView.fields.forEach((field) => { + if (field.type === 'date') { + dateFields.push(field.name); + } else if ( + field.type === ES_GEO_FIELD_TYPE.GEO_POINT || + field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE + ) { + geoFields.push(field.name); + } + }); return { dateFields, diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.test.tsx index 7c6f9f34b1f56..7491f6bc2b049 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.test.tsx @@ -11,19 +11,38 @@ import userEvent from '@testing-library/user-event'; import { UpdateSourceEditor } from './update_source_editor'; import { ESQLSource } from './esql_source'; -jest.mock('./esql_utils', () => ({})); - -describe('UpdateSourceEditor', () => { - beforeEach(() => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - require('./esql_utils').getFields = async () => { +jest.mock('../../../kibana_services', () => { + return { + getIndexPatternService() { return { - dateFields: ['timestamp', 'utc_timestamp'], - geoFields: ['location', 'dest_location'], + get: async () => { + return { + fields: [ + { + name: 'timestamp', + type: 'date', + }, + { + name: 'utc_timestamp', + type: 'date', + }, + { + name: 'location', + type: 'geo_point', + }, + { + name: 'utc_timestamp', + type: 'geo_point', + }, + ], + }; + }, }; - }; - }); + }, + }; +}); +describe('UpdateSourceEditor', () => { describe('narrow by map bounds switch', () => { function getNarrowByMapBoundsSwitch() { return screen.getByText('Narrow ES|QL statement by visible map area'); @@ -32,6 +51,7 @@ describe('UpdateSourceEditor', () => { test('should set geoField when checked and geo field is not set', async () => { const onChange = jest.fn(); const sourceDescriptor = ESQLSource.createDescriptor({ + dataViewId: '1234', esql: 'from logs | keep location | limit 10000', columns: [ { @@ -55,6 +75,7 @@ describe('UpdateSourceEditor', () => { test('should not reset geoField when checked and geoField is set', async () => { const onChange = jest.fn(); const sourceDescriptor = ESQLSource.createDescriptor({ + dataViewId: '1234', esql: 'from logs | keep location | limit 10000', columns: [ { @@ -82,6 +103,7 @@ describe('UpdateSourceEditor', () => { test('should set dateField when checked and date field is not set', async () => { const onChange = jest.fn(); const sourceDescriptor = ESQLSource.createDescriptor({ + dataViewId: '1234', esql: 'from logs | keep location | limit 10000', columns: [ { @@ -105,6 +127,7 @@ describe('UpdateSourceEditor', () => { test('should not reset dateField when checked and dateField is set', async () => { const onChange = jest.fn(); const sourceDescriptor = ESQLSource.createDescriptor({ + dataViewId: '1234', esql: 'from logs | keep location | limit 10000', columns: [ { diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.tsx index 1a147cf93085f..e77bcf862f929 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/update_source_editor.tsx @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import type { ESQLSourceDescriptor } from '../../../../common/descriptor_types'; import type { OnSourceChangeArgs } from '../source'; import { ForceRefreshCheckbox } from '../../../components/force_refresh_checkbox'; +import { getIndexPatternService } from '../../../kibana_services'; import { ESQLEditor } from './esql_editor'; import { NarrowByMapBounds, NarrowByTime } from './narrow_by_field'; import { getFields } from './esql_utils'; @@ -35,11 +36,13 @@ export function UpdateSourceEditor(props: Props) { useEffect(() => { let ignore = false; - getFields(props.sourceDescriptor.esql) - .then((fields) => { + getIndexPatternService() + .get(props.sourceDescriptor.dataViewId) + .then((dataView) => { if (ignore) { return; } + const fields = getFields(dataView); setDateFields(fields.dateFields); setGeoFields(fields.geoFields); setIsInitialized(true); @@ -79,6 +82,7 @@ export function UpdateSourceEditor(props: Props) { setGeoFields(change.geoFields); const changes: OnSourceChangeArgs[] = [ { propName: 'columns', value: change.columns }, + { propName: 'dataViewId', value: change.adhocDataViewId }, { propName: 'esql', value: change.esql }, ]; function ensureField(key: 'dateField' | 'geoField', fields: string[]) { diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.ts b/x-pack/plugins/maps/public/maps_vis_type_alias.ts index e161689643612..48a96a2d0f988 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.ts +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.ts @@ -39,7 +39,7 @@ export function getMapsVisTypeAlias() { searchFields: ['title^3'], client: getMapClient, toListItem(mapItem: MapItem) { - const { id, type, updatedAt, attributes } = mapItem; + const { id, type, updatedAt, attributes, managed } = mapItem; const { title, description } = attributes; return { @@ -47,6 +47,7 @@ export function getMapsVisTypeAlias() { title, description, updatedAt, + managed, editor: { editUrl: getEditPath(id), editApp: APP_ID, diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index ec035ac1c5623..78950c1ab2e7f 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -23,6 +23,7 @@ import { import { VectorStyle } from '../classes/styles/vector/vector_style'; import { isLayerGroup, LayerGroup } from '../classes/layers/layer_group'; import { HeatmapLayer } from '../classes/layers/heatmap_layer'; +import { InvalidLayer } from '../classes/layers/invalid_layer'; import { getTimeFilter } from '../kibana_services'; import { getChartsPaletteServiceGetColor } from '../reducers/non_serializable_instances'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/copy_persistent_state'; @@ -76,54 +77,58 @@ export function createLayerInstance( customIcons: CustomIcon[], chartsPaletteServiceGetColor?: (value: string) => string | null ): ILayer { - if (layerDescriptor.type === LAYER_TYPE.LAYER_GROUP) { - return new LayerGroup({ layerDescriptor: layerDescriptor as LayerGroupDescriptor }); - } + try { + if (layerDescriptor.type === LAYER_TYPE.LAYER_GROUP) { + return new LayerGroup({ layerDescriptor: layerDescriptor as LayerGroupDescriptor }); + } - const source: ISource = createSourceInstance(layerDescriptor.sourceDescriptor); - switch (layerDescriptor.type) { - case LAYER_TYPE.RASTER_TILE: - return new RasterTileLayer({ layerDescriptor, source: source as IRasterSource }); - case LAYER_TYPE.EMS_VECTOR_TILE: - return new EmsVectorTileLayer({ - layerDescriptor: layerDescriptor as EMSVectorTileLayerDescriptor, - source: source as EMSTMSSource, - }); - case LAYER_TYPE.HEATMAP: - return new HeatmapLayer({ - layerDescriptor: layerDescriptor as HeatmapLayerDescriptor, - source: source as ESGeoGridSource, - }); - case LAYER_TYPE.GEOJSON_VECTOR: - return new GeoJsonVectorLayer({ - layerDescriptor: layerDescriptor as VectorLayerDescriptor, - source: source as IVectorSource, - joins: createJoinInstances( - layerDescriptor as VectorLayerDescriptor, - source as IVectorSource - ), - customIcons, - chartsPaletteServiceGetColor, - }); - case LAYER_TYPE.BLENDED_VECTOR: - return new BlendedVectorLayer({ - layerDescriptor: layerDescriptor as VectorLayerDescriptor, - source: source as IVectorSource, - customIcons, - chartsPaletteServiceGetColor, - }); - case LAYER_TYPE.MVT_VECTOR: - return new MvtVectorLayer({ - layerDescriptor: layerDescriptor as VectorLayerDescriptor, - source: source as IVectorSource, - joins: createJoinInstances( - layerDescriptor as VectorLayerDescriptor, - source as IVectorSource - ), - customIcons, - }); - default: - throw new Error(`Unrecognized layerType ${layerDescriptor.type}`); + const source: ISource = createSourceInstance(layerDescriptor.sourceDescriptor); + switch (layerDescriptor.type) { + case LAYER_TYPE.RASTER_TILE: + return new RasterTileLayer({ layerDescriptor, source: source as IRasterSource }); + case LAYER_TYPE.EMS_VECTOR_TILE: + return new EmsVectorTileLayer({ + layerDescriptor: layerDescriptor as EMSVectorTileLayerDescriptor, + source: source as EMSTMSSource, + }); + case LAYER_TYPE.HEATMAP: + return new HeatmapLayer({ + layerDescriptor: layerDescriptor as HeatmapLayerDescriptor, + source: source as ESGeoGridSource, + }); + case LAYER_TYPE.GEOJSON_VECTOR: + return new GeoJsonVectorLayer({ + layerDescriptor: layerDescriptor as VectorLayerDescriptor, + source: source as IVectorSource, + joins: createJoinInstances( + layerDescriptor as VectorLayerDescriptor, + source as IVectorSource + ), + customIcons, + chartsPaletteServiceGetColor, + }); + case LAYER_TYPE.BLENDED_VECTOR: + return new BlendedVectorLayer({ + layerDescriptor: layerDescriptor as VectorLayerDescriptor, + source: source as IVectorSource, + customIcons, + chartsPaletteServiceGetColor, + }); + case LAYER_TYPE.MVT_VECTOR: + return new MvtVectorLayer({ + layerDescriptor: layerDescriptor as VectorLayerDescriptor, + source: source as IVectorSource, + joins: createJoinInstances( + layerDescriptor as VectorLayerDescriptor, + source as IVectorSource + ), + customIcons, + }); + default: + throw new Error(`Unrecognized layerType ${layerDescriptor.type}`); + } + } catch (error) { + return new InvalidLayer(layerDescriptor, error); } } diff --git a/x-pack/plugins/ml/public/application/components/delete_space_aware_item_check_modal/delete_space_aware_item_check_modal.tsx b/x-pack/plugins/ml/public/application/components/delete_space_aware_item_check_modal/delete_space_aware_item_check_modal.tsx index d436a52f7ccb3..22500f936e705 100644 --- a/x-pack/plugins/ml/public/application/components/delete_space_aware_item_check_modal/delete_space_aware_item_check_modal.tsx +++ b/x-pack/plugins/ml/public/application/components/delete_space_aware_item_check_modal/delete_space_aware_item_check_modal.tsx @@ -21,6 +21,7 @@ import { EuiText, EuiSpacer, } from '@elastic/eui'; +import useDebounce from 'react-use/lib/useDebounce'; import type { CanDeleteMLSpaceAwareItemsResponse, MlSavedObjectType, @@ -246,12 +247,16 @@ export const DeleteSpaceAwareItemCheckModal: FC<Props> = ({ const [itemCheckRespSummary, setItemCheckRespSummary] = useState< CanDeleteMLSpaceAwareItemsSummary | undefined >(); + const [showModal, setShowModal] = useState<boolean>(false); const { savedObjects: { canDeleteMLSpaceAwareItems, removeItemFromCurrentSpace }, } = useMlApiContext(); const { displayErrorToast, displaySuccessToast } = useToastNotificationService(); + // delay showing the modal to avoid flickering + useDebounce(() => setShowModal(true), 1000); + useEffect(() => { setIsLoading(true); // Do the spaces check and set the content for the modal and buttons depending on results @@ -321,6 +326,10 @@ export const DeleteSpaceAwareItemCheckModal: FC<Props> = ({ } }; + if (showModal === false) { + return null; + } + return ( <EuiModal onClose={onCloseCallback} data-test-subj="mlDeleteSpaceAwareItemCheckModalOverlay"> <> diff --git a/x-pack/plugins/ml/public/application/services/field_format_service.ts b/x-pack/plugins/ml/public/application/services/field_format_service.ts index a43e134f84cfb..8519a13e7d7bc 100644 --- a/x-pack/plugins/ml/public/application/services/field_format_service.ts +++ b/x-pack/plugins/ml/public/application/services/field_format_service.ts @@ -8,16 +8,20 @@ import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; import { getDataViewById, getDataViewIdFromName } from '../util/index_utils'; import { mlJobService } from './job_service'; +import type { MlIndexUtils } from '../util/index_service'; +import type { MlApiServices } from './ml_api_service'; type FormatsByJobId = Record<string, any>; type IndexPatternIdsByJob = Record<string, any>; // Service for accessing FieldFormat objects configured for a Kibana data view // for use in formatting the actual and typical values from anomalies. -class FieldFormatService { +export class FieldFormatService { indexPatternIdsByJob: IndexPatternIdsByJob = {}; formatsByJob: FormatsByJobId = {}; + constructor(private mlApiServices?: MlApiServices, private mlIndexUtils?: MlIndexUtils) {} + // Populate the service with the FieldFormats for the list of jobs with the // specified IDs. List of Kibana data views is passed, with a title // attribute set in each pattern which will be compared to the indices @@ -32,10 +36,17 @@ class FieldFormatService { ( await Promise.all( jobIds.map(async (jobId) => { - const jobObj = mlJobService.getJob(jobId); + const getDataViewId = this.mlIndexUtils?.getDataViewIdFromName ?? getDataViewIdFromName; + let jobObj; + if (this.mlApiServices) { + const { jobs } = await this.mlApiServices.getJobs({ jobId }); + jobObj = jobs[0]; + } else { + jobObj = mlJobService.getJob(jobId); + } return { jobId, - dataViewId: await getDataViewIdFromName(jobObj.datafeed_config.indices.join(',')), + dataViewId: await getDataViewId(jobObj.datafeed_config!.indices.join(',')), }; }) ) @@ -68,41 +79,40 @@ class FieldFormatService { } } - getFormatsForJob(jobId: string): Promise<any[]> { - return new Promise((resolve, reject) => { - const jobObj = mlJobService.getJob(jobId); - const detectors = jobObj.analysis_config.detectors || []; - const formatsByDetector: any[] = []; + async getFormatsForJob(jobId: string): Promise<any[]> { + let jobObj; + const getDataView = this.mlIndexUtils?.getDataViewById ?? getDataViewById; + if (this.mlApiServices) { + const { jobs } = await this.mlApiServices.getJobs({ jobId }); + jobObj = jobs[0]; + } else { + jobObj = mlJobService.getJob(jobId); + } + const detectors = jobObj.analysis_config.detectors || []; + const formatsByDetector: any[] = []; - const dataViewId = this.indexPatternIdsByJob[jobId]; - if (dataViewId !== undefined) { - // Load the full data view configuration to obtain the formats of each field. - getDataViewById(dataViewId) - .then((dataView) => { - // Store the FieldFormat for each job by detector_index. - const fieldList = dataView.fields; - detectors.forEach((dtr) => { - const esAgg = mlFunctionToESAggregation(dtr.function); - // distinct_count detectors should fall back to the default - // formatter as the values are just counts. - if (dtr.field_name !== undefined && esAgg !== 'cardinality') { - const field = fieldList.getByName(dtr.field_name); - if (field !== undefined) { - formatsByDetector[dtr.detector_index!] = dataView.getFormatterForField(field); - } - } - }); + const dataViewId = this.indexPatternIdsByJob[jobId]; + if (dataViewId !== undefined) { + // Load the full data view configuration to obtain the formats of each field. + const dataView = await getDataView(dataViewId); + // Store the FieldFormat for each job by detector_index. + const fieldList = dataView.fields; + detectors.forEach((dtr) => { + const esAgg = mlFunctionToESAggregation(dtr.function); + // distinct_count detectors should fall back to the default + // formatter as the values are just counts. + if (dtr.field_name !== undefined && esAgg !== 'cardinality') { + const field = fieldList.getByName(dtr.field_name); + if (field !== undefined) { + formatsByDetector[dtr.detector_index!] = dataView.getFormatterForField(field); + } + } + }); + } - resolve(formatsByDetector); - }) - .catch((err) => { - reject(err); - }); - } else { - resolve(formatsByDetector); - } - }); + return formatsByDetector; } } export const mlFieldFormatService = new FieldFormatService(); +export type MlFieldFormatService = typeof mlFieldFormatService; diff --git a/x-pack/plugins/ml/public/application/services/field_format_service_factory.ts b/x-pack/plugins/ml/public/application/services/field_format_service_factory.ts new file mode 100644 index 0000000000000..daefab69154c5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/services/field_format_service_factory.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type MlFieldFormatService, FieldFormatService } from './field_format_service'; +import type { MlIndexUtils } from '../util/index_service'; +import type { MlApiServices } from './ml_api_service'; + +export function fieldFormatServiceFactory( + mlApiServices: MlApiServices, + mlIndexUtils: MlIndexUtils +): MlFieldFormatService { + return new FieldFormatService(mlApiServices, mlIndexUtils); +} diff --git a/x-pack/plugins/ml/public/application/services/forecast_service.d.ts b/x-pack/plugins/ml/public/application/services/forecast_service.d.ts index 0bfd8f56385d6..55df37b2307da 100644 --- a/x-pack/plugins/ml/public/application/services/forecast_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/forecast_service.d.ts @@ -32,3 +32,5 @@ export const mlForecastService: { getForecastDateRange: (job: Job, forecastId: string) => Promise<ForecastDateRange>; }; + +export type MlForecastService = typeof mlForecastService; diff --git a/x-pack/plugins/ml/public/application/services/forecast_service_provider.ts b/x-pack/plugins/ml/public/application/services/forecast_service_provider.ts new file mode 100644 index 0000000000000..c776a79a6f475 --- /dev/null +++ b/x-pack/plugins/ml/public/application/services/forecast_service_provider.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. + */ + +// Service for carrying out requests to run ML forecasts and to obtain +// data on forecasts that have been performed. +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { get, find, each } from 'lodash'; +import { map } from 'rxjs/operators'; +import type { MlApiServices } from './ml_api_service'; +import type { Job } from '../../../common/types/anomaly_detection_jobs'; + +export interface AggType { + avg: string; + max: string; + min: string; +} + +// TODO Consolidate with legacy code in +// `x-pack/plugins/ml/public/application/services/forecast_service.js` and +// `x-pack/plugins/ml/public/application/services/forecast_service.d.ts`. +export function forecastServiceProvider(mlApiServices: MlApiServices) { + return { + // Gets a basic summary of the most recently run forecasts for the specified + // job, with results at or later than the supplied timestamp. + // Extra query object can be supplied, or pass null if no additional query. + // Returned response contains a forecasts property, which is an array of objects + // containing id, earliest and latest keys. + getForecastsSummary(job: Job, query: any, earliestMs: number, maxResults: any) { + return new Promise((resolve, reject) => { + const obj: { success: boolean; forecasts: Record<string, any> } = { + success: true, + forecasts: [], + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID, result type and earliest time, plus + // the additional query if supplied. + const filterCriteria = [ + { + term: { result_type: 'model_forecast_request_stats' }, + }, + { + term: { job_id: job.job_id }, + }, + { + range: { + timestamp: { + gte: earliestMs, + format: 'epoch_millis', + }, + }, + }, + ]; + + if (query) { + filterCriteria.push(query); + } + + mlApiServices.results + .anomalySearch( + { + // @ts-expect-error SearchRequest type has not been updated to include size + size: maxResults, + body: { + query: { + bool: { + filter: filterCriteria, + }, + }, + sort: [{ forecast_create_timestamp: { order: 'desc' } }], + }, + }, + [job.job_id] + ) + .then((resp) => { + if (resp.hits.total.value > 0) { + obj.forecasts = resp.hits.hits.map((hit) => hit._source); + } + + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); + }, + // Obtains the earliest and latest timestamps for the forecast data from + // the forecast with the specified ID. + // Returned response contains earliest and latest properties which are the + // timestamps of the first and last model_forecast results. + getForecastDateRange(job: Job, forecastId: string) { + return new Promise((resolve, reject) => { + const obj = { + success: true, + earliest: null, + latest: null, + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID, forecast ID, result type and time range. + const filterCriteria = [ + { + query_string: { + query: 'result_type:model_forecast', + analyze_wildcard: true, + }, + }, + { + term: { job_id: job.job_id }, + }, + { + term: { forecast_id: forecastId }, + }, + ]; + + // TODO - add in criteria for detector index and entity fields (by, over, partition) + // once forecasting with these parameters is supported. + + mlApiServices.results + .anomalySearch( + { + // @ts-expect-error SearchRequest type has not been updated to include size + size: 0, + body: { + query: { + bool: { + filter: filterCriteria, + }, + }, + aggs: { + earliest: { + min: { + field: 'timestamp', + }, + }, + latest: { + max: { + field: 'timestamp', + }, + }, + }, + }, + }, + [job.job_id] + ) + .then((resp) => { + obj.earliest = get(resp, 'aggregations.earliest.value', null); + obj.latest = get(resp, 'aggregations.latest.value', null); + if (obj.earliest === null || obj.latest === null) { + reject(resp); + } else { + resolve(obj); + } + }) + .catch((resp) => { + reject(resp); + }); + }); + }, + // Obtains the requested forecast model data for the forecast with the specified ID. + getForecastData( + job: Job, + detectorIndex: number, + forecastId: string, + entityFields: any, + earliestMs: number, + latestMs: number, + intervalMs: number, + aggType?: AggType + ) { + // Extract the partition, by, over fields on which to filter. + const criteriaFields = []; + const detector = job.analysis_config.detectors[detectorIndex]; + if (detector.partition_field_name !== undefined) { + const partitionEntity = find(entityFields, { fieldName: detector.partition_field_name }); + if (partitionEntity !== undefined) { + criteriaFields.push( + { fieldName: 'partition_field_name', fieldValue: partitionEntity.fieldName }, + { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue } + ); + } + } + + if (detector.over_field_name !== undefined) { + const overEntity = find(entityFields, { fieldName: detector.over_field_name }); + if (overEntity !== undefined) { + criteriaFields.push( + { fieldName: 'over_field_name', fieldValue: overEntity.fieldName }, + { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue } + ); + } + } + + if (detector.by_field_name !== undefined) { + const byEntity = find(entityFields, { fieldName: detector.by_field_name }); + if (byEntity !== undefined) { + criteriaFields.push( + { fieldName: 'by_field_name', fieldValue: byEntity.fieldName }, + { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue } + ); + } + } + + const obj: { success: boolean; results: Record<number, any> } = { + success: true, + results: {}, + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID, forecast ID, detector index, result type and time range. + const filterCriteria: estypes.QueryDslQueryContainer[] = [ + { + query_string: { + query: 'result_type:model_forecast', + analyze_wildcard: true, + }, + }, + { + term: { job_id: job.job_id }, + }, + { + term: { forecast_id: forecastId }, + }, + { + term: { detector_index: detectorIndex }, + }, + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + ]; + + // Add in term queries for each of the specified criteria. + each(criteriaFields, (criteria) => { + filterCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue, + }, + }); + }); + + // If an aggType object has been passed in, use it. + // Otherwise default to avg, min and max aggs for the + // forecast prediction, upper and lower + const forecastAggs = + aggType === undefined + ? { avg: 'avg', max: 'max', min: 'min' } + : { + avg: aggType.avg, + max: aggType.max, + min: aggType.min, + }; + + return mlApiServices.results + .anomalySearch$( + { + // @ts-expect-error SearchRequest type has not been updated to include size + size: 0, + body: { + query: { + bool: { + filter: filterCriteria, + }, + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + fixed_interval: `${intervalMs}ms`, + min_doc_count: 1, + }, + aggs: { + prediction: { + [forecastAggs.avg]: { + field: 'forecast_prediction', + }, + }, + forecastUpper: { + [forecastAggs.max]: { + field: 'forecast_upper', + }, + }, + forecastLower: { + [forecastAggs.min]: { + field: 'forecast_lower', + }, + }, + }, + }, + }, + }, + }, + [job.job_id] + ) + .pipe( + map((resp) => { + const aggregationsByTime = get(resp, ['aggregations', 'times', 'buckets'], []); + each(aggregationsByTime, (dataForTime) => { + const time = dataForTime.key; + obj.results[time] = { + prediction: get(dataForTime, ['prediction', 'value']), + forecastUpper: get(dataForTime, ['forecastUpper', 'value']), + forecastLower: get(dataForTime, ['forecastLower', 'value']), + }; + }); + + return obj; + }) + ); + }, + // Runs a forecast + runForecast(jobId: string, duration?: string) { + // eslint-disable-next-line no-console + console.log('ML forecast service run forecast with duration:', duration); + return new Promise((resolve, reject) => { + mlApiServices + .forecast({ + jobId, + duration, + }) + .then((resp) => { + resolve(resp); + }) + .catch((err) => { + reject(err); + }); + }); + }, + // Gets stats for a forecast that has been run on the specified job. + // Returned response contains a stats property, including + // forecast_progress (a value from 0 to 1), + // and forecast_status ('finished' when complete) properties. + getForecastRequestStats(job: Job, forecastId: string) { + return new Promise((resolve, reject) => { + const obj = { + success: true, + stats: {}, + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID, result type and earliest time. + const filterCriteria = [ + { + query_string: { + query: 'result_type:model_forecast_request_stats', + analyze_wildcard: true, + }, + }, + { + term: { job_id: job.job_id }, + }, + { + term: { forecast_id: forecastId }, + }, + ]; + + mlApiServices.results + .anomalySearch( + { + // @ts-expect-error SearchRequest type has not been updated to include size + size: 1, + body: { + query: { + bool: { + filter: filterCriteria, + }, + }, + }, + }, + [job.job_id] + ) + .then((resp) => { + if (resp.hits.total.value > 0) { + obj.stats = resp.hits.hits[0]._source; + } + resolve(obj); + }) + .catch((resp) => { + reject(resp); + }); + }); + }, + }; +} + +export type MlForecastService = ReturnType<typeof forecastServiceProvider>; diff --git a/x-pack/plugins/ml/public/application/services/results_service/index.ts b/x-pack/plugins/ml/public/application/services/results_service/index.ts index 4fe6b7add2a6b..883b54dd73e72 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/index.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { useMemo } from 'react'; import { resultsServiceRxProvider } from './result_service_rx'; import { resultsServiceProvider } from './results_service'; import { ml, MlApiServices } from '../ml_api_service'; +import { useMlKibana } from '../../contexts/kibana'; export type MlResultsService = typeof mlResultsService; @@ -29,3 +31,14 @@ export function mlResultsServiceProvider(mlApiServices: MlApiServices) { ...resultsServiceRxProvider(mlApiServices), }; } + +export function useMlResultsService(): MlResultsService { + const { + services: { + mlServices: { mlApiServices }, + }, + } = useMlKibana(); + + const resultsService = useMemo(() => mlResultsServiceProvider(mlApiServices), [mlApiServices]); + return resultsService; +} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx index c707bbee2c5b9..493e74755588b 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx @@ -9,9 +9,11 @@ import React, { useCallback, useEffect } from 'react'; import { EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils'; +import { MlJob } from '@elastic/elasticsearch/lib/api/types'; import { mlJobService } from '../../../services/job_service'; import { getFunctionDescription, isMetricDetector } from '../../get_function_description'; import { useToastNotificationService } from '../../../services/toast_notification_service'; +import { useMlResultsService } from '../../../services/results_service'; import type { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; const plotByFunctionOptions = [ @@ -36,6 +38,7 @@ const plotByFunctionOptions = [ ]; export const PlotByFunctionControls = ({ functionDescription, + job, setFunctionDescription, selectedDetectorIndex, selectedJobId, @@ -43,6 +46,7 @@ export const PlotByFunctionControls = ({ entityControlsCount, }: { functionDescription: undefined | string; + job?: CombinedJob | MlJob; setFunctionDescription: (func: string) => void; selectedDetectorIndex: number; selectedJobId: string; @@ -50,6 +54,7 @@ export const PlotByFunctionControls = ({ entityControlsCount: number; }) => { const toastNotificationService = useToastNotificationService(); + const mlResultsService = useMlResultsService(); const getFunctionDescriptionToPlot = useCallback( async ( @@ -65,18 +70,19 @@ export const PlotByFunctionControls = ({ selectedJobId: _selectedJobId, selectedJob: _selectedJob, }, - toastNotificationService + toastNotificationService, + mlResultsService ); setFunctionDescription(functionToPlot); }, - [setFunctionDescription, toastNotificationService] + [setFunctionDescription, toastNotificationService, mlResultsService] ); useEffect(() => { if (functionDescription !== undefined) { return; } - const selectedJob = mlJobService.getJob(selectedJobId); + const selectedJob = (job ?? mlJobService.getJob(selectedJobId)) as CombinedJob; // if no controls, it's okay to fetch // if there are series controls, only fetch if user has selected something const validEntities = diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx index 23bc2f80eb1a8..666d56f15fbc8 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx @@ -12,9 +12,10 @@ import { debounce } from 'lodash'; import { lastValueFrom } from 'rxjs'; import { useStorage } from '@kbn/ml-local-storage'; import type { MlEntityFieldType } from '@kbn/ml-anomaly-utils'; +import { MlJob } from '@elastic/elasticsearch/lib/api/types'; import { EntityControl } from '../entity_control'; import { mlJobService } from '../../../services/job_service'; -import { Detector, JobId } from '../../../../../common/types/anomaly_detection_jobs'; +import { CombinedJob, Detector, JobId } from '../../../../../common/types/anomaly_detection_jobs'; import { useMlKibana } from '../../../contexts/kibana'; import { APP_STATE_ACTION } from '../../timeseriesexplorer_constants'; import { @@ -67,12 +68,13 @@ const getDefaultFieldConfig = ( }; interface SeriesControlsProps { - selectedDetectorIndex: number; - selectedJobId: JobId; - bounds: any; appStateHandler: Function; + bounds: any; + functionDescription?: string; + job?: CombinedJob | MlJob; + selectedDetectorIndex: number; selectedEntities: Record<string, any>; - functionDescription: string; + selectedJobId: JobId; setFunctionDescription: (func: string) => void; } @@ -80,13 +82,14 @@ interface SeriesControlsProps { * Component for handling the detector and entities controls. */ export const SeriesControls: FC<SeriesControlsProps> = ({ - bounds, - selectedDetectorIndex, - selectedJobId, appStateHandler, + bounds, children, - selectedEntities, functionDescription, + job, + selectedDetectorIndex, + selectedEntities, + selectedJobId, setFunctionDescription, }) => { const { @@ -97,7 +100,11 @@ export const SeriesControls: FC<SeriesControlsProps> = ({ }, } = useMlKibana(); - const selectedJob = useMemo(() => mlJobService.getJob(selectedJobId), [selectedJobId]); + const selectedJob: CombinedJob | MlJob = useMemo( + () => job ?? mlJobService.getJob(selectedJobId), + // eslint-disable-next-line react-hooks/exhaustive-deps + [selectedJobId] + ); const isModelPlotEnabled = !!selectedJob.model_plot_config?.enabled; @@ -108,11 +115,17 @@ export const SeriesControls: FC<SeriesControlsProps> = ({ index: number; detector_description: Detector['detector_description']; }> = useMemo(() => { - return getViewableDetectors(selectedJob); + return getViewableDetectors(selectedJob as CombinedJob); }, [selectedJob]); const entityControls = useMemo(() => { - return getControlsForDetector(selectedDetectorIndex, selectedEntities, selectedJobId); + return getControlsForDetector( + selectedDetectorIndex, + selectedEntities, + selectedJobId, + selectedJob as CombinedJob + ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedDetectorIndex, selectedEntities, selectedJobId]); const [storageFieldsConfig, setStorageFieldsConfig] = useStorage< @@ -318,6 +331,7 @@ export const SeriesControls: FC<SeriesControlsProps> = ({ ); })} <PlotByFunctionControls + job={job} selectedJobId={selectedJobId} selectedDetectorIndex={selectedDetectorIndex} selectedEntities={selectedEntities} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index b459f0bffcee0..9ed67ed6ec446 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -36,7 +36,7 @@ import { showMultiBucketAnomalyTooltip, getMultiBucketImpactTooltipValue, } from '../../../util/chart_utils'; -import { getTimeBucketsFromCache } from '../../../util/time_buckets'; +import { timeBucketsServiceFactory } from '../../../util/time_buckets_service'; import { mlTableService } from '../../../services/table_service'; import { ContextChartMask } from '../context_chart_mask'; import { findChartPointForAnomalyTime } from '../../timeseriesexplorer_utils'; @@ -53,6 +53,7 @@ import { ANNOTATION_MIN_WIDTH, } from './timeseries_chart_annotations'; import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; +import { context } from '@kbn/kibana-react-plugin/public'; import { LinksMenuUI } from '../../../components/anomalies_table/links_menu'; import { RuleEditorFlyout } from '../../../components/rule_editor'; @@ -113,6 +114,7 @@ class TimeseriesChartIntl extends Component { contextForecastData: PropTypes.array, contextChartSelected: PropTypes.func.isRequired, detectorIndex: PropTypes.number, + embeddableMode: PropTypes.bool, focusAggregationInterval: PropTypes.object, focusAnnotationData: PropTypes.array, focusChartData: PropTypes.array, @@ -133,6 +135,9 @@ class TimeseriesChartIntl extends Component { sourceIndicesWithGeoFields: PropTypes.object.isRequired, }; + static contextType = context; + getTimeBuckets; + rowMouseenterSubscriber = null; rowMouseleaveSubscriber = null; @@ -154,6 +159,10 @@ class TimeseriesChartIntl extends Component { } componentDidMount() { + this.getTimeBuckets = timeBucketsServiceFactory( + this.context.services.uiSettings + ).getTimeBuckets; + const { svgWidth } = this.props; this.vizWidth = svgWidth - margin.left - margin.right; @@ -295,7 +304,12 @@ class TimeseriesChartIntl extends Component { chartElement.selectAll('*').remove(); if (typeof selectedJob !== 'undefined') { - this.fieldFormat = mlFieldFormatService.getFieldFormat(selectedJob.job_id, detectorIndex); + this.fieldFormat = this.context?.services?.mlServices?.mlFieldFormatService + ? this.context.services.mlServices.mlFieldFormatService.getFieldFormat( + selectedJob.job_id, + detectorIndex + ) + : mlFieldFormatService.getFieldFormat(selectedJob.job_id, detectorIndex); } else { return; } @@ -367,7 +381,7 @@ class TimeseriesChartIntl extends Component { ); }) .remove(); - d3.select('.temp-axis-label').remove(); + chartElement.select('.temp-axis-label').remove(); margin.left = Math.max(maxYAxisLabelWidth, 40); this.vizWidth = Math.max(svgWidth - margin.left - margin.right, 0); @@ -586,6 +600,7 @@ class TimeseriesChartIntl extends Component { renderFocusChart() { const { + embeddableMode, focusAggregationInterval, focusAnnotationData: focusAnnotationDataOriginalPropValue, focusChartData, @@ -614,12 +629,12 @@ class TimeseriesChartIntl extends Component { const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); const hideFocusChartTooltip = this.props.tooltipService.hide.bind(this.props.tooltipService); - const focusChart = d3.select('.focus-chart'); + const chartElement = d3.select(this.rootNode); + const focusChart = chartElement.select('.focus-chart'); // Update the plot interval labels. const focusAggInt = focusAggregationInterval.expression; const bucketSpan = selectedJob.analysis_config.bucket_span; - const chartElement = d3.select(this.rootNode); chartElement.select('.zoom-aggregation-interval').text( i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomAggregationIntervalLabel', { defaultMessage: '(aggregation interval: {focusAggInt}, bucket span: {bucketSpan})', @@ -726,7 +741,7 @@ class TimeseriesChartIntl extends Component { } // Get the scaled date format to use for x axis tick labels. - const timeBuckets = getTimeBucketsFromCache(); + const timeBuckets = this.getTimeBuckets(); timeBuckets.setInterval('auto'); timeBuckets.setBounds(bounds); const xAxisTickFormat = timeBuckets.getScaledDateFormat(); @@ -761,8 +776,10 @@ class TimeseriesChartIntl extends Component { this.props.annotationUpdatesService ); - // disable brushing (creation of annotations) when annotations aren't shown - focusChart.select('.mlAnnotationBrush').style('display', showAnnotations ? null : 'none'); + // disable brushing (creation of annotations) when annotations aren't shown or when in embeddable mode + focusChart + .select('.mlAnnotationBrush') + .style('display', !showAnnotations || embeddableMode ? 'none' : null); focusChart.select('.values-line').attr('d', this.focusValuesLine(data)); drawLineChartDots(data, focusChart, this.focusValuesLine); @@ -771,7 +788,7 @@ class TimeseriesChartIntl extends Component { // These are used for displaying tooltips on mouseover. // Don't render dots where value=null (data gaps, with no anomalies) // or for multi-bucket anomalies. - const dots = d3 + const dots = chartElement .select('.focus-chart-markers') .selectAll('.metric-value') .data( @@ -822,7 +839,7 @@ class TimeseriesChartIntl extends Component { }); // Render cross symbols for any multi-bucket anomalies. - const multiBucketMarkers = d3 + const multiBucketMarkers = chartElement .select('.focus-chart-markers') .selectAll('.multi-bucket') .data( @@ -857,7 +874,7 @@ class TimeseriesChartIntl extends Component { .attr('class', (d) => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore).id}`); // Add rectangular markers for any scheduled events. - const scheduledEventMarkers = d3 + const scheduledEventMarkers = chartElement .select('.focus-chart-markers') .selectAll('.scheduled-event-marker') .data(data.filter((d) => d.scheduledEvents !== undefined)); @@ -898,7 +915,7 @@ class TimeseriesChartIntl extends Component { .attr('d', this.focusValuesLine(focusForecastData)) .classed('hidden', !showForecast); - const forecastDots = d3 + const forecastDots = chartElement .select('.focus-chart-markers.forecast') .selectAll('.metric-value') .data(focusForecastData); @@ -1007,7 +1024,7 @@ class TimeseriesChartIntl extends Component { const chartElement = d3.select(this.rootNode); chartElement.selectAll('.focus-zoom a').on('click', function () { d3.event.preventDefault(); - setZoomInterval(d3.select(this).attr('data-ms')); + setZoomInterval(this.getAttribute('data-ms')); }); } @@ -1129,7 +1146,7 @@ class TimeseriesChartIntl extends Component { .attr('y2', brushChartHeight); // Add x axis. - const timeBuckets = getTimeBucketsFromCache(); + const timeBuckets = this.getTimeBuckets(); timeBuckets.setInterval('auto'); timeBuckets.setBounds(bounds); const xAxisTickFormat = timeBuckets.getScaledDateFormat(); @@ -1328,6 +1345,7 @@ class TimeseriesChartIntl extends Component { </svg> </div>`); + const that = this; function brushing() { const brushExtent = brush.extent(); mask.reveal(brushExtent); @@ -1345,11 +1363,11 @@ class TimeseriesChartIntl extends Component { topBorder.attr('width', topBorderWidth); const isEmpty = brush.empty(); - d3.selectAll('.brush-handle').style('visibility', isEmpty ? 'hidden' : 'visible'); + const chartElement = d3.select(that.rootNode); + chartElement.selectAll('.brush-handle').style('visibility', isEmpty ? 'hidden' : 'visible'); } brushing(); - const that = this; function brushed() { const isEmpty = brush.empty(); const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); @@ -1478,18 +1496,19 @@ class TimeseriesChartIntl extends Component { // Sets the extent of the brush on the context chart to the // supplied from and to Date objects. setContextBrushExtent = (from, to) => { + const chartElement = d3.select(this.rootNode); const brush = this.brush; const brushExtent = brush.extent(); const newExtent = [from, to]; brush.extent(newExtent); - brush(d3.select('.brush')); + brush(chartElement.select('.brush')); if ( newExtent[0].getTime() !== brushExtent[0].getTime() || newExtent[1].getTime() !== brushExtent[1].getTime() ) { - brush.event(d3.select('.brush')); + brush.event(chartElement.select('.brush')); } }; @@ -1867,12 +1886,13 @@ class TimeseriesChartIntl extends Component { anomalyTime, focusAggregationInterval ); + const chartElement = d3.select(this.rootNode); // Render an additional highlighted anomaly marker on the focus chart. // TODO - plot anomaly markers for cases where there is an anomaly due // to the absence of data and model plot is enabled. if (markerToSelect !== undefined) { - const selectedMarker = d3 + const selectedMarker = chartElement .select('.focus-chart-markers') .selectAll('.focus-chart-highlighted-marker') .data([markerToSelect]); @@ -1905,7 +1925,6 @@ class TimeseriesChartIntl extends Component { // Display the chart tooltip for this marker. // Note the values of the record and marker may differ depending on the levels of aggregation. - const chartElement = d3.select(this.rootNode); const anomalyMarker = chartElement.selectAll( '.focus-chart-markers .anomaly-marker.highlighted' ); @@ -1916,7 +1935,8 @@ class TimeseriesChartIntl extends Component { } unhighlightFocusChartAnomaly() { - d3.select('.focus-chart-markers').selectAll('.anomaly-marker.highlighted').remove(); + const chartElement = d3.select(this.rootNode); + chartElement.select('.focus-chart-markers').selectAll('.anomaly-marker.highlighted').remove(); this.props.tooltipService.hide(); } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx index 66da1e4222887..b9e09158bf280 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx @@ -15,7 +15,7 @@ import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs' import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../common/constants/search'; import { Annotation } from '../../../../../common/types/annotations'; import { useMlKibana, useNotifications } from '../../../contexts/kibana'; -import { getBoundsRoundedToInterval } from '../../../util/time_buckets'; +import { useTimeBucketsService } from '../../../util/time_buckets_service'; import { getControlsForDetector } from '../../get_controls_for_detector'; import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; import { SourceIndicesWithGeoFields } from '../../../explorer/explorer_utils'; @@ -23,6 +23,7 @@ import { SourceIndicesWithGeoFields } from '../../../explorer/explorer_utils'; interface TimeSeriesChartWithTooltipsProps { bounds: any; detectorIndex: number; + embeddableMode?: boolean; renderFocusChartOnly: boolean; selectedJob: CombinedJob; selectedEntities: Record<string, any>; @@ -41,6 +42,7 @@ interface TimeSeriesChartWithTooltipsProps { export const TimeSeriesChartWithTooltips: FC<TimeSeriesChartWithTooltipsProps> = ({ bounds, detectorIndex, + embeddableMode, renderFocusChartOnly, selectedJob, selectedEntities, @@ -80,13 +82,19 @@ export const TimeSeriesChartWithTooltips: FC<TimeSeriesChartWithTooltipsProps> = // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const mlTimeBucketsService = useTimeBucketsService(); + useEffect(() => { let unmounted = false; const entities = getControlsForDetector(detectorIndex, selectedEntities, selectedJob.job_id); const nonBlankEntities = Array.isArray(entities) ? entities.filter((entity) => entity.fieldValue !== null) : undefined; - const searchBounds = getBoundsRoundedToInterval(bounds, contextAggregationInterval, false); + const searchBounds = mlTimeBucketsService.getBoundsRoundedToInterval( + bounds, + contextAggregationInterval, + false + ); /** * Loads the full list of annotations for job without any aggs or time boundaries @@ -138,6 +146,7 @@ export const TimeSeriesChartWithTooltips: FC<TimeSeriesChartWithTooltipsProps> = annotationData={annotationData} bounds={bounds} detectorIndex={detectorIndex} + embeddableMode={embeddableMode} renderFocusChartOnly={renderFocusChartOnly} selectedJob={selectedJob} showAnnotations={showAnnotations} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts index cf8e1f0aa989c..30f097dabb8ab 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_controls_for_detector.ts @@ -7,7 +7,7 @@ import { mlJobService } from '../services/job_service'; import { Entity } from './components/entity_control/entity_control'; -import { JobId } from '../../../common/types/anomaly_detection_jobs'; +import type { JobId, CombinedJob } from '../../../common/types/anomaly_detection_jobs'; /** * Extracts entities from the detector configuration @@ -15,9 +15,10 @@ import { JobId } from '../../../common/types/anomaly_detection_jobs'; export function getControlsForDetector( selectedDetectorIndex: number, selectedEntities: Record<string, any>, - selectedJobId: JobId + selectedJobId: JobId, + job?: CombinedJob ): Entity[] { - const selectedJob = mlJobService.getJob(selectedJobId); + const selectedJob = job ?? mlJobService.getJob(selectedJobId); const entities: Entity[] = []; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts index d0dfdc9ed372b..e6f1a2ec65afd 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/get_function_description.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { lastValueFrom } from 'rxjs'; import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils'; -import { mlResultsService } from '../services/results_service'; +import { type MlResultsService } from '../services/results_service'; import { ToastNotificationService } from '../services/toast_notification_service'; import { getControlsForDetector } from './get_controls_for_detector'; import { getCriteriaFields } from './get_criteria_fields'; @@ -41,7 +41,8 @@ export const getFunctionDescription = async ( selectedJobId: string; selectedJob: CombinedJob; }, - toastNotificationService: ToastNotificationService + toastNotificationService: ToastNotificationService, + mlResultsService: MlResultsService ) => { // if the detector's function is metric, fetch the highest scoring anomaly record // and set to plot the function_description (avg/min/max) of that record by default diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index 044d166ca5efa..05adc2355ef3c 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -6,6 +6,7 @@ */ import { each, find, get, filter } from 'lodash'; +import type { ES_AGGREGATION } from '@kbn/ml-anomaly-utils'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -15,7 +16,6 @@ import { isModelPlotChartableForDetector, isModelPlotEnabled, } from '../../../common/util/job_utils'; -// @ts-ignore import { buildConfigFromDetector } from '../util/chart_config_builder'; import { mlResultsService } from '../services/results_service'; import { ModelPlotOutput } from '../services/results_service/result_service_rx'; @@ -113,29 +113,48 @@ function getMetricData( } } -// Builds chart detail information (charting function description and entity counts) used -// in the title area of the time series chart. -// Queries Elasticsearch if necessary to obtain the distinct count of entities -// for which data is being plotted. +interface TimeSeriesExplorerChartDetails { + success: boolean; + results: { + functionLabel: string | null; + entityData: { count?: number; entities: Array<{ fieldName: string; cardinality?: number }> }; + }; +} + +/** + * Builds chart detail information (charting function description and entity counts) used + * in the title area of the time series chart. + * Queries Elasticsearch if necessary to obtain the distinct count of entities + * for which data is being plotted. + * @param job Job config info + * @param detectorIndex The index of the detector in the job config + * @param entityFields Array of field name - field value pairs + * @param earliestMs Earliest timestamp in milliseconds + * @param latestMs Latest timestamp in milliseconds + * @param metricFunctionDescription The underlying function (min, max, avg) for "metric" detector type + * @returns chart data to plot for Single Metric Viewer/Time series explorer + */ function getChartDetails( job: Job, detectorIndex: number, - entityFields: any[], + entityFields: MlEntityField[], earliestMs: number, - latestMs: number + latestMs: number, + metricFunctionDescription?: ES_AGGREGATION ) { return new Promise((resolve, reject) => { - const obj: any = { + const obj: TimeSeriesExplorerChartDetails = { success: true, results: { functionLabel: '', entityData: { entities: [] } }, }; - const chartConfig = buildConfigFromDetector(job, detectorIndex); + const chartConfig = buildConfigFromDetector(job, detectorIndex, metricFunctionDescription); + let functionLabel: string | null = chartConfig.metricFunction; if (chartConfig.metricFieldName !== undefined) { - functionLabel += ' '; - functionLabel += chartConfig.metricFieldName; + functionLabel += ` ${chartConfig.metricFieldName}`; } + obj.results.functionLabel = functionLabel; const blankEntityFields = filter(entityFields, (entity) => { diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 757f4cb06543e..76c0ffb038971 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -77,6 +77,7 @@ import { processMetricPlotResults, processRecordScoreResults, getFocusData, + getTimeseriesexplorerDefaultState, } from './timeseriesexplorer_utils'; import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings'; import { getControlsForDetector } from './get_controls_for_detector'; @@ -96,46 +97,6 @@ const allValuesLabel = i18n.translate('xpack.ml.timeSeriesExplorer.allPartitionV defaultMessage: 'all', }); -function getTimeseriesexplorerDefaultState() { - return { - chartDetails: undefined, - contextAggregationInterval: undefined, - contextChartData: undefined, - contextForecastData: undefined, - // Not chartable if e.g. model plot with terms for a varp detector - dataNotChartable: false, - entitiesLoading: false, - entityValues: {}, - focusAnnotationData: [], - focusAggregationInterval: {}, - focusChartData: undefined, - focusForecastData: undefined, - fullRefresh: true, - hasResults: false, - // Counter to keep track of what data sets have been loaded. - loadCounter: 0, - loading: false, - modelPlotEnabled: false, - // Toggles display of annotations in the focus chart - showAnnotations: true, - showAnnotationsCheckbox: true, - // Toggles display of forecast data in the focus chart - showForecast: true, - showForecastCheckbox: false, - // Toggles display of model bounds in the focus chart - showModelBounds: true, - showModelBoundsCheckbox: false, - svgWidth: 0, - tableData: undefined, - zoomFrom: undefined, - zoomTo: undefined, - zoomFromFocusLoaded: undefined, - zoomToFocusLoaded: undefined, - chartDataError: undefined, - sourceIndicesWithGeoFields: {}, - }; -} - const containerPadding = 34; export class TimeSeriesExplorer extends React.Component { @@ -265,7 +226,7 @@ export class TimeSeriesExplorer extends React.Component { } /** - * Gets focus data for the current component state/ + * Gets focus data for the current component state */ getFocusData(selection) { const { selectedJobId, selectedForecastId, selectedDetectorIndex, functionDescription } = @@ -607,7 +568,8 @@ export class TimeSeriesExplorer extends React.Component { detectorIndex, entityControls, searchBounds.min.valueOf(), - searchBounds.max.valueOf() + searchBounds.max.valueOf(), + this.props.functionDescription ) .then((resp) => { stateUpdate.chartDetails = resp.results; @@ -745,7 +707,6 @@ export class TimeSeriesExplorer extends React.Component { ); } } - // Required to redraw the time series chart when the container is resized. this.resizeChecker = new ResizeChecker(this.resizeRef.current); this.resizeChecker.on('resize', () => { @@ -1091,7 +1052,6 @@ export class TimeSeriesExplorer extends React.Component { entities={entityControls} /> )} - {arePartitioningFieldsProvided && jobs.length > 0 && (fullRefresh === false || loading === false) && diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts index d66dca5f565d7..5d13c73f8401f 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts @@ -17,7 +17,9 @@ export const APP_STATE_ACTION = { SET_ZOOM: 'SET_ZOOM', UNSET_ZOOM: 'UNSET_ZOOM', SET_FUNCTION_DESCRIPTION: 'SET_FUNCTION_DESCRIPTION', -}; +} as const; + +export type TimeseriesexplorerActionType = typeof APP_STATE_ACTION[keyof typeof APP_STATE_ACTION]; export const CHARTS_POINT_TARGET = 500; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/index.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/index.ts new file mode 100644 index 0000000000000..b81b4bc96a434 --- /dev/null +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { TimeSeriesExplorerEmbeddableChart } from './timeseriesexplorer_embeddable_chart'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_checkbox.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_checkbox.tsx new file mode 100644 index 0000000000000..e1136e54180ac --- /dev/null +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_checkbox.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, { FC, useMemo } from 'react'; +import { EuiCheckbox, EuiFlexItem, htmlIdGenerator } from '@elastic/eui'; + +interface Props { + id: string; + label: string; + checked: boolean; + onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; +} + +export const TimeseriesExplorerCheckbox: FC<Props> = ({ id, label, checked, onChange }) => { + const checkboxId = useMemo(() => `id-${htmlIdGenerator()()}`, []); + return ( + <EuiFlexItem grow={false}> + <EuiCheckbox id={checkboxId} label={label} checked={checked} onChange={onChange} /> + </EuiFlexItem> + ); +}; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js new file mode 100644 index 0000000000000..fd6b0239199bc --- /dev/null +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js @@ -0,0 +1,897 @@ +/* + * 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. + */ + +/* + * React component for rendering Single Metric Viewer. + */ + +import { isEqual } from 'lodash'; +import moment from 'moment-timezone'; +import { Subject, Subscription, forkJoin } from 'rxjs'; +import { debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators'; + +import PropTypes from 'prop-types'; +import React, { Fragment } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { context } from '@kbn/kibana-react-plugin/public'; + +import { + EuiCallOut, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, + EuiTextColor, +} from '@elastic/eui'; +import { TimeSeriesExplorerHelpPopover } from '../timeseriesexplorer_help_popover'; + +import { + isModelPlotEnabled, + isModelPlotChartableForDetector, + isSourceDataChartableForDetector, +} from '../../../../common/util/job_utils'; + +import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; +import { TimeseriesexplorerNoChartData } from '../components/timeseriesexplorer_no_chart_data'; + +import { + APP_STATE_ACTION, + CHARTS_POINT_TARGET, + TIME_FIELD_NAME, +} from '../timeseriesexplorer_constants'; +import { getControlsForDetector } from '../get_controls_for_detector'; +import { TimeSeriesChartWithTooltips } from '../components/timeseries_chart/timeseries_chart_with_tooltip'; +import { aggregationTypeTransform } from '@kbn/ml-anomaly-utils'; +import { isMetricDetector } from '../get_function_description'; +import { TimeseriesexplorerChartDataError } from '../components/timeseriesexplorer_chart_data_error'; +import { TimeseriesExplorerCheckbox } from './timeseriesexplorer_checkbox'; +import { timeBucketsServiceFactory } from '../../util/time_buckets_service'; +import { timeSeriesExplorerServiceFactory } from '../../util/time_series_explorer_service'; +import { getTimeseriesexplorerDefaultState } from '../timeseriesexplorer_utils'; + +// Used to indicate the chart is being plotted across +// all partition field values, where the cardinality of the field cannot be +// obtained as it is not aggregatable e.g. 'all distinct kpi_indicator values' +const allValuesLabel = i18n.translate('xpack.ml.timeSeriesExplorer.allPartitionValuesLabel', { + defaultMessage: 'all', +}); + +export class TimeSeriesExplorerEmbeddableChart extends React.Component { + static propTypes = { + appStateHandler: PropTypes.func.isRequired, + autoZoomDuration: PropTypes.number.isRequired, + bounds: PropTypes.object.isRequired, + chartWidth: PropTypes.number.isRequired, + lastRefresh: PropTypes.number.isRequired, + previousRefresh: PropTypes.number.isRequired, + selectedJobId: PropTypes.string.isRequired, + selectedDetectorIndex: PropTypes.number, + selectedEntities: PropTypes.object, + selectedForecastId: PropTypes.string, + zoom: PropTypes.object, + toastNotificationService: PropTypes.object, + dataViewsService: PropTypes.object, + }; + + state = getTimeseriesexplorerDefaultState(); + + subscriptions = new Subscription(); + + unmounted = false; + + /** + * Subject for listening brush time range selection. + */ + contextChart$ = new Subject(); + + /** + * Access ML services in react context. + */ + static contextType = context; + + getBoundsRoundedToInterval; + mlTimeSeriesExplorer; + + /** + * Returns field names that don't have a selection yet. + */ + getFieldNamesWithEmptyValues = () => { + const latestEntityControls = this.getControlsForDetector(); + return latestEntityControls + .filter(({ fieldValue }) => fieldValue === null) + .map(({ fieldName }) => fieldName); + }; + + /** + * Checks if all entity control dropdowns have a selection. + */ + arePartitioningFieldsProvided = () => { + const fieldNamesWithEmptyValues = this.getFieldNamesWithEmptyValues(); + return fieldNamesWithEmptyValues.length === 0; + }; + + toggleShowAnnotationsHandler = () => { + this.setState((prevState) => ({ + showAnnotations: !prevState.showAnnotations, + })); + }; + + toggleShowForecastHandler = () => { + this.setState((prevState) => ({ + showForecast: !prevState.showForecast, + })); + }; + + toggleShowModelBoundsHandler = () => { + this.setState({ + showModelBounds: !this.state.showModelBounds, + }); + }; + + setFunctionDescription = (selectedFuction) => { + this.props.appStateHandler(APP_STATE_ACTION.SET_FUNCTION_DESCRIPTION, selectedFuction); + }; + + previousChartProps = {}; + previousShowAnnotations = undefined; + previousShowForecast = undefined; + previousShowModelBounds = undefined; + + tableFilter = (field, value, operator) => { + const entities = this.getControlsForDetector(); + const entity = entities.find(({ fieldName }) => fieldName === field); + + if (entity === undefined) { + return; + } + + const { appStateHandler } = this.props; + + let resultValue = ''; + if (operator === '+' && entity.fieldValue !== value) { + resultValue = value; + } else if (operator === '-' && entity.fieldValue === value) { + resultValue = null; + } else { + return; + } + + const resultEntities = { + ...entities.reduce((appStateEntities, appStateEntity) => { + appStateEntities[appStateEntity.fieldName] = appStateEntity.fieldValue; + return appStateEntities; + }, {}), + [entity.fieldName]: resultValue, + }; + + appStateHandler(APP_STATE_ACTION.SET_ENTITIES, resultEntities); + }; + + contextChartSelectedInitCallDone = false; + + getFocusAggregationInterval(selection) { + const { selectedJob } = this.props; + + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; + + return this.mlTimeSeriesExplorer.calculateAggregationInterval( + bounds, + CHARTS_POINT_TARGET, + selectedJob + ); + } + + /** + * Gets focus data for the current component state + */ + getFocusData(selection) { + const { selectedForecastId, selectedDetectorIndex, functionDescription, selectedJob } = + this.props; + const { modelPlotEnabled } = this.state; + if (isMetricDetector(selectedJob, selectedDetectorIndex) && functionDescription === undefined) { + return; + } + const entityControls = this.getControlsForDetector(); + + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; + const focusAggregationInterval = this.getFocusAggregationInterval(selection); + + // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. + // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected + // to some extent with all detector functions if not searching complete buckets. + const searchBounds = this.getBoundsRoundedToInterval(bounds, focusAggregationInterval, false); + + return this.mlTimeSeriesExplorer.getFocusData( + this.getCriteriaFields(selectedDetectorIndex, entityControls), + selectedDetectorIndex, + focusAggregationInterval, + selectedForecastId, + modelPlotEnabled, + entityControls.filter((entity) => entity.fieldValue !== null), + searchBounds, + selectedJob, + functionDescription, + TIME_FIELD_NAME + ); + } + + contextChartSelected = (selection) => { + const zoomState = { + from: selection.from.toISOString(), + to: selection.to.toISOString(), + }; + + if ( + isEqual(this.props.zoom, zoomState) && + this.state.focusChartData !== undefined && + this.props.previousRefresh === this.props.lastRefresh + ) { + return; + } + + this.contextChart$.next(selection); + this.props.appStateHandler(APP_STATE_ACTION.SET_ZOOM, zoomState); + }; + + setForecastId = (forecastId) => { + this.props.appStateHandler(APP_STATE_ACTION.SET_FORECAST_ID, forecastId); + }; + + displayErrorToastMessages = (error, errorMsg) => { + if (this.props.toastNotificationService) { + this.props.toastNotificationService.displayErrorToast(error, errorMsg, 2000); + } + this.setState({ loading: false, chartDataError: errorMsg }); + }; + + loadSingleMetricData = (fullRefresh = true) => { + const { + autoZoomDuration, + bounds, + selectedDetectorIndex, + zoom, + functionDescription, + selectedJob, + } = this.props; + + const { loadCounter: currentLoadCounter } = this.state; + if (selectedJob === undefined) { + return; + } + if (isMetricDetector(selectedJob, selectedDetectorIndex) && functionDescription === undefined) { + return; + } + + const functionToPlotByIfMetric = aggregationTypeTransform.toES(functionDescription); + + this.contextChartSelectedInitCallDone = false; + + // Only when `fullRefresh` is true we'll reset all data + // and show the loading spinner within the page. + const entityControls = this.getControlsForDetector(); + this.setState( + { + fullRefresh, + loadCounter: currentLoadCounter + 1, + loading: true, + chartDataError: undefined, + ...(fullRefresh + ? { + chartDetails: undefined, + contextChartData: undefined, + contextForecastData: undefined, + focusChartData: undefined, + focusForecastData: undefined, + modelPlotEnabled: + isModelPlotChartableForDetector(selectedJob, selectedDetectorIndex) && + isModelPlotEnabled(selectedJob, selectedDetectorIndex, entityControls), + hasResults: false, + dataNotChartable: false, + } + : {}), + }, + () => { + const { loadCounter, modelPlotEnabled } = this.state; + const { selectedJob } = this.props; + + const detectorIndex = selectedDetectorIndex; + + let awaitingCount = 3; + + const stateUpdate = {}; + + // finish() function, called after each data set has been loaded and processed. + // The last one to call it will trigger the page render. + const finish = (counterVar) => { + awaitingCount--; + if (awaitingCount === 0 && counterVar === loadCounter) { + stateUpdate.hasResults = + (Array.isArray(stateUpdate.contextChartData) && + stateUpdate.contextChartData.length > 0) || + (Array.isArray(stateUpdate.contextForecastData) && + stateUpdate.contextForecastData.length > 0); + stateUpdate.loading = false; + + // Set zoomFrom/zoomTo attributes in scope which will result in the metric chart automatically + // selecting the specified range in the context chart, and so loading that date range in the focus chart. + // Only touch the zoom range if data for the context chart has been loaded and all necessary + // partition fields have a selection. + if ( + stateUpdate.contextChartData.length && + this.arePartitioningFieldsProvided() === true + ) { + // Check for a zoom parameter in the appState (URL). + let focusRange = this.mlTimeSeriesExplorer.calculateInitialFocusRange( + zoom, + stateUpdate.contextAggregationInterval, + bounds + ); + if ( + focusRange === undefined || + this.previousSelectedForecastId !== this.props.selectedForecastId + ) { + focusRange = this.mlTimeSeriesExplorer.calculateDefaultFocusRange( + autoZoomDuration, + stateUpdate.contextAggregationInterval, + stateUpdate.contextChartData, + stateUpdate.contextForecastData + ); + this.previousSelectedForecastId = this.props.selectedForecastId; + } + + this.contextChartSelected({ + from: focusRange[0], + to: focusRange[1], + }); + } + + this.setState(stateUpdate); + } + }; + + const nonBlankEntities = entityControls.filter((entity) => { + return entity.fieldValue !== null; + }); + + if ( + modelPlotEnabled === false && + isSourceDataChartableForDetector(selectedJob, detectorIndex) === false && + nonBlankEntities.length > 0 + ) { + // For detectors where model plot has been enabled with a terms filter and the + // selected entity(s) are not in the terms list, indicate that data cannot be viewed. + stateUpdate.hasResults = false; + stateUpdate.loading = false; + stateUpdate.dataNotChartable = true; + this.setState(stateUpdate); + return; + } + + // Calculate the aggregation interval for the context chart. + // Context chart swimlane will display bucket anomaly score at the same interval. + stateUpdate.contextAggregationInterval = + this.mlTimeSeriesExplorer.calculateAggregationInterval( + bounds, + CHARTS_POINT_TARGET, + selectedJob + ); + + // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. + // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected + // to some extent with all detector functions if not searching complete buckets. + const searchBounds = this.getBoundsRoundedToInterval( + bounds, + stateUpdate.contextAggregationInterval, + false + ); + + // Query 1 - load metric data at low granularity across full time range. + // Pass a counter flag into the finish() function to make sure we only process the results + // for the most recent call to the load the data in cases where the job selection and time filter + // have been altered in quick succession (such as from the job picker with 'Apply time range'). + const counter = loadCounter; + this.context.services.mlServices.mlTimeSeriesSearchService + .getMetricData( + selectedJob, + detectorIndex, + nonBlankEntities, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + stateUpdate.contextAggregationInterval.asMilliseconds(), + functionToPlotByIfMetric + ) + .toPromise() + .then((resp) => { + const fullRangeChartData = this.mlTimeSeriesExplorer.processMetricPlotResults( + resp.results, + modelPlotEnabled + ); + stateUpdate.contextChartData = fullRangeChartData; + finish(counter); + }) + .catch((err) => { + const errorMsg = i18n.translate('xpack.ml.timeSeriesExplorer.metricDataErrorMessage', { + defaultMessage: 'Error getting metric data', + }); + this.displayErrorToastMessages(err, errorMsg); + }); + + // Query 2 - load max record score at same granularity as context chart + // across full time range for use in the swimlane. + this.context.services.mlServices.mlResultsService + .getRecordMaxScoreByTime( + selectedJob.job_id, + this.getCriteriaFields(detectorIndex, entityControls), + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + stateUpdate.contextAggregationInterval.asMilliseconds(), + functionToPlotByIfMetric + ) + .then((resp) => { + const fullRangeRecordScoreData = this.mlTimeSeriesExplorer.processRecordScoreResults( + resp.results + ); + stateUpdate.swimlaneData = fullRangeRecordScoreData; + finish(counter); + }) + .catch((err) => { + const errorMsg = i18n.translate( + 'xpack.ml.timeSeriesExplorer.bucketAnomalyScoresErrorMessage', + { + defaultMessage: 'Error getting bucket anomaly scores', + } + ); + + this.displayErrorToastMessages(err, errorMsg); + }); + + // Query 3 - load details on the chart used in the chart title (charting function and entity(s)). + this.context.services.mlServices.mlTimeSeriesSearchService + .getChartDetails( + selectedJob, + detectorIndex, + entityControls, + searchBounds.min.valueOf(), + searchBounds.max.valueOf() + ) + .then((resp) => { + stateUpdate.chartDetails = resp.results; + finish(counter); + }) + .catch((err) => { + this.displayErrorToastMessages( + err, + i18n.translate('xpack.ml.timeSeriesExplorer.entityCountsErrorMessage', { + defaultMessage: 'Error getting entity counts', + }) + ); + }); + } + ); + }; + + /** + * Updates local state of detector related controls from the global state. + * @param callback to invoke after a state update. + */ + getControlsForDetector = () => { + const { selectedDetectorIndex, selectedEntities, selectedJobId, selectedJob } = this.props; + return getControlsForDetector( + selectedDetectorIndex, + selectedEntities, + selectedJobId, + selectedJob + ); + }; + + /** + * Updates criteria fields for API calls, e.g. getAnomaliesTableData + * @param detectorIndex + * @param entities + */ + getCriteriaFields(detectorIndex, entities) { + // Only filter on the entity if the field has a value. + const nonBlankEntities = entities.filter((entity) => entity.fieldValue !== null); + return [ + { + fieldName: 'detector_index', + fieldValue: detectorIndex, + }, + ...nonBlankEntities, + ]; + } + + async componentDidMount() { + this.getBoundsRoundedToInterval = timeBucketsServiceFactory( + this.context.services.uiSettings + ).getBoundsRoundedToInterval; + + this.mlTimeSeriesExplorer = timeSeriesExplorerServiceFactory( + this.context.services.uiSettings, + this.context.services.mlServices.mlApiServices, + this.context.services.mlServices.mlResultsService + ); + + // Listen for context chart updates. + this.subscriptions.add( + this.contextChart$ + .pipe( + tap((selection) => { + this.setState({ + zoomFrom: selection.from, + zoomTo: selection.to, + }); + }), + debounceTime(500), + tap((selection) => { + const { + contextChartData, + contextForecastData, + focusChartData, + zoomFromFocusLoaded, + zoomToFocusLoaded, + } = this.state; + + if ( + (contextChartData === undefined || contextChartData.length === 0) && + (contextForecastData === undefined || contextForecastData.length === 0) + ) { + return; + } + + if ( + (this.contextChartSelectedInitCallDone === false && focusChartData === undefined) || + zoomFromFocusLoaded.getTime() !== selection.from.getTime() || + zoomToFocusLoaded.getTime() !== selection.to.getTime() + ) { + this.contextChartSelectedInitCallDone = true; + + this.setState({ + loading: true, + fullRefresh: false, + }); + } + }), + switchMap((selection) => { + return forkJoin([this.getFocusData(selection)]); + }), + withLatestFrom(this.contextChart$) + ) + .subscribe(([[refreshFocusData, tableData], selection]) => { + const { modelPlotEnabled } = this.state; + + // All the data is ready now for a state update. + this.setState({ + focusAggregationInterval: this.getFocusAggregationInterval({ + from: selection.from, + to: selection.to, + }), + loading: false, + showModelBoundsCheckbox: modelPlotEnabled && refreshFocusData.focusChartData.length > 0, + zoomFromFocusLoaded: selection.from, + zoomToFocusLoaded: selection.to, + ...refreshFocusData, + ...tableData, + }); + }) + ); + + if (this.context && this.props.selectedJob !== undefined) { + // Populate the map of jobs / detectors / field formatters for the selected IDs and refresh. + this.context.services.mlServices.mlFieldFormatService.populateFormats([ + this.props.selectedJob.job_id, + ]); + } + + this.componentDidUpdate(); + } + + componentDidUpdate(previousProps) { + if ( + previousProps === undefined || + previousProps.selectedForecastId !== this.props.selectedForecastId + ) { + if (this.props.selectedForecastId !== undefined) { + // Ensure the forecast data will be shown if hidden previously. + this.setState({ showForecast: true }); + // Not best practice but we need the previous value for another comparison + // once all the data was loaded. + if (previousProps !== undefined) { + this.previousSelectedForecastId = previousProps.selectedForecastId; + } + } + } + + if ( + previousProps === undefined || + !isEqual(previousProps.bounds, this.props.bounds) || + (!isEqual(previousProps.lastRefresh, this.props.lastRefresh) && + previousProps.lastRefresh !== 0) || + !isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) || + !isEqual(previousProps.selectedEntities, this.props.selectedEntities) || + previousProps.selectedForecastId !== this.props.selectedForecastId || + previousProps.selectedJobId !== this.props.selectedJobId || + previousProps.functionDescription !== this.props.functionDescription + ) { + const fullRefresh = + previousProps === undefined || + !isEqual(previousProps.bounds, this.props.bounds) || + !isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) || + !isEqual(previousProps.selectedEntities, this.props.selectedEntities) || + previousProps.selectedForecastId !== this.props.selectedForecastId || + previousProps.selectedJobId !== this.props.selectedJobId || + previousProps.functionDescription !== this.props.functionDescription; + this.loadSingleMetricData(fullRefresh); + } + + if (previousProps === undefined) { + return; + } + } + + componentWillUnmount() { + this.subscriptions.unsubscribe(); + this.unmounted = true; + } + + render() { + const { + autoZoomDuration, + bounds, + chartWidth, + lastRefresh, + selectedDetectorIndex, + selectedJob, + } = this.props; + + const { + chartDetails, + contextAggregationInterval, + contextChartData, + contextForecastData, + dataNotChartable, + focusAggregationInterval, + focusAnnotationData, + focusChartData, + focusForecastData, + fullRefresh, + hasResults, + loading, + modelPlotEnabled, + showAnnotations, + showAnnotationsCheckbox, + showForecast, + showForecastCheckbox, + showModelBounds, + showModelBoundsCheckbox, + swimlaneData, + zoomFrom, + zoomTo, + zoomFromFocusLoaded, + zoomToFocusLoaded, + chartDataError, + } = this.state; + const chartProps = { + modelPlotEnabled, + contextChartData, + contextChartSelected: this.contextChartSelected, + contextForecastData, + contextAggregationInterval, + swimlaneData, + focusAnnotationData, + focusChartData, + focusForecastData, + focusAggregationInterval, + svgWidth: chartWidth, + zoomFrom, + zoomTo, + zoomFromFocusLoaded, + zoomToFocusLoaded, + autoZoomDuration, + }; + + const entityControls = this.getControlsForDetector(); + const fieldNamesWithEmptyValues = this.getFieldNamesWithEmptyValues(); + const arePartitioningFieldsProvided = this.arePartitioningFieldsProvided(); + + let renderFocusChartOnly = true; + + if ( + isEqual(this.previousChartProps.focusForecastData, chartProps.focusForecastData) && + isEqual(this.previousChartProps.focusChartData, chartProps.focusChartData) && + isEqual(this.previousChartProps.focusAnnotationData, chartProps.focusAnnotationData) && + this.previousShowForecast === showForecast && + this.previousShowModelBounds === showModelBounds && + this.props.previousRefresh === lastRefresh + ) { + renderFocusChartOnly = false; + } + + this.previousChartProps = chartProps; + this.previousShowForecast = showForecast; + this.previousShowModelBounds = showModelBounds; + + return ( + <> + {fieldNamesWithEmptyValues.length > 0 && ( + <> + <EuiCallOut + title={ + <FormattedMessage + id="xpack.ml.timeSeriesExplorer.singleMetricRequiredMessage" + defaultMessage="To view a single metric, select {missingValuesCount, plural, one {a value for {fieldName1}} other {values for {fieldName1} and {fieldName2}}}." + values={{ + missingValuesCount: fieldNamesWithEmptyValues.length, + fieldName1: fieldNamesWithEmptyValues[0], + fieldName2: fieldNamesWithEmptyValues[1], + }} + /> + } + iconType="help" + size="s" + /> + <EuiSpacer size="m" /> + </> + )} + + {fullRefresh && loading === true && ( + <LoadingIndicator + label={i18n.translate('xpack.ml.timeSeriesExplorer.loadingLabel', { + defaultMessage: 'Loading', + })} + /> + )} + + {loading === false && chartDataError !== undefined && ( + <TimeseriesexplorerChartDataError errorMsg={chartDataError} /> + )} + + {arePartitioningFieldsProvided && + selectedJob && + (fullRefresh === false || loading === false) && + hasResults === false && + chartDataError === undefined && ( + <TimeseriesexplorerNoChartData + dataNotChartable={dataNotChartable} + entities={entityControls} + /> + )} + {arePartitioningFieldsProvided && + selectedJob && + (fullRefresh === false || loading === false) && + hasResults === true && ( + <div> + <EuiFlexGroup gutterSize="xs" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle size={'xs'}> + <h2> + <span> + {i18n.translate( + 'xpack.ml.timeSeriesExplorer.singleTimeSeriesAnalysisTitle', + { + defaultMessage: 'Single time series analysis of {functionLabel}', + values: { functionLabel: chartDetails.functionLabel }, + } + )} + </span> +   + {chartDetails.entityData.count === 1 && ( + <EuiTextColor color={'success'} size={'s'} component={'span'}> + {chartDetails.entityData.entities.length > 0 && '('} + {chartDetails.entityData.entities + .map((entity) => { + return `${entity.fieldName}: ${entity.fieldValue}`; + }) + .join(', ')} + {chartDetails.entityData.entities.length > 0 && ')'} + </EuiTextColor> + )} + {chartDetails.entityData.count !== 1 && ( + <EuiTextColor color={'success'} size={'s'} component={'span'}> + {chartDetails.entityData.entities.map((countData, i) => { + return ( + <Fragment key={countData.fieldName}> + {i18n.translate( + 'xpack.ml.timeSeriesExplorer.countDataInChartDetailsDescription', + { + defaultMessage: + '{openBrace}{cardinalityValue} distinct {fieldName} {cardinality, plural, one {} other { values}}{closeBrace}', + values: { + openBrace: i === 0 ? '(' : '', + closeBrace: + i === chartDetails.entityData.entities.length - 1 + ? ')' + : '', + cardinalityValue: + countData.cardinality === 0 + ? allValuesLabel + : countData.cardinality, + cardinality: countData.cardinality, + fieldName: countData.fieldName, + }, + } + )} + {i !== chartDetails.entityData.entities.length - 1 ? ', ' : ''} + </Fragment> + ); + })} + </EuiTextColor> + )} + </h2> + </EuiTitle> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <TimeSeriesExplorerHelpPopover embeddableMode /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup style={{ float: 'right' }}> + {showModelBoundsCheckbox && ( + <TimeseriesExplorerCheckbox + id="toggleModelBoundsCheckbox" + label={i18n.translate('xpack.ml.timeSeriesExplorer.showModelBoundsLabel', { + defaultMessage: 'show model bounds', + })} + checked={showModelBounds} + onChange={this.toggleShowModelBoundsHandler} + /> + )} + + {showAnnotationsCheckbox && ( + <TimeseriesExplorerCheckbox + id="toggleAnnotationsCheckbox" + label={i18n.translate('xpack.ml.timeSeriesExplorer.annotationsLabel', { + defaultMessage: 'annotations', + })} + checked={showAnnotations} + onChange={this.toggleShowAnnotationsHandler} + /> + )} + + {showForecastCheckbox && ( + <EuiFlexItem grow={false}> + <EuiCheckbox + id="toggleShowForecastCheckbox" + label={ + <span data-test-subj={'mlForecastCheckbox'}> + {i18n.translate('xpack.ml.timeSeriesExplorer.showForecastLabel', { + defaultMessage: 'show forecast', + })} + </span> + } + checked={showForecast} + onChange={this.toggleShowForecastHandler} + /> + </EuiFlexItem> + )} + </EuiFlexGroup> + + <TimeSeriesChartWithTooltips + chartProps={chartProps} + contextAggregationInterval={contextAggregationInterval} + bounds={bounds} + detectorIndex={selectedDetectorIndex} + embeddableMode + renderFocusChartOnly={renderFocusChartOnly} + selectedJob={selectedJob} + selectedEntities={this.props.selectedEntities} + showAnnotations={showAnnotations} + showForecast={showForecast} + showModelBounds={showModelBounds} + lastRefresh={lastRefresh} + /> + </div> + )} + </> + ); + } +} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx index afd93fd5acee1..3557523c113fc 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx @@ -5,12 +5,14 @@ * 2.0. */ -import React from 'react'; +import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { HelpPopover } from '../components/help_popover/help_popover'; -export const TimeSeriesExplorerHelpPopover = () => { +export const TimeSeriesExplorerHelpPopover: FC<{ embeddableMode: boolean }> = ({ + embeddableMode, +}) => { return ( <HelpPopover anchorPosition="upCenter" @@ -36,12 +38,14 @@ export const TimeSeriesExplorerHelpPopover = () => { defaultMessage="If you create a forecast, predicted data values are added to the chart. A shaded area around these values represents the confidence level; as you forecast further into the future, the confidence level generally decreases." /> </p> - <p> - <FormattedMessage - id="xpack.ml.timeSeriesExplorer.popoverAnnotationsExplanation" - defaultMessage="You can also optionally annotate your job results by drag-selecting a period of time in the chart and adding a description. Some annotations are generated automatically to indicate noteworthy occurrences." - /> - </p> + {!embeddableMode && ( + <p> + <FormattedMessage + id="xpack.ml.timeSeriesExplorer.popoverAnnotationsExplanation" + defaultMessage="You can also optionally annotate your job results by drag-selecting a period of time in the chart and adding a description. Some annotations are generated automatically to indicate noteworthy occurrences." + /> + </p> + )} <p> <FormattedMessage id="xpack.ml.timeSeriesExplorer.popoverModelPlotExplanation" diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts index a70530f476ba0..14443ea02abb7 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -9,6 +9,7 @@ import { forkJoin, Observable, of } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { extractErrorMessage } from '@kbn/ml-error-utils'; import { aggregationTypeTransform } from '@kbn/ml-anomaly-utils'; +import { MlAnomalyRecordDoc } from '@kbn/ml-anomaly-utils'; import { ml } from '../../services/ml_api_service'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../common/constants/search'; import { mlTimeSeriesSearchService } from '../timeseries_search_service'; @@ -30,11 +31,18 @@ export interface Interval { expression: string; } +export interface ChartDataPoint { + date: Date; + value: number | null; + upper?: number | null; + lower?: number | null; +} + export interface FocusData { - focusChartData: any; - anomalyRecords: any; + focusChartData: ChartDataPoint[]; + anomalyRecords: MlAnomalyRecordDoc[]; scheduledEvents: any; - showForecastCheckbox?: any; + showForecastCheckbox?: boolean; focusAnnotationError?: string; focusAnnotationData?: any[]; focusForecastData?: any; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_timeseriesexplorer_default_state.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_timeseriesexplorer_default_state.ts new file mode 100644 index 0000000000000..01185d467eb25 --- /dev/null +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_timeseriesexplorer_default_state.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function getTimeseriesexplorerDefaultState() { + return { + chartDetails: undefined, + contextAggregationInterval: undefined, + contextChartData: undefined, + contextForecastData: undefined, + // Not chartable if e.g. model plot with terms for a varp detector + dataNotChartable: false, + entitiesLoading: false, + entityValues: {}, + focusAnnotationData: [], + focusAggregationInterval: {}, + focusChartData: undefined, + focusForecastData: undefined, + fullRefresh: true, + hasResults: false, + // Counter to keep track of what data sets have been loaded. + loadCounter: 0, + loading: false, + modelPlotEnabled: false, + // Toggles display of annotations in the focus chart + showAnnotations: true, + showAnnotationsCheckbox: true, + // Toggles display of forecast data in the focus chart + showForecast: true, + showForecastCheckbox: false, + // Toggles display of model bounds in the focus chart + showModelBounds: true, + showModelBoundsCheckbox: false, + svgWidth: 0, + tableData: undefined, + zoomFrom: undefined, + zoomTo: undefined, + zoomFromFocusLoaded: undefined, + zoomToFocusLoaded: undefined, + chartDataError: undefined, + sourceIndicesWithGeoFields: {}, + }; +} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts index f33a8e06fe1a5..b3270edf21cf1 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts @@ -8,3 +8,4 @@ export { getFocusData } from './get_focus_data'; export * from './timeseriesexplorer_utils'; export { validateJobSelection } from './validate_job_selection'; +export { getTimeseriesexplorerDefaultState } from './get_timeseriesexplorer_default_state'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service.ts new file mode 100644 index 0000000000000..ef090083e0b90 --- /dev/null +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service.ts @@ -0,0 +1,187 @@ +/* + * 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 { each, find, get, filter } from 'lodash'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import type { MlEntityField } from '@kbn/ml-anomaly-utils'; +import type { Job } from '../../../../common/types/anomaly_detection_jobs'; +import type { ModelPlotOutput } from '../../services/results_service/result_service_rx'; +import type { MlApiServices } from '../../services/ml_api_service'; +import type { MlResultsService } from '../../services/results_service'; +import { buildConfigFromDetector } from '../../util/chart_config_builder'; +import { + isModelPlotChartableForDetector, + isModelPlotEnabled, +} from '../../../../common/util/job_utils'; + +// TODO Consolidate with legacy code in +// `x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts` +export function timeSeriesSearchServiceFactory( + mlResultsService: MlResultsService, + mlApiServices: MlApiServices +) { + return { + getMetricData( + job: Job, + detectorIndex: number, + entityFields: MlEntityField[], + earliestMs: number, + latestMs: number, + intervalMs: number, + esMetricFunction?: string + ): Observable<ModelPlotOutput> { + if ( + isModelPlotChartableForDetector(job, detectorIndex) && + isModelPlotEnabled(job, detectorIndex, entityFields) + ) { + // Extract the partition, by, over fields on which to filter. + const criteriaFields = []; + const detector = job.analysis_config.detectors[detectorIndex]; + if (detector.partition_field_name !== undefined) { + const partitionEntity: any = find(entityFields, { + fieldName: detector.partition_field_name, + }); + if (partitionEntity !== undefined) { + criteriaFields.push( + { fieldName: 'partition_field_name', fieldValue: partitionEntity.fieldName }, + { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue } + ); + } + } + + if (detector.over_field_name !== undefined) { + const overEntity: any = find(entityFields, { fieldName: detector.over_field_name }); + if (overEntity !== undefined) { + criteriaFields.push( + { fieldName: 'over_field_name', fieldValue: overEntity.fieldName }, + { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue } + ); + } + } + + if (detector.by_field_name !== undefined) { + const byEntity: any = find(entityFields, { fieldName: detector.by_field_name }); + if (byEntity !== undefined) { + criteriaFields.push( + { fieldName: 'by_field_name', fieldValue: byEntity.fieldName }, + { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue } + ); + } + } + + return mlResultsService.getModelPlotOutput( + job.job_id, + detectorIndex, + criteriaFields, + earliestMs, + latestMs, + intervalMs + ); + } else { + const obj: ModelPlotOutput = { + success: true, + results: {}, + }; + + const chartConfig = buildConfigFromDetector(job, detectorIndex); + + return mlResultsService + .getMetricData( + chartConfig.datafeedConfig.indices.join(','), + entityFields, + chartConfig.datafeedConfig.query, + esMetricFunction ?? chartConfig.metricFunction, + chartConfig.metricFieldName, + chartConfig.summaryCountFieldName, + chartConfig.timeField, + earliestMs, + latestMs, + intervalMs, + chartConfig?.datafeedConfig + ) + .pipe( + map((resp) => { + each(resp.results, (value, time) => { + // @ts-ignore + obj.results[time] = { + actual: value, + }; + }); + return obj; + }) + ); + } + }, + // Builds chart detail information (charting function description and entity counts) used + // in the title area of the time series chart. + // Queries Elasticsearch if necessary to obtain the distinct count of entities + // for which data is being plotted. + getChartDetails( + job: Job, + detectorIndex: number, + entityFields: any[], + earliestMs: number, + latestMs: number + ) { + return new Promise((resolve, reject) => { + const obj: any = { + success: true, + results: { functionLabel: '', entityData: { entities: [] } }, + }; + + const chartConfig = buildConfigFromDetector(job, detectorIndex); + let functionLabel: string | null = chartConfig.metricFunction; + if (chartConfig.metricFieldName !== undefined) { + functionLabel += ' '; + functionLabel += chartConfig.metricFieldName; + } + obj.results.functionLabel = functionLabel; + + const blankEntityFields = filter(entityFields, (entity) => { + return entity.fieldValue === null; + }); + + // Look to see if any of the entity fields have defined values + // (i.e. blank input), and if so obtain the cardinality. + if (blankEntityFields.length === 0) { + obj.results.entityData.count = 1; + obj.results.entityData.entities = entityFields; + resolve(obj); + } else { + const entityFieldNames: string[] = blankEntityFields.map((f) => f.fieldName); + mlApiServices + .getCardinalityOfFields({ + index: chartConfig.datafeedConfig.indices.join(','), + fieldNames: entityFieldNames, + query: chartConfig.datafeedConfig.query, + timeFieldName: chartConfig.timeField, + earliestMs, + latestMs, + }) + .then((results: any) => { + each(blankEntityFields, (field) => { + // results will not contain keys for non-aggregatable fields, + // so store as 0 to indicate over all field values. + obj.results.entityData.entities.push({ + fieldName: field.fieldName, + cardinality: get(results, field.fieldName, 0), + }); + }); + + resolve(obj); + }) + .catch((resp: any) => { + reject(resp); + }); + } + }); + }, + }; +} + +export type MlTimeSeriesSeachService = ReturnType<typeof timeSeriesSearchServiceFactory>; diff --git a/x-pack/plugins/ml/public/application/util/chart_config_builder.ts b/x-pack/plugins/ml/public/application/util/chart_config_builder.ts index e9c322d30a6e5..c1057fc335e82 100644 --- a/x-pack/plugins/ml/public/application/util/chart_config_builder.ts +++ b/x-pack/plugins/ml/public/application/util/chart_config_builder.ts @@ -19,9 +19,19 @@ import { Job } from '../../../common/types/anomaly_detection_jobs'; import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; -// Builds the basic configuration to plot a chart of the source data -// analyzed by the the detector at the given index from the specified ML job. -export function buildConfigFromDetector(job: Job, detectorIndex: number) { +/** + * Builds the basic configuration to plot a chart of the source data + * analyzed by the the detector at the given index from the specified ML job. + * @param job Job config info + * @param detectorIndex The index of the detector in the job config + * @param metricFunctionDescription The underlying function (min, max, avg) for "metric" detector type + * @returns + */ +export function buildConfigFromDetector( + job: Job, + detectorIndex: number, + metricFunctionDescription?: ES_AGGREGATION +) { const analysisConfig = job.analysis_config; const detector = analysisConfig.detectors[detectorIndex]; @@ -38,6 +48,9 @@ export function buildConfigFromDetector(job: Job, detectorIndex: number) { datafeedConfig: job.datafeed_config!, summaryCountFieldName: job.analysis_config.summary_count_field_name, }; + if (detector.function === ML_JOB_AGGREGATION.METRIC && metricFunctionDescription !== undefined) { + config.metricFunction = metricFunctionDescription; + } if (detector.field_name !== undefined) { config.metricFieldName = detector.field_name; diff --git a/x-pack/plugins/ml/public/application/util/index_service.ts b/x-pack/plugins/ml/public/application/util/index_service.ts new file mode 100644 index 0000000000000..f4b6a1fc13d77 --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/index_service.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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'; +import type { Job } from '../../../common/types/anomaly_detection_jobs'; + +// TODO Consolidate with legacy code in `ml/public/application/util/index_utils.ts`. +export function indexServiceFactory(dataViewsService: DataViewsContract) { + return { + /** + * Retrieves the data view ID from the given name. + * If a job is passed in, a temporary data view will be created if the requested data view doesn't exist. + * @param name - The name or index pattern of the data view. + * @param job - Optional job object. + * @returns The data view ID or null if it doesn't exist. + */ + async getDataViewIdFromName(name: string, job?: Job): Promise<string | null> { + if (dataViewsService === null) { + throw new Error('Data views are not initialized!'); + } + const dataViews = await dataViewsService.find(name); + const dataView = dataViews.find((dv) => dv.getIndexPattern() === name); + if (!dataView) { + if (job !== undefined) { + const tempDataView = await dataViewsService.create({ + id: undefined, + name, + title: name, + timeFieldName: job.data_description.time_field!, + }); + return tempDataView.id ?? null; + } + return null; + } + return dataView.id ?? dataView.getIndexPattern(); + }, + getDataViewById(id: string): Promise<DataView> { + if (dataViewsService === null) { + throw new Error('Data views are not initialized!'); + } + + if (id) { + return dataViewsService.get(id); + } else { + return dataViewsService.create({}); + } + }, + }; +} + +export type MlIndexUtils = ReturnType<typeof indexServiceFactory>; diff --git a/x-pack/plugins/ml/public/application/util/time_buckets.d.ts b/x-pack/plugins/ml/public/application/util/time_buckets.d.ts index 9a5410918a099..0f413ed9c2c71 100644 --- a/x-pack/plugins/ml/public/application/util/time_buckets.d.ts +++ b/x-pack/plugins/ml/public/application/util/time_buckets.d.ts @@ -33,6 +33,7 @@ export declare class TimeBuckets { public setBounds(bounds: TimeRangeBounds): void; public getBounds(): { min: any; max: any }; public getInterval(): TimeBucketsInterval; + public getIntervalToNearestMultiple(divisorSecs: any): TimeBucketsInterval; public getScaledDateFormat(): string; } diff --git a/x-pack/plugins/ml/public/application/util/time_buckets_service.ts b/x-pack/plugins/ml/public/application/util/time_buckets_service.ts new file mode 100644 index 0000000000000..480f279a603b1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/time_buckets_service.ts @@ -0,0 +1,57 @@ +/* + * 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 { useMemo } from 'react'; +import type { IUiSettingsClient } from '@kbn/core/public'; +import { UI_SETTINGS } from '@kbn/data-plugin/public'; +import moment from 'moment'; +import { type TimeRangeBounds, type TimeBucketsInterval, TimeBuckets } from './time_buckets'; +import { useMlKibana } from '../contexts/kibana'; + +// TODO Consolidate with legacy code in `ml/public/application/util/time_buckets.js`. +export function timeBucketsServiceFactory(uiSettings: IUiSettingsClient) { + function getTimeBuckets(): InstanceType<typeof TimeBuckets> { + return new TimeBuckets({ + [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), + dateFormat: uiSettings.get('dateFormat'), + 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), + }); + } + function getBoundsRoundedToInterval( + bounds: TimeRangeBounds, + interval: TimeBucketsInterval, + inclusiveEnd: boolean = false + ): Required<TimeRangeBounds> { + // Returns new bounds, created by flooring the min of the provided bounds to the start of + // the specified interval (a moment duration), and rounded upwards (Math.ceil) to 1ms before + // the start of the next interval (Kibana dashboards search >= bounds min, and <= bounds max, + // so we subtract 1ms off the max to avoid querying start of the new Elasticsearch aggregation bucket). + const intervalMs = interval.asMilliseconds(); + const adjustedMinMs = Math.floor(bounds.min!.valueOf() / intervalMs) * intervalMs; + let adjustedMaxMs = Math.ceil(bounds.max!.valueOf() / intervalMs) * intervalMs; + + // Don't include the start ms of the next bucket unless specified.. + if (inclusiveEnd === false) { + adjustedMaxMs = adjustedMaxMs - 1; + } + return { min: moment(adjustedMinMs), max: moment(adjustedMaxMs) }; + } + + return { getTimeBuckets, getBoundsRoundedToInterval }; +} + +export type TimeBucketsService = ReturnType<typeof timeBucketsServiceFactory>; + +export function useTimeBucketsService(): TimeBucketsService { + const { + services: { uiSettings }, + } = useMlKibana(); + + const mlTimeBucketsService = useMemo(() => timeBucketsServiceFactory(uiSettings), [uiSettings]); + return mlTimeBucketsService; +} diff --git a/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts b/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts new file mode 100644 index 0000000000000..4af8d98093cbd --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts @@ -0,0 +1,648 @@ +/* + * 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 { useMemo } from 'react'; +import type { IUiSettingsClient } from '@kbn/core/public'; +import { aggregationTypeTransform } from '@kbn/ml-anomaly-utils'; +import { isMultiBucketAnomaly, ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; +import moment from 'moment'; +import { forkJoin, Observable, of } from 'rxjs'; +import { each, get } from 'lodash'; +import { catchError, map } from 'rxjs/operators'; +import { type MlAnomalyRecordDoc } from '@kbn/ml-anomaly-utils'; +import { parseInterval } from '../../../common/util/parse_interval'; +import type { GetAnnotationsResponse } from '../../../common/types/annotations'; +import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; +import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search'; +import { CHARTS_POINT_TARGET } from '../timeseriesexplorer/timeseriesexplorer_constants'; +import { timeBucketsServiceFactory } from './time_buckets_service'; +import type { TimeRangeBounds } from './time_buckets'; +import type { Job } from '../../../common/types/anomaly_detection_jobs'; +import type { TimeBucketsInterval } from './time_buckets'; +import type { + ChartDataPoint, + FocusData, + Interval, +} from '../timeseriesexplorer/timeseriesexplorer_utils/get_focus_data'; +import type { CriteriaField } from '../services/results_service'; +import { + MAX_SCHEDULED_EVENTS, + TIME_FIELD_NAME, +} from '../timeseriesexplorer/timeseriesexplorer_constants'; +import type { MlApiServices } from '../services/ml_api_service'; +import { mlResultsServiceProvider, type MlResultsService } from '../services/results_service'; +import { forecastServiceProvider } from '../services/forecast_service_provider'; +import { timeSeriesSearchServiceFactory } from '../timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service'; +import { useMlKibana } from '../contexts/kibana'; + +// TODO Consolidate with legacy code in +// `ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js`. +export function timeSeriesExplorerServiceFactory( + uiSettings: IUiSettingsClient, + mlApiServices: MlApiServices, + mlResultsService: MlResultsService +) { + const timeBuckets = timeBucketsServiceFactory(uiSettings); + const mlForecastService = forecastServiceProvider(mlApiServices); + const mlTimeSeriesSearchService = timeSeriesSearchServiceFactory(mlResultsService, mlApiServices); + + function getAutoZoomDuration(selectedJob: Job) { + // Calculate the 'auto' zoom duration which shows data at bucket span granularity. + // Get the minimum bucket span of selected jobs. + let autoZoomDuration; + if (selectedJob.analysis_config.bucket_span) { + const bucketSpan = parseInterval(selectedJob.analysis_config.bucket_span); + const bucketSpanSeconds = bucketSpan!.asSeconds(); + + // In most cases the duration can be obtained by simply multiplying the points target + // Check that this duration returns the bucket span when run back through the + // TimeBucket interval calculation. + autoZoomDuration = bucketSpanSeconds * 1000 * (CHARTS_POINT_TARGET - 1); + + // Use a maxBars of 10% greater than the target. + const maxBars = Math.floor(1.1 * CHARTS_POINT_TARGET); + const buckets = timeBuckets.getTimeBuckets(); + buckets.setInterval('auto'); + buckets.setBarTarget(Math.floor(CHARTS_POINT_TARGET)); + buckets.setMaxBars(maxBars); + + // Set bounds from 'now' for testing the auto zoom duration. + const nowMs = new Date().getTime(); + const max = moment(nowMs); + const min = moment(nowMs - autoZoomDuration); + buckets.setBounds({ min, max }); + + const calculatedInterval = buckets.getIntervalToNearestMultiple(bucketSpanSeconds); + const calculatedIntervalSecs = calculatedInterval.asSeconds(); + if (calculatedIntervalSecs !== bucketSpanSeconds) { + // If we haven't got the span back, which may occur depending on the 'auto' ranges + // used in TimeBuckets and the bucket span of the job, then multiply by the ratio + // of the bucket span to the calculated interval. + autoZoomDuration = autoZoomDuration * (bucketSpanSeconds / calculatedIntervalSecs); + } + } + + return autoZoomDuration; + } + + function calculateAggregationInterval( + bounds: TimeRangeBounds, + bucketsTarget: number | undefined, + selectedJob: Job + ) { + // Aggregation interval used in queries should be a function of the time span of the chart + // and the bucket span of the selected job(s). + const barTarget = bucketsTarget !== undefined ? bucketsTarget : 100; + // Use a maxBars of 10% greater than the target. + const maxBars = Math.floor(1.1 * barTarget); + const buckets = timeBuckets.getTimeBuckets(); + buckets.setInterval('auto'); + buckets.setBounds(bounds); + buckets.setBarTarget(Math.floor(barTarget)); + buckets.setMaxBars(maxBars); + let aggInterval; + + if (selectedJob.analysis_config.bucket_span) { + // Ensure the aggregation interval is always a multiple of the bucket span to avoid strange + // behaviour such as adjacent chart buckets holding different numbers of job results. + const bucketSpan = parseInterval(selectedJob.analysis_config.bucket_span); + const bucketSpanSeconds = bucketSpan!.asSeconds(); + aggInterval = buckets.getIntervalToNearestMultiple(bucketSpanSeconds); + + // Set the interval back to the job bucket span if the auto interval is smaller. + const secs = aggInterval.asSeconds(); + if (secs < bucketSpanSeconds) { + buckets.setInterval(bucketSpanSeconds + 's'); + aggInterval = buckets.getInterval(); + } + } + + return aggInterval; + } + + function calculateInitialFocusRange( + zoomState: any, + contextAggregationInterval: any, + bounds: TimeRangeBounds + ) { + if (zoomState !== undefined) { + // Check that the zoom times are valid. + // zoomFrom must be at or after context chart search bounds earliest, + // zoomTo must be at or before context chart search bounds latest. + const zoomFrom = moment(zoomState.from, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true); + const zoomTo = moment(zoomState.to, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true); + const searchBounds = timeBuckets.getBoundsRoundedToInterval( + bounds, + contextAggregationInterval, + true + ); + const earliest = searchBounds.min; + const latest = searchBounds.max; + + if ( + zoomFrom.isValid() && + zoomTo.isValid() && + zoomTo.isAfter(zoomFrom) && + zoomFrom.isBetween(earliest, latest, null, '[]') && + zoomTo.isBetween(earliest, latest, null, '[]') + ) { + return [zoomFrom.toDate(), zoomTo.toDate()]; + } + } + + return undefined; + } + + function calculateDefaultFocusRange( + autoZoomDuration: any, + contextAggregationInterval: any, + contextChartData: any, + contextForecastData: any + ) { + const isForecastData = contextForecastData !== undefined && contextForecastData.length > 0; + + const combinedData = + isForecastData === false ? contextChartData : contextChartData.concat(contextForecastData); + const earliestDataDate = combinedData[0].date; + const latestDataDate = combinedData[combinedData.length - 1].date; + + let rangeEarliestMs; + let rangeLatestMs; + + if (isForecastData === true) { + // Return a range centred on the start of the forecast range, depending + // on the time range of the forecast and data. + const earliestForecastDataDate = contextForecastData[0].date; + const latestForecastDataDate = contextForecastData[contextForecastData.length - 1].date; + + rangeLatestMs = Math.min( + earliestForecastDataDate.getTime() + autoZoomDuration / 2, + latestForecastDataDate.getTime() + ); + rangeEarliestMs = Math.max(rangeLatestMs - autoZoomDuration, earliestDataDate.getTime()); + } else { + // Returns the range that shows the most recent data at bucket span granularity. + rangeLatestMs = latestDataDate.getTime() + contextAggregationInterval.asMilliseconds(); + rangeEarliestMs = Math.max(earliestDataDate.getTime(), rangeLatestMs - autoZoomDuration); + } + + return [new Date(rangeEarliestMs), new Date(rangeLatestMs)]; + } + + // Return dataset in format used by the swimlane. + // i.e. array of Objects with keys date (JavaScript date) and score. + function processRecordScoreResults(scoreData: any) { + const bucketScoreData: any = []; + each(scoreData, (dataForTime, time) => { + bucketScoreData.push({ + date: new Date(+time), + score: dataForTime.score, + }); + }); + + return bucketScoreData; + } + + // Return dataset in format used by the single metric chart. + // i.e. array of Objects with keys date (JavaScript date) and value, + // plus lower and upper keys if model plot is enabled for the series. + function processMetricPlotResults(metricPlotData: any, modelPlotEnabled: any) { + const metricPlotChartData: any = []; + if (modelPlotEnabled === true) { + each(metricPlotData, (dataForTime, time) => { + metricPlotChartData.push({ + date: new Date(+time), + lower: dataForTime.modelLower, + value: dataForTime.actual, + upper: dataForTime.modelUpper, + }); + }); + } else { + each(metricPlotData, (dataForTime, time) => { + metricPlotChartData.push({ + date: new Date(+time), + value: dataForTime.actual, + }); + }); + } + + return metricPlotChartData; + } + + // Returns forecast dataset in format used by the single metric chart. + // i.e. array of Objects with keys date (JavaScript date), isForecast, + // value, lower and upper keys. + function processForecastResults(forecastData: any) { + const forecastPlotChartData: any = []; + each(forecastData, (dataForTime, time) => { + forecastPlotChartData.push({ + date: new Date(+time), + isForecast: true, + lower: dataForTime.forecastLower, + value: dataForTime.prediction, + upper: dataForTime.forecastUpper, + }); + }); + + return forecastPlotChartData; + } + + // Finds the chart point which corresponds to an anomaly with the + // specified time. + function findChartPointForAnomalyTime( + chartData: any, + anomalyTime: any, + aggregationInterval: any + ) { + let chartPoint; + if (chartData === undefined) { + return chartPoint; + } + + for (let i = 0; i < chartData.length; i++) { + if (chartData[i].date.getTime() === anomalyTime) { + chartPoint = chartData[i]; + break; + } + } + + if (chartPoint === undefined) { + // Find the time of the point which falls immediately before the + // time of the anomaly. This is the start of the chart 'bucket' + // which contains the anomalous bucket. + let foundItem; + const intervalMs = aggregationInterval.asMilliseconds(); + for (let i = 0; i < chartData.length; i++) { + const itemTime = chartData[i].date.getTime(); + if (anomalyTime - itemTime < intervalMs) { + foundItem = chartData[i]; + break; + } + } + + chartPoint = foundItem; + } + + return chartPoint; + } + + // Uses data from the list of anomaly records to add anomalyScore, + // function, actual and typical properties, plus causes and multi-bucket + // info if applicable, to the chartData entries for anomalous buckets. + function processDataForFocusAnomalies( + chartData: ChartDataPoint[], + anomalyRecords: MlAnomalyRecordDoc[], + aggregationInterval: Interval, + modelPlotEnabled: boolean, + functionDescription?: string + ) { + const timesToAddPointsFor: number[] = []; + + // Iterate through the anomaly records making sure we have chart points for each anomaly. + const intervalMs = aggregationInterval.asMilliseconds(); + let lastChartDataPointTime: any; + if (chartData !== undefined && chartData.length > 0) { + lastChartDataPointTime = chartData[chartData.length - 1].date.getTime(); + } + anomalyRecords.forEach((record: MlAnomalyRecordDoc) => { + const recordTime = record[TIME_FIELD_NAME]; + const chartPoint = findChartPointForAnomalyTime(chartData, recordTime, aggregationInterval); + if (chartPoint === undefined) { + const timeToAdd = Math.floor(recordTime / intervalMs) * intervalMs; + if (timesToAddPointsFor.indexOf(timeToAdd) === -1 && timeToAdd !== lastChartDataPointTime) { + timesToAddPointsFor.push(timeToAdd); + } + } + }); + + timesToAddPointsFor.sort((a, b) => a - b); + + timesToAddPointsFor.forEach((time) => { + const pointToAdd: ChartDataPoint = { + date: new Date(time), + value: null, + }; + + if (modelPlotEnabled === true) { + pointToAdd.upper = null; + pointToAdd.lower = null; + } + chartData.push(pointToAdd); + }); + + // Iterate through the anomaly records adding the + // various properties required for display. + anomalyRecords.forEach((record) => { + // Look for a chart point with the same time as the record. + // If none found, find closest time in chartData set. + const recordTime = record[TIME_FIELD_NAME]; + if ( + record.function === ML_JOB_AGGREGATION.METRIC && + record.function_description !== functionDescription + ) + return; + + const chartPoint = findChartPointForAnomalyTime(chartData, recordTime, aggregationInterval); + if (chartPoint !== undefined) { + // If chart aggregation interval > bucket span, there may be more than + // one anomaly record in the interval, so use the properties from + // the record with the highest anomalyScore. + const recordScore = record.record_score; + const pointScore = chartPoint.anomalyScore; + if (pointScore === undefined || pointScore < recordScore) { + chartPoint.anomalyScore = recordScore; + chartPoint.function = record.function; + + if (record.actual !== undefined) { + // If cannot match chart point for anomaly time + // substitute the value with the record's actual so it won't plot as null/0 + if (chartPoint.value === null || record.function === ML_JOB_AGGREGATION.METRIC) { + chartPoint.value = Array.isArray(record.actual) ? record.actual[0] : record.actual; + } + + chartPoint.actual = record.actual; + chartPoint.typical = record.typical; + } else { + const causes = get(record, 'causes', []); + if (causes.length > 0) { + chartPoint.byFieldName = record.by_field_name; + chartPoint.numberOfCauses = causes.length; + if (causes.length === 1) { + // If only a single cause, copy actual and typical values to the top level. + const cause = record.causes![0]; + chartPoint.actual = cause.actual; + chartPoint.typical = cause.typical; + // substitute the value with the record's actual so it won't plot as null/0 + if (chartPoint.value === null) { + chartPoint.value = cause.actual; + } + } + } + } + + if ( + record.anomaly_score_explanation !== undefined && + record.anomaly_score_explanation.multi_bucket_impact !== undefined + ) { + chartPoint.multiBucketImpact = record.anomaly_score_explanation.multi_bucket_impact; + } + + chartPoint.isMultiBucketAnomaly = isMultiBucketAnomaly(record); + } + } + }); + + return chartData; + } + + function findChartPointForScheduledEvent(chartData: any, eventTime: any) { + let chartPoint; + if (chartData === undefined) { + return chartPoint; + } + + for (let i = 0; i < chartData.length; i++) { + if (chartData[i].date.getTime() === eventTime) { + chartPoint = chartData[i]; + break; + } + } + + return chartPoint; + } + // Adds a scheduledEvents property to any points in the chart data set + // which correspond to times of scheduled events for the job. + function processScheduledEventsForChart( + chartData: ChartDataPoint[], + scheduledEvents: Array<{ events: any; time: number }> | undefined, + aggregationInterval: TimeBucketsInterval + ) { + if (scheduledEvents !== undefined) { + const timesToAddPointsFor: number[] = []; + + // Iterate through the scheduled events making sure we have a chart point for each event. + const intervalMs = aggregationInterval.asMilliseconds(); + let lastChartDataPointTime: number | undefined; + if (chartData !== undefined && chartData.length > 0) { + lastChartDataPointTime = chartData[chartData.length - 1].date.getTime(); + } + + // In case there's no chart data/sparse data during these scheduled events + // ensure we add chart points at every aggregation interval for these scheduled events. + let sortRequired = false; + each(scheduledEvents, (events, time) => { + const exactChartPoint = findChartPointForScheduledEvent(chartData, +time); + + if (exactChartPoint !== undefined) { + exactChartPoint.scheduledEvents = events; + } else { + const timeToAdd: number = Math.floor(time / intervalMs) * intervalMs; + if ( + timesToAddPointsFor.indexOf(timeToAdd) === -1 && + timeToAdd !== lastChartDataPointTime + ) { + const pointToAdd = { + date: new Date(timeToAdd), + value: null, + scheduledEvents: events, + }; + + chartData.push(pointToAdd); + sortRequired = true; + } + } + }); + + // Sort chart data by time if extra points were added at the end of the array for scheduled events. + if (sortRequired) { + chartData.sort((a, b) => a.date.getTime() - b.date.getTime()); + } + } + + return chartData; + } + + function getFocusData( + criteriaFields: CriteriaField[], + detectorIndex: number, + focusAggregationInterval: TimeBucketsInterval, + forecastId: string, + modelPlotEnabled: boolean, + nonBlankEntities: any[], + searchBounds: any, + selectedJob: Job, + functionDescription?: string | undefined + ): Observable<FocusData> { + const esFunctionToPlotIfMetric = + functionDescription !== undefined + ? aggregationTypeTransform.toES(functionDescription) + : functionDescription; + + return forkJoin([ + // Query 1 - load metric data across selected time range. + mlTimeSeriesSearchService.getMetricData( + selectedJob, + detectorIndex, + nonBlankEntities, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.asMilliseconds(), + esFunctionToPlotIfMetric + ), + // Query 2 - load all the records across selected time range for the chart anomaly markers. + mlApiServices.results.getAnomalyRecords$( + [selectedJob.job_id], + criteriaFields, + 0, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.expression, + functionDescription + ), + // Query 3 - load any scheduled events for the selected job. + mlResultsService.getScheduledEventsByBucket( + [selectedJob.job_id], + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.asMilliseconds(), + 1, + MAX_SCHEDULED_EVENTS + ), + // Query 4 - load any annotations for the selected job. + mlApiServices.annotations + .getAnnotations$({ + jobIds: [selectedJob.job_id], + earliestMs: searchBounds.min.valueOf(), + latestMs: searchBounds.max.valueOf(), + maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + detectorIndex, + entities: nonBlankEntities, + }) + .pipe( + catchError((resp) => + of({ + annotations: {}, + totalCount: 0, + error: extractErrorMessage(resp), + success: false, + } as GetAnnotationsResponse) + ) + ), + // Plus query for forecast data if there is a forecastId stored in the appState. + forecastId !== undefined + ? (() => { + let aggType; + const detector = selectedJob.analysis_config.detectors[detectorIndex]; + const esAgg = mlFunctionToESAggregation(detector.function); + if (!modelPlotEnabled && (esAgg === 'sum' || esAgg === 'count')) { + aggType = { avg: 'sum', max: 'sum', min: 'sum' }; + } + return mlForecastService.getForecastData( + selectedJob, + detectorIndex, + forecastId, + nonBlankEntities, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.asMilliseconds(), + aggType + ); + })() + : of(null), + ]).pipe( + map( + ([metricData, recordsForCriteria, scheduledEventsByBucket, annotations, forecastData]) => { + // Sort in descending time order before storing in scope. + const anomalyRecords = recordsForCriteria?.records + .sort((a, b) => a[TIME_FIELD_NAME] - b[TIME_FIELD_NAME]) + .reverse(); + + const scheduledEvents = scheduledEventsByBucket?.events[selectedJob.job_id]; + + let focusChartData = processMetricPlotResults(metricData.results, modelPlotEnabled); + // Tell the results container directives to render the focus chart. + focusChartData = processDataForFocusAnomalies( + focusChartData, + anomalyRecords, + focusAggregationInterval, + modelPlotEnabled, + functionDescription + ); + focusChartData = processScheduledEventsForChart( + focusChartData, + scheduledEvents, + focusAggregationInterval + ); + + const refreshFocusData: FocusData = { + scheduledEvents, + anomalyRecords, + focusChartData, + }; + + if (annotations) { + if (annotations.error !== undefined) { + refreshFocusData.focusAnnotationError = annotations.error; + refreshFocusData.focusAnnotationData = []; + } else { + refreshFocusData.focusAnnotationData = ( + annotations.annotations[selectedJob.job_id] ?? [] + ) + .sort((a, b) => { + return a.timestamp - b.timestamp; + }) + .map((d, i: number) => { + d.key = (i + 1).toString(); + return d; + }); + } + } + + if (forecastData) { + refreshFocusData.focusForecastData = processForecastResults(forecastData.results); + refreshFocusData.showForecastCheckbox = refreshFocusData.focusForecastData.length > 0; + } + return refreshFocusData; + } + ) + ); + } + + return { + getAutoZoomDuration, + calculateAggregationInterval, + calculateInitialFocusRange, + calculateDefaultFocusRange, + processRecordScoreResults, + processMetricPlotResults, + processForecastResults, + findChartPointForAnomalyTime, + processDataForFocusAnomalies, + findChartPointForScheduledEvent, + processScheduledEventsForChart, + getFocusData, + }; +} + +export function useTimeSeriesExplorerService(): TimeSeriesExplorerService { + const { + services: { + uiSettings, + mlServices: { mlApiServices }, + }, + } = useMlKibana(); + const mlResultsService = mlResultsServiceProvider(mlApiServices); + + const mlTimeSeriesExplorer = useMemo( + () => timeSeriesExplorerServiceFactory(uiSettings, mlApiServices, mlResultsService), + [uiSettings, mlApiServices, mlResultsService] + ); + return mlTimeSeriesExplorer; +} + +export type TimeSeriesExplorerService = ReturnType<typeof timeSeriesExplorerServiceFactory>; diff --git a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx index 00c4a02d4e929..182d070266c9a 100644 --- a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx +++ b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx @@ -26,7 +26,8 @@ import { JobSelectorFlyout } from './components/job_selector_flyout'; */ export async function resolveJobSelection( coreStart: CoreStart, - selectedJobIds?: JobId[] + selectedJobIds?: JobId[], + singleSelection: boolean = false ): Promise<{ jobIds: string[]; groups: Array<{ groupId: string; jobIds: string[] }> }> { const { http, @@ -74,7 +75,7 @@ export async function resolveJobSelection( selectedIds={selectedJobIds} withTimeRangeSelector={false} dateFormatTz={dateFormatTz} - singleSelection={false} + singleSelection={singleSelection} timeseriesOnly={true} onFlyoutClose={onFlyoutClose} onSelectionConfirmed={onSelectionConfirmed} diff --git a/x-pack/plugins/ml/public/embeddables/constants.ts b/x-pack/plugins/ml/public/embeddables/constants.ts index cfe50f25cd889..1001cd89c7498 100644 --- a/x-pack/plugins/ml/public/embeddables/constants.ts +++ b/x-pack/plugins/ml/public/embeddables/constants.ts @@ -7,6 +7,7 @@ export const ANOMALY_SWIMLANE_EMBEDDABLE_TYPE = 'ml_anomaly_swimlane' as const; export const ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE = 'ml_anomaly_charts' as const; +export const ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE = 'ml_single_metric_viewer' as const; export type AnomalySwimLaneEmbeddableType = typeof ANOMALY_SWIMLANE_EMBEDDABLE_TYPE; export type AnomalyExplorerChartsEmbeddableType = typeof ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE; diff --git a/x-pack/plugins/ml/public/embeddables/index.ts b/x-pack/plugins/ml/public/embeddables/index.ts index 0a505fe04ea85..9f0d2d75b1162 100644 --- a/x-pack/plugins/ml/public/embeddables/index.ts +++ b/x-pack/plugins/ml/public/embeddables/index.ts @@ -9,6 +9,7 @@ import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; import { AnomalySwimlaneEmbeddableFactory } from './anomaly_swimlane'; import type { MlCoreSetup } from '../plugin'; import { AnomalyChartsEmbeddableFactory } from './anomaly_charts'; +import { SingleMetricViewerEmbeddableFactory } from './single_metric_viewer'; export * from './constants'; export * from './types'; @@ -25,6 +26,8 @@ export function registerEmbeddables(embeddable: EmbeddableSetup, core: MlCoreSet ); const anomalyChartsFactory = new AnomalyChartsEmbeddableFactory(core.getStartServices); - embeddable.registerEmbeddableFactory(anomalyChartsFactory.type, anomalyChartsFactory); + + const singleMetricViewerFactory = new SingleMetricViewerEmbeddableFactory(core.getStartServices); + embeddable.registerEmbeddableFactory(singleMetricViewerFactory.type, singleMetricViewerFactory); } diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/_index.scss b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/_index.scss new file mode 100644 index 0000000000000..b6f91cc749dcc --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/_index.scss @@ -0,0 +1,6 @@ +// ML has it's own variables for coloring +@import '../../application/variables'; + +// Protect the rest of Kibana from ML generic namespacing +@import '../../application/timeseriesexplorer/timeseriesexplorer'; +@import '../../application/timeseriesexplorer/timeseriesexplorer_annotations'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx new file mode 100644 index 0000000000000..88c120c9747e1 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container.tsx @@ -0,0 +1,204 @@ +/* + * 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, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { EuiResizeObserver } from '@elastic/eui'; +import { Observable } from 'rxjs'; +import { throttle } from 'lodash'; +import { MlJob } from '@elastic/elasticsearch/lib/api/types'; +import usePrevious from 'react-use/lib/usePrevious'; +import { useToastNotificationService } from '../../application/services/toast_notification_service'; +import { useEmbeddableExecutionContext } from '../common/use_embeddable_execution_context'; +import { useSingleMetricViewerInputResolver } from './use_single_metric_viewer_input_resolver'; +import type { ISingleMetricViewerEmbeddable } from './single_metric_viewer_embeddable'; +import type { + SingleMetricViewerEmbeddableInput, + AnomalyChartsEmbeddableOutput, + SingleMetricViewerEmbeddableServices, +} from '..'; +import { ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE } from '..'; +import { TimeSeriesExplorerEmbeddableChart } from '../../application/timeseriesexplorer/timeseriesexplorer_embeddable_chart'; +import { APP_STATE_ACTION } from '../../application/timeseriesexplorer/timeseriesexplorer_constants'; +import { useTimeSeriesExplorerService } from '../../application/util/time_series_explorer_service'; +import './_index.scss'; + +const RESIZE_THROTTLE_TIME_MS = 500; + +interface AppStateZoom { + from?: string; + to?: string; +} + +export interface EmbeddableSingleMetricViewerContainerProps { + id: string; + embeddableContext: InstanceType<ISingleMetricViewerEmbeddable>; + embeddableInput: Observable<SingleMetricViewerEmbeddableInput>; + services: SingleMetricViewerEmbeddableServices; + refresh: Observable<void>; + onInputChange: (input: Partial<SingleMetricViewerEmbeddableInput>) => void; + onOutputChange: (output: Partial<AnomalyChartsEmbeddableOutput>) => void; + onRenderComplete: () => void; + onLoading: () => void; + onError: (error: Error) => void; +} + +export const EmbeddableSingleMetricViewerContainer: FC< + EmbeddableSingleMetricViewerContainerProps +> = ({ + id, + embeddableContext, + embeddableInput, + services, + refresh, + onInputChange, + onOutputChange, + onRenderComplete, + onError, + onLoading, +}) => { + useEmbeddableExecutionContext<SingleMetricViewerEmbeddableInput>( + services[0].executionContext, + embeddableInput, + ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE, + id + ); + const [chartWidth, setChartWidth] = useState<number>(0); + const [zoom, setZoom] = useState<AppStateZoom | undefined>(); + const [selectedForecastId, setSelectedForecastId] = useState<string | undefined>(); + const [detectorIndex, setDetectorIndex] = useState<number>(0); + const [selectedJob, setSelectedJob] = useState<MlJob | undefined>(); + const [autoZoomDuration, setAutoZoomDuration] = useState<number | undefined>(); + + const { mlApiServices } = services[2]; + const { data, bounds, lastRefresh } = useSingleMetricViewerInputResolver( + embeddableInput, + refresh, + services[1].data.query.timefilter.timefilter, + onRenderComplete + ); + const selectedJobId = data?.jobIds[0]; + const previousRefresh = usePrevious(lastRefresh ?? 0); + const mlTimeSeriesExplorer = useTimeSeriesExplorerService(); + + // Holds the container height for previously fetched data + const containerHeightRef = useRef<number>(); + const toastNotificationService = useToastNotificationService(); + + useEffect( + function setUpSelectedJob() { + async function fetchSelectedJob() { + if (mlApiServices && selectedJobId !== undefined) { + const { jobs } = await mlApiServices.getJobs({ jobId: selectedJobId }); + const job = jobs[0]; + setSelectedJob(job); + } + } + fetchSelectedJob(); + }, + [selectedJobId, mlApiServices] + ); + + useEffect( + function setUpAutoZoom() { + let zoomDuration: number | undefined; + if (selectedJobId !== undefined && selectedJob !== undefined) { + zoomDuration = mlTimeSeriesExplorer.getAutoZoomDuration(selectedJob); + setAutoZoomDuration(zoomDuration); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [selectedJobId, selectedJob?.job_id, mlTimeSeriesExplorer] + ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const resizeHandler = useCallback( + throttle((e: { width: number; height: number }) => { + // Keep previous container height so it doesn't change the page layout + containerHeightRef.current = e.height; + + if (Math.abs(chartWidth - e.width) > 20) { + setChartWidth(e.width); + } + }, RESIZE_THROTTLE_TIME_MS), + [chartWidth] + ); + + const appStateHandler = useCallback( + (action: string, payload?: any) => { + /** + * Empty zoom indicates that chart hasn't been rendered yet, + * hence any updates prior that should replace the URL state. + */ + + switch (action) { + case APP_STATE_ACTION.SET_DETECTOR_INDEX: + setDetectorIndex(payload); + break; + + case APP_STATE_ACTION.SET_FORECAST_ID: + setSelectedForecastId(payload); + setZoom(undefined); + break; + + case APP_STATE_ACTION.SET_ZOOM: + setZoom(payload); + break; + + case APP_STATE_ACTION.UNSET_ZOOM: + setZoom(undefined); + break; + } + }, + + [setZoom, setDetectorIndex, setSelectedForecastId] + ); + + const containerPadding = 10; + + return ( + <EuiResizeObserver onResize={resizeHandler}> + {(resizeRef) => ( + <div + id={`mlSingleMetricViewerEmbeddableWrapper-${id}`} + style={{ + width: '100%', + overflowY: 'auto', + overflowX: 'hidden', + padding: '8px', + }} + data-test-subj={`mlSingleMetricViewer_${embeddableContext.id}`} + ref={resizeRef} + className="ml-time-series-explorer" + > + {data !== undefined && autoZoomDuration !== undefined && ( + <TimeSeriesExplorerEmbeddableChart + chartWidth={chartWidth - containerPadding} + dataViewsService={services[1].data.dataViews} + toastNotificationService={toastNotificationService} + appStateHandler={appStateHandler} + autoZoomDuration={autoZoomDuration} + bounds={bounds} + lastRefresh={lastRefresh ?? 0} + previousRefresh={previousRefresh} + selectedJobId={selectedJobId} + selectedDetectorIndex={detectorIndex} + selectedEntities={data.selectedEntities} + selectedForecastId={selectedForecastId} + zoom={zoom} + functionDescription={data.functionDescription} + selectedJob={selectedJob} + /> + )} + </div> + )} + </EuiResizeObserver> + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default EmbeddableSingleMetricViewerContainer; diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container_lazy.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container_lazy.tsx new file mode 100644 index 0000000000000..0a69aaf2c2deb --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/embeddable_single_metric_viewer_container_lazy.tsx @@ -0,0 +1,12 @@ +/* + * 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'; + +export const EmbeddableSingleMetricViewerContainer = React.lazy( + () => import('./embeddable_single_metric_viewer_container') +); diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/index.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/index.ts new file mode 100644 index 0000000000000..9afdbe3d1298c --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { SingleMetricViewerEmbeddableFactory } from './single_metric_viewer_embeddable_factory'; diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx new file mode 100644 index 0000000000000..82a1b5abc8b63 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable.tsx @@ -0,0 +1,136 @@ +/* + * 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, { Suspense } from 'react'; +import ReactDOM from 'react-dom'; +import { pick } from 'lodash'; + +import { Embeddable } from '@kbn/embeddable-plugin/public'; + +import { CoreStart } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { Subject } from 'rxjs'; +import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { IContainer } from '@kbn/embeddable-plugin/public'; +import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; +import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { EmbeddableSingleMetricViewerContainer } from './embeddable_single_metric_viewer_container_lazy'; +import type { JobId } from '../../../common/types/anomaly_detection_jobs'; +import type { MlDependencies } from '../../application/app'; +import { + ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE, + SingleMetricViewerEmbeddableInput, + AnomalyChartsEmbeddableOutput, + SingleMetricViewerServices, +} from '..'; +import { EmbeddableLoading } from '../common/components/embeddable_loading_fallback'; + +export const getDefaultSingleMetricViewerPanelTitle = (jobIds: JobId[]) => + i18n.translate('xpack.ml.singleMetricViewerEmbeddable.title', { + defaultMessage: 'ML single metric viewer chart for {jobIds}', + values: { jobIds: jobIds.join(', ') }, + }); + +export type ISingleMetricViewerEmbeddable = typeof SingleMetricViewerEmbeddable; + +export class SingleMetricViewerEmbeddable extends Embeddable< + SingleMetricViewerEmbeddableInput, + AnomalyChartsEmbeddableOutput +> { + private node?: HTMLElement; + private reload$ = new Subject<void>(); + public readonly type: string = ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE; + + constructor( + initialInput: SingleMetricViewerEmbeddableInput, + public services: [CoreStart, MlDependencies, SingleMetricViewerServices], + parent?: IContainer + ) { + super(initialInput, {} as AnomalyChartsEmbeddableOutput, parent); + } + + public onLoading() { + this.renderComplete.dispatchInProgress(); + this.updateOutput({ loading: true, error: undefined }); + } + + public onError(error: Error) { + this.renderComplete.dispatchError(); + this.updateOutput({ loading: false, error: { name: error.name, message: error.message } }); + } + + public onRenderComplete() { + this.renderComplete.dispatchComplete(); + this.updateOutput({ loading: false, error: undefined }); + } + + public render(node: HTMLElement) { + super.render(node); + this.node = node; + + // required for the export feature to work + this.node.setAttribute('data-shared-item', ''); + + const I18nContext = this.services[0].i18n.Context; + const theme$ = this.services[0].theme.theme$; + + const datePickerDeps: DatePickerDependencies = { + ...pick(this.services[0], ['http', 'notifications', 'theme', 'uiSettings', 'i18n']), + data: this.services[1].data, + uiSettingsKeys: UI_SETTINGS, + showFrozenDataTierChoice: false, + }; + + ReactDOM.render( + <I18nContext> + <KibanaThemeProvider theme$={theme$}> + <KibanaContextProvider + services={{ + mlServices: { + ...this.services[2], + }, + ...this.services[0], + }} + > + <DatePickerContextProvider {...datePickerDeps}> + <Suspense fallback={<EmbeddableLoading />}> + <EmbeddableSingleMetricViewerContainer + id={this.input.id} + embeddableContext={this} + embeddableInput={this.getInput$()} + services={this.services} + refresh={this.reload$.asObservable()} + onInputChange={this.updateInput.bind(this)} + onOutputChange={this.updateOutput.bind(this)} + onRenderComplete={this.onRenderComplete.bind(this)} + onLoading={this.onLoading.bind(this)} + onError={this.onError.bind(this)} + /> + </Suspense> + </DatePickerContextProvider> + </KibanaContextProvider> + </KibanaThemeProvider> + </I18nContext>, + node + ); + } + + public destroy() { + super.destroy(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } + + public reload() { + this.reload$.next(); + } + + public supportedTriggers() { + return []; + } +} diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.ts new file mode 100644 index 0000000000000..06b2f9b024bfa --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.ts @@ -0,0 +1,131 @@ +/* + * 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'; + +import type { StartServicesAccessor } from '@kbn/core/public'; +import type { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public'; + +import { PLUGIN_ICON, PLUGIN_ID, ML_APP_NAME } from '../../../common/constants/app'; +import { + ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE, + SingleMetricViewerEmbeddableInput, + SingleMetricViewerEmbeddableServices, +} from '..'; +import type { MlPluginStart, MlStartDependencies } from '../../plugin'; +import type { MlDependencies } from '../../application/app'; +import { HttpService } from '../../application/services/http_service'; +import { AnomalyExplorerChartsService } from '../../application/services/anomaly_explorer_charts_service'; + +export class SingleMetricViewerEmbeddableFactory + implements EmbeddableFactoryDefinition<SingleMetricViewerEmbeddableInput> +{ + public readonly type = ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE; + + public readonly grouping = [ + { + id: PLUGIN_ID, + getDisplayName: () => ML_APP_NAME, + getIconType: () => PLUGIN_ICON, + }, + ]; + + constructor( + private getStartServices: StartServicesAccessor<MlStartDependencies, MlPluginStart> + ) {} + + public async isEditable() { + return true; + } + + public getDisplayName() { + return i18n.translate('xpack.ml.components.mlSingleMetricViewerEmbeddable.displayName', { + defaultMessage: 'Single metric viewer', + }); + } + + public getDescription() { + return i18n.translate('xpack.ml.components.mlSingleMetricViewerEmbeddable.description', { + defaultMessage: 'View anomaly detection single metric results in a chart.', + }); + } + + public async getExplicitInput(): Promise<Partial<SingleMetricViewerEmbeddableInput>> { + const [coreStart, pluginStart, singleMetricServices] = await this.getServices(); + + try { + const { resolveEmbeddableSingleMetricViewerUserInput } = await import( + './single_metric_viewer_setup_flyout' + ); + return await resolveEmbeddableSingleMetricViewerUserInput( + coreStart, + pluginStart, + singleMetricServices + ); + } catch (e) { + return Promise.reject(); + } + } + + private async getServices(): Promise<SingleMetricViewerEmbeddableServices> { + const [ + [coreStart, pluginsStart], + { AnomalyDetectorService }, + { fieldFormatServiceFactory }, + { indexServiceFactory }, + { mlApiServicesProvider }, + { mlResultsServiceProvider }, + { timeSeriesSearchServiceFactory }, + ] = await Promise.all([ + await this.getStartServices(), + await import('../../application/services/anomaly_detector_service'), + await import('../../application/services/field_format_service_factory'), + await import('../../application/util/index_service'), + await import('../../application/services/ml_api_service'), + await import('../../application/services/results_service'), + await import( + '../../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service' + ), + ]); + + const httpService = new HttpService(coreStart.http); + const anomalyDetectorService = new AnomalyDetectorService(httpService); + const mlApiServices = mlApiServicesProvider(httpService); + const mlResultsService = mlResultsServiceProvider(mlApiServices); + const mlIndexUtils = indexServiceFactory(pluginsStart.data.dataViews); + const mlTimeSeriesSearchService = timeSeriesSearchServiceFactory( + mlResultsService, + mlApiServices + ); + const mlFieldFormatService = fieldFormatServiceFactory(mlApiServices, mlIndexUtils); + + const anomalyExplorerService = new AnomalyExplorerChartsService( + pluginsStart.data.query.timefilter.timefilter, + mlApiServices, + mlResultsService + ); + + return [ + coreStart, + pluginsStart as MlDependencies, + { + anomalyDetectorService, + anomalyExplorerService, + mlResultsService, + mlApiServices, + mlTimeSeriesSearchService, + mlFieldFormatService, + }, + ]; + } + + public async create(initialInput: SingleMetricViewerEmbeddableInput, parent?: IContainer) { + const services = await this.getServices(); + const { SingleMetricViewerEmbeddable } = await import('./single_metric_viewer_embeddable'); + return new SingleMetricViewerEmbeddable(initialInput, services, parent); + } +} diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_initializer.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_initializer.tsx new file mode 100644 index 0000000000000..89af056068063 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_initializer.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiForm, + EuiFormRow, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiFieldText, + EuiModal, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { MlJob } from '@elastic/elasticsearch/lib/api/types'; +import type { SingleMetricViewerServices } from '..'; +import { TimeRangeBounds } from '../../application/util/time_buckets'; +import { SeriesControls } from '../../application/timeseriesexplorer/components/series_controls'; +import { + APP_STATE_ACTION, + type TimeseriesexplorerActionType, +} from '../../application/timeseriesexplorer/timeseriesexplorer_constants'; + +export interface SingleMetricViewerInitializerProps { + bounds: TimeRangeBounds; + defaultTitle: string; + initialInput?: SingleMetricViewerServices; + job: MlJob; + onCreate: (props: { + panelTitle: string; + functionDescription?: string; + selectedDetectorIndex: number; + selectedEntities: any; + }) => void; + onCancel: () => void; +} + +export const SingleMetricViewerInitializer: FC<SingleMetricViewerInitializerProps> = ({ + bounds, + defaultTitle, + initialInput, + job, + onCreate, + onCancel, +}) => { + const [panelTitle, setPanelTitle] = useState<string>(defaultTitle); + const [functionDescription, setFunctionDescription] = useState<string | undefined>(); + const [selectedDetectorIndex, setSelectedDetectorIndex] = useState<number>(0); + const [selectedEntities, setSelectedEntities] = useState<any>(); + + const isPanelTitleValid = panelTitle.length > 0; + + const handleStateUpdate = (action: TimeseriesexplorerActionType, payload: any) => { + switch (action) { + case APP_STATE_ACTION.SET_ENTITIES: + setSelectedEntities(payload); + break; + case APP_STATE_ACTION.SET_FUNCTION_DESCRIPTION: + setFunctionDescription(payload); + break; + case APP_STATE_ACTION.SET_DETECTOR_INDEX: + setSelectedDetectorIndex(payload); + break; + default: + break; + } + }; + + return ( + <EuiModal + maxWidth={false} + initialFocus="[name=panelTitle]" + onClose={onCancel} + data-test-subj={'mlSingleMetricViewerEmbeddableInitializer'} + > + <EuiModalHeader> + <EuiModalHeaderTitle> + <FormattedMessage + id="xpack.ml.SingleMetricViewerEmbeddable.setupModal.title" + defaultMessage="Single metric viewer configuration" + /> + </EuiModalHeaderTitle> + </EuiModalHeader> + + <EuiModalBody> + <EuiForm> + <EuiFormRow + label={ + <FormattedMessage + id="xpack.ml.singleMetricViewerEmbeddable.panelTitleLabel" + defaultMessage="Panel title" + /> + } + isInvalid={!isPanelTitleValid} + > + <EuiFieldText + data-test-subj="panelTitleInput" + id="panelTitle" + name="panelTitle" + value={panelTitle} + onChange={(e) => setPanelTitle(e.target.value)} + isInvalid={!isPanelTitleValid} + /> + </EuiFormRow> + <EuiSpacer /> + <SeriesControls + selectedJobId={job.job_id} + job={job} + appStateHandler={handleStateUpdate} + selectedDetectorIndex={selectedDetectorIndex} + selectedEntities={selectedEntities} + bounds={bounds} + functionDescription={functionDescription} + setFunctionDescription={setFunctionDescription} + /> + </EuiForm> + </EuiModalBody> + + <EuiModalFooter> + <EuiButtonEmpty + onClick={onCancel} + data-test-subj="mlsingleMetricViewerInitializerCancelButton" + > + <FormattedMessage + id="xpack.ml.singleMetricViewerEmbeddable.setupModal.cancelButtonLabel" + defaultMessage="Cancel" + /> + </EuiButtonEmpty> + + <EuiButton + data-test-subj="mlsingleMetricViewerInitializerConfirmButton" + isDisabled={!isPanelTitleValid} + onClick={onCreate.bind(null, { + functionDescription, + panelTitle, + selectedDetectorIndex, + selectedEntities, + })} + fill + > + <FormattedMessage + id="xpack.ml.singleMetricViewerEmbeddable.setupModal.confirmButtonLabel" + defaultMessage="Confirm configurations" + /> + </EuiButton> + </EuiModalFooter> + </EuiModal> + ); +}; diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.tsx new file mode 100644 index 0000000000000..e9822c01f865a --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/single_metric_viewer_setup_flyout.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 from 'react'; +import type { CoreStart } from '@kbn/core/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { getDefaultSingleMetricViewerPanelTitle } from './single_metric_viewer_embeddable'; +import type { SingleMetricViewerEmbeddableInput, SingleMetricViewerServices } from '..'; +import { resolveJobSelection } from '../common/resolve_job_selection'; +import { SingleMetricViewerInitializer } from './single_metric_viewer_initializer'; +import type { MlStartDependencies } from '../../plugin'; + +export async function resolveEmbeddableSingleMetricViewerUserInput( + coreStart: CoreStart, + pluginStart: MlStartDependencies, + input: SingleMetricViewerServices +): Promise<Partial<SingleMetricViewerEmbeddableInput>> { + const { overlays, theme, i18n } = coreStart; + const { mlApiServices } = input; + const timefilter = pluginStart.data.query.timefilter.timefilter; + + return new Promise(async (resolve, reject) => { + try { + const { jobIds } = await resolveJobSelection(coreStart, undefined, true); + const title = getDefaultSingleMetricViewerPanelTitle(jobIds); + const { jobs } = await mlApiServices.getJobs({ jobId: jobIds.join(',') }); + + const modalSession = overlays.openModal( + toMountPoint( + <KibanaContextProvider + services={{ + mlServices: { ...input }, + ...coreStart, + }} + > + <SingleMetricViewerInitializer + defaultTitle={title} + initialInput={input} + job={jobs[0]} + bounds={timefilter.getActiveBounds()!} + onCreate={({ + functionDescription, + panelTitle, + selectedDetectorIndex, + selectedEntities, + }) => { + modalSession.close(); + resolve({ + jobIds, + title: panelTitle, + functionDescription, + panelTitle, + selectedDetectorIndex, + selectedEntities, + }); + }} + onCancel={() => { + modalSession.close(); + reject(); + }} + /> + </KibanaContextProvider>, + { theme, i18n } + ) + ); + } catch (error) { + reject(error); + } + }); +} diff --git a/x-pack/plugins/ml/public/embeddables/single_metric_viewer/use_single_metric_viewer_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/use_single_metric_viewer_input_resolver.ts new file mode 100644 index 0000000000000..c9f9d57fd7803 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/single_metric_viewer/use_single_metric_viewer_input_resolver.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; +import { combineLatest, Observable } from 'rxjs'; +import { startWith } from 'rxjs/operators'; +import { TimefilterContract } from '@kbn/data-plugin/public'; +import { SingleMetricViewerEmbeddableInput } from '..'; +import type { TimeRangeBounds } from '../../application/util/time_buckets'; + +export function useSingleMetricViewerInputResolver( + embeddableInput: Observable<SingleMetricViewerEmbeddableInput>, + refresh: Observable<void>, + timefilter: TimefilterContract, + onRenderComplete: () => void +) { + const [data, setData] = useState<any>(); + const [bounds, setBounds] = useState<TimeRangeBounds | undefined>(); + const [lastRefresh, setLastRefresh] = useState<number | undefined>(); + + useEffect(function subscribeToEmbeddableInput() { + const subscription = combineLatest([embeddableInput, refresh.pipe(startWith(null))]).subscribe( + (input) => { + if (input !== undefined) { + setData(input[0]); + if (timefilter !== undefined) { + const { timeRange } = input[0]; + const currentBounds = timefilter.calculateBounds(timeRange); + setBounds(currentBounds); + setLastRefresh(Date.now()); + } + onRenderComplete(); + } + } + ); + + return () => subscription.unsubscribe(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { data, bounds, lastRefresh }; +} diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 48a7b5d43a5ac..56a33f488d534 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -27,6 +27,9 @@ import { MlEmbeddableTypes, } from './constants'; import { MlResultsService } from '../application/services/results_service'; +import type { MlApiServices } from '../application/services/ml_api_service'; +import type { MlFieldFormatService } from '../application/services/field_format_service'; +import type { MlTimeSeriesSeachService } from '../application/timeseriesexplorer/timeseriesexplorer_utils/time_series_search_service'; export interface AnomalySwimlaneEmbeddableCustomInput { jobIds: JobId[]; @@ -100,13 +103,45 @@ export interface AnomalyChartsEmbeddableCustomInput { export type AnomalyChartsEmbeddableInput = EmbeddableInput & AnomalyChartsEmbeddableCustomInput; +export interface SingleMetricViewerEmbeddableCustomInput { + jobIds: JobId[]; + title: string; + functionDescription?: string; + panelTitle: string; + selectedDetectorIndex: number; + selectedEntities: MlEntityField[]; + // Embeddable inputs which are not included in the default interface + filters: Filter[]; + query: Query; + refreshConfig: RefreshInterval; + timeRange: TimeRange; +} + +export type SingleMetricViewerEmbeddableInput = EmbeddableInput & + SingleMetricViewerEmbeddableCustomInput; + export interface AnomalyChartsServices { anomalyDetectorService: AnomalyDetectorService; anomalyExplorerService: AnomalyExplorerChartsService; mlResultsService: MlResultsService; + mlApiServices?: MlApiServices; +} + +export interface SingleMetricViewerServices { + anomalyExplorerService: AnomalyExplorerChartsService; + anomalyDetectorService: AnomalyDetectorService; + mlApiServices: MlApiServices; + mlFieldFormatService: MlFieldFormatService; + mlResultsService: MlResultsService; + mlTimeSeriesSearchService?: MlTimeSeriesSeachService; } export type AnomalyChartsEmbeddableServices = [CoreStart, MlDependencies, AnomalyChartsServices]; +export type SingleMetricViewerEmbeddableServices = [ + CoreStart, + MlDependencies, + SingleMetricViewerServices +]; export interface AnomalyChartsCustomOutput { entityFields?: MlEntityField[]; severity?: number; diff --git a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts index 707a594d5eff3..345b6cf6054af 100644 --- a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts +++ b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts @@ -766,12 +766,12 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu } // Build the tooltip data for the chart info icon, showing further details on what is being plotted. - let functionLabel = `${config.metricFunction}`; + let functionLabel = `${fullSeriesConfig.metricFunction ?? config.metricFunction}`; if ( fullSeriesConfig.metricFieldName !== undefined && fullSeriesConfig.metricFieldName !== null ) { - functionLabel += ` ${fullSeriesConfig.metricFieldName}`; + functionLabel += `(${fullSeriesConfig.metricFieldName})`; } fullSeriesConfig.infoTooltip = { diff --git a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts index 70fc5098fe3b5..656007b3e1e1c 100644 --- a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts @@ -144,4 +144,5 @@ export const getAnomalyRecordsSchema = schema.object({ latestMs: schema.number(), criteriaFields: schema.arrayOf(schema.any()), interval: schema.string(), + functionDescription: schema.maybe(schema.nullable(schema.string())), }); diff --git a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap index 500bcfdaaa3f2..2217c13f37ed2 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap @@ -21,91 +21,87 @@ exports[`NoData should show a default message if reason is unknown 1`] = ` class="emotion-euiPageSection__content-l-center" > <div - class="euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge emotion-euiPanel-m-plain-hasShadow" + class="euiPanel euiPanel--plain euiEmptyPrompt emotion-euiPanel-m-plain-hasShadow-euiEmptyPrompt-vertical" > <div - class="euiEmptyPrompt__main" + class="euiEmptyPrompt__main emotion-euiEmptyPrompt__main-vertical-l" > <div - class="euiEmptyPrompt__icon" + class="euiEmptyPrompt__icon emotion-euiEmptyPrompt__icon-vertical" > <span data-euiicon-type="monitoringApp" /> </div> <div - class="euiEmptyPrompt__content" + class="euiEmptyPrompt__content emotion-euiEmptyPrompt__content-vertical" > + <h2 + class="euiTitle emotion-euiTitle-m" + > + No monitoring data found + </h2> + <div + class="euiSpacer euiSpacer--m emotion-euiSpacer-m" + /> <div - class="euiEmptyPrompt__contentInner" + class="euiText emotion-euiText-m-euiTextColor-subdued" > - <h2 - class="euiTitle emotion-euiTitle-m" - > - No monitoring data found - </h2> - <div - class="euiSpacer euiSpacer--m emotion-euiSpacer-m" - /> <div - class="euiText emotion-euiText-m-euiTextColor-subdued" + class="euiText emotion-euiText-m" > - <div - class="euiText emotion-euiText-m" - > - <p> - Have you set up monitoring yet? If so, make sure that the selected time period in the upper right includes monitoring data. - </p> - <p> - If you have configured monitoring data to be sent to a dedicated monitoring cluster you should access that data with the Kibana instance attached to the monitoring cluster. - </p> - </div> + <p> + Have you set up monitoring yet? If so, make sure that the selected time period in the upper right includes monitoring data. + </p> + <p> + If you have configured monitoring data to be sent to a dedicated monitoring cluster you should access that data with the Kibana instance attached to the monitoring cluster. + </p> </div> + </div> + <div + class="euiSpacer euiSpacer--l emotion-euiSpacer-l" + /> + <div + class="euiFlexGroup emotion-euiFlexGroup-responsive-s-spaceAround-center-row" + > <div - class="euiSpacer euiSpacer--l emotion-euiSpacer-l" - /> - <div - class="euiFlexGroup emotion-euiFlexGroup-responsive-s-spaceAround-center-row" + class="euiFlexItem emotion-euiFlexItem-growZero" > - <div - class="euiFlexItem emotion-euiFlexItem-growZero" + <button + class="euiButton emotion-euiButtonDisplay-m-defaultMinWidth-fill-primary" + data-test-subj="enableCollectionInterval" + type="button" > - <button - class="euiButton emotion-euiButtonDisplay-m-defaultMinWidth-fill-primary" - data-test-subj="enableCollectionInterval" - type="button" + <span + class="emotion-euiButtonDisplayContent" > - <span - class="emotion-euiButtonDisplayContent" - > - Set up monitoring with Metricbeat - </span> - </button> - </div> + Set up monitoring with Metricbeat + </span> + </button> </div> - <hr - class="euiHorizontalRule euiHorizontalRule--half euiHorizontalRule--marginLarge emotion-euiHorizontalRule-half-l" - /> - <button - class="euiButtonEmpty emotion-euiButtonDisplay-euiButtonEmpty-m-empty-primary" - data-test-subj="useInternalCollection" - type="button" + </div> + <hr + class="euiHorizontalRule euiHorizontalRule--half euiHorizontalRule--marginLarge emotion-euiHorizontalRule-half-l" + /> + <button + class="euiButtonEmpty emotion-euiButtonDisplay-euiButtonEmpty-m-empty-primary" + data-test-subj="useInternalCollection" + type="button" + > + <span + class="euiButtonEmpty__content emotion-euiButtonDisplayContent" > <span - class="euiButtonEmpty__content emotion-euiButtonDisplayContent" + class="eui-textTruncate euiButtonEmpty__text" > <span - class="eui-textTruncate euiButtonEmpty__text" + class="emotion-euiTextColor-subdued" > - <span - class="emotion-euiTextColor-subdued" - > - Or, set up with self monitoring - </span> + Or, set up with self monitoring </span> </span> - </button> - </div> + </span> + </button> </div> </div> </div> @@ -136,91 +132,87 @@ exports[`NoData should show text next to the spinner while checking a setting 1` class="emotion-euiPageSection__content-l-center" > <div - class="euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge emotion-euiPanel-m-plain-hasShadow" + class="euiPanel euiPanel--plain euiEmptyPrompt emotion-euiPanel-m-plain-hasShadow-euiEmptyPrompt-vertical" > <div - class="euiEmptyPrompt__main" + class="euiEmptyPrompt__main emotion-euiEmptyPrompt__main-vertical-l" > <div - class="euiEmptyPrompt__icon" + class="euiEmptyPrompt__icon emotion-euiEmptyPrompt__icon-vertical" > <span data-euiicon-type="monitoringApp" /> </div> <div - class="euiEmptyPrompt__content" + class="euiEmptyPrompt__content emotion-euiEmptyPrompt__content-vertical" > + <h2 + class="euiTitle emotion-euiTitle-m" + > + No monitoring data found + </h2> + <div + class="euiSpacer euiSpacer--m emotion-euiSpacer-m" + /> <div - class="euiEmptyPrompt__contentInner" + class="euiText emotion-euiText-m-euiTextColor-subdued" > - <h2 - class="euiTitle emotion-euiTitle-m" - > - No monitoring data found - </h2> - <div - class="euiSpacer euiSpacer--m emotion-euiSpacer-m" - /> <div - class="euiText emotion-euiText-m-euiTextColor-subdued" + class="euiText emotion-euiText-m" > - <div - class="euiText emotion-euiText-m" - > - <p> - Have you set up monitoring yet? If so, make sure that the selected time period in the upper right includes monitoring data. - </p> - <p> - If you have configured monitoring data to be sent to a dedicated monitoring cluster you should access that data with the Kibana instance attached to the monitoring cluster. - </p> - </div> + <p> + Have you set up monitoring yet? If so, make sure that the selected time period in the upper right includes monitoring data. + </p> + <p> + If you have configured monitoring data to be sent to a dedicated monitoring cluster you should access that data with the Kibana instance attached to the monitoring cluster. + </p> </div> + </div> + <div + class="euiSpacer euiSpacer--l emotion-euiSpacer-l" + /> + <div + class="euiFlexGroup emotion-euiFlexGroup-responsive-s-spaceAround-center-row" + > <div - class="euiSpacer euiSpacer--l emotion-euiSpacer-l" - /> - <div - class="euiFlexGroup emotion-euiFlexGroup-responsive-s-spaceAround-center-row" + class="euiFlexItem emotion-euiFlexItem-growZero" > - <div - class="euiFlexItem emotion-euiFlexItem-growZero" + <button + class="euiButton emotion-euiButtonDisplay-m-defaultMinWidth-fill-primary" + data-test-subj="enableCollectionInterval" + type="button" > - <button - class="euiButton emotion-euiButtonDisplay-m-defaultMinWidth-fill-primary" - data-test-subj="enableCollectionInterval" - type="button" + <span + class="emotion-euiButtonDisplayContent" > - <span - class="emotion-euiButtonDisplayContent" - > - Set up monitoring with Metricbeat - </span> - </button> - </div> + Set up monitoring with Metricbeat + </span> + </button> </div> - <hr - class="euiHorizontalRule euiHorizontalRule--half euiHorizontalRule--marginLarge emotion-euiHorizontalRule-half-l" - /> - <button - class="euiButtonEmpty emotion-euiButtonDisplay-euiButtonEmpty-m-empty-primary" - data-test-subj="useInternalCollection" - type="button" + </div> + <hr + class="euiHorizontalRule euiHorizontalRule--half euiHorizontalRule--marginLarge emotion-euiHorizontalRule-half-l" + /> + <button + class="euiButtonEmpty emotion-euiButtonDisplay-euiButtonEmpty-m-empty-primary" + data-test-subj="useInternalCollection" + type="button" + > + <span + class="euiButtonEmpty__content emotion-euiButtonDisplayContent" > <span - class="euiButtonEmpty__content emotion-euiButtonDisplayContent" + class="eui-textTruncate euiButtonEmpty__text" > <span - class="eui-textTruncate euiButtonEmpty__text" + class="emotion-euiTextColor-subdued" > - <span - class="emotion-euiTextColor-subdued" - > - Or, set up with self monitoring - </span> + Or, set up with self monitoring </span> </span> - </button> - </div> + </span> + </button> </div> </div> </div> diff --git a/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap b/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap index e83555e1d5eb3..bfba0e77b29aa 100644 --- a/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap @@ -15,13 +15,13 @@ exports[`PageLoading should show a simple page loading component 1`] = ` class="emotion-euiPageSection__content-l-center" > <div - class="euiPanel euiPanel--plain euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge emotion-euiPanel-m-plain-hasShadow" + class="euiPanel euiPanel--plain euiEmptyPrompt emotion-euiPanel-m-plain-hasShadow-euiEmptyPrompt-vertical" > <div - class="euiEmptyPrompt__main" + class="euiEmptyPrompt__main emotion-euiEmptyPrompt__main-vertical-l" > <div - class="euiEmptyPrompt__icon" + class="euiEmptyPrompt__icon emotion-euiEmptyPrompt__icon-vertical" > <span aria-label="Loading" @@ -30,16 +30,12 @@ exports[`PageLoading should show a simple page loading component 1`] = ` /> </div> <div - class="euiEmptyPrompt__content" + class="euiEmptyPrompt__content emotion-euiEmptyPrompt__content-vertical" > <div - class="euiEmptyPrompt__contentInner" + class="euiText emotion-euiText-m-euiTextColor-subdued" > - <div - class="euiText emotion-euiText-m-euiTextColor-subdued" - > - Loading… - </div> + Loading… </div> </div> </div> diff --git a/x-pack/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts b/x-pack/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts index c8e0d9be29784..58cf458dcc682 100644 --- a/x-pack/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts +++ b/x-pack/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { Aggregators } from './types'; import { LocatorPublic } from '@kbn/share-plugin/common'; +import { LogsExplorerLocatorParams } from '@kbn/deeplinks-observability'; import { getViewInAppUrl, GetViewInAppUrlArgs } from './get_view_in_app_url'; describe('getViewInAppUrl', () => { const logsExplorerLocator = { getRedirectUrl: jest.fn(() => 'mockedGetRedirectUrl'), - } as unknown as LocatorPublic<DiscoverAppLocatorParams>; + } as unknown as LocatorPublic<LogsExplorerLocatorParams>; const startedAt = '2023-12-07T16:30:15.403Z'; const endedAt = '2023-12-07T20:30:15.403Z'; const returnedTimeRange = { diff --git a/x-pack/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.ts b/x-pack/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.ts index 79e40fcfc65f3..af441fb8069f7 100644 --- a/x-pack/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.ts +++ b/x-pack/plugins/observability/common/custom_threshold_rule/get_view_in_app_url.ts @@ -6,9 +6,9 @@ */ import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; -import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import type { TimeRange } from '@kbn/es-query'; import type { LocatorPublic } from '@kbn/share-plugin/common'; +import { LogsExplorerLocatorParams } from '@kbn/deeplinks-observability'; import type { CustomThresholdExpressionMetric } from './types'; export interface GetViewInAppUrlArgs { @@ -16,7 +16,7 @@ export interface GetViewInAppUrlArgs { endedAt?: string; startedAt?: string; filter?: string; - logsExplorerLocator?: LocatorPublic<DiscoverAppLocatorParams>; + logsExplorerLocator?: LocatorPublic<LogsExplorerLocatorParams>; metrics?: CustomThresholdExpressionMetric[]; } diff --git a/x-pack/plugins/observability/public/components/alerts_table/common/render_cell_value.tsx b/x-pack/plugins/observability/public/components/alerts_table/common/render_cell_value.tsx index 3bde70900d334..d68b11115396e 100644 --- a/x-pack/plugins/observability/public/components/alerts_table/common/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/components/alerts_table/common/render_cell_value.tsx @@ -100,11 +100,12 @@ export const getRenderCellValue = ({ case ALERT_SEVERITY: return <AlertSeverityBadge severityLevel={value ?? undefined} />; case ALERT_EVALUATION_VALUE: - const values = getMappedNonEcsValue({ + const valuesField = getMappedNonEcsValue({ data, fieldName: ALERT_EVALUATION_VALUES, }); - return values ? values : value; + const values = getRenderValue(valuesField); + return valuesField ? values : value; case ALERT_REASON: const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); const alert = parseAlert(observabilityRuleTypeRegistry)(dataFieldEs); diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap index b963137281b70..a9a77b477f1a2 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap @@ -28,7 +28,6 @@ Array [ }, ], "dataView": undefined, - "filterQuery": "", "groupBy": Array [ "host.hostname", ], @@ -46,6 +45,13 @@ Array [ "timeSize": 15, "timeUnit": "m", }, + "searchConfiguration": Object { + "index": "mockedIndex", + "query": Object { + "language": "kuery", + "query": "host.hostname: Users-System.local and service.type: system", + }, + }, "seriesType": "bar_stacked", "timeRange": Object { "from": "2023-03-28T10:43:13.802Z", diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx index 2506516efd81a..f07a6ddac4501 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useEffect, useState } from 'react'; @@ -54,7 +53,6 @@ import { LogRateAnalysis } from './log_rate_analysis'; import { Groups } from './groups'; import { Tags } from './tags'; import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart'; -import { getFilterQuery } from './helpers/get_filter_query'; // TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690 export type CustomThresholdRule = Rule<CustomThresholdRuleTypeParams>; @@ -118,7 +116,6 @@ export default function AlertDetailsAppSection({ const { euiTheme } = useEuiTheme(); const hasLogRateAnalysisLicense = hasAtLeast('platinum'); const [dataView, setDataView] = useState<DataView>(); - const [filterQuery, setFilterQuery] = useState<string>(''); const [, setDataViewError] = useState<Error>(); const ruleParams = rule.params as RuleTypeParams & AlertParams; const chartProps = { @@ -204,11 +201,6 @@ export default function AlertDetailsAppSection({ setAlertSummaryFields(alertSummaryFields); }, [groups, tags, rule, ruleLink, setAlertSummaryFields]); - useEffect(() => { - const query = `${(ruleParams.searchConfiguration?.query as Query)?.query as string}`; - setFilterQuery(getFilterQuery(query, groups)); - }, [groups, ruleParams.searchConfiguration]); - useEffect(() => { const initDataView = async () => { const ruleSearchConfiguration = ruleParams.searchConfiguration; @@ -271,7 +263,7 @@ export default function AlertDetailsAppSection({ <RuleConditionChart metricExpression={criterion} dataView={dataView} - filterQuery={filterQuery} + searchConfiguration={ruleParams.searchConfiguration} groupBy={ruleParams.groupBy} annotations={annotations} timeRange={timeRange} diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.test.tsx b/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.test.tsx index 7aab6dd0d636b..d07eb08de4474 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.test.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; import { act } from 'react-dom/test-utils'; import { DataView } from '@kbn/data-views-plugin/common'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; @@ -35,7 +36,7 @@ describe('Rule condition chart', () => { <RuleConditionChart metricExpression={expression} dataView={dataView} - filterQuery={''} + searchConfiguration={{} as SerializedSearchSourceFields} groupBy={[]} error={{}} timeRange={{ from: 'now-15m', to: 'now' }} diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.tsx b/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.tsx index e1eefcfc2f706..3cf10659ee66a 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/rule_condition_chart/rule_condition_chart.tsx @@ -4,8 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import React, { useState, useEffect } from 'react'; +import { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; import { EuiEmptyPrompt, useEuiTheme } from '@elastic/eui'; +import { Query } from '@kbn/es-query'; import { FillStyle, SeriesType } from '@kbn/lens-plugin/public'; import { DataView } from '@kbn/data-views-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -38,8 +41,8 @@ import { interface RuleConditionChartProps { metricExpression: MetricExpression; + searchConfiguration: SerializedSearchSourceFields; dataView?: DataView; - filterQuery?: string; groupBy?: string | string[]; error?: IErrorObject; timeRange: TimeRange; @@ -47,10 +50,15 @@ interface RuleConditionChartProps { seriesType?: SeriesType; } +const defaultQuery: Query = { + language: 'kuery', + query: '', +}; + export function RuleConditionChart({ metricExpression, + searchConfiguration, dataView, - filterQuery, groupBy, error, annotations, @@ -283,7 +291,7 @@ export function RuleConditionChart({ comparator, dataView, equation, - filterQuery, + searchConfiguration, formula, formulaAsync.value, groupBy, @@ -337,10 +345,8 @@ export function RuleConditionChart({ timeRange={timeRange} attributes={attributes} disableTriggers={true} - query={{ - language: 'kuery', - query: filterQuery || '', - }} + query={(searchConfiguration.query as Query) || defaultQuery} + filters={searchConfiguration.filter} /> </div> ); diff --git a/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx b/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx index cd93a5e134c2b..19a1c0fcd164e 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx @@ -405,7 +405,7 @@ export default function Expressions(props: Props) { indexPatterns={dataView ? [dataView] : undefined} showQueryInput={true} showQueryMenu={false} - showFilterBar={false} + showFilterBar={!!ruleParams.searchConfiguration?.filter} showDatePicker={false} showSubmitButton={false} displayStyle="inPage" @@ -413,6 +413,16 @@ export default function Expressions(props: Props) { onQuerySubmit={onFilterChange} dataTestSubj="thresholdRuleUnifiedSearchBar" query={ruleParams.searchConfiguration?.query as Query} + filters={ruleParams.searchConfiguration?.filter} + onFiltersUpdated={(filter) => { + // Since rule params will be sent to the API as is, and we only need meta and query parameters to be + // saved in the rule's saved object, we filter extra fields here (such as $state). + const filters = filter.map(({ meta, query }) => ({ meta, query })); + setRuleParams('searchConfiguration', { + ...ruleParams.searchConfiguration, + filter: filters, + }); + }} /> {errors.filterQuery && ( <EuiFormErrorText data-test-subj="thresholdRuleDataViewErrorNoTimestamp"> @@ -454,7 +464,7 @@ export default function Expressions(props: Props) { <PreviewChart metricExpression={e} dataView={dataView} - filterQuery={(ruleParams.searchConfiguration?.query as Query)?.query as string} + searchConfiguration={ruleParams.searchConfiguration} groupBy={ruleParams.groupBy} error={(errors[idx] as IErrorObject) || emptyError} timeRange={{ from: `now-${(timeSize ?? 1) * 20}${timeUnit}`, to: 'now' }} diff --git a/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts index 6921c1f68cb15..b702e153fba5b 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_create_slo.ts @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import { encode } from '@kbn/rison'; import type { CreateSLOInput, CreateSLOResponse, FindSLOResponse } from '@kbn/slo-schema'; import { QueryKey, useMutation, useQueryClient } from '@tanstack/react-query'; -import { v4 as uuidv4 } from 'uuid'; import { paths } from '../../../common/locators/paths'; import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; @@ -37,46 +36,17 @@ export function useCreateSlo() { return http.post<CreateSLOResponse>(`/api/observability/slos`, { body }); }, { - onMutate: async ({ slo }) => { - await queryClient.cancelQueries({ queryKey: sloKeys.lists(), exact: false }); - - const queriesData = queryClient.getQueriesData<FindSLOResponse>({ - queryKey: sloKeys.lists(), - exact: false, - }); - - const [queryKey, previousData] = queriesData?.at(0) ?? []; - - const newItem = { ...slo, id: uuidv4(), summary: undefined }; - - const optimisticUpdate = { - page: previousData?.page ?? 1, - perPage: previousData?.perPage ?? 25, - total: previousData?.total ? previousData.total + 1 : 1, - results: [...(previousData?.results ?? []), newItem], - }; - - if (queryKey) { - queryClient.setQueryData(queryKey, optimisticUpdate); - } - - return { queryKey, previousData }; - }, onSuccess: (_data, { slo }) => { + queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false }); + toasts.addSuccess( i18n.translate('xpack.observability.slo.create.successNotification', { defaultMessage: 'Successfully created {name}', values: { name: slo.name }, }) ); - - queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false }); }, onError: (error, { slo }, context) => { - if (context?.previousData && context?.queryKey) { - queryClient.setQueryData(context.queryKey, context.previousData); - } - toasts.addError(new Error(error.body?.message ?? error.message), { title: i18n.translate('xpack.observability.slo.create.errorNotification', { defaultMessage: 'Something went wrong while creating {name}', diff --git a/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts index 0ba0c93266bda..2e77623fc991f 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { QueryKey, useMutation, useQueryClient } from '@tanstack/react-query'; +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { FindSLOResponse } from '@kbn/slo-schema'; -import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import { QueryKey, useMutation, useQueryClient } from '@tanstack/react-query'; import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; @@ -36,38 +36,7 @@ export function useDeleteSlo() { } }, { - onMutate: async (slo) => { - await queryClient.cancelQueries({ queryKey: sloKeys.lists(), exact: false }); - - const queriesData = queryClient.getQueriesData<FindSLOResponse>({ - queryKey: sloKeys.lists(), - exact: false, - }); - const [queryKey, previousData] = queriesData?.at(0) ?? []; - - // taking into account partitioned slo - const matchingSloCount = - previousData?.results?.filter((result) => result.id === slo.id)?.length ?? 0; - - const optimisticUpdate = { - page: previousData?.page ?? 1, - perPage: previousData?.perPage ?? 25, - total: previousData?.total ? previousData.total - matchingSloCount : 0, - results: previousData?.results?.filter((result) => result.id !== slo.id) ?? [], - }; - - if (queryKey) { - queryClient.setQueryData(queryKey, optimisticUpdate); - } - - return { previousData, queryKey }; - }, - // If the mutation fails, use the context returned from onMutate to roll back onError: (error, { name }, context) => { - if (context?.previousData && context?.queryKey) { - queryClient.setQueryData(context.queryKey, context.previousData); - } - toasts.addError(new Error(error.body?.message ?? error.message), { title: i18n.translate('xpack.observability.slo.slo.delete.errorNotification', { defaultMessage: 'Failed to delete {name}', @@ -76,6 +45,8 @@ export function useDeleteSlo() { }); }, onSuccess: (_data, { name }) => { + queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false }); + toasts.addSuccess( i18n.translate('xpack.observability.slo.slo.delete.successNotification', { defaultMessage: 'Deleted {name}', diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_inspect.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_inspect.ts new file mode 100644 index 0000000000000..c83b398a8fb3b --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_inspect.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { CreateSLOInput, SLOResponse } from '@kbn/slo-schema'; +import { useQuery } from '@tanstack/react-query'; +import { useKibana } from '../../utils/kibana_react'; + +interface SLOInspectResponse { + slo: SLOResponse; + pipeline: Record<string, any>; + rollUpTransform: TransformPutTransformRequest; + summaryTransform: TransformPutTransformRequest; + temporaryDoc: Record<string, any>; +} + +export interface UseInspectSLOResponse { + data: SLOInspectResponse | undefined; + isLoading: boolean; + isSuccess: boolean; + isError: boolean; +} + +export function useFetchSloInspect(slo: CreateSLOInput, shouldInspect: boolean) { + const { http } = useKibana().services; + + const { isLoading, isError, isSuccess, data } = useQuery({ + queryKey: ['slo', 'inspect'], + queryFn: async ({ signal }) => { + try { + const body = JSON.stringify(slo); + const response = await http.post<SLOInspectResponse>( + '/internal/api/observability/slos/_inspect', + { + body, + signal, + } + ); + + return response; + } catch (error) { + // ignore error + } + }, + enabled: shouldInspect, + refetchOnWindowFocus: false, + keepPreviousData: true, + }); + + return { + data, + isLoading, + isSuccess, + isError, + }; +} diff --git a/x-pack/plugins/observability/public/hooks/slo/use_inspect_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_inspect_slo.ts deleted file mode 100644 index 6eaa3165aee77..0000000000000 --- a/x-pack/plugins/observability/public/hooks/slo/use_inspect_slo.ts +++ /dev/null @@ -1,41 +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 { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; -import type { FindSLOResponse, SLOResponse } from '@kbn/slo-schema'; -import { QueryKey, useMutation } from '@tanstack/react-query'; -import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { CreateSLOInput } from '@kbn/slo-schema'; -import { useKibana } from '../../utils/kibana_react'; - -type ServerError = IHttpFetchError<ResponseErrorBody>; - -interface SLOInspectResponse { - slo: SLOResponse; - pipeline: Record<string, any>; - rollUpTransform: TransformPutTransformRequest; - summaryTransform: TransformPutTransformRequest; - temporaryDoc: Record<string, any>; -} - -export function useInspectSlo() { - const { http } = useKibana().services; - - return useMutation< - SLOInspectResponse, - ServerError, - { slo: CreateSLOInput }, - { previousData?: FindSLOResponse; queryKey?: QueryKey } - >( - ['inspectSlo'], - ({ slo }) => { - const body = JSON.stringify(slo); - return http.post<SLOInspectResponse>(`/internal/api/observability/slos/_inspect`, { body }); - }, - {} - ); -} diff --git a/x-pack/plugins/observability/public/hooks/slo/use_reset_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_reset_slo.ts index 35f166e89b766..9f67c5efe3972 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_reset_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_reset_slo.ts @@ -4,10 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useMutation } from '@tanstack/react-query'; -import { i18n } from '@kbn/i18n'; import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useKibana } from '../../utils/kibana_react'; +import { sloKeys } from './query_key_factory'; type ServerError = IHttpFetchError<ResponseErrorBody>; @@ -16,6 +17,8 @@ export function useResetSlo() { http, notifications: { toasts }, } = useKibana().services; + const queryClient = useQueryClient(); + return useMutation<string, ServerError, { id: string; name: string }>( ['resetSlo'], ({ id, name }) => { @@ -40,6 +43,7 @@ export function useResetSlo() { }); }, onSuccess: (_data, { name }) => { + queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false }); toasts.addSuccess( i18n.translate('xpack.observability.slo.slo.reset.successNotification', { defaultMessage: '{name} reset successfully', diff --git a/x-pack/plugins/observability/public/hooks/slo/use_update_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_update_slo.ts index 8f81075f0f2df..ff12ab2acdfe2 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_update_slo.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_update_slo.ts @@ -7,11 +7,11 @@ import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { encode } from '@kbn/rison'; import type { FindSLOResponse, UpdateSLOInput, UpdateSLOResponse } from '@kbn/slo-schema'; import { QueryKey, useMutation, useQueryClient } from '@tanstack/react-query'; -import { encode } from '@kbn/rison'; -import { useKibana } from '../../utils/kibana_react'; import { paths } from '../../../common/locators/paths'; +import { useKibana } from '../../utils/kibana_react'; import { sloKeys } from './query_key_factory'; type ServerError = IHttpFetchError<ResponseErrorBody>; @@ -36,47 +36,17 @@ export function useUpdateSlo() { return http.put<UpdateSLOResponse>(`/api/observability/slos/${sloId}`, { body }); }, { - onMutate: async ({ sloId, slo }) => { - await queryClient.cancelQueries({ queryKey: sloKeys.lists(), exact: false }); - - const queriesData = queryClient.getQueriesData<FindSLOResponse>({ - queryKey: sloKeys.lists(), - exact: false, - }); - const [queryKey, previousData] = queriesData?.at(0) ?? []; - - const updatedItem = { ...slo, id: sloId }; - const optimisticUpdate = { - page: previousData?.page ?? 1, - perPage: previousData?.perPage ?? 25, - total: previousData?.total ? previousData.total : 1, - results: [ - ...(previousData?.results?.filter((result) => result.id !== sloId) ?? []), - updatedItem, - ], - }; - - if (queryKey) { - queryClient.setQueryData(queryKey, optimisticUpdate); - } - - return { previousData, queryKey, sloId }; - }, onSuccess: (_data, { slo: { name } }) => { + queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false }); + toasts.addSuccess( i18n.translate('xpack.observability.slo.update.successNotification', { defaultMessage: 'Successfully updated {name}', values: { name }, }) ); - - queryClient.invalidateQueries({ queryKey: sloKeys.lists(), exact: false }); }, - onError: (error, { slo }, context) => { - if (context?.previousData && context?.queryKey) { - queryClient.setQueryData(context.queryKey, context.previousData); - } - + onError: (error, { slo, sloId }, context) => { toasts.addError(new Error(error.body?.message ?? error.message), { title: i18n.translate('xpack.observability.slo.update.errorNotification', { defaultMessage: 'Something went wrong when updating {name}', @@ -84,13 +54,9 @@ export function useUpdateSlo() { }), }); - if (context?.sloId) { - navigateToUrl( - http.basePath.prepend( - paths.observability.sloEditWithEncodedForm(context.sloId, encode(slo)) - ) - ); - } + navigateToUrl( + http.basePath.prepend(paths.observability.sloEditWithEncodedForm(sloId, encode(slo))) + ); }, } ); diff --git a/x-pack/plugins/observability/public/pages/overview/components/sections/logs/logs_section.tsx b/x-pack/plugins/observability/public/pages/overview/components/sections/logs/logs_section.tsx index 63087aad6dd30..2651cef2191d0 100644 --- a/x-pack/plugins/observability/public/pages/overview/components/sections/logs/logs_section.tsx +++ b/x-pack/plugins/observability/public/pages/overview/components/sections/logs/logs_section.tsx @@ -142,7 +142,11 @@ export function LogsSection({ bucketSize }: Props) { showLegend legendPosition={Position.Right} xDomain={{ min, max }} - showLegendExtra + // Please double check if the data passed to the chart contains all the buckets, even the empty ones. + // the showLegendExtra will display the last element of the data array as the default legend value + // and if empty buckets are filtered out you can probably see a value that doesn't correspond + // to the value in the last time bucket visualized. + // showLegendExtra locale={i18n.getLocale()} /> {series && diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/common/equivalent_api_request.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/common/equivalent_api_request.tsx index 6bf597b12d9a9..3f76c0df540e5 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/common/equivalent_api_request.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/common/equivalent_api_request.tsx @@ -4,9 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { i18n } from '@kbn/i18n'; - -import React, { useEffect, useState } from 'react'; import { EuiButtonEmpty, EuiCodeBlock, @@ -18,29 +15,30 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { useFormContext } from 'react-hook-form'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { CreateSLOInput } from '@kbn/slo-schema'; -import { CreateSLOForm } from '../../types'; +import { CreateSLOInput, GetSLOResponse } from '@kbn/slo-schema'; +import React, { useEffect, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; import { transformCreateSLOFormToCreateSLOInput } from '../../helpers/process_slo_form_values'; +import { CreateSLOForm } from '../../types'; -export function EquivalentApiRequest({ - isCreateSloLoading, - isUpdateSloLoading, -}: { - isCreateSloLoading: boolean; - isUpdateSloLoading: boolean; -}) { - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); +interface Props { + isEditMode: boolean; + disabled: boolean; + slo?: GetSLOResponse; +} +export function EquivalentApiRequest({ disabled, isEditMode, slo }: Props) { + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const { getValues, trigger } = useFormContext<CreateSLOForm>(); - const [sloData, setSloData] = useState<CreateSLOInput>(); useEffect(() => { if (!isFlyoutVisible) { return; } + trigger().then((isValid) => { if (isValid) { setSloData(transformCreateSLOFormToCreateSLOInput(getValues())); @@ -72,7 +70,7 @@ export function EquivalentApiRequest({ </EuiText> <EuiSpacer size="s" /> <EuiCodeBlock language="javascript" isCopyable paddingSize="s"> - {'POST /api/observability/slos'} + {isEditMode ? `PUT /api/observability/slos/${slo!.id}` : 'POST /api/observability/slos'} </EuiCodeBlock> <EuiSpacer size="s" /> <EuiText> @@ -115,7 +113,7 @@ export function EquivalentApiRequest({ color="primary" iconType="copyClipboard" data-test-subj="sloFormCopyJsonButton" - disabled={isCreateSloLoading || isUpdateSloLoading} + disabled={disabled} onClick={() => setIsFlyoutVisible(true)} > {i18n.translate('xpack.observability.slo.sloEdit.equivalentApiRequest', { diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/common/inspect_slo_portal.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/common/inspect_slo_portal.tsx deleted file mode 100644 index d03772c9bf4ce..0000000000000 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/common/inspect_slo_portal.tsx +++ /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. - */ - -import React from 'react'; -import { InPortal } from 'react-reverse-portal'; -import { GetSLOResponse } from '@kbn/slo-schema'; -import { CreateSLOForm } from '../../types'; -import { SLOInspectWrapper } from './slo_inspect'; -import { InspectSLOPortalNode } from '../../slo_edit'; - -export interface SloInspectPortalProps { - getValues: () => CreateSLOForm; - trigger: () => Promise<boolean>; - slo?: GetSLOResponse; -} -export function InspectSLOPortal(props: SloInspectPortalProps) { - return ( - <InPortal node={InspectSLOPortalNode}> - <SLOInspectWrapper {...props} /> - </InPortal> - ); -} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/common/slo_inspect.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/common/slo_inspect.tsx index 2c96c0d2d05bc..9f911bb1787ea 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/common/slo_inspect.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/common/slo_inspect.tsx @@ -4,109 +4,114 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { i18n } from '@kbn/i18n'; - -import React, { ReactNode, useState } from 'react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { useFetcher } from '@kbn/observability-shared-plugin/public'; import { - EuiFlyout, + EuiAccordion, EuiButton, + EuiButtonEmpty, + EuiButtonIcon, EuiCodeBlock, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutFooter, - EuiSpacer, - EuiFlyoutBody, - EuiToolTip, EuiFlexGroup, EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, EuiLoadingSpinner, - EuiAccordion, - EuiButtonIcon, + EuiSpacer, + EuiTitle, + EuiToolTip, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { + IngestPipelinesListParams, INGEST_PIPELINES_APP_LOCATOR, INGEST_PIPELINES_PAGES, - IngestPipelinesListParams, } from '@kbn/ingest-pipelines-plugin/public'; -import { SloInspectPortalProps } from './inspect_slo_portal'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useFetcher } from '@kbn/observability-shared-plugin/public'; +import { GetSLOResponse } from '@kbn/slo-schema'; +import React, { ReactNode, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; import { ObservabilityPublicPluginsStart } from '../../../..'; -import { useInspectSlo } from '../../../../hooks/slo/use_inspect_slo'; -import { transformCreateSLOFormToCreateSLOInput } from '../../helpers/process_slo_form_values'; import { enableInspectEsQueries } from '../../../../../common'; +import { useFetchSloInspect } from '../../../../hooks/slo/use_fetch_slo_inspect'; import { usePluginContext } from '../../../../hooks/use_plugin_context'; +import { transformCreateSLOFormToCreateSLOInput } from '../../helpers/process_slo_form_values'; +import { CreateSLOForm } from '../../types'; + +interface Props { + slo?: GetSLOResponse; + disabled: boolean; +} -export function SLOInspectWrapper(props: SloInspectPortalProps) { +export function SLOInspectWrapper({ slo, disabled }: Props) { const { services: { uiSettings }, } = useKibana(); const { isDev } = usePluginContext(); - const isInspectorEnabled = uiSettings?.get<boolean>(enableInspectEsQueries); - return isDev || isInspectorEnabled ? <SLOInspect {...props} /> : null; + return isDev || isInspectorEnabled ? <SLOInspect slo={slo} disabled={disabled} /> : null; } -function SLOInspect({ getValues, trigger, slo }: SloInspectPortalProps) { +function SLOInspect({ slo, disabled }: Props) { const { share, http } = useKibana<ObservabilityPublicPluginsStart>().services; + const { trigger, getValues } = useFormContext<CreateSLOForm>(); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const { mutateAsync: inspectSlo, data, isLoading } = useInspectSlo(); + const [isFormValid, setFormValid] = useState(false); - const { data: sloData } = useFetcher(async () => { - if (!isFlyoutVisible) { - return; - } - const isValid = await trigger(); - if (!isValid) { - return; - } - const sloForm = transformCreateSLOFormToCreateSLOInput(getValues()); - inspectSlo({ slo: { ...sloForm, id: slo?.id, revision: slo?.revision } }); - return sloForm; - }, [isFlyoutVisible, trigger, getValues, inspectSlo, slo]); + const sloFormValues = transformCreateSLOFormToCreateSLOInput(getValues()); + const { data: inspectSloData, isLoading } = useFetchSloInspect( + { ...sloFormValues, id: slo?.id, revision: slo?.revision }, + isFlyoutVisible && isFormValid + ); const { data: pipeLineUrl } = useFetcher(async () => { const ingestPipeLocator = share.url.locators.get<IngestPipelinesListParams>( INGEST_PIPELINES_APP_LOCATOR ); - const ingestPipeLineId = data?.pipeline?.id; + const ingestPipeLineId = inspectSloData?.pipeline?.id; return ingestPipeLocator?.getUrl({ pipelineId: ingestPipeLineId, page: INGEST_PIPELINES_PAGES.LIST, }); - }, [data?.pipeline?.id, share.url.locators]); + }, [inspectSloData?.pipeline?.id, share.url.locators]); const closeFlyout = () => { setIsFlyoutVisible(false); - setIsInspecting(false); + setFormValid(false); }; - const [isInspecting, setIsInspecting] = useState(false); - const onButtonClick = () => { - trigger().then((isValid) => { - if (isValid) { - setIsInspecting(() => !isInspecting); - setIsFlyoutVisible(() => !isFlyoutVisible); - } - }); + const handleInspectButtonClick = async () => { + const valid = await trigger(); + if (!valid) { + setFormValid(false); + return; + } + + setFormValid(true); + setIsFlyoutVisible(true); }; let flyout; - if (isFlyoutVisible) { flyout = ( <EuiFlyout ownFocus onClose={closeFlyout} aria-labelledby="flyoutTitle"> <EuiFlyoutHeader hasBorder> <EuiTitle size="m"> - <h2 id="flyoutTitle">{CONFIG_LABEL}</h2> + <h2 id="flyoutTitle"> + {i18n.translate('xpack.observability.monitorInspect.configLabel', { + defaultMessage: 'SLO Configurations', + })} + </h2> </EuiTitle> </EuiFlyoutHeader> <EuiFlyoutBody> {isLoading && <LoadingState />} <EuiSpacer size="m" /> - {data && ( + {inspectSloData && ( <> <CodeBlockAccordion id="slo" @@ -114,7 +119,7 @@ function SLOInspect({ getValues, trigger, slo }: SloInspectPortalProps) { 'xpack.observability.sLOInspect.codeBlockAccordion.sloConfigurationLabel', { defaultMessage: 'SLO configuration' } )} - json={data.slo} + json={inspectSloData.slo} /> <EuiSpacer size="s" /> <CodeBlockAccordion @@ -123,7 +128,7 @@ function SLOInspect({ getValues, trigger, slo }: SloInspectPortalProps) { 'xpack.observability.sLOInspect.codeBlockAccordion.rollupTransformLabel', { defaultMessage: 'Rollup transform' } )} - json={data.rollUpTransform} + json={inspectSloData.rollUpTransform} extraAction={ <EuiButtonIcon iconType="link" @@ -140,7 +145,7 @@ function SLOInspect({ getValues, trigger, slo }: SloInspectPortalProps) { 'xpack.observability.sLOInspect.codeBlockAccordion.summaryTransformLabel', { defaultMessage: 'Summary transform' } )} - json={data.summaryTransform} + json={inspectSloData.summaryTransform} extraAction={ <EuiButtonIcon iconType="link" @@ -164,7 +169,7 @@ function SLOInspect({ getValues, trigger, slo }: SloInspectPortalProps) { href={pipeLineUrl} /> } - json={data.pipeline} + json={inspectSloData.pipeline} /> <EuiSpacer size="s" /> @@ -174,7 +179,7 @@ function SLOInspect({ getValues, trigger, slo }: SloInspectPortalProps) { 'xpack.observability.sLOInspect.codeBlockAccordion.temporaryDocumentLabel', { defaultMessage: 'Temporary document' } )} - json={data.temporaryDoc} + json={inspectSloData.temporaryDoc} /> </> )} @@ -193,20 +198,33 @@ function SLOInspect({ getValues, trigger, slo }: SloInspectPortalProps) { </EuiFlyout> ); } + return ( <> <EuiToolTip - content={sloData ? VIEW_FORMATTED_CONFIG_LABEL : VALID_CONFIG_LABEL} + content={ + isFormValid + ? i18n.translate('xpack.observability.slo.viewFormattedResourcesConfigsButtonLabel', { + defaultMessage: 'View formatted resources configs for SLO', + }) + : i18n.translate('xpack.observability.slo.formattedConfigLabel.valid', { + defaultMessage: 'Only valid form configurations can be inspected.', + }) + } repositionOnScroll > - <EuiButton + <EuiButtonEmpty + color="primary" data-test-subj="syntheticsMonitorInspectShowFlyoutExampleButton" - onClick={onButtonClick} + onClick={handleInspectButtonClick} + disabled={disabled} iconType="inspect" iconSide="left" > - {SLO_INSPECT_LABEL} - </EuiButton> + {i18n.translate('xpack.observability.sLOInspect.sLOInspectButtonLabel', { + defaultMessage: 'SLO Inspect', + })} + </EuiButtonEmpty> </EuiToolTip> {flyout} @@ -251,20 +269,3 @@ export function LoadingState() { </EuiFlexGroup> ); } - -const SLO_INSPECT_LABEL = i18n.translate('xpack.observability.sLOInspect.sLOInspectButtonLabel', { - defaultMessage: 'SLO Inspect', -}); - -const VIEW_FORMATTED_CONFIG_LABEL = i18n.translate( - 'xpack.observability.slo.viewFormattedResourcesConfigsButtonLabel', - { defaultMessage: 'View formatted resources configs for SLO' } -); - -const VALID_CONFIG_LABEL = i18n.translate('xpack.observability.slo.formattedConfigLabel.valid', { - defaultMessage: 'Only valid form configurations can be inspected.', -}); - -const CONFIG_LABEL = i18n.translate('xpack.observability.monitorInspect.configLabel', { - defaultMessage: 'SLO Configurations', -}); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx index dfc5fb1a6f563..9b5ddfc29b07d 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import type { GetSLOResponse } from '@kbn/slo-schema'; import React, { useCallback } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { InspectSLOPortal } from './common/inspect_slo_portal'; import { EquivalentApiRequest } from './common/equivalent_api_request'; import { paths } from '../../../../common/locators/paths'; import { useCreateSlo } from '../../../hooks/slo/use_create_slo'; @@ -32,6 +31,7 @@ import { SloEditFormObjectiveSection } from './slo_edit_form_objective_section'; import { useCreateRule } from '../../../hooks/use_create_rule'; import { createBurnRateRuleRequestBody } from '../helpers/create_burn_rate_rule_request_body'; import { BurnRateRuleParams } from '../../../typings'; +import { SLOInspectWrapper } from './common/slo_inspect'; export interface Props { slo?: GetSLOResponse; @@ -165,12 +165,13 @@ export function SloEditForm({ slo }: Props) { </EuiButtonEmpty> <EquivalentApiRequest - isCreateSloLoading={isCreateSloLoading} - isUpdateSloLoading={isUpdateSloLoading} + slo={slo} + disabled={isCreateSloLoading || isUpdateSloLoading} + isEditMode={isEditMode} /> + <SLOInspectWrapper slo={slo} disabled={isCreateSloLoading || isUpdateSloLoading} /> </EuiFlexGroup> </EuiFlexGroup> - <InspectSLOPortal trigger={trigger} getValues={getValues} slo={slo} /> </FormProvider> </> ); diff --git a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx index f14f1d7a1446e..09eafbfdcc8c7 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx @@ -5,23 +5,19 @@ * 2.0. */ -import React from 'react'; -import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; - -import { createHtmlPortalNode, OutPortal } from 'react-reverse-portal'; +import React from 'react'; +import { useParams } from 'react-router-dom'; import { paths } from '../../../common/locators/paths'; -import { useKibana } from '../../utils/kibana_react'; -import { usePluginContext } from '../../hooks/use_plugin_context'; -import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details'; -import { useLicense } from '../../hooks/use_license'; import { useCapabilities } from '../../hooks/slo/use_capabilities'; import { useFetchSloGlobalDiagnosis } from '../../hooks/slo/use_fetch_global_diagnosis'; -import { SloEditForm } from './components/slo_edit_form'; +import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details'; +import { useLicense } from '../../hooks/use_license'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { useKibana } from '../../utils/kibana_react'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; - -export const InspectSLOPortalNode = createHtmlPortalNode(); +import { SloEditForm } from './components/slo_edit_form'; export function SloEditPage() { const { @@ -78,7 +74,6 @@ export function SloEditPage() { : i18n.translate('xpack.observability.sloCreatePageTitle', { defaultMessage: 'Create new SLO', }), - rightSideItems: [<OutPortal node={InspectSLOPortalNode} />], bottomBorder: false, }} data-test-subj="slosEditPage" diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx index 19617273659b1..66a8ea84fcce8 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx @@ -76,7 +76,7 @@ export function SloList() { itemsPerPage={perPage} itemsPerPageOptions={[10, 25, 50, 100]} onChangeItemsPerPage={(newPerPage) => { - storeState({ perPage: newPerPage, page: 0 }); + onStateChange({ perPage: newPerPage }); }} /> </EuiFlexItem> diff --git a/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts b/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts index 1915778fc2c4b..67c84960722a9 100644 --- a/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts +++ b/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts @@ -14,8 +14,8 @@ import { ALERT_START, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, } from '@kbn/rule-data-utils'; -import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import type { LocatorPublic } from '@kbn/share-plugin/common'; +import { LogsExplorerLocatorParams } from '@kbn/deeplinks-observability'; import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; import type { MetricExpression } from '../components/custom_threshold/types'; import type { @@ -91,7 +91,7 @@ const getDataViewId = (searchConfiguration?: SerializedSearchSourceFields) => export const registerObservabilityRuleTypes = async ( observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry, uiSettings: IUiSettingsClient, - logsExplorerLocator?: LocatorPublic<DiscoverAppLocatorParams> + logsExplorerLocator?: LocatorPublic<LogsExplorerLocatorParams> ) => { observabilityRuleTypeRegistry.register({ id: SLO_BURN_RATE_RULE_TYPE_ID, diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts index 9e3eab1e8a054..51fc7e7227cdf 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts @@ -75,11 +75,13 @@ const mockOptions = { previousStartedAt: null, params: { searchConfiguration: { + index: {}, query: { query: mockQuery, language: 'kuery', }, }, + alertOnNoData: true, }, state: { wrapped: initialRuleState, @@ -573,6 +575,7 @@ describe('The custom threshold alert type', () => { }, ], searchConfiguration: { + index: {}, query: { query: filterQuery, language: 'kuery', diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts index 39dbade62ce62..b59c3f532daea 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts @@ -10,6 +10,7 @@ import { LogsExplorerLocatorParams } from '@kbn/deeplinks-observability'; import { ALERT_ACTION_GROUP, ALERT_EVALUATION_VALUES, + ALERT_EVALUATION_THRESHOLD, ALERT_REASON, ALERT_GROUP, } from '@kbn/rule-data-utils'; @@ -17,13 +18,14 @@ import { LocatorPublic } from '@kbn/share-plugin/common'; import { RecoveredActionGroup } from '@kbn/alerting-plugin/common'; import { IBasePath, Logger } from '@kbn/core/server'; import { LifecycleRuleExecutor } from '@kbn/rule-registry-plugin/server'; +import { getEvaluationValues, getThreshold } from './lib/get_values'; import { AlertsLocatorParams, getAlertUrl } from '../../../../common'; import { getViewInAppUrl } from '../../../../common/custom_threshold_rule/get_view_in_app_url'; import { ObservabilityConfig } from '../../..'; import { FIRED_ACTIONS_ID, NO_DATA_ACTIONS_ID, UNGROUPED_FACTORY_KEY } from './constants'; import { AlertStates, - CustomThresholdRuleParams, + CustomThresholdRuleTypeParams, CustomThresholdRuleTypeState, CustomThresholdAlertState, CustomThresholdAlertContext, @@ -66,7 +68,7 @@ export const createCustomThresholdExecutor = ({ config: ObservabilityConfig; locators: CustomThresholdLocators; }): LifecycleRuleExecutor< - CustomThresholdRuleParams, + CustomThresholdRuleTypeParams, CustomThresholdRuleTypeState, CustomThresholdAlertState, CustomThresholdAlertContext, @@ -108,6 +110,7 @@ export const createCustomThresholdExecutor = ({ actionGroup, additionalContext, evaluationValues, + threshold, group ) => alertWithLifecycle({ @@ -116,6 +119,7 @@ export const createCustomThresholdExecutor = ({ [ALERT_REASON]: reason, [ALERT_ACTION_GROUP]: actionGroup, [ALERT_EVALUATION_VALUES]: evaluationValues, + [ALERT_EVALUATION_THRESHOLD]: threshold, [ALERT_GROUP]: group, ...flattenAdditionalContext(additionalContext), }, @@ -139,7 +143,7 @@ export const createCustomThresholdExecutor = ({ ? state.missingGroups : []; - const initialSearchSource = await searchSourceClient.create(params.searchConfiguration!); + const initialSearchSource = await searchSourceClient.create(params.searchConfiguration); const dataView = initialSearchSource.getField('index')!; const { id: dataViewId, timeFieldName } = dataView; const dataViewIndexPattern = dataView.getIndexPattern(); @@ -241,6 +245,8 @@ export const createCustomThresholdExecutor = ({ if (reason) { const timestamp = startedAt.toISOString(); + const threshold = getThreshold(criteria); + const evaluationValues = getEvaluationValues(alertResults, group); const actionGroupId: CustomThresholdActionGroup = nextState === AlertStates.OK ? RecoveredActionGroup.id @@ -258,19 +264,13 @@ export const createCustomThresholdExecutor = ({ new Set([...(additionalContext.tags ?? []), ...options.rule.tags]) ); - const evaluationValues = alertResults.reduce((acc: Array<number | null>, result) => { - if (result[group]) { - acc.push(result[group].currentValue); - } - return acc; - }, []); - const alert = alertFactory( `${group}`, reason, actionGroupId, additionalContext, evaluationValues, + threshold, groupByKeysObjectMapping[group] ); const alertUuid = getAlertUuid(group); diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/check_missing_group.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/check_missing_group.ts index c45120cc62fa3..45bb92426eca8 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/check_missing_group.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/check_missing_group.ts @@ -6,11 +6,13 @@ */ import { ElasticsearchClient } from '@kbn/core/server'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { Logger } from '@kbn/logging'; import { isString, get, identity } from 'lodash'; +import { SearchConfigurationType } from '../types'; import { CustomMetricExpressionParams } from '../../../../../common/custom_threshold_rule/types'; import type { BucketKey } from './get_data'; -import { calculateCurrentTimeFrame, createBaseFilters } from './metric_query'; +import { calculateCurrentTimeFrame, createBoolQuery } from './metric_query'; export interface MissingGroupsRecord { key: string; @@ -23,7 +25,7 @@ export const checkMissingGroups = async ( indexPattern: string, timeFieldName: string, groupBy: string | undefined | string[], - filterQuery: string | undefined, + searchConfiguration: SearchConfigurationType, logger: Logger, timeframe: { start: number; end: number }, missingGroups: MissingGroupsRecord[] = [] @@ -32,28 +34,31 @@ export const checkMissingGroups = async ( return missingGroups; } const currentTimeFrame = calculateCurrentTimeFrame(metricParams, timeframe); - const baseFilters = createBaseFilters(currentTimeFrame, timeFieldName, filterQuery); const groupByFields = isString(groupBy) ? [groupBy] : groupBy ? groupBy : []; const searches = missingGroups.flatMap((group) => { - const groupByFilters = Object.values(group.bucketKey).map((key, index) => { - return { - match: { - [groupByFields[index]]: key, - }, - }; - }); + const groupByQueries: QueryDslQueryContainer[] = Object.values(group.bucketKey).map( + (key, index) => { + return { + match: { + [groupByFields[index]]: key, + }, + }; + } + ); + const query = createBoolQuery( + currentTimeFrame, + timeFieldName, + searchConfiguration, + groupByQueries + ); return [ { index: indexPattern }, { size: 0, terminate_after: 1, track_total_hits: true, - query: { - bool: { - filter: [...baseFilters, ...groupByFilters], - }, - }, + query, }, ]; }); diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts index 59f5801613dd0..87b7d9983465b 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts @@ -69,7 +69,7 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate dataView, timeFieldName, groupBy, - searchConfiguration.query.query, + searchConfiguration, compositeSize, alertOnGroupDisappear, calculatedTimerange, @@ -83,7 +83,7 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate dataView, timeFieldName, groupBy, - searchConfiguration.query.query, + searchConfiguration, logger, calculatedTimerange, missingGroups diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_data.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_data.ts index 9658e0876ca1e..1e488ffc57060 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_data.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_data.ts @@ -9,6 +9,7 @@ import { SearchResponse, AggregationsAggregate } from '@elastic/elasticsearch/li import { ElasticsearchClient } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy'; +import { SearchConfigurationType } from '../types'; import { CustomMetricExpressionParams } from '../../../../../common/custom_threshold_rule/types'; import { UNGROUPED_FACTORY_KEY } from '../constants'; @@ -100,7 +101,7 @@ export const getData = async ( index: string, timeFieldName: string, groupBy: string | undefined | string[], - filterQuery: string | undefined, + searchConfiguration: SearchConfigurationType, compositeSize: number, alertOnGroupDisappear: boolean, timeframe: { start: number; end: number }, @@ -159,7 +160,7 @@ export const getData = async ( index, timeFieldName, groupBy, - filterQuery, + searchConfiguration, compositeSize, alertOnGroupDisappear, timeframe, @@ -202,9 +203,9 @@ export const getData = async ( timeFieldName, compositeSize, alertOnGroupDisappear, + searchConfiguration, lastPeriodEnd, groupBy, - filterQuery, afterKey, fieldsExisted ), diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.test.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.test.ts new file mode 100644 index 0000000000000..ddf2b7b1b1a45 --- /dev/null +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { alertResultsMultipleConditions } from '../mocks/custom_threshold_alert_result'; +import { criteriaMultipleConditions } from '../mocks/custom_threshold_metric_params'; +import { getEvaluationValues, getThreshold } from './get_values'; + +describe('getValue helpers', () => { + describe('getThreshold', () => { + test('should return threshold for one condition', () => { + expect(getThreshold([criteriaMultipleConditions[1]])).toEqual([4, 5]); + }); + + test('should return threshold for multiple conditions', () => { + expect(getThreshold(criteriaMultipleConditions)).toEqual([1, 2, 4, 5]); + }); + }); + + describe('getEvaluationValues', () => { + test('should return evaluation values ', () => { + expect(getEvaluationValues(alertResultsMultipleConditions, '*')).toEqual([1.0, 3.0]); + }); + }); +}); diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.ts new file mode 100644 index 0000000000000..13c0cabb9b590 --- /dev/null +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/get_values.ts @@ -0,0 +1,29 @@ +/* + * 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 { CustomMetricExpressionParams } from '../../../../../common/custom_threshold_rule/types'; +import { Evaluation } from './evaluate_rule'; + +export const getEvaluationValues = ( + alertResults: Array<Record<string, Evaluation>>, + group: string +): Array<number | null> => { + return alertResults.reduce((acc: Array<number | null>, result) => { + if (result[group]) { + acc.push(result[group].currentValue); + } + return acc; + }, []); +}; + +export const getThreshold = (criteria: CustomMetricExpressionParams[]): number[] => { + const threshold = criteria.map((c) => c.threshold); + + return threshold.reduce((acc: number[], t) => { + return acc.concat(...t); + }, []); +}; diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.test.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.test.ts index 01d94dd5f7d67..c12f14a7aeb5b 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.test.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.test.ts @@ -11,6 +11,7 @@ import { Aggregators, CustomMetricExpressionParams, } from '../../../../../common/custom_threshold_rule/types'; +import { SearchConfigurationType } from '../types'; import { getElasticsearchMetricQuery } from './metric_query'; describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { @@ -27,6 +28,18 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { threshold: [1], comparator: Comparator.GT, }; + const searchConfiguration: SearchConfigurationType = { + index: { + id: 'dataset-logs-*-*', + name: 'All logs', + timeFieldName: '@timestamp', + title: 'logs-*-*', + }, + query: { + language: 'kuery', + query: '', + }, + }; const groupBy = 'host.doggoname'; const timeFieldName = 'mockedTimeFieldName'; @@ -35,13 +48,14 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { end: moment().valueOf(), }; - describe('when passed no filterQuery', () => { + describe('when passed no KQL query', () => { const searchBody = getElasticsearchMetricQuery( expressionParams, timeframe, timeFieldName, 100, true, + searchConfiguration, void 0, groupBy ); @@ -78,11 +92,18 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { }); }); - describe('when passed a filterQuery', () => { + describe('when passed a KQL query', () => { // This is adapted from a real-world query that previously broke alerts // We want to make sure it doesn't override any existing filters // https://github.com/elastic/kibana/issues/68492 - const filterQuery = 'NOT host.name:dv* and NOT host.name:ts*'; + const query = 'NOT host.name:dv* and NOT host.name:ts*'; + const currentSearchConfiguration = { + ...searchConfiguration, + query: { + language: 'kuery', + query, + }, + }; const searchBody = getElasticsearchMetricQuery( expressionParams, @@ -90,9 +111,9 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { timeFieldName, 100, true, + currentSearchConfiguration, void 0, - groupBy, - filterQuery + groupBy ); test('includes a range filter', () => { expect( @@ -164,4 +185,60 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { ); }); }); + + describe('when passed a filter', () => { + const currentSearchConfiguration = { + ...searchConfiguration, + query: { + language: 'kuery', + query: '', + }, + filter: [ + { + meta: { + alias: null, + disabled: false, + field: 'service.name', + key: 'service.name', + negate: false, + params: { + query: 'synth-node-2', + }, + type: 'phrase', + index: 'dataset-logs-*-*', + }, + query: { + match_phrase: { + 'service.name': 'synth-node-2', + }, + }, + }, + ], + }; + + const searchBody = getElasticsearchMetricQuery( + expressionParams, + timeframe, + timeFieldName, + 100, + true, + currentSearchConfiguration, + void 0, + groupBy + ); + test('includes a range filter', () => { + expect( + searchBody.query.bool.filter.find((filter) => filter.hasOwnProperty('range')) + ).toBeTruthy(); + }); + + test('includes a metric field filter', () => { + expect(searchBody.query.bool.filter).toMatchObject( + expect.arrayContaining([ + { range: { mockedTimeFieldName: expect.any(Object) } }, + { match_phrase: { 'service.name': 'synth-node-2' } }, + ]) + ); + }); + }); }); diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.ts index 3cc1eee92fec9..14c18e4af1334 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.ts @@ -6,10 +6,14 @@ */ import moment from 'moment'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { Filter } from '@kbn/es-query'; import { Aggregators, CustomMetricExpressionParams, } from '../../../../../common/custom_threshold_rule/types'; +import { getSearchConfigurationBoolQuery } from '../../../../utils/get_parsed_filtered_query'; +import { SearchConfigurationType } from '../types'; import { createCustomMetricsAggregations } from './create_custom_metrics_aggregations'; import { CONTAINER_ID, @@ -20,7 +24,6 @@ import { } from '../utils'; import { createBucketSelector } from './create_bucket_selector'; import { wrapInCurrentPeriod } from './wrap_in_period'; -import { getParsedFilterQuery } from '../../../../utils/get_parsed_filtered_query'; export const calculateCurrentTimeFrame = ( metricParams: CustomMetricExpressionParams, @@ -38,25 +41,30 @@ export const calculateCurrentTimeFrame = ( }; }; -export const createBaseFilters = ( +const QueryDslQueryContainerToFilter = (queries: QueryDslQueryContainer[]): Filter[] => { + return queries.map((query) => ({ + meta: {}, + query, + })); +}; + +export const createBoolQuery = ( timeframe: { start: number; end: number }, timeFieldName: string, - filterQuery?: string + searchConfiguration: SearchConfigurationType, + additionalQueries: QueryDslQueryContainer[] = [] ) => { - const rangeFilters = [ - { - range: { - [timeFieldName]: { - gte: moment(timeframe.start).toISOString(), - lte: moment(timeframe.end).toISOString(), - }, + const rangeQuery: QueryDslQueryContainer = { + range: { + [timeFieldName]: { + gte: moment(timeframe.start).toISOString(), + lte: moment(timeframe.end).toISOString(), }, }, - ]; - - const parsedFilterQuery = getParsedFilterQuery(filterQuery); + }; + const filters = QueryDslQueryContainerToFilter([rangeQuery, ...additionalQueries]); - return [...rangeFilters, ...parsedFilterQuery]; + return getSearchConfigurationBoolQuery(searchConfiguration, filters); }; export const getElasticsearchMetricQuery = ( @@ -65,9 +73,9 @@ export const getElasticsearchMetricQuery = ( timeFieldName: string, compositeSize: number, alertOnGroupDisappear: boolean, + searchConfiguration: SearchConfigurationType, lastPeriodEnd?: number, groupBy?: string | string[], - filterQuery?: string, afterKey?: Record<string, string>, fieldsExisted?: Record<string, boolean> | null ) => { @@ -196,15 +204,11 @@ export const getElasticsearchMetricQuery = ( aggs.groupings.composite.after = afterKey; } - const baseFilters = createBaseFilters(timeframe, timeFieldName, filterQuery); + const query = createBoolQuery(timeframe, timeFieldName, searchConfiguration); return { track_total_hits: true, - query: { - bool: { - filter: baseFilters, - }, - }, + query, size: 0, aggs, }; diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_alert_result.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_alert_result.ts new file mode 100644 index 0000000000000..b8ab169b7a90d --- /dev/null +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_alert_result.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + Aggregators, + Comparator, + CustomMetricExpressionParams, +} from '../../../../../common/custom_threshold_rule/types'; +import { Evaluation } from '../lib/evaluate_rule'; + +const customThresholdNonCountCriterion: CustomMetricExpressionParams = { + comparator: Comparator.GT, + metrics: [ + { + aggType: Aggregators.AVERAGE, + name: 'A', + field: 'test.metric.1', + }, + ], + timeSize: 1, + timeUnit: 'm', + threshold: [0], +}; + +export const alertResultsMultipleConditions: Array<Record<string, Evaluation>> = [ + { + '*': { + ...customThresholdNonCountCriterion, + comparator: Comparator.GT, + threshold: [0.75], + currentValue: 1.0, + timestamp: new Date().toISOString(), + shouldFire: true, + isNoData: false, + bucketKey: { groupBy0: '*' }, + }, + }, + { + '*': { + ...customThresholdNonCountCriterion, + comparator: Comparator.GT, + threshold: [0.75], + currentValue: 3.0, + timestamp: new Date().toISOString(), + shouldFire: true, + isNoData: false, + bucketKey: { groupBy0: '*' }, + }, + }, +]; diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_metric_params.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_metric_params.ts new file mode 100644 index 0000000000000..9d09c90859659 --- /dev/null +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/mocks/custom_threshold_metric_params.ts @@ -0,0 +1,51 @@ +/* + * 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 { + Aggregators, + Comparator, + CustomMetricExpressionParams, +} from '../../../../../common/custom_threshold_rule/types'; + +export const criteriaMultipleConditions: CustomMetricExpressionParams[] = [ + { + metrics: [ + { + name: 'A', + aggType: Aggregators.AVERAGE, + field: 'system.is.a.good.puppy.dog', + }, + { + name: 'B', + aggType: Aggregators.AVERAGE, + field: 'system.is.a.bad.kitty', + }, + ], + timeUnit: 'm', + timeSize: 1, + threshold: [1, 2], + comparator: Comparator.GT, + }, + { + metrics: [ + { + name: 'A', + aggType: Aggregators.AVERAGE, + field: 'system.is.a.good.puppy.dog', + }, + { + name: 'B', + aggType: Aggregators.AVERAGE, + field: 'system.is.a.bad.kitty', + }, + ], + timeUnit: 'm', + timeSize: 1, + threshold: [4, 5], + comparator: Comparator.GT, + }, +]; diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts index 5e9c2e0cea019..df64a67ca8e4a 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts @@ -58,6 +58,14 @@ export const searchConfigurationSchema = schema.object({ }), query: schema.string(), }), + filter: schema.maybe( + schema.arrayOf( + schema.object({ + query: schema.maybe(schema.recordOf(schema.string(), schema.any())), + meta: schema.recordOf(schema.string(), schema.any()), + }) + ) + ), }); type CreateLifecycleExecutor = ReturnType<typeof createLifecycleExecutor>; diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/types.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/types.ts index 97881f55d8d32..f688088d6b816 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/types.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/types.ts @@ -14,6 +14,8 @@ import { } from '@kbn/alerting-plugin/common'; import { Alert } from '@kbn/alerting-plugin/server'; import { TypeOf } from '@kbn/config-schema'; +import { DataViewSpec } from '@kbn/data-views-plugin/common'; +import { CustomMetricExpressionParams } from '../../../../common/custom_threshold_rule/types'; import { FIRED_ACTIONS_ID, NO_DATA_ACTIONS_ID, FIRED_ACTION, NO_DATA_ACTION } from './constants'; import { MissingGroupsRecord } from './lib/check_missing_group'; import { AdditionalContext } from './utils'; @@ -28,13 +30,22 @@ export enum AlertStates { // Executor types export type SearchConfigurationType = TypeOf<typeof searchConfigurationSchema>; +export type RuleTypeParams = Record<string, unknown>; + +export interface CustomThresholdRuleTypeParams extends RuleTypeParams { + criteria: CustomMetricExpressionParams[]; + // Index will be a data view spec after extracting references + searchConfiguration: Omit<SearchConfigurationType, 'index'> & { index: DataViewSpec }; + groupBy?: string | string[]; + alertOnNoData: boolean; + alertOnGroupDisappear?: boolean; +} -export type CustomThresholdRuleParams = Record<string, any>; export type CustomThresholdRuleTypeState = RuleTypeState & { lastRunTimestamp?: number; missingGroups?: Array<string | MissingGroupsRecord>; groupBy?: string | string[]; - searchConfiguration?: SearchConfigurationType; + searchConfiguration?: Omit<SearchConfigurationType, 'index'> & { index: DataViewSpec }; }; export type CustomThresholdAlertState = AlertState; // no specific instance state used export type CustomThresholdAlertContext = AlertContext & { @@ -64,6 +75,7 @@ export type CustomThresholdAlertFactory = ( actionGroup: CustomThresholdActionGroup, additionalContext?: AdditionalContext | null, evaluationValues?: Array<number | null>, + threshold?: Array<number | null>, group?: Group ) => CustomThresholdAlert; diff --git a/x-pack/plugins/observability/server/utils/get_parsed_filtered_query.ts b/x-pack/plugins/observability/server/utils/get_parsed_filtered_query.ts index fabefa63f0695..033a6cadc282e 100644 --- a/x-pack/plugins/observability/server/utils/get_parsed_filtered_query.ts +++ b/x-pack/plugins/observability/server/utils/get_parsed_filtered_query.ts @@ -5,7 +5,14 @@ * 2.0. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { + BoolQuery, + buildEsQuery, + Filter, + fromKueryExpression, + toElasticsearchQuery, +} from '@kbn/es-query'; +import { SearchConfigurationType } from '../lib/rules/custom_threshold/types'; export const getParsedFilterQuery: (filter: string | undefined) => Array<Record<string, any>> = ( filter @@ -19,3 +26,24 @@ export const getParsedFilterQuery: (filter: string | undefined) => Array<Record< return []; } }; + +export const getSearchConfigurationBoolQuery: ( + searchConfiguration: SearchConfigurationType, + additionalFilters: Filter[] +) => { bool: BoolQuery } = (searchConfiguration, additionalFilters) => { + try { + const searchConfigurationFilters = (searchConfiguration.filter as Filter[]) || []; + const filters = [...additionalFilters, ...searchConfigurationFilters]; + + return buildEsQuery(undefined, searchConfiguration.query, filters, {}); + } catch (error) { + return { + bool: { + must: [], + must_not: [], + filter: [], + should: [], + }, + }; + } +}; diff --git a/x-pack/plugins/observability_ai_assistant/common/connectors.ts b/x-pack/plugins/observability_ai_assistant/common/connectors.ts new file mode 100644 index 0000000000000..2b834081a7ac9 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/common/connectors.ts @@ -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. + */ + +export enum ObservabilityAIAssistantConnectorType { + Bedrock = '.bedrock', + OpenAI = '.gen-ai', +} + +export const SUPPORTED_CONNECTOR_TYPES = [ + ObservabilityAIAssistantConnectorType.OpenAI, + ObservabilityAIAssistantConnectorType.Bedrock, +]; + +export function isSupportedConnectorType( + type: string +): type is ObservabilityAIAssistantConnectorType { + return ( + type === ObservabilityAIAssistantConnectorType.Bedrock || + type === ObservabilityAIAssistantConnectorType.OpenAI + ); +} 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 f5fe0d37408c2..b082478bba100 100644 --- a/x-pack/plugins/observability_ai_assistant/common/conversation_complete.ts +++ b/x-pack/plugins/observability_ai_assistant/common/conversation_complete.ts @@ -14,6 +14,7 @@ export enum StreamingChatResponseEventType { ConversationUpdate = 'conversationUpdate', MessageAdd = 'messageAdd', ChatCompletionError = 'chatCompletionError', + BufferFlush = 'bufferFlush', } type StreamingChatResponseEventBase< @@ -76,6 +77,13 @@ export type ChatCompletionErrorEvent = StreamingChatResponseEventBase< } >; +export type BufferFlushEvent = StreamingChatResponseEventBase< + StreamingChatResponseEventType.BufferFlush, + { + data?: string; + } +>; + export type StreamingChatResponseEvent = | ChatCompletionChunkEvent | ConversationCreateEvent @@ -129,7 +137,14 @@ export function createConversationNotFoundError() { ); } -export function createInternalServerError(originalErrorMessage: string) { +export function createInternalServerError( + originalErrorMessage: string = i18n.translate( + 'xpack.observabilityAiAssistant.chatCompletionError.internalServerError', + { + defaultMessage: 'An internal server error occurred', + } + ) +) { return new ChatCompletionError(ChatCompletionErrorCode.InternalError, originalErrorMessage); } diff --git a/x-pack/plugins/observability_ai_assistant/common/utils/process_openai_stream.ts b/x-pack/plugins/observability_ai_assistant/common/utils/process_openai_stream.ts index 2487fca287cc7..8b6ef27ee8ebd 100644 --- a/x-pack/plugins/observability_ai_assistant/common/utils/process_openai_stream.ts +++ b/x-pack/plugins/observability_ai_assistant/common/utils/process_openai_stream.ts @@ -19,7 +19,6 @@ export function processOpenAiStream() { const id = v4(); return source.pipe( - map((line) => line.substring(6)), filter((line) => !!line && line !== '[DONE]'), map( (line) => diff --git a/x-pack/plugins/observability_ai_assistant/kibana.jsonc b/x-pack/plugins/observability_ai_assistant/kibana.jsonc index 3f346cccff0c1..a3eaad0d216a3 100644 --- a/x-pack/plugins/observability_ai_assistant/kibana.jsonc +++ b/x-pack/plugins/observability_ai_assistant/kibana.jsonc @@ -25,7 +25,9 @@ "ml" ], "requiredBundles": [ "kibanaReact", "kibanaUtils"], - "optionalPlugins": [], + "optionalPlugins": [ + "cloud" + ], "extraPublicDirs": [] } } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx index f11f2c8b56bc6..c357d9d22e8f6 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ObservabilityAIAssistantChatServiceProvider } from '../../context/observability_ai_assistant_chat_service_provider'; @@ -30,6 +30,20 @@ export function ObservabilityAIAssistantActionMenuItem() { const initialMessages = useMemo(() => [], []); + useEffect(() => { + const keyboardListener = (event: KeyboardEvent) => { + if (event.ctrlKey && event.code === 'Semicolon') { + setIsOpen(true); + } + }; + + window.addEventListener('keypress', keyboardListener); + + return () => { + window.removeEventListener('keypress', keyboardListener); + }; + }, []); + if (!service.isEnabled()) { return null; } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx index 4186f2c70c04d..f9635f5808072 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover } from '@elastic/eui'; +import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover, EuiToolTip } from '@elastic/eui'; import { useKibana } from '../../hooks/use_kibana'; import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; import { getSettingsHref } from '../../utils/get_settings_href'; @@ -52,16 +52,24 @@ export function ChatActionsMenu({ <EuiPopover isOpen={isOpen} button={ - <EuiButtonIcon - data-test-subj="observabilityAiAssistantChatActionsMenuButtonIcon" - disabled={disabled} - iconType="boxesVertical" - onClick={toggleActionsMenu} - aria-label={i18n.translate( - 'xpack.observabilityAiAssistant.chatActionsMenu.euiButtonIcon.menuLabel', - { defaultMessage: 'Menu' } + <EuiToolTip + content={i18n.translate( + 'xpack.observabilityAiAssistant.chatActionsMenu.euiToolTip.moreActionsLabel', + { defaultMessage: 'More actions' } )} - /> + display="block" + > + <EuiButtonIcon + data-test-subj="observabilityAiAssistantChatActionsMenuButtonIcon" + disabled={disabled} + iconType="boxesVertical" + onClick={toggleActionsMenu} + aria-label={i18n.translate( + 'xpack.observabilityAiAssistant.chatActionsMenu.euiButtonIcon.menuLabel', + { defaultMessage: 'Menu' } + )} + /> + </EuiToolTip> } panelPaddingSize="none" closePopover={toggleActionsMenu} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx index 373e35641ff8f..a89419c366a2d 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx @@ -35,14 +35,11 @@ import { ChatTimeline } from './chat_timeline'; import { Feedback } from '../feedback_buttons'; import { IncorrectLicensePanel } from './incorrect_license_panel'; import { WelcomeMessage } from './welcome_message'; -import { - ChatActionClickHandler, - ChatActionClickType, - type ChatFlyoutSecondSlotHandler, -} from './types'; +import { ChatActionClickHandler, ChatActionClickType } from './types'; import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../../i18n'; import type { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; import { TELEMETRY, sendEvent } from '../../analytics'; +import { FlyoutWidthMode } from './chat_flyout'; const fullHeightClassName = css` height: 100%; @@ -93,27 +90,29 @@ const animClassName = css` const PADDING_AND_BORDER = 32; export function ChatBody({ - initialTitle, - initialMessages, - initialConversationId, connectors, - knowledgeBase, currentUser, + flyoutWidthMode, + initialConversationId, + initialMessages, + initialTitle, + knowledgeBase, showLinkToConversationsApp, startedFrom, - chatFlyoutSecondSlotHandler, onConversationUpdate, + onToggleFlyoutWidthMode, }: { + connectors: UseGenAIConnectorsResult; + currentUser?: Pick<AuthenticatedUser, 'full_name' | 'username'>; + flyoutWidthMode?: FlyoutWidthMode; initialTitle?: string; initialMessages?: Message[]; initialConversationId?: string; - connectors: UseGenAIConnectorsResult; knowledgeBase: UseKnowledgeBaseResult; - currentUser?: Pick<AuthenticatedUser, 'full_name' | 'username'>; showLinkToConversationsApp: boolean; startedFrom?: StartedFrom; - chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void; + onToggleFlyoutWidthMode?: (flyoutWidthMode: FlyoutWidthMode) => void; }) { const license = useLicense(); const hasCorrectLicense = license?.hasAtLeast('enterprise'); @@ -357,7 +356,6 @@ export function ChatBody({ onStopGenerating={() => { stop(); }} - chatFlyoutSecondSlotHandler={chatFlyoutSecondSlotHandler} onActionClick={handleActionClick} /> )} @@ -455,6 +453,7 @@ export function ChatBody({ ? conversation.value.conversation.id : undefined } + flyoutWidthMode={flyoutWidthMode} licenseInvalid={!hasCorrectLicense && !initialConversationId} loading={isLoading} showLinkToConversationsApp={showLinkToConversationsApp} @@ -463,6 +462,7 @@ export function ChatBody({ onSaveTitle={(newTitle) => { saveTitle(newTitle); }} + onToggleFlyoutWidthMode={onToggleFlyoutWidthMode} /> </EuiFlexItem> <EuiFlexItem grow={false}> diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx index 83c1496befa4f..5a8b0ee3b3776 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx @@ -9,7 +9,16 @@ import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; import { v4 } from 'uuid'; import { css } from '@emotion/css'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFlyout, useEuiTheme } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiPopover, + EuiToolTip, + useEuiTheme, +} from '@elastic/eui'; +import { ObservabilityAIAssistantMultipaneFlyoutProvider } from '../../context/observability_ai_assistant_multipane_flyout_provider'; import { useForceUpdate } from '../../hooks/use_force_update'; import { useCurrentUser } from '../../hooks/use_current_user'; import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; @@ -25,6 +34,8 @@ const CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED = 34; const SIDEBAR_WIDTH = 400; +export type FlyoutWidthMode = 'side' | 'full'; + export function ChatFlyout({ initialTitle, initialMessages, @@ -48,26 +59,36 @@ export function ChatFlyout({ const [conversationId, setConversationId] = useState<string | undefined>(undefined); - const [expanded, setExpanded] = useState(false); + const [flyoutWidthMode, setFlyoutWidthMode] = useState<FlyoutWidthMode>('side'); + + const [conversationsExpanded, setConversationsExpanded] = useState(false); + const [secondSlotContainer, setSecondSlotContainer] = useState<HTMLDivElement | null>(null); const [isSecondSlotVisible, setIsSecondSlotVisible] = useState(false); const sidebarClass = css` - max-width: ${expanded ? CONVERSATIONS_SIDEBAR_WIDTH : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px; - min-width: ${expanded ? CONVERSATIONS_SIDEBAR_WIDTH : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px; + max-width: ${conversationsExpanded + ? CONVERSATIONS_SIDEBAR_WIDTH + : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px; + min-width: ${conversationsExpanded + ? CONVERSATIONS_SIDEBAR_WIDTH + : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px; border-right: solid 1px ${euiTheme.border.color}; `; - const expandButtonClassName = css` + const expandButtonContainerClassName = css` position: absolute; margin-top: 16px; - margin-left: ${expanded + margin-left: ${conversationsExpanded ? CONVERSATIONS_SIDEBAR_WIDTH - CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED : 5}px; - padding: ${euiTheme.size.s}; z-index: 1; `; + const expandButtonClassName = css` + color: ${euiTheme.colors.primary}; + `; + const containerClassName = css` height: 100%; `; @@ -79,10 +100,9 @@ export function ChatFlyout({ const newChatButtonClassName = css` position: absolute; bottom: 31px; - margin-left: ${expanded + margin-left: ${conversationsExpanded ? CONVERSATIONS_SIDEBAR_WIDTH - CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED : 5}px; - padding: ${euiTheme.size.s}; z-index: 1; `; @@ -110,104 +130,156 @@ export function ChatFlyout({ } }; + const handleToggleFlyoutWidthMode = (newFlyoutWidthMode: FlyoutWidthMode) => { + setFlyoutWidthMode(newFlyoutWidthMode); + }; + return isOpen ? ( - <EuiFlyout - closeButtonProps={{ - css: { marginRight: `${euiTheme.size.s}`, marginTop: `${euiTheme.size.s}` }, - }} - size={getFlyoutWidth({ expanded, isSecondSlotVisible })} - paddingSize="m" - onClose={() => { - onClose(); - setIsSecondSlotVisible(false); - if (secondSlotContainer) { - ReactDOM.unmountComponentAtNode(secondSlotContainer); - } + <ObservabilityAIAssistantMultipaneFlyoutProvider + value={{ + container: secondSlotContainer, + setVisibility: setIsSecondSlotVisible, }} > - <EuiFlexGroup gutterSize="none" className={containerClassName}> - <EuiFlexItem className={sidebarClass}> - <EuiButtonIcon - aria-label={i18n.translate( - 'xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel', - { defaultMessage: 'Expand conversation list' } - )} - className={expandButtonClassName} - color="text" - data-test-subj="observabilityAiAssistantChatFlyoutButton" - iconType={expanded ? 'transitionLeftIn' : 'transitionLeftOut'} - onClick={() => setExpanded(!expanded)} - /> - - {expanded ? ( - <ConversationList - selected={conversationId ?? ''} - onClickDeleteConversation={handleClickDeleteConversation} - onClickChat={handleClickChat} - onClickNewChat={handleClickNewChat} + <EuiFlyout + closeButtonProps={{ + css: { marginRight: `${euiTheme.size.s}`, marginTop: `${euiTheme.size.s}` }, + }} + size={getFlyoutWidth({ + expanded: conversationsExpanded, + isSecondSlotVisible, + flyoutWidthMode, + })} + paddingSize="m" + onClose={() => { + onClose(); + setIsSecondSlotVisible(false); + if (secondSlotContainer) { + ReactDOM.unmountComponentAtNode(secondSlotContainer); + } + }} + > + <EuiFlexGroup gutterSize="none" className={containerClassName}> + <EuiFlexItem className={sidebarClass}> + <EuiPopover + anchorPosition="downLeft" + className={expandButtonContainerClassName} + button={ + <EuiToolTip + content={ + conversationsExpanded + ? i18n.translate( + 'xpack.observabilityAiAssistant.chatFlyout.euiToolTip.collapseConversationListLabel', + { defaultMessage: 'Collapse conversation list' } + ) + : i18n.translate( + 'xpack.observabilityAiAssistant.chatFlyout.euiToolTip.expandConversationListLabel', + { defaultMessage: 'Expand conversation list' } + ) + } + display="block" + > + <EuiButtonIcon + aria-label={i18n.translate( + 'xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.expandConversationListLabel', + { defaultMessage: 'Expand conversation list' } + )} + className={expandButtonClassName} + color="text" + data-test-subj="observabilityAiAssistantChatFlyoutButton" + iconType={conversationsExpanded ? 'transitionLeftIn' : 'transitionLeftOut'} + onClick={() => setConversationsExpanded(!conversationsExpanded)} + /> + </EuiToolTip> + } /> - ) : ( - <EuiButtonIcon - aria-label={i18n.translate( - 'xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.newChatLabel', - { defaultMessage: 'New chat' } - )} - className={newChatButtonClassName} - data-test-subj="observabilityAiAssistantNewChatFlyoutButton" - iconType="plusInCircle" - onClick={handleClickNewChat} + + {conversationsExpanded ? ( + <ConversationList + selected={conversationId ?? ''} + onClickDeleteConversation={handleClickDeleteConversation} + onClickChat={handleClickChat} + onClickNewChat={handleClickNewChat} + /> + ) : ( + <EuiPopover + anchorPosition="downLeft" + button={ + <EuiToolTip + content={i18n.translate( + 'xpack.observabilityAiAssistant.chatFlyout.euiToolTip.newChatLabel', + { defaultMessage: 'New chat' } + )} + display="block" + > + <EuiButtonIcon + aria-label={i18n.translate( + 'xpack.observabilityAiAssistant.chatFlyout.euiButtonIcon.newChatLabel', + { defaultMessage: 'New chat' } + )} + data-test-subj="observabilityAiAssistantNewChatFlyoutButton" + iconType="plusInCircle" + onClick={handleClickNewChat} + /> + </EuiToolTip> + } + className={newChatButtonClassName} + /> + )} + </EuiFlexItem> + + <EuiFlexItem className={chatBodyContainerClassName}> + <ChatBody + key={chatBodyKeyRef.current} + connectors={connectors} + currentUser={currentUser} + flyoutWidthMode={flyoutWidthMode} + initialTitle={initialTitle} + initialMessages={initialMessages} + initialConversationId={conversationId} + knowledgeBase={knowledgeBase} + showLinkToConversationsApp + startedFrom={startedFrom} + onConversationUpdate={(conversation) => { + setConversationId(conversation.conversation.id); + }} + onToggleFlyoutWidthMode={handleToggleFlyoutWidthMode} /> - )} - </EuiFlexItem> - - <EuiFlexItem className={chatBodyContainerClassName}> - <ChatBody - key={chatBodyKeyRef.current} - connectors={connectors} - initialTitle={initialTitle} - initialMessages={initialMessages} - initialConversationId={conversationId} - currentUser={currentUser} - knowledgeBase={knowledgeBase} - startedFrom={startedFrom} - onConversationUpdate={(conversation) => { - setConversationId(conversation.conversation.id); - }} - chatFlyoutSecondSlotHandler={{ - container: secondSlotContainer, - setVisibility: setIsSecondSlotVisible, - }} - showLinkToConversationsApp - /> - </EuiFlexItem> - - <EuiFlexItem - style={{ - maxWidth: isSecondSlotVisible ? SIDEBAR_WIDTH : 0, - paddingTop: '56px', - }} - > - <ChatInlineEditingContent - setContainer={setSecondSlotContainer} - visible={isSecondSlotVisible} + </EuiFlexItem> + + <EuiFlexItem style={{ - borderTop: `solid 1px ${euiTheme.border.color}`, - borderLeft: `solid 1px ${euiTheme.border.color}`, + maxWidth: isSecondSlotVisible ? SIDEBAR_WIDTH : 0, + paddingTop: '56px', }} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyout> + > + <ChatInlineEditingContent + setContainer={setSecondSlotContainer} + visible={isSecondSlotVisible} + style={{ + borderTop: `solid 1px ${euiTheme.border.color}`, + borderLeft: `solid 1px ${euiTheme.border.color}`, + }} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyout> + </ObservabilityAIAssistantMultipaneFlyoutProvider> ) : null; } const getFlyoutWidth = ({ expanded, isSecondSlotVisible, + flyoutWidthMode, }: { expanded: boolean; isSecondSlotVisible: boolean; + flyoutWidthMode?: FlyoutWidthMode; }) => { + if (flyoutWidthMode === 'full') { + return '100%'; + } if (!expanded && !isSecondSlotVisible) { return '40vw'; } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx index cd4dc0d824590..0dc37c7829482 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx @@ -6,11 +6,14 @@ */ import React, { useEffect, useState } from 'react'; import { + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiInlineEditTitle, EuiLoadingSpinner, EuiPanel, + EuiPopover, + EuiToolTip, useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -18,6 +21,8 @@ import { css } from '@emotion/css'; import { AssistantAvatar } from '../assistant_avatar'; import { ChatActionsMenu } from './chat_actions_menu'; import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { FlyoutWidthMode } from './chat_flyout'; +import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; // needed to prevent InlineTextEdit component from expanding container const minWidthClassName = css` @@ -30,36 +35,54 @@ const chatHeaderClassName = css` `; export function ChatHeader({ - title, - loading, - licenseInvalid, connectors, conversationId, + flyoutWidthMode, + licenseInvalid, + loading, showLinkToConversationsApp, + title, onCopyConversation, onSaveTitle, + onToggleFlyoutWidthMode, }: { - title: string; - loading: boolean; - licenseInvalid: boolean; connectors: UseGenAIConnectorsResult; conversationId?: string; + flyoutWidthMode?: FlyoutWidthMode; + licenseInvalid: boolean; + loading: boolean; showLinkToConversationsApp: boolean; + title: string; onCopyConversation: () => void; onSaveTitle: (title: string) => void; + onToggleFlyoutWidthMode?: (newFlyoutWidthMode: FlyoutWidthMode) => void; }) { const theme = useEuiTheme(); + const router = useObservabilityAIAssistantRouter(); + const [newTitle, setNewTitle] = useState(title); useEffect(() => { setNewTitle(title); }, [title]); - const chatActionsMenuWrapper = css` - position: absolute; - right: 46px; - `; + const handleToggleFlyoutWidthMode = () => { + onToggleFlyoutWidthMode?.(flyoutWidthMode === 'side' ? 'full' : 'side'); + }; + + const handleNavigateToConversations = () => { + if (conversationId) { + router.push('/conversations/{conversationId}', { + path: { + conversationId, + }, + query: {}, + }); + } else { + router.push('/conversations/new', { path: {}, query: {} }); + } + }; return ( <EuiPanel @@ -105,17 +128,80 @@ export function ChatHeader({ }} /> </EuiFlexItem> - <EuiFlexItem - grow={false} - className={showLinkToConversationsApp ? chatActionsMenuWrapper : ''} - > - <ChatActionsMenu - connectors={connectors} - conversationId={conversationId} - disabled={licenseInvalid} - showLinkToConversationsApp={showLinkToConversationsApp} - onCopyConversationClick={onCopyConversation} - /> + + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="s"> + {flyoutWidthMode && onToggleFlyoutWidthMode ? ( + <> + <EuiFlexItem grow={false}> + <EuiPopover + anchorPosition="downLeft" + button={ + <EuiToolTip + content={i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel', + { defaultMessage: 'Navigate to conversations' } + )} + display="block" + > + <EuiButtonIcon + aria-label={i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel', + { defaultMessage: 'Navigate to conversations' } + )} + data-test-subj="observabilityAiAssistantChatHeaderButton" + iconType="discuss" + onClick={handleNavigateToConversations} + /> + </EuiToolTip> + } + /> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiPopover + anchorPosition="downLeft" + button={ + <EuiToolTip + content={ + flyoutWidthMode === 'side' + ? i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.euiToolTip.expandFlyoutWidthModeLabel', + { defaultMessage: 'Expand flyout' } + ) + : i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.euiToolTip.collapseFlyoutWidthModeLabel', + { defaultMessage: 'Collapse flyout' } + ) + } + display="block" + > + <EuiButtonIcon + aria-label={i18n.translate( + 'xpack.observabilityAiAssistant.chatActionsMenu.euiButtonIcon.toggleFlyoutWidthModeLabel', + { defaultMessage: 'Toggle flyout width mode' } + )} + data-test-subj="observabilityAiAssistantChatHeaderButton" + iconType={flyoutWidthMode === 'side' ? 'expand' : 'minimize'} + onClick={handleToggleFlyoutWidthMode} + /> + </EuiToolTip> + } + /> + </EuiFlexItem> + </> + ) : null} + + <EuiFlexItem grow={false}> + <ChatActionsMenu + connectors={connectors} + conversationId={conversationId} + disabled={licenseInvalid} + showLinkToConversationsApp={showLinkToConversationsApp} + onCopyConversationClick={onCopyConversation} + /> + </EuiFlexItem> + </EuiFlexGroup> </EuiFlexItem> </EuiFlexGroup> </EuiPanel> diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx index 48cf4070b0b96..0baccaf979f1f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx @@ -13,7 +13,7 @@ import { omit } from 'lodash'; import type { Feedback } from '../feedback_buttons'; import type { Message } from '../../../common'; import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; -import type { ChatActionClickHandler, ChatFlyoutSecondSlotHandler } from './types'; +import type { ChatActionClickHandler } from './types'; import type { ObservabilityAIAssistantChatService } from '../../types'; import type { TelemetryEventTypeWithPayload } from '../../analytics'; import { ChatItem } from './chat_item'; @@ -54,7 +54,6 @@ export interface ChatTimelineProps { chatState: ChatState; currentUser?: Pick<AuthenticatedUser, 'full_name' | 'username'>; startedFrom?: StartedFrom; - chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; onEdit: (message: Message, messageAfterEdit: Message) => void; onFeedback: (message: Message, feedback: Feedback) => void; onRegenerate: (message: Message) => void; @@ -69,7 +68,6 @@ export function ChatTimeline({ hasConnector, currentUser, startedFrom, - chatFlyoutSecondSlotHandler, onEdit, onFeedback, onRegenerate, @@ -86,7 +84,6 @@ export function ChatTimeline({ currentUser, startedFrom, chatState, - chatFlyoutSecondSlotHandler, onActionClick, }); @@ -110,16 +107,7 @@ export function ChatTimeline({ } return consolidatedChatItems; - }, [ - chatService, - hasConnector, - messages, - currentUser, - startedFrom, - chatState, - chatFlyoutSecondSlotHandler, - onActionClick, - ]); + }, [chatService, hasConnector, messages, currentUser, startedFrom, chatState, onActionClick]); return ( <EuiCommentList diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx index bf514691f7d93..0227ef42e2808 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx @@ -25,6 +25,7 @@ import { Disclaimer } from './disclaimer'; import { WelcomeMessageConnectors } from './welcome_message_connectors'; import { WelcomeMessageKnowledgeBase } from './welcome_message_knowledge_base'; import { useKibana } from '../../hooks/use_kibana'; +import { isSupportedConnectorType } from '../../../common/connectors'; const fullHeightClassName = css` height: 100%; @@ -68,7 +69,7 @@ export function WelcomeMessage({ const onConnectorCreated = (createdConnector: ActionConnector) => { setConnectorFlyoutOpen(false); - if (createdConnector.actionTypeId === '.gen-ai') { + if (isSupportedConnectorType(createdConnector.actionTypeId)) { connectors.reloadConnectors(); } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/render_function.tsx b/x-pack/plugins/observability_ai_assistant/public/components/render_function.tsx index 91547deed0021..eded1c30e59e6 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/render_function.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/render_function.tsx @@ -7,27 +7,20 @@ import React from 'react'; import { Message } from '../../common'; import { useObservabilityAIAssistantChatService } from '../hooks/use_observability_ai_assistant_chat_service'; -import type { ChatActionClickHandler, ChatFlyoutSecondSlotHandler } from './chat/types'; +import type { ChatActionClickHandler } from './chat/types'; interface Props { name: string; arguments: string | undefined; response: Message['message']; onActionClick: ChatActionClickHandler; - chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; } export function RenderFunction(props: Props) { const chatService = useObservabilityAIAssistantChatService(); return ( <> - {chatService.renderFunction( - props.name, - props.arguments, - props.response, - props.onActionClick, - props.chatFlyoutSecondSlotHandler - )} + {chatService.renderFunction(props.name, props.arguments, props.response, props.onActionClick)} </> ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_multipane_flyout_provider.tsx b/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_multipane_flyout_provider.tsx new file mode 100644 index 0000000000000..93a091ff4a7d4 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_multipane_flyout_provider.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createContext } from 'react'; +import type { ChatFlyoutSecondSlotHandler } from '../types'; + +export const ObservabilityAIAssistantMultipaneFlyoutContext = createContext< + ChatFlyoutSecondSlotHandler | undefined +>(undefined); + +export const ObservabilityAIAssistantMultipaneFlyoutProvider = + ObservabilityAIAssistantMultipaneFlyoutContext.Provider; diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.test.tsx b/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.test.tsx index 789697fbaaaa8..de7c4f04f241c 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.test.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.test.tsx @@ -12,6 +12,7 @@ import type { LensPublicStart } from '@kbn/lens-plugin/public'; import { lensPluginMock } from '@kbn/lens-plugin/public/mocks/lens_plugin_mock'; import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { ObservabilityAIAssistantMultipaneFlyoutProvider } from '../context/observability_ai_assistant_multipane_flyout_provider'; import { VisualizeESQL } from './visualize_esql'; describe('VisualizeESQL', () => { @@ -50,19 +51,22 @@ describe('VisualizeESQL', () => { }, ] as DatatableColumn[]; render( - <VisualizeESQL - lens={lensService} - dataViews={dataViewsService} - uiActions={uiActionsService} - columns={columns} - query={'from foo | keep bytes, destination'} - onActionClick={jest.fn()} - userOverrides={userOverrides} - chatFlyoutSecondSlotHandler={{ + <ObservabilityAIAssistantMultipaneFlyoutProvider + value={{ container: document.createElement('div'), setVisibility: setVisibilitySpy ?? jest.fn(), }} - /> + > + <VisualizeESQL + lens={lensService} + dataViews={dataViewsService} + uiActions={uiActionsService} + columns={columns} + query={'from foo | keep bytes, destination'} + onActionClick={jest.fn()} + userOverrides={userOverrides} + /> + </ObservabilityAIAssistantMultipaneFlyoutProvider> ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.tsx b/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.tsx index 05145c6130b4f..61295f4faf6f7 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.tsx @@ -22,7 +22,7 @@ import type { TypedLensByValueInput, InlineEditLensEmbeddableContext, } from '@kbn/lens-plugin/public'; -import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useContext } from 'react'; import ReactDOM from 'react-dom'; import useAsync from 'react-use/lib/useAsync'; import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; @@ -30,17 +30,14 @@ import { VisualizeESQLFunctionArguments, VisualizeESQLUserIntention, } from '../../common/functions/visualize_esql'; +import { ObservabilityAIAssistantMultipaneFlyoutContext } from '../context/observability_ai_assistant_multipane_flyout_provider'; import type { ObservabilityAIAssistantPluginStartDependencies, ObservabilityAIAssistantService, RegisterRenderFunctionDefinition, RenderFunction, } from '../types'; -import { - type ChatActionClickHandler, - ChatActionClickType, - ChatFlyoutSecondSlotHandler, -} from '../components/chat/types'; +import { type ChatActionClickHandler, ChatActionClickType } from '../components/chat/types'; interface VisualizeLensResponse { content: DatatableColumn[]; @@ -63,12 +60,6 @@ interface VisualizeESQLProps { * If not given, the embeddable gets them from the suggestions api */ userOverrides?: unknown; - /** Optional, should be passed if the embeddable is rendered in a flyout - * If not given, the inline editing push flyout won't open - * The code will be significantly improved, - * if this is addressed https://github.com/elastic/eui/issues/7443 - */ - chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; /** User's preferation chart type as it comes from the model */ preferredChartType?: string; } @@ -85,7 +76,6 @@ export function VisualizeESQL({ query, onActionClick, userOverrides, - chatFlyoutSecondSlotHandler, preferredChartType, }: VisualizeESQLProps) { // fetch the pattern from the query @@ -100,6 +90,8 @@ export function VisualizeESQL({ }); }, [indexPattern]); + const chatFlyoutSecondSlotHandler = useContext(ObservabilityAIAssistantMultipaneFlyoutContext); + const [isSaveModalOpen, setIsSaveModalOpen] = useState(false); const [lensInput, setLensInput] = useState<TypedLensByValueInput | undefined>( userOverrides as TypedLensByValueInput @@ -316,7 +308,6 @@ export function registerVisualizeQueryRenderFunction({ arguments: { query, userOverrides, intention }, response, onActionClick, - chatFlyoutSecondSlotHandler, }: Parameters<RenderFunction<VisualizeESQLFunctionArguments, {}>>[0]) => { const { content } = response as VisualizeLensResponse; @@ -370,7 +361,6 @@ export function registerVisualizeQueryRenderFunction({ query={query} onActionClick={onActionClick} userOverrides={userOverrides} - chatFlyoutSecondSlotHandler={chatFlyoutSecondSlotHandler} preferredChartType={preferredChartType} /> ); diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts index 160a4835d0ffb..16e27d1f8505a 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_router.ts @@ -7,7 +7,6 @@ import { PathsOf, TypeAsArgs, TypeOf } from '@kbn/typed-react-router-config'; import { useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; import { ObservabilityAIAssistantRouter, ObservabilityAIAssistantRoutes } from '../routes/config'; import { observabilityAIAssistantRouter } from '../routes/config'; import { useKibana } from './use_kibana'; @@ -24,10 +23,11 @@ interface StatefulObservabilityAIAssistantRouter extends ObservabilityAIAssistan } export function useObservabilityAIAssistantRouter(): StatefulObservabilityAIAssistantRouter { - const history = useHistory(); - const { - services: { http }, + services: { + http, + application: { navigateToApp }, + }, } = useKibana(); const link = (...args: any[]) => { @@ -40,17 +40,16 @@ export function useObservabilityAIAssistantRouter(): StatefulObservabilityAIAssi ...observabilityAIAssistantRouter, push: (...args) => { const next = link(...args); - - history.push(next); + navigateToApp('observabilityAIAssistant', { path: next, replace: false }); }, replace: (path, ...args) => { const next = link(path, ...args); - history.replace(next); + navigateToApp('observabilityAIAssistant', { path: next, replace: true }); }, link: (path, ...args) => { return http.basePath.prepend('/app/observabilityAIAssistant' + link(path, ...args)); }, }), - [http, history] + [navigateToApp, http.basePath] ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx b/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx index 4620c0cf2775d..0f49389c1c60d 100644 --- a/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx @@ -199,10 +199,6 @@ export function ConversationView() { showLinkToConversationsApp={false} startedFrom="conversationView" onConversationUpdate={handleConversationUpdate} - chatFlyoutSecondSlotHandler={{ - container: secondSlotContainer, - setVisibility: setIsSecondSlotVisible, - }} /> <div className={sidebarContainerClass}> diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts index 211f25b045b77..5fe933835eecd 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts @@ -9,8 +9,10 @@ import { AnalyticsServiceStart, HttpResponse } from '@kbn/core/public'; import { AbortError } from '@kbn/kibana-utils-plugin/common'; import { IncomingMessage } from 'http'; import { pick } from 'lodash'; -import { concatMap, delay, map, Observable, of, scan, shareReplay, timestamp } from 'rxjs'; +import { concatMap, delay, filter, map, Observable, of, scan, shareReplay, timestamp } from 'rxjs'; import { + BufferFlushEvent, + StreamingChatResponseEventType, StreamingChatResponseEventWithoutError, type StreamingChatResponseEvent, } from '../../common/conversation_complete'; @@ -116,7 +118,7 @@ export async function createChatService({ return { analytics, - renderFunction: (name, args, response, onActionClick, chatFlyoutSecondSlotHandler) => { + renderFunction: (name, args, response, onActionClick) => { const fn = renderFunctionRegistry.get(name); if (!fn) { @@ -134,7 +136,6 @@ export async function createChatService({ response: parsedResponse, arguments: parsedArguments, onActionClick, - chatFlyoutSecondSlotHandler, }); }, getContexts: () => contextDefinitions, @@ -164,7 +165,11 @@ export async function createChatService({ const response = _response as unknown as HttpResponse<IncomingMessage>; const response$ = toObservable(response) .pipe( - map((line) => JSON.parse(line) as StreamingChatResponseEvent), + map((line) => JSON.parse(line) as StreamingChatResponseEvent | BufferFlushEvent), + filter( + (line): line is StreamingChatResponseEvent => + line.type !== StreamingChatResponseEventType.BufferFlush + ), throwSerializedChatCompletionErrors() ) .subscribe(subscriber); @@ -225,7 +230,11 @@ export async function createChatService({ const subscription = toObservable(response) .pipe( - map((line) => JSON.parse(line) as StreamingChatResponseEvent), + map((line) => JSON.parse(line) as StreamingChatResponseEvent | BufferFlushEvent), + filter( + (line): line is StreamingChatResponseEvent => + line.type !== StreamingChatResponseEventType.BufferFlush + ), throwSerializedChatCompletionErrors() ) .subscribe(subscriber); diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index 418c7eca16b19..e303b01a5c9e9 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -49,6 +49,7 @@ import type { UseGenAIConnectorsResult } from './hooks/use_genai_connectors'; export type { CreateChatCompletionResponseChunk } from '../common/types'; export type { PendingMessage }; +export type { ChatFlyoutSecondSlotHandler }; export interface ObservabilityAIAssistantChatService { analytics: AnalyticsServiceStart; @@ -76,8 +77,7 @@ export interface ObservabilityAIAssistantChatService { name: string, args: string | undefined, response: { data?: string; content?: string }, - onActionClick: ChatActionClickHandler, - chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler + onActionClick: ChatActionClickHandler ) => React.ReactNode; } @@ -95,7 +95,6 @@ export type RenderFunction<TArguments, TResponse extends FunctionResponse> = (op arguments: TArguments; response: TResponse; onActionClick: ChatActionClickHandler; - chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; }) => React.ReactNode; export type RegisterRenderFunctionDefinition< diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx index 600256d66a7bc..8135111a6f548 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx @@ -231,10 +231,6 @@ describe('getTimelineItemsFromConversation', () => { }, ], onActionClick: jest.fn(), - chatFlyoutSecondSlotHandler: { - container: null, - setVisibility: jest.fn(), - }, }); }); @@ -270,8 +266,7 @@ describe('getTimelineItemsFromConversation', () => { 'my_render_function', JSON.stringify({ foo: 'bar' }), { content: '[]', name: 'my_render_function', role: 'user' }, - expect.any(Function), - { container: null, setVisibility: expect.any(Function) } + expect.any(Function) ); expect(container.textContent).toEqual('Rendered'); diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx index 40b54708e5b6c..d1f14e30d6097 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx @@ -17,7 +17,7 @@ import { RenderFunction } from '../components/render_function'; import type { ObservabilityAIAssistantChatService } from '../types'; import { ChatState } from '../hooks/use_chat'; import { safeJsonParse } from './safe_json_parse'; -import type { ChatActionClickHandler, ChatFlyoutSecondSlotHandler } from '../components/chat/types'; +import type { ChatActionClickHandler } from '../components/chat/types'; function convertMessageToMarkdownCodeBlock(message: Message['message']) { let value: object; @@ -65,7 +65,6 @@ export function getTimelineItemsfromConversation({ messages, startedFrom, chatState, - chatFlyoutSecondSlotHandler, onActionClick, }: { chatService: ObservabilityAIAssistantChatService; @@ -74,7 +73,6 @@ export function getTimelineItemsfromConversation({ messages: Message[]; startedFrom?: StartedFrom; chatState: ChatState; - chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; onActionClick: ChatActionClickHandler; }): ChatTimelineItem[] { const messagesWithoutSystem = messages.filter( @@ -169,7 +167,6 @@ export function getTimelineItemsfromConversation({ arguments={prevFunctionCall?.arguments} response={message.message} onActionClick={onActionClick} - chatFlyoutSecondSlotHandler={chatFlyoutSecondSlotHandler} /> ) : undefined; diff --git a/x-pack/plugins/observability_ai_assistant/scripts/evaluation/README.md b/x-pack/plugins/observability_ai_assistant/scripts/evaluation/README.md index c5ff90ed582f2..76bf8a7fe7df2 100644 --- a/x-pack/plugins/observability_ai_assistant/scripts/evaluation/README.md +++ b/x-pack/plugins/observability_ai_assistant/scripts/evaluation/README.md @@ -26,7 +26,7 @@ By default, the tool will look for a Kibana instance running locally (at `http:/ #### Connector -Use `--connectorId` to specify a `.gen-ai` connector to use. If none are given, it will prompt you to select a connector based on the ones that are available. If only a single `.gen-ai` connector is found, it will be used without prompting. +Use `--connectorId` to specify a `.gen-ai` or `.bedrock` connector to use. If none are given, it will prompt you to select a connector based on the ones that are available. If only a single supported connector is found, it will be used without prompting. #### Persisting conversations diff --git a/x-pack/plugins/observability_ai_assistant/scripts/evaluation/kibana_client.ts b/x-pack/plugins/observability_ai_assistant/scripts/evaluation/kibana_client.ts index d77e37a2b55a8..d0aa91f7ac53e 100644 --- a/x-pack/plugins/observability_ai_assistant/scripts/evaluation/kibana_client.ts +++ b/x-pack/plugins/observability_ai_assistant/scripts/evaluation/kibana_client.ts @@ -12,7 +12,9 @@ import { format, parse, UrlObject } from 'url'; import { ToolingLog } from '@kbn/tooling-log'; import pRetry from 'p-retry'; import { Message, MessageRole } from '../../common'; +import { isSupportedConnectorType } from '../../common/connectors'; import { + BufferFlushEvent, ChatCompletionChunkEvent, ChatCompletionErrorEvent, ConversationCreateEvent, @@ -217,7 +219,17 @@ export class KibanaClient { ) ).data ).pipe( - map((line) => JSON.parse(line) as ChatCompletionChunkEvent | ChatCompletionErrorEvent), + map( + (line) => + JSON.parse(line) as + | ChatCompletionChunkEvent + | ChatCompletionErrorEvent + | BufferFlushEvent + ), + filter( + (line): line is ChatCompletionChunkEvent | ChatCompletionErrorEvent => + line.type !== StreamingChatResponseEventType.BufferFlush + ), throwSerializedChatCompletionErrors(), concatenateChatCompletionChunks() ); @@ -270,13 +282,13 @@ export class KibanaClient { ) ).data ).pipe( - map((line) => JSON.parse(line) as StreamingChatResponseEvent), - throwSerializedChatCompletionErrors(), + map((line) => JSON.parse(line) as StreamingChatResponseEvent | BufferFlushEvent), filter( (event): event is MessageAddEvent | ConversationCreateEvent => event.type === StreamingChatResponseEventType.MessageAdd || event.type === StreamingChatResponseEventType.ConversationCreate ), + throwSerializedChatCompletionErrors(), toArray() ); @@ -427,6 +439,8 @@ export class KibanaClient { }) ); - return connectors.data.filter((connector) => connector.connector_type_id === '.gen-ai'); + return connectors.data.filter((connector) => + isSupportedConnectorType(connector.connector_type_id) + ); } } diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/index.ts b/x-pack/plugins/observability_ai_assistant/server/functions/index.ts index d02f943c3523e..708f77da33321 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/index.ts @@ -53,7 +53,6 @@ export const registerFunctions: ChatRegistrationFunction = async ({ If multiple functions are suitable, use the most specific and easy one. E.g., when the user asks to visualise APM data, use the APM functions (if available) rather than "query". - Use the "get_dataset_info" function if it is not clear what fields or indices the user means, or if you want to get more information about the mappings. Note that ES|QL (the Elasticsearch query language, which is NOT Elasticsearch SQL, but a new piped language) is the preferred query language. @@ -66,6 +65,8 @@ export const registerFunctions: ChatRegistrationFunction = async ({ When the "visualize_query" function has been called, a visualization has been displayed to the user. DO NOT UNDER ANY CIRCUMSTANCES follow up a "visualize_query" function call with your own visualization attempt. If the "execute_query" function has been called, summarize these results for the user. The user does not see a visualization in this case. + Use the "get_dataset_info" function if it is not clear what fields or indices the user means, or if you want to get more information about the mappings. + If the "get_dataset_info" function returns no data, and the user asks for a query, generate a query anyway with the "query" function, but be explicit about it potentially being incorrect. ` ); diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/query/index.ts b/x-pack/plugins/observability_ai_assistant/server/functions/query/index.ts index b69188d81b84a..86da0c0395587 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/query/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/query/index.ts @@ -113,6 +113,7 @@ export function registerQueryFunction({ type: 'boolean', }, }, + required: ['switch'], } as const, }, async ({ messages, connectorId }, signal) => { @@ -129,54 +130,58 @@ export function registerQueryFunction({ const source$ = ( await client.chat('classify_esql', { connectorId, - messages: withEsqlSystemMessage( - `Use the classify_esql function to classify the user's request - and get more information about specific functions and commands - you think are candidates for answering the question. - + messages: withEsqlSystemMessage().concat({ + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.User, + content: `Use the classify_esql function to classify the user's request + in the user message before this. + and get more information about specific functions and commands + you think are candidates for answering the question. - Examples for functions and commands: - Do you need to group data? Request \`STATS\`. - Extract data? Request \`DISSECT\` AND \`GROK\`. - Convert a column based on a set of conditionals? Request \`EVAL\` and \`CASE\`. - - For determining the intention of the user, the following options are available: - - ${VisualizeESQLUserIntention.generateQueryOnly}: the user only wants to generate the query, - but not run it. - - ${VisualizeESQLUserIntention.executeAndReturnResults}: the user wants to execute the query, - and have the assistant return/analyze/summarize the results. they don't need a - visualization. - - ${VisualizeESQLUserIntention.visualizeAuto}: The user wants to visualize the data from the - query, but wants us to pick the best visualization type, or their preferred - visualization is unclear. - - These intentions will display a specific visualization: - ${VisualizeESQLUserIntention.visualizeBar} - ${VisualizeESQLUserIntention.visualizeDonut} - ${VisualizeESQLUserIntention.visualizeHeatmap} - ${VisualizeESQLUserIntention.visualizeLine} - ${VisualizeESQLUserIntention.visualizeTagcloud} - ${VisualizeESQLUserIntention.visualizeTreemap} - ${VisualizeESQLUserIntention.visualizeWaffle} - ${VisualizeESQLUserIntention.visualizeXy} - - Some examples: - "Show me the avg of x" => ${VisualizeESQLUserIntention.executeAndReturnResults} - "Show me the results of y" => ${VisualizeESQLUserIntention.executeAndReturnResults} - "Display the sum of z" => ${VisualizeESQLUserIntention.executeAndReturnResults} - - "I want a query that ..." => ${VisualizeESQLUserIntention.generateQueryOnly} - "... Just show me the query" => ${VisualizeESQLUserIntention.generateQueryOnly} - "Create a query that ..." => ${VisualizeESQLUserIntention.generateQueryOnly} - - "Show me the avg of x over time" => ${VisualizeESQLUserIntention.visualizeAuto} - "I want a bar chart of ... " => ${VisualizeESQLUserIntention.visualizeBar} - "I want to see a heat map of ..." => ${VisualizeESQLUserIntention.visualizeHeatmap} - ` - ), + Examples for functions and commands: + Do you need to group data? Request \`STATS\`. + Extract data? Request \`DISSECT\` AND \`GROK\`. + Convert a column based on a set of conditionals? Request \`EVAL\` and \`CASE\`. + + For determining the intention of the user, the following options are available: + + ${VisualizeESQLUserIntention.generateQueryOnly}: the user only wants to generate the query, + but not run it. + + ${VisualizeESQLUserIntention.executeAndReturnResults}: the user wants to execute the query, + and have the assistant return/analyze/summarize the results. they don't need a + visualization. + + ${VisualizeESQLUserIntention.visualizeAuto}: The user wants to visualize the data from the + query, but wants us to pick the best visualization type, or their preferred + visualization is unclear. + + These intentions will display a specific visualization: + ${VisualizeESQLUserIntention.visualizeBar} + ${VisualizeESQLUserIntention.visualizeDonut} + ${VisualizeESQLUserIntention.visualizeHeatmap} + ${VisualizeESQLUserIntention.visualizeLine} + ${VisualizeESQLUserIntention.visualizeTagcloud} + ${VisualizeESQLUserIntention.visualizeTreemap} + ${VisualizeESQLUserIntention.visualizeWaffle} + ${VisualizeESQLUserIntention.visualizeXy} + + Some examples: + "Show me the avg of x" => ${VisualizeESQLUserIntention.executeAndReturnResults} + "Show me the results of y" => ${VisualizeESQLUserIntention.executeAndReturnResults} + "Display the sum of z" => ${VisualizeESQLUserIntention.executeAndReturnResults} + + "I want a query that ..." => ${VisualizeESQLUserIntention.generateQueryOnly} + "... Just show me the query" => ${VisualizeESQLUserIntention.generateQueryOnly} + "Create a query that ..." => ${VisualizeESQLUserIntention.generateQueryOnly} + + "Show me the avg of x over time" => ${VisualizeESQLUserIntention.visualizeAuto} + "I want a bar chart of ... " => ${VisualizeESQLUserIntention.visualizeBar} + "I want to see a heat map of ..." => ${VisualizeESQLUserIntention.visualizeHeatmap} + `, + }, + }), signal, functions: [ { @@ -184,6 +189,9 @@ export function registerQueryFunction({ description: `Use this function to determine: - what ES|QL functions and commands are candidates for answering the user's question - whether the user has requested a query, and if so, it they want it to be executed, or just shown. + + All parameters are required. Make sure the functions and commands you request are available in the + system message. `, parameters: { type: 'object', @@ -218,6 +226,10 @@ export function registerQueryFunction({ const response = await lastValueFrom(source$); + if (!response.message.function_call.arguments) { + throw new Error('LLM did not call classify_esql function'); + } + const args = JSON.parse(response.message.function_call.arguments) as { commands: string[]; functions: string[]; diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts b/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts index 7e966fa0e5508..125c9a2f6eea0 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts @@ -9,8 +9,9 @@ import { decodeOrThrow, jsonRt } from '@kbn/io-ts-utils'; import type { Serializable } from '@kbn/utility-types'; import dedent from 'dedent'; import * as t from 'io-ts'; -import { last, omit } from 'lodash'; +import { compact, last, omit } from 'lodash'; import { lastValueFrom } from 'rxjs'; +import { Logger } from '@kbn/logging'; import { FunctionRegistrationParameters } from '.'; import { MessageRole, type Message } from '../../common/types'; import { concatenateChatCompletionChunks } from '../../common/utils/concatenate_chat_completion_chunks'; @@ -87,18 +88,19 @@ export function registerRecallFunction({ messages.filter((message) => message.message.role === MessageRole.User) ); + const nonEmptyQueries = compact(queries); + + const queriesOrUserPrompt = nonEmptyQueries.length + ? nonEmptyQueries + : compact([userMessage?.message.content]); + const suggestions = await retrieveSuggestions({ userMessage, client, - signal, categories, - queries, + queries: queriesOrUserPrompt, }); - resources.logger.debug(`Received ${suggestions.length} suggestions`); - - resources.logger.debug(JSON.stringify(suggestions, null, 2)); - if (suggestions.length === 0) { return { content: [] as unknown as Serializable, @@ -107,17 +109,14 @@ export function registerRecallFunction({ const relevantDocuments = await scoreSuggestions({ suggestions, - systemMessage, - userMessage, - queries, + queries: queriesOrUserPrompt, + messages, client, connectorId, signal, + logger: resources.logger, }); - resources.logger.debug(`Received ${relevantDocuments.length} relevant documents`); - resources.logger.debug(JSON.stringify(relevantDocuments, null, 2)); - return { content: relevantDocuments as unknown as Serializable, }; @@ -126,25 +125,17 @@ export function registerRecallFunction({ } async function retrieveSuggestions({ - userMessage, queries, client, categories, - signal, }: { userMessage?: Message; queries: string[]; client: ObservabilityAIAssistantClient; categories: Array<'apm' | 'lens'>; - signal: AbortSignal; }) { - const queriesWithUserPrompt = - userMessage && userMessage.message.content - ? [userMessage.message.content, ...queries] - : queries; - const recallResponse = await client.recall({ - queries: queriesWithUserPrompt, + queries, categories, }); @@ -161,50 +152,44 @@ const scoreFunctionRequestRt = t.type({ }); const scoreFunctionArgumentsRt = t.type({ - scores: t.array( - t.type({ - id: t.string, - score: t.number, - }) - ), + scores: t.string, }); async function scoreSuggestions({ suggestions, - systemMessage, - userMessage, + messages, queries, client, connectorId, signal, + logger, }: { suggestions: Awaited<ReturnType<typeof retrieveSuggestions>>; - systemMessage: Message; - userMessage?: Message; + messages: Message[]; queries: string[]; client: ObservabilityAIAssistantClient; connectorId: string; signal: AbortSignal; + logger: Logger; }) { - const systemMessageExtension = - dedent(`You have the function called score available to help you inform the user about how relevant you think a given document is to the conversation. - Please give a score between 1 and 7, fractions are allowed. - A higher score means it is more relevant.`); - const extendedSystemMessage = { - ...systemMessage, - message: { - ...systemMessage.message, - content: `${systemMessage.message.content}\n\n${systemMessageExtension}`, - }, - }; - - const userMessageOrQueries = - userMessage && userMessage.message.content ? userMessage.message.content : queries.join(','); + const indexedSuggestions = suggestions.map((suggestion, index) => ({ ...suggestion, id: index })); const newUserMessageContent = - dedent(`Given the question "${userMessageOrQueries}", can you give me a score for how relevant the following documents are? + dedent(`Given the following question, score the documents that are relevant to the question. on a scale from 0 to 7, + 0 being completely relevant, and 7 being extremely relevant. Information is relevant to the question if it helps in + answering the question. Judge it according to the following criteria: + + - The document is relevant to the question, and the rest of the conversation + - The document has information relevant to the question that is not mentioned, + or more detailed than what is available in the conversation + - The document has a high amount of information relevant to the question compared to other documents + - The document contains new information not mentioned before in the conversation - ${JSON.stringify(suggestions, null, 2)}`); + Question: + ${queries.join('\n')} + + Documents: + ${JSON.stringify(indexedSuggestions, null, 2)}`); const newUserMessage: Message = { '@timestamp': new Date().toISOString(), @@ -223,22 +208,13 @@ async function scoreSuggestions({ additionalProperties: false, properties: { scores: { - description: 'The document IDs and their scores', - type: 'array', - items: { - type: 'object', - additionalProperties: false, - properties: { - id: { - description: 'The ID of the document', - type: 'string', - }, - score: { - description: 'The score for the document', - type: 'number', - }, - }, - }, + description: `The document IDs and their scores, as CSV. Example: + + my_id,7 + my_other_id,3 + my_third_id,4 + `, + type: 'string', }, }, required: ['score'], @@ -250,18 +226,28 @@ async function scoreSuggestions({ ( await client.chat('score_suggestions', { connectorId, - messages: [extendedSystemMessage, newUserMessage], + messages: [...messages.slice(0, -1), newUserMessage], functions: [scoreFunction], functionCall: 'score', signal, }) ).pipe(concatenateChatCompletionChunks()) ); + const scoreFunctionRequest = decodeOrThrow(scoreFunctionRequestRt)(response); - const { scores } = decodeOrThrow(jsonRt.pipe(scoreFunctionArgumentsRt))( + const { scores: scoresAsString } = decodeOrThrow(jsonRt.pipe(scoreFunctionArgumentsRt))( scoreFunctionRequest.message.function_call.arguments ); + const scores = scoresAsString.split('\n').map((line) => { + const [index, score] = line + .split(',') + .map((value) => value.trim()) + .map(Number); + + return { id: suggestions[index].id, score }; + }); + if (scores.length === 0) { return []; } @@ -279,5 +265,7 @@ async function scoreSuggestions({ relevantDocumentIds.includes(suggestion.id) ); + logger.debug(`Relevant documents: ${JSON.stringify(relevantDocuments, null, 2)}`); + return relevantDocuments; } diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts index 517cc48f9f27c..a9c58a9a59e00 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts @@ -5,13 +5,13 @@ * 2.0. */ import { notImplemented } from '@hapi/boom'; -import * as t from 'io-ts'; import { toBooleanRt } from '@kbn/io-ts-utils'; -import type OpenAI from 'openai'; +import * as t from 'io-ts'; import { Readable } from 'stream'; +import { flushBuffer } from '../../service/util/flush_buffer'; +import { observableIntoStream } from '../../service/util/observable_into_stream'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; import { messageRt } from '../runtime_types'; -import { observableIntoStream } from '../../service/util/observable_into_stream'; const chatRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/chat', @@ -40,7 +40,10 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ handler: async (resources): Promise<Readable> => { const { request, params, service } = resources; - const client = await service.getClient({ request }); + const [client, cloudStart] = await Promise.all([ + service.getClient({ request }), + resources.plugins.cloud?.start(), + ]); if (!client) { throw notImplemented(); @@ -68,7 +71,7 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ : {}), }); - return observableIntoStream(response$); + return observableIntoStream(response$.pipe(flushBuffer(!!cloudStart?.isCloudEnabled))); }, }); @@ -90,10 +93,13 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ }), ]), }), - handler: async (resources): Promise<Readable | OpenAI.Chat.ChatCompletion> => { + handler: async (resources): Promise<Readable> => { const { request, params, service } = resources; - const client = await service.getClient({ request }); + const [client, cloudStart] = await Promise.all([ + service.getClient({ request }), + resources.plugins.cloud?.start() || Promise.resolve(undefined), + ]); if (!client) { throw notImplemented(); @@ -125,7 +131,7 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ functionClient, }); - return observableIntoStream(response$); + return observableIntoStream(response$.pipe(flushBuffer(!!cloudStart?.isCloudEnabled))); }, }); diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/connectors/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/connectors/route.ts index 894896fec6b3c..79134b9fef8d0 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/connectors/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/connectors/route.ts @@ -5,6 +5,7 @@ * 2.0. */ import { FindActionResult } from '@kbn/actions-plugin/server'; +import { isSupportedConnectorType } from '../../../common/connectors'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; const listConnectorsRoute = createObservabilityAIAssistantServerRoute({ @@ -21,7 +22,7 @@ const listConnectorsRoute = createObservabilityAIAssistantServerRoute({ const connectors = await actionsClient.getAll(); - return connectors.filter((connector) => connector.actionTypeId === '.gen-ai'); + return connectors.filter((connector) => isSupportedConnectorType(connector.actionTypeId)); }, }); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/bedrock_claude_adapter.test.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/bedrock_claude_adapter.test.ts new file mode 100644 index 0000000000000..e92d14088d337 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/bedrock_claude_adapter.test.ts @@ -0,0 +1,239 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import dedent from 'dedent'; +import { last } from 'lodash'; +import { MessageRole } from '../../../../common'; +import { createBedrockClaudeAdapter } from './bedrock_claude_adapter'; +import { LlmApiAdapterFactory } from './types'; + +describe('createBedrockClaudeAdapter', () => { + describe('getSubAction', () => { + function callSubActionFactory(overrides?: Partial<Parameters<LlmApiAdapterFactory>[0]>) { + const subActionParams = createBedrockClaudeAdapter({ + logger: { + debug: jest.fn(), + } as unknown as Logger, + functions: [ + { + name: 'my_tool', + description: 'My tool', + parameters: { + properties: { + myParam: { + type: 'string', + }, + }, + }, + }, + ], + messages: [ + { + '@timestamp': new Date().toString(), + message: { + role: MessageRole.System, + content: '', + }, + }, + { + '@timestamp': new Date().toString(), + message: { + role: MessageRole.User, + content: 'How can you help me?', + }, + }, + ], + ...overrides, + }).getSubAction().subActionParams as { + temperature: number; + messages: Array<{ role: string; content: string }>; + }; + + return { + ...subActionParams, + messages: subActionParams.messages.map((msg) => ({ ...msg, content: dedent(msg.content) })), + }; + } + describe('with functions', () => { + it('sets the temperature to 0', () => { + expect(callSubActionFactory().temperature).toEqual(0); + }); + + it('formats the functions', () => { + expect(callSubActionFactory().messages[0].content).toContain( + dedent(`<tools> + <tool_description> + <tool_name>my_tool</tool_name> + <description>My tool</description> + <parameters> + <parameter> + <name>myParam</name> + <type>string</type> + <description> + + Required: false + Multiple: false + + </description> + </parameter> + </parameters> + </tool_description> + </tools>`) + ); + }); + + it('replaces mentions of functions with tools', () => { + const messages = [ + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.System, + content: + 'Call the "esql" tool. You can chain successive function calls, using the functions available.', + }, + }, + ]; + + const content = callSubActionFactory({ messages }).messages[0].content; + + expect(content).not.toContain(`"esql" function`); + expect(content).toContain(`"esql" tool`); + expect(content).not.toContain(`functions`); + expect(content).toContain(`tools`); + expect(content).toContain(`function calls`); + }); + + it('mentions to explicitly call the specified function if given', () => { + expect(last(callSubActionFactory({ functionCall: 'my_tool' }).messages)!.content).toContain( + 'Remember, use the my_tool tool to answer this question.' + ); + }); + + it('formats the function requests as XML', () => { + const messages = [ + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.System, + content: '', + }, + }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.Assistant, + function_call: { + name: 'my_tool', + arguments: JSON.stringify({ myParam: 'myValue' }), + trigger: MessageRole.User as const, + }, + }, + }, + ]; + + expect(last(callSubActionFactory({ messages }).messages)!.content).toContain( + dedent(`<function_calls> + <invoke> + <tool_name>my_tool</tool_name> + <parameters> + <myParam>myValue</myParam> + </parameters> + </invoke> + </function_calls>`) + ); + }); + + it('formats errors', () => { + const messages = [ + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.System, + content: '', + }, + }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.Assistant, + function_call: { + name: 'my_tool', + arguments: JSON.stringify({ myParam: 'myValue' }), + trigger: MessageRole.User as const, + }, + }, + }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.User, + name: 'my_tool', + content: JSON.stringify({ error: 'An internal server error occurred' }), + }, + }, + ]; + + expect(last(callSubActionFactory({ messages }).messages)!.content).toContain( + dedent(`<function_results> + <system> + <error>An internal server error occurred</error> + </system> + </function_results>`) + ); + }); + + it('formats function responses as XML + JSON', () => { + const messages = [ + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.System, + content: '', + }, + }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.Assistant, + function_call: { + name: 'my_tool', + arguments: JSON.stringify({ myParam: 'myValue' }), + trigger: MessageRole.User as const, + }, + }, + }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.User, + name: 'my_tool', + content: JSON.stringify({ myResponse: { myParam: 'myValue' } }), + }, + }, + ]; + + expect(last(callSubActionFactory({ messages }).messages)!.content).toContain( + dedent(`<function_results> + <result> + <tool_name>my_tool</tool_name> + <stdout> + <myResponse> +<myParam>myValue</myParam> +</myResponse> + </stdout> + </result> + </function_results>`) + ); + }); + }); + }); + + describe('streamIntoObservable', () => { + // this data format is heavily encoded, so hard to reproduce. + // will leave this empty until we have some sample data. + }); +}); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/bedrock_claude_adapter.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/bedrock_claude_adapter.ts new file mode 100644 index 0000000000000..d5ba0d726ab12 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/bedrock_claude_adapter.ts @@ -0,0 +1,228 @@ +/* + * 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 dedent from 'dedent'; +import { castArray } from 'lodash'; +import { filter, tap } from 'rxjs'; +import { Builder } from 'xml2js'; +import { createInternalServerError } from '../../../../common/conversation_complete'; +import { + BedrockChunkMember, + eventstreamSerdeIntoObservable, +} from '../../util/eventstream_serde_into_observable'; +import { jsonSchemaToFlatParameters } from '../../util/json_schema_to_flat_parameters'; +import { processBedrockStream } from './process_bedrock_stream'; +import type { LlmApiAdapterFactory } from './types'; + +function replaceFunctionsWithTools(content: string) { + return content.replaceAll(/(function)(s)?(?!\scall)/g, (match, p1, p2) => { + return `tool${p2 || ''}`; + }); +} + +// Most of the work here is to re-format OpenAI-compatible functions for Claude. +// See https://github.com/anthropics/anthropic-tools/blob/main/tool_use_package/prompt_constructors.py + +export const createBedrockClaudeAdapter: LlmApiAdapterFactory = ({ + messages, + functions, + functionCall, + logger, +}) => ({ + getSubAction: () => { + const [systemMessage, ...otherMessages] = messages; + + const filteredFunctions = functionCall + ? functions?.filter((fn) => fn.name === functionCall) + : functions; + + let functionsPrompt: string = ''; + + if (filteredFunctions?.length) { + functionsPrompt = `In this environment, you have access to a set of tools you can use to answer the user's question. + + When deciding what tool to use, keep in mind that you can call other tools in successive requests, so decide what tool + would be a good first step. + + You MUST only invoke a single tool, and invoke it once. Other invocations will be ignored. + You MUST wait for the results before invoking another. + You can call multiple tools in successive messages. This means you can chain function calls. If any tool was used in a previous + message, consider whether it still makes sense to follow it up with another function call. + + ${ + functions?.find((fn) => fn.name === 'recall') + ? `The "recall" function is ALWAYS used after a user question. Even if it was used before, your job is to answer the last user question, + even if the "recall" function was executed after that. Consider the tools you need to answer the user's question.` + : '' + } + + Rather than explaining how you would call a function, just generate the XML to call the function. It will automatically be + executed and returned to you. + + These results are generally not visible to the user. Treat them as if they are not, + unless specified otherwise. + + ONLY respond with XML, do not add any text. + + If a parameter allows multiple values, separate the values by "," + + You may call them like this. + + <function_calls> + <invoke> + <tool_name>$TOOL_NAME</tool_name> + <parameters> + <$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME> + ... + </parameters> + </invoke> + </function_calls> + + Here are the tools available: + + <tools> + ${filteredFunctions + .map( + (fn) => `<tool_description> + <tool_name>${fn.name}</tool_name> + <description>${fn.description}</description> + <parameters> + ${jsonSchemaToFlatParameters(fn.parameters).map((param) => { + return `<parameter> + <name>${param.name}</name> + <type>${param.type}</type> + <description> + ${param.description || ''} + Required: ${!!param.required} + Multiple: ${!!param.array} + ${ + param.enum || param.constant + ? `Allowed values: ${castArray(param.constant || param.enum).join(', ')}` + : '' + } + </description> + </parameter>`; + })} + </parameters> + </tool_description>` + ) + .join('\n')} + </tools> + + + Examples: + + Assistant: + <function_calls> + <invoke> + <tool_name>my_tool</tool_name> + <parameters> + <myParam>foo</myParam> + </parameters> + </invoke> + </function_calls> + + Assistant: + <function_calls> + <invoke> + <tool_name>another_tool</tool_name> + <parameters> + <myParam>foo</myParam> + </parameters> + </invoke> + </function_calls> + + `; + } + + const formattedMessages = [ + { + role: 'system', + content: `${replaceFunctionsWithTools(systemMessage.message.content!)} + + ${functionsPrompt} + `, + }, + ...otherMessages.map((message, index) => { + const builder = new Builder({ headless: true }); + if (message.message.name) { + const deserialized = JSON.parse(message.message.content || '{}'); + + if ('error' in deserialized) { + return { + role: message.message.role, + content: dedent(`<function_results> + <system> + ${builder.buildObject(deserialized)} + </system> + </function_results> + `), + }; + } + + return { + role: message.message.role, + content: dedent(` + <function_results> + <result> + <tool_name>${message.message.name}</tool_name> + <stdout> + ${builder.buildObject(deserialized)} + </stdout> + </result> + </function_results>`), + }; + } + + let content = replaceFunctionsWithTools(message.message.content || ''); + + if (message.message.function_call?.name) { + content += builder.buildObject({ + function_calls: { + invoke: { + tool_name: message.message.function_call.name, + parameters: JSON.parse(message.message.function_call.arguments || '{}'), + }, + }, + }); + } + + if (index === otherMessages.length - 1 && functionCall) { + content += ` + + Remember, use the ${functionCall} tool to answer this question.`; + } + + return { + role: message.message.role, + content, + }; + }), + ]; + + return { + subAction: 'invokeStream', + subActionParams: { + messages: formattedMessages, + temperature: 0, + stopSequences: ['\n\nHuman:', '</function_calls>'], + }, + }; + }, + streamIntoObservable: (readable) => + eventstreamSerdeIntoObservable(readable).pipe( + tap((value) => { + if ('modelStreamErrorException' in value) { + throw createInternalServerError(value.modelStreamErrorException.originalMessage); + } + }), + filter((value): value is BedrockChunkMember => { + return 'chunk' in value && value.chunk?.headers?.[':event-type']?.value === 'chunk'; + }), + processBedrockStream({ logger, functions }) + ), +}); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/openai_adapter.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/openai_adapter.ts new file mode 100644 index 0000000000000..61935d891a1db --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/openai_adapter.ts @@ -0,0 +1,69 @@ +/* + * 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 { compact, isEmpty, omit } from 'lodash'; +import OpenAI from 'openai'; +import { MessageRole } from '../../../../common'; +import { processOpenAiStream } from '../../../../common/utils/process_openai_stream'; +import { eventsourceStreamIntoObservable } from '../../util/eventsource_stream_into_observable'; +import { LlmApiAdapterFactory } from './types'; + +export const createOpenAiAdapter: LlmApiAdapterFactory = ({ + messages, + functions, + functionCall, + logger, +}) => { + return { + getSubAction: () => { + const messagesForOpenAI: Array< + Omit<OpenAI.ChatCompletionMessageParam, 'role'> & { + role: MessageRole; + } + > = compact( + messages + .filter((message) => message.message.content || message.message.function_call?.name) + .map((message) => { + const role = + message.message.role === MessageRole.Elastic + ? MessageRole.User + : message.message.role; + + return { + role, + content: message.message.content, + function_call: isEmpty(message.message.function_call?.name) + ? undefined + : omit(message.message.function_call, 'trigger'), + name: message.message.name, + }; + }) + ); + + const functionsForOpenAI = functions; + + const request: Omit<OpenAI.ChatCompletionCreateParams, 'model'> & { model?: string } = { + messages: messagesForOpenAI as OpenAI.ChatCompletionCreateParams['messages'], + stream: true, + ...(!!functions?.length ? { functions: functionsForOpenAI } : {}), + temperature: 0, + function_call: functionCall ? { name: functionCall } : undefined, + }; + + return { + subAction: 'stream', + subActionParams: { + body: JSON.stringify(request), + stream: true, + }, + }; + }, + streamIntoObservable: (readable) => { + return eventsourceStreamIntoObservable(readable).pipe(processOpenAiStream()); + }, + }; +}; diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/process_bedrock_stream.test.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/process_bedrock_stream.test.ts new file mode 100644 index 0000000000000..78775b4d79d51 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/process_bedrock_stream.test.ts @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fromUtf8 } from '@smithy/util-utf8'; +import { lastValueFrom, of } from 'rxjs'; +import { Logger } from '@kbn/logging'; +import { concatenateChatCompletionChunks } from '../../../../common/utils/concatenate_chat_completion_chunks'; +import { processBedrockStream } from './process_bedrock_stream'; +import { MessageRole } from '../../../../common'; + +describe('processBedrockStream', () => { + const encode = (completion: string, stop?: string) => { + return { + chunk: { + headers: { + '::event-type': { value: 'chunk', type: 'uuid' as const }, + }, + body: fromUtf8( + JSON.stringify({ + bytes: Buffer.from(JSON.stringify({ completion, stop }), 'utf-8').toString('base64'), + }) + ), + }, + }; + }; + + function getLoggerMock() { + return { + debug: jest.fn(), + } as unknown as Logger; + } + + it('parses normal text messages', async () => { + expect( + await lastValueFrom( + of(encode('This'), encode(' is'), encode(' some normal'), encode(' text')).pipe( + processBedrockStream({ logger: getLoggerMock() }), + concatenateChatCompletionChunks() + ) + ) + ).toEqual({ + message: { + content: 'This is some normal text', + function_call: { + arguments: '', + name: '', + trigger: MessageRole.Assistant, + }, + role: MessageRole.Assistant, + }, + }); + }); + + it('parses function calls when no text is given', async () => { + expect( + await lastValueFrom( + of( + encode('<function_calls><invoke'), + encode('><tool_name>my_tool</tool_name><parameters'), + encode('><my_param>my_value</my_param'), + encode('></parameters></invoke'), + encode('>', '</function_calls>') + ).pipe( + processBedrockStream({ + logger: getLoggerMock(), + functions: [ + { + name: 'my_tool', + description: '', + parameters: { + properties: { + my_param: { + type: 'string', + }, + }, + }, + }, + ], + }), + concatenateChatCompletionChunks() + ) + ) + ).toEqual({ + message: { + content: '', + function_call: { + arguments: JSON.stringify({ my_param: 'my_value' }), + name: 'my_tool', + trigger: MessageRole.Assistant, + }, + role: MessageRole.Assistant, + }, + }); + }); + + it('parses function calls when they are prefaced by text', async () => { + expect( + await lastValueFrom( + of( + encode('This is'), + encode(' my text\n<function_calls><invoke'), + encode('><tool_name>my_tool</tool_name><parameters'), + encode('><my_param>my_value</my_param'), + encode('></parameters></invoke'), + encode('>', '</function_calls>') + ).pipe( + processBedrockStream({ + logger: getLoggerMock(), + functions: [ + { + name: 'my_tool', + description: '', + parameters: { + properties: { + my_param: { + type: 'string', + }, + }, + }, + }, + ], + }), + concatenateChatCompletionChunks() + ) + ) + ).toEqual({ + message: { + content: 'This is my text', + function_call: { + arguments: JSON.stringify({ my_param: 'my_value' }), + name: 'my_tool', + trigger: MessageRole.Assistant, + }, + role: MessageRole.Assistant, + }, + }); + }); + + it('throws an error if the XML cannot be parsed', async () => { + expect( + async () => + await lastValueFrom( + of( + encode('<function_calls><invoke'), + encode('><tool_name>my_tool</tool><parameters'), + encode('><my_param>my_value</my_param'), + encode('></parameters></invoke'), + encode('>', '</function_calls>') + ).pipe( + processBedrockStream({ + logger: getLoggerMock(), + functions: [ + { + name: 'my_tool', + description: '', + parameters: { + properties: { + my_param: { + type: 'string', + }, + }, + }, + }, + ], + }), + concatenateChatCompletionChunks() + ) + ) + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Unexpected close tag + Line: 0 + Column: 49 + Char: >" + `); + }); + + it('throws an error if the function does not exist', async () => { + expect( + async () => + await lastValueFrom( + of( + encode('<function_calls><invoke'), + encode('><tool_name>my_other_tool</tool_name><parameters'), + encode('><my_param>my_value</my_param'), + encode('></parameters></invoke'), + encode('>', '</function_calls>') + ).pipe( + processBedrockStream({ + logger: getLoggerMock(), + functions: [ + { + name: 'my_tool', + description: '', + parameters: { + properties: { + my_param: { + type: 'string', + }, + }, + }, + }, + ], + }), + concatenateChatCompletionChunks() + ) + ) + ).rejects.toThrowError( + 'Function definition for my_other_tool not found. Available are: my_tool' + ); + }); + + it('successfully invokes a function without parameters', async () => { + expect( + await lastValueFrom( + of( + encode('<function_calls><invoke'), + encode('><tool_name>my_tool</tool_name><parameters'), + encode('></parameters></invoke'), + encode('>', '</function_calls>') + ).pipe( + processBedrockStream({ + logger: getLoggerMock(), + functions: [ + { + name: 'my_tool', + description: '', + parameters: { + properties: { + my_param: { + type: 'string', + }, + }, + }, + }, + ], + }), + concatenateChatCompletionChunks() + ) + ) + ).toEqual({ + message: { + content: '', + function_call: { + arguments: '{}', + name: 'my_tool', + trigger: MessageRole.Assistant, + }, + role: MessageRole.Assistant, + }, + }); + }); +}); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/process_bedrock_stream.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/process_bedrock_stream.ts new file mode 100644 index 0000000000000..41bc19717485c --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/process_bedrock_stream.ts @@ -0,0 +1,151 @@ +/* + * 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 { toUtf8 } from '@smithy/util-utf8'; +import { Observable } from 'rxjs'; +import { v4 } from 'uuid'; +import { Parser } from 'xml2js'; +import type { Logger } from '@kbn/logging'; +import { JSONSchema } from 'json-schema-to-ts'; +import { + ChatCompletionChunkEvent, + createInternalServerError, + StreamingChatResponseEventType, +} from '../../../../common/conversation_complete'; +import type { BedrockChunkMember } from '../../util/eventstream_serde_into_observable'; +import { convertDeserializedXmlWithJsonSchema } from '../../util/convert_deserialized_xml_with_json_schema'; + +async function parseFunctionCallXml({ + xml, + functions, +}: { + xml: string; + functions?: Array<{ name: string; description: string; parameters: JSONSchema }>; +}) { + const parser = new Parser(); + + const parsedValue = await parser.parseStringPromise(xml); + const invoke = parsedValue.function_calls.invoke[0]; + const fnName = invoke.tool_name[0]; + const parameters: Array<Record<string, string[]>> = invoke.parameters ?? []; + const functionDef = functions?.find((fn) => fn.name === fnName); + + if (!functionDef) { + throw createInternalServerError( + `Function definition for ${fnName} not found. ${ + functions?.length + ? 'Available are: ' + functions.map((fn) => fn.name).join(', ') + '.' + : 'No functions are available.' + }` + ); + } + + const args = convertDeserializedXmlWithJsonSchema(parameters, functionDef.parameters); + + return { + name: fnName, + arguments: JSON.stringify(args), + }; +} + +export function processBedrockStream({ + logger, + functions, +}: { + logger: Logger; + functions?: Array<{ name: string; description: string; parameters: JSONSchema }>; +}) { + return (source: Observable<BedrockChunkMember>) => + new Observable<ChatCompletionChunkEvent>((subscriber) => { + let functionCallsBuffer: string = ''; + const id = v4(); + + // We use this to make sure we don't complete the Observable + // before all operations have completed. + let nextPromise = Promise.resolve(); + + // As soon as we see a `<function` token, we write all chunks + // to a buffer, that we flush as a function request if we + // spot the stop sequence. + + async function handleNext(value: BedrockChunkMember) { + const response: { + completion: string; + stop_reason: string | null; + stop: null | string; + } = JSON.parse( + Buffer.from(JSON.parse(toUtf8(value.chunk.body)).bytes, 'base64').toString('utf-8') + ); + + let completion = response.completion; + + const isStartOfFunctionCall = !functionCallsBuffer && completion.includes('<function'); + + const isEndOfFunctionCall = functionCallsBuffer && response.stop === '</function_calls>'; + + const isInFunctionCall = !!functionCallsBuffer; + + if (isStartOfFunctionCall) { + const [before, after] = completion.split('<function'); + functionCallsBuffer += `<function${after}`; + completion = before.trimEnd(); + } else if (isEndOfFunctionCall) { + completion = ''; + functionCallsBuffer += response.completion + response.stop; + + logger.debug(`Parsing xml:\n${functionCallsBuffer}`); + + subscriber.next({ + id, + type: StreamingChatResponseEventType.ChatCompletionChunk, + message: { + content: '', + function_call: await parseFunctionCallXml({ + xml: functionCallsBuffer, + functions, + }), + }, + }); + + functionCallsBuffer = ''; + } else if (isInFunctionCall) { + completion = ''; + functionCallsBuffer += response.completion; + } + + if (completion.trim()) { + // OpenAI tokens come roughly separately, Bedrock/Claude + // chunks are bigger, so we split them up to give a more + // responsive feel in the UI + const parts = completion.split(' '); + parts.forEach((part, index) => { + subscriber.next({ + id, + type: StreamingChatResponseEventType.ChatCompletionChunk, + message: { + content: index === parts.length - 1 ? part : part + ' ', + }, + }); + }); + } + } + + source.subscribe({ + next: (value) => { + nextPromise = nextPromise.then(() => + handleNext(value).catch((error) => subscriber.error(error)) + ); + }, + error: (err) => { + subscriber.error(err); + }, + complete: () => { + nextPromise.then(() => subscriber.complete()); + }, + }); + }); +} diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/types.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/types.ts new file mode 100644 index 0000000000000..6ef3611bb4aae --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/types.ts @@ -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 type { Readable } from 'node:stream'; +import type { Observable } from 'rxjs'; +import type { Logger } from '@kbn/logging'; +import type { Message } from '../../../../common'; +import type { ChatCompletionChunkEvent } from '../../../../common/conversation_complete'; +import type { CompatibleJSONSchema } from '../../../../common/types'; + +export type LlmApiAdapterFactory = (options: { + logger: Logger; + messages: Message[]; + functions?: Array<{ name: string; description: string; parameters: CompatibleJSONSchema }>; + functionCall?: string; +}) => LlmApiAdapter; + +export interface LlmApiAdapter { + getSubAction: () => { subAction: string; subActionParams: Record<string, any> }; + streamIntoObservable: (readable: Readable) => Observable<ChatCompletionChunkEvent>; +} diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts index fb22828247474..cbcbf0ea3fa3a 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts @@ -16,6 +16,7 @@ import { finished } from 'stream/promises'; import { ObservabilityAIAssistantClient } from '.'; import { createResourceNamesMap } from '..'; import { MessageRole, type Message } from '../../../common'; +import { ObservabilityAIAssistantConnectorType } from '../../../common/connectors'; import { ChatCompletionChunkEvent, ChatCompletionErrorCode, @@ -63,7 +64,7 @@ function createLlmSimulator() { ], }; await new Promise<void>((resolve, reject) => { - stream.write(`data: ${JSON.stringify(chunk)}\n`, undefined, (err) => { + stream.write(`data: ${JSON.stringify(chunk)}\n\n`, undefined, (err) => { return err ? reject(err) : resolve(); }); }); @@ -72,7 +73,7 @@ function createLlmSimulator() { if (stream.destroyed) { throw new Error('Stream is already destroyed'); } - await new Promise((resolve) => stream.write('data: [DONE]', () => stream.end(resolve))); + await new Promise((resolve) => stream.write('data: [DONE]\n\n', () => stream.end(resolve))); }, error: (error: Error) => { stream.destroy(error); @@ -85,6 +86,7 @@ describe('Observability AI Assistant client', () => { const actionsClientMock: DeeplyMockedKeys<ActionsClient> = { execute: jest.fn(), + get: jest.fn(), } as any; const internalUserEsClientMock: DeeplyMockedKeys<ElasticsearchClient> = { @@ -125,6 +127,15 @@ describe('Observability AI Assistant client', () => { return name !== 'recall'; }); + actionsClientMock.get.mockResolvedValue({ + actionTypeId: ObservabilityAIAssistantConnectorType.OpenAI, + id: 'foo', + name: 'My connector', + isPreconfigured: false, + isDeprecated: false, + isSystemAction: false, + }); + currentUserEsClientMock.search.mockResolvedValue({ hits: { hits: [], @@ -491,6 +502,8 @@ describe('Observability AI Assistant client', () => { stream.on('data', dataHandler); + await nextTick(); + await llmSimulator.next({ content: 'Hello' }); await llmSimulator.complete(); @@ -590,6 +603,8 @@ describe('Observability AI Assistant client', () => { stream.on('data', dataHandler); + await nextTick(); + await llmSimulator.next({ content: 'Hello' }); await new Promise((resolve) => @@ -598,7 +613,7 @@ describe('Observability AI Assistant client', () => { error: { message: 'Connection unexpectedly closed', }, - })}\n`, + })}\n\n`, resolve ) ); @@ -694,6 +709,8 @@ describe('Observability AI Assistant client', () => { stream.on('data', dataHandler); + await nextTick(); + await llmSimulator.next({ content: 'Hello', function_call: { name: 'my-function', arguments: JSON.stringify({ foo: 'bar' }) }, @@ -1259,6 +1276,8 @@ describe('Observability AI Assistant client', () => { await nextLlmCallPromise; } + await nextTick(); + await requestAlertsFunctionCall(); await requestAlertsFunctionCall(); @@ -1348,6 +1367,8 @@ describe('Observability AI Assistant client', () => { stream.on('data', dataHandler); + await nextTick(); + await llmSimulator.next({ function_call: { name: 'get_top_alerts' } }); await llmSimulator.complete(); 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 76749e75daed1..2fc6bb7be34cc 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 @@ -12,18 +12,26 @@ import type { Logger } from '@kbn/logging'; import type { PublicMethodsOf } from '@kbn/utility-types'; import apm from 'elastic-apm-node'; import { decode, encode } from 'gpt-tokenizer'; -import { compact, isEmpty, last, merge, noop, omit, pick, take } from 'lodash'; -import type OpenAI from 'openai'; -import { filter, isObservable, lastValueFrom, Observable, shareReplay, toArray } from 'rxjs'; +import { last, merge, noop, omit, pick, take } from 'lodash'; +import { + filter, + isObservable, + last as lastOperator, + lastValueFrom, + Observable, + shareReplay, + toArray, +} from 'rxjs'; import { Readable } from 'stream'; import { v4 } from 'uuid'; +import { ObservabilityAIAssistantConnectorType } from '../../../common/connectors'; import { ChatCompletionChunkEvent, ChatCompletionErrorEvent, createConversationNotFoundError, + createTokenLimitReachedError, MessageAddEvent, StreamingChatResponseEventType, - createTokenLimitReachedError, type StreamingChatResponseEvent, } from '../../../common/conversation_complete'; import { @@ -39,7 +47,6 @@ import { } from '../../../common/types'; import { concatenateChatCompletionChunks } from '../../../common/utils/concatenate_chat_completion_chunks'; import { emitWithConcatenatedMessage } from '../../../common/utils/emit_with_concatenated_message'; -import { processOpenAiStream } from '../../../common/utils/process_openai_stream'; import type { ChatFunctionClient } from '../chat_function_client'; import { KnowledgeBaseEntryOperationType, @@ -48,7 +55,9 @@ import { } from '../knowledge_base_service'; import type { ObservabilityAIAssistantResourceNames } from '../types'; import { getAccessQuery } from '../util/get_access_query'; -import { streamIntoObservable } from '../util/stream_into_observable'; +import { createBedrockClaudeAdapter } from './adapters/bedrock_claude_adapter'; +import { createOpenAiAdapter } from './adapters/openai_adapter'; +import { LlmApiAdapter } from './adapters/types'; export class ObservabilityAIAssistantClient { constructor( @@ -320,6 +329,10 @@ export class ObservabilityAIAssistantClient { }, }; + this.dependencies.logger.debug( + `Function response: ${JSON.stringify(functionResponseMessage, null, 2)}` + ); + nextMessages = nextMessages.concat(functionResponseMessage); subscriber.next({ @@ -357,6 +370,8 @@ export class ObservabilityAIAssistantClient { return await next(nextMessages); } + this.dependencies.logger.debug(`Conversation: ${JSON.stringify(nextMessages, null, 2)}`); + if (!persist) { subscriber.complete(); return; @@ -449,91 +464,104 @@ export class ObservabilityAIAssistantClient { ): Promise<Observable<ChatCompletionChunkEvent>> => { const span = apm.startSpan(`chat ${name}`); - const messagesForOpenAI: Array< - Omit<OpenAI.ChatCompletionMessageParam, 'role'> & { - role: MessageRole; + const spanId = (span?.ids['span.id'] || '').substring(0, 6); + + try { + const connector = await this.dependencies.actionsClient.get({ + id: connectorId, + }); + + let adapter: LlmApiAdapter; + + switch (connector.actionTypeId) { + case ObservabilityAIAssistantConnectorType.OpenAI: + adapter = createOpenAiAdapter({ + logger: this.dependencies.logger, + messages, + functionCall, + functions, + }); + break; + + case ObservabilityAIAssistantConnectorType.Bedrock: + adapter = createBedrockClaudeAdapter({ + logger: this.dependencies.logger, + messages, + functionCall, + functions, + }); + break; + + default: + throw new Error(`Connector type is not supported: ${connector.actionTypeId}`); } - > = compact( - messages - .filter((message) => message.message.content || message.message.function_call?.name) - .map((message) => { - const role = - message.message.role === MessageRole.Elastic ? MessageRole.User : message.message.role; - - return { - role, - content: message.message.content, - function_call: isEmpty(message.message.function_call?.name) - ? undefined - : omit(message.message.function_call, 'trigger'), - name: message.message.name, - }; - }) - ); - const functionsForOpenAI = functions; + const subAction = adapter.getSubAction(); - const request: Omit<OpenAI.ChatCompletionCreateParams, 'model'> & { model?: string } = { - messages: messagesForOpenAI as OpenAI.ChatCompletionCreateParams['messages'], - stream: true, - ...(!!functions?.length ? { functions: functionsForOpenAI } : {}), - temperature: 0, - function_call: functionCall ? { name: functionCall } : undefined, - }; + this.dependencies.logger.debug(`Sending conversation to connector`); + this.dependencies.logger.trace(JSON.stringify(subAction.subActionParams, null, 2)); - this.dependencies.logger.debug(`Sending conversation to connector`); - this.dependencies.logger.trace(JSON.stringify(request, null, 2)); + const now = performance.now(); - const executeResult = await this.dependencies.actionsClient.execute({ - actionId: connectorId, - params: { - subAction: 'stream', - subActionParams: { - body: JSON.stringify(request), - stream: true, - }, - }, - }); + const executeResult = await this.dependencies.actionsClient.execute({ + actionId: connectorId, + params: subAction, + }); - this.dependencies.logger.debug(`Received action client response: ${executeResult.status}`); + this.dependencies.logger.debug( + `Received action client response: ${executeResult.status} (took: ${Math.round( + performance.now() - now + )}ms)${spanId ? ` (${spanId})` : ''}` + ); - 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 (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 (tokenLimitRegexResult) { + const [, tokenLimit, tokenCount] = tokenLimitRegexResult; + throw createTokenLimitReachedError(parseInt(tokenLimit, 10), parseInt(tokenCount, 10)); + } } - } - if (executeResult.status === 'error') { - throw internal(`${executeResult?.message} - ${executeResult?.serviceMessage}`); - } + if (executeResult.status === 'error') { + throw internal(`${executeResult?.message} - ${executeResult?.serviceMessage}`); + } - const response = executeResult.data as Readable; + const response = executeResult.data as Readable; - signal.addEventListener('abort', () => response.destroy()); + signal.addEventListener('abort', () => response.destroy()); - const observable = streamIntoObservable(response).pipe(processOpenAiStream(), shareReplay()); + const response$ = adapter.streamIntoObservable(response).pipe(shareReplay()); - if (span) { - lastValueFrom(observable) - .then( - () => { - span.setOutcome('success'); - }, - () => { - span.setOutcome('failure'); - } - ) + response$.pipe(concatenateChatCompletionChunks(), lastOperator()).subscribe({ + error: (error) => { + this.dependencies.logger.debug('Error in chat response'); + this.dependencies.logger.debug(error); + }, + next: (message) => { + this.dependencies.logger.debug(`Received message:\n${JSON.stringify(message)}`); + }, + }); + + lastValueFrom(response$) + .then(() => { + span?.setOutcome('success'); + }) + .catch(() => { + span?.setOutcome('failure'); + }) .finally(() => { - span.end(); + span?.end(); }); - } - return observable; + return response$; + } catch (error) { + span?.setOutcome('failure'); + span?.end(); + throw error; + } }; find = async (options?: { query?: string }): Promise<{ conversations: Conversation[] }> => { @@ -595,13 +623,36 @@ export class ObservabilityAIAssistantClient { }) => { const response$ = await this.chat('generate_title', { messages: [ + { + '@timestamp': new Date().toString(), + message: { + role: MessageRole.System, + content: `You are a helpful assistant for Elastic Observability. Assume the following message is the start of a conversation between you and a user; give this conversation a title based on the content below. DO NOT UNDER ANY CIRCUMSTANCES wrap this title in single or double quotes. This title is shown in a list of conversations to the user, so title it for the user, not for you.`, + }, + }, { '@timestamp': new Date().toISOString(), message: { role: MessageRole.User, content: messages.slice(1).reduce((acc, curr) => { return `${acc} ${curr.message.role}: ${curr.message.content}`; - }, 'You are a helpful assistant for Elastic Observability. Assume the following message is the start of a conversation between you and a user; give this conversation a title based on the content below. DO NOT UNDER ANY CIRCUMSTANCES wrap this title in single or double quotes. This title is shown in a list of conversations to the user, so title it for the user, not for you. Here is the content:'), + }, 'Generate a title, using the title_conversation_function, based on the following conversation:\n\n'), + }, + }, + ], + functions: [ + { + name: 'title_conversation', + description: + 'Use this function to title the conversation. Do not wrap the title in quotes', + parameters: { + type: 'object', + properties: { + title: { + type: 'string', + }, + }, + required: ['title'], }, }, ], @@ -611,7 +662,10 @@ export class ObservabilityAIAssistantClient { const response = await lastValueFrom(response$.pipe(concatenateChatCompletionChunks())); - const input = response.message?.content || ''; + const input = + (response.message.function_call.name + ? JSON.parse(response.message.function_call.arguments).title + : response.message?.content) || ''; // This regular expression captures a string enclosed in single or double quotes. // It extracts the string content without the quotes. diff --git a/x-pack/plugins/observability_ai_assistant/server/service/knowledge_base_service/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/knowledge_base_service/index.ts index 9442985602e98..6783f972f6b4c 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/knowledge_base_service/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/knowledge_base_service/index.ts @@ -436,6 +436,7 @@ export class KnowledgeBaseService { }): Promise<{ entries: RecalledEntry[]; }> => { + this.dependencies.logger.debug(`Recalling entries from KB for queries: "${queries}"`); const modelId = await this.dependencies.getModelId(); const [documentsFromKb, documentsFromConnectors] = await Promise.all([ @@ -482,10 +483,9 @@ export class KnowledgeBaseService { } } - if (returnedEntries.length <= sortedEntries.length) { - this.dependencies.logger.debug( - `Dropped ${sortedEntries.length - returnedEntries.length} entries because of token limit` - ); + const droppedEntries = sortedEntries.length - returnedEntries.length; + if (droppedEntries > 0) { + this.dependencies.logger.info(`Dropped ${droppedEntries} entries because of token limit`); } return { diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/convert_deserialized_xml_with_json_schema.test.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/convert_deserialized_xml_with_json_schema.test.ts new file mode 100644 index 0000000000000..8d1d64721abc4 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/convert_deserialized_xml_with_json_schema.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { convertDeserializedXmlWithJsonSchema } from './convert_deserialized_xml_with_json_schema'; + +describe('deserializeXmlWithJsonSchema', () => { + it('deserializes XML into a JSON object according to the JSON schema', () => { + expect( + convertDeserializedXmlWithJsonSchema( + [ + { + foo: ['bar'], + }, + ], + { + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + } + ) + ).toEqual({ foo: 'bar' }); + }); + + it('converts strings to numbers if needed', () => { + expect( + convertDeserializedXmlWithJsonSchema( + [ + { + myNumber: ['0'], + }, + ], + { + type: 'object', + properties: { + myNumber: { + type: 'number', + }, + }, + } + ) + ).toEqual({ myNumber: 0 }); + }); + + it('de-dots object paths', () => { + expect( + convertDeserializedXmlWithJsonSchema( + [ + { + 'myObject.foo': ['bar'], + }, + ], + { + type: 'object', + properties: { + myObject: { + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + }, + }, + } + ) + ).toEqual({ + myObject: { + foo: 'bar', + }, + }); + }); + + it('casts to an array if needed', () => { + expect( + convertDeserializedXmlWithJsonSchema( + [ + { + myNumber: ['0'], + }, + ], + { + type: 'object', + properties: { + myNumber: { + type: 'number', + }, + }, + } + ) + ).toEqual({ + myNumber: 0, + }); + + expect( + convertDeserializedXmlWithJsonSchema( + [ + { + 'labels.myProp': ['myFirstValue, mySecondValue'], + }, + ], + { + type: 'object', + properties: { + labels: { + type: 'array', + items: { + type: 'object', + properties: { + myProp: { + type: 'string', + }, + }, + }, + }, + }, + } + ) + ).toEqual({ + labels: [{ myProp: 'myFirstValue' }, { myProp: 'mySecondValue' }], + }); + }); +}); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/convert_deserialized_xml_with_json_schema.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/convert_deserialized_xml_with_json_schema.ts new file mode 100644 index 0000000000000..a351edb9a33a1 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/convert_deserialized_xml_with_json_schema.ts @@ -0,0 +1,106 @@ +/* + * 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 { set } from '@kbn/safer-lodash-set'; +import { unflatten } from 'flat'; +import type { JSONSchema } from 'json-schema-to-ts'; +import { forEach, get, isPlainObject } from 'lodash'; +import { jsonSchemaToFlatParameters } from './json_schema_to_flat_parameters'; + +// JS to XML is "lossy", e.g. everything becomes an array and a string, +// so we need a JSON schema to deserialize it + +export function convertDeserializedXmlWithJsonSchema( + parameterResults: Array<Record<string, string[]>>, + schema: JSONSchema +): Record<string, any> { + const parameters = jsonSchemaToFlatParameters(schema); + + const result: Record<string, any> = Object.fromEntries( + parameterResults.flatMap((parameterResult) => { + return Object.keys(parameterResult).map((name) => { + return [name, parameterResult[name]]; + }); + }) + ); + + parameters.forEach((param) => { + const key = param.name; + let value: any[] = result[key] ?? []; + value = param.array + ? String(value) + .split(',') + .map((val) => val.trim()) + : value; + + switch (param.type) { + case 'number': + value = value.map((val) => Number(val)); + break; + + case 'integer': + value = value.map((val) => Math.floor(Number(val))); + break; + + case 'boolean': + value = value.map((val) => String(val).toLowerCase() === 'true' || val === '1'); + break; + } + + result[key] = param.array ? value : value[0]; + }); + + function getArrayPaths(subSchema: JSONSchema, path: string = ''): string[] { + if (typeof subSchema === 'boolean') { + return []; + } + + if (subSchema.type === 'object') { + return Object.keys(subSchema.properties!).flatMap((key) => { + return getArrayPaths(subSchema.properties![key], path ? path + '.' + key : key); + }); + } + + if (subSchema.type === 'array') { + return [path, ...getArrayPaths(subSchema.items as JSONSchema, path)]; + } + + return []; + } + + const arrayPaths = getArrayPaths(schema); + + const unflattened: Record<string, any> = unflatten(result); + + arrayPaths.forEach((arrayPath) => { + const target: any[] = []; + function walk(value: any, path: string) { + if (Array.isArray(value)) { + value.forEach((val, index) => { + if (!target[index]) { + target[index] = {}; + } + if (path) { + set(target[index], path, val); + } else { + target[index] = val; + } + }); + } else if (isPlainObject(value)) { + forEach(value, (val, key) => { + walk(val, path ? path + '.' + key : key); + }); + } + } + const val = get(unflattened, arrayPath); + + walk(val, ''); + + set(unflattened, arrayPath, target); + }); + + return unflattened; +} diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/eventsource_stream_into_observable.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/eventsource_stream_into_observable.ts new file mode 100644 index 0000000000000..5ff332128f8ac --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/eventsource_stream_into_observable.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createParser } from 'eventsource-parser'; +import { Readable } from 'node:stream'; +import { Observable } from 'rxjs'; + +// OpenAI sends server-sent events, so we can use a library +// to deal with parsing, buffering, unicode etc + +export function eventsourceStreamIntoObservable(readable: Readable) { + return new Observable<string>((subscriber) => { + const parser = createParser((event) => { + if (event.type === 'event') { + subscriber.next(event.data); + } + }); + + async function processStream() { + for await (const chunk of readable) { + parser.feed(chunk.toString()); + } + } + + processStream().then( + () => { + subscriber.complete(); + }, + (error) => { + subscriber.error(error); + } + ); + }); +} diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/eventstream_serde_into_observable.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/eventstream_serde_into_observable.ts new file mode 100644 index 0000000000000..9252ec7588e3e --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/eventstream_serde_into_observable.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 { EventStreamMarshaller } from '@smithy/eventstream-serde-node'; +import { fromUtf8, toUtf8 } from '@smithy/util-utf8'; +import { identity } from 'lodash'; +import { Observable } from 'rxjs'; +import { Readable } from 'stream'; +import { Message } from '@smithy/types'; + +interface ModelStreamErrorException { + name: 'ModelStreamErrorException'; + originalStatusCode?: number; + originalMessage?: string; +} + +export interface BedrockChunkMember { + chunk: Message; +} + +export interface ModelStreamErrorExceptionMember { + modelStreamErrorException: ModelStreamErrorException; +} + +export type BedrockStreamMember = BedrockChunkMember | ModelStreamErrorExceptionMember; + +// AWS uses SerDe to send over serialized data, so we use their +// @smithy library to parse the stream data + +export function eventstreamSerdeIntoObservable(readable: Readable) { + return new Observable<BedrockStreamMember>((subscriber) => { + const marshaller = new EventStreamMarshaller({ + utf8Encoder: toUtf8, + utf8Decoder: fromUtf8, + }); + + async function processStream() { + for await (const chunk of marshaller.deserialize(readable, identity)) { + if (chunk) { + subscriber.next(chunk as BedrockStreamMember); + } + } + } + + processStream().then( + () => { + subscriber.complete(); + }, + (error) => { + subscriber.error(error); + } + ); + }); +} diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/flush_buffer.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/flush_buffer.ts new file mode 100644 index 0000000000000..22723f1e49966 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/flush_buffer.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { repeat } from 'lodash'; +import { identity, Observable, OperatorFunction } from 'rxjs'; +import { + BufferFlushEvent, + StreamingChatResponseEventType, + StreamingChatResponseEventWithoutError, +} from '../../../common/conversation_complete'; + +// The Cloud proxy currently buffers 4kb or 8kb of data until flushing. +// This decreases the responsiveness of the streamed response, +// so we manually insert some data every 250ms if needed to force it +// to flush. + +export function flushBuffer<T extends StreamingChatResponseEventWithoutError>( + isCloud: boolean +): OperatorFunction<T, T | BufferFlushEvent> { + if (!isCloud) { + return identity; + } + + return (source: Observable<T>) => + new Observable<T | BufferFlushEvent>((subscriber) => { + const cloudProxyBufferSize = 4096; + let currentBufferSize: number = 0; + + const flushBufferIfNeeded = () => { + if (currentBufferSize && currentBufferSize <= cloudProxyBufferSize) { + subscriber.next({ + data: repeat('0', cloudProxyBufferSize * 2), + type: StreamingChatResponseEventType.BufferFlush, + }); + currentBufferSize = 0; + } + }; + + const intervalId = setInterval(flushBufferIfNeeded, 250); + + source.subscribe({ + next: (value) => { + currentBufferSize = + currentBufferSize <= cloudProxyBufferSize + ? JSON.stringify(value).length + currentBufferSize + : cloudProxyBufferSize; + subscriber.next(value); + }, + error: (error) => { + clearInterval(intervalId); + subscriber.error(error); + }, + complete: () => { + clearInterval(intervalId); + subscriber.complete(); + }, + }); + }); +} diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/json_schema_to_flat_parameters.test.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/json_schema_to_flat_parameters.test.ts new file mode 100644 index 0000000000000..afcfedf71dc85 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/json_schema_to_flat_parameters.test.ts @@ -0,0 +1,208 @@ +/* + * 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 { jsonSchemaToFlatParameters } from './json_schema_to_flat_parameters'; + +describe('jsonSchemaToFlatParameters', () => { + it('converts a simple object', () => { + expect( + jsonSchemaToFlatParameters({ + type: 'object', + properties: { + str: { + type: 'string', + }, + bool: { + type: 'boolean', + }, + }, + }) + ).toEqual([ + { + name: 'str', + type: 'string', + required: false, + }, + { + name: 'bool', + type: 'boolean', + required: false, + }, + ]); + }); + + it('handles descriptions', () => { + expect( + jsonSchemaToFlatParameters({ + type: 'object', + properties: { + str: { + type: 'string', + description: 'My string', + }, + }, + }) + ).toEqual([ + { + name: 'str', + type: 'string', + required: false, + description: 'My string', + }, + ]); + }); + + it('handles required properties', () => { + expect( + jsonSchemaToFlatParameters({ + type: 'object', + properties: { + str: { + type: 'string', + }, + bool: { + type: 'boolean', + }, + }, + required: ['str'], + }) + ).toEqual([ + { + name: 'str', + type: 'string', + required: true, + }, + { + name: 'bool', + type: 'boolean', + required: false, + }, + ]); + }); + + it('handles objects', () => { + expect( + jsonSchemaToFlatParameters({ + type: 'object', + properties: { + nested: { + type: 'object', + properties: { + str: { + type: 'string', + }, + }, + }, + }, + required: ['str'], + }) + ).toEqual([ + { + name: 'nested.str', + required: false, + type: 'string', + }, + ]); + }); + + it('handles arrays', () => { + expect( + jsonSchemaToFlatParameters({ + type: 'object', + properties: { + arr: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + required: ['str'], + }) + ).toEqual([ + { + name: 'arr', + required: false, + array: true, + type: 'string', + }, + ]); + + expect( + jsonSchemaToFlatParameters({ + type: 'object', + properties: { + arr: { + type: 'array', + items: { + type: 'object', + properties: { + foo: { + type: 'string', + }, + bar: { + type: 'object', + properties: { + baz: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + required: ['arr.foo.bar'], + }) + ).toEqual([ + { + name: 'arr.foo', + required: false, + array: true, + type: 'string', + }, + { + name: 'arr.bar.baz', + required: false, + array: true, + type: 'string', + }, + ]); + }); + + it('handles enum and const', () => { + expect( + jsonSchemaToFlatParameters({ + type: 'object', + properties: { + constant: { + type: 'string', + const: 'foo', + }, + enum: { + type: 'number', + enum: ['foo', 'bar'], + }, + }, + required: ['str'], + }) + ).toEqual([ + { + name: 'constant', + required: false, + type: 'string', + constant: 'foo', + }, + { + name: 'enum', + required: false, + type: 'number', + enum: ['foo', 'bar'], + }, + ]); + }); +}); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/json_schema_to_flat_parameters.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/json_schema_to_flat_parameters.ts new file mode 100644 index 0000000000000..cd984b0cfd7d0 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/json_schema_to_flat_parameters.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 type { JSONSchema } from 'json-schema-to-ts'; +import { castArray, isArray } from 'lodash'; + +interface Parameter { + name: string; + type: string; + description?: string; + required?: boolean; + enum?: unknown[]; + constant?: unknown; + array?: boolean; +} + +export function jsonSchemaToFlatParameters( + schema: JSONSchema, + name: string = '', + options: { required?: boolean; array?: boolean } = {} +): Parameter[] { + if (typeof schema === 'boolean') { + return []; + } + + switch (schema.type) { + case 'string': + case 'number': + case 'boolean': + case 'integer': + case 'null': + return [ + { + name, + type: schema.type, + description: schema.description, + array: options.array, + required: options.required, + constant: schema.const, + enum: schema.enum !== undefined ? castArray(schema.enum) : schema.enum, + }, + ]; + + case 'array': + if ( + typeof schema.items === 'boolean' || + typeof schema.items === 'undefined' || + isArray(schema.items) + ) { + return []; + } + return jsonSchemaToFlatParameters(schema.items as JSONSchema, name, { + ...options, + array: true, + }); + + default: + case 'object': + if (typeof schema.properties === 'undefined') { + return []; + } + return Object.entries(schema.properties).flatMap(([key, subSchema]) => { + return jsonSchemaToFlatParameters(subSchema, name ? `${name}.${key}` : key, { + ...options, + required: schema.required && schema.required.includes(key) ? true : false, + }); + }); + } +} 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 a1ec52918453f..3ca09acde2b6f 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 @@ -8,14 +8,15 @@ import { Observable } from 'rxjs'; import { PassThrough } from 'stream'; import { + BufferFlushEvent, ChatCompletionErrorEvent, isChatCompletionError, - StreamingChatResponseEvent, StreamingChatResponseEventType, + StreamingChatResponseEventWithoutError, } from '../../../common/conversation_complete'; export function observableIntoStream( - source: Observable<Exclude<StreamingChatResponseEvent, ChatCompletionErrorEvent>> + source: Observable<StreamingChatResponseEventWithoutError | BufferFlushEvent> ) { const stream = new PassThrough(); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/stream_into_observable.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/stream_into_observable.ts index 764e39fdec152..b2c65c51da9cc 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/util/stream_into_observable.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/stream_into_observable.ts @@ -5,20 +5,25 @@ * 2.0. */ -import { concatMap, filter, from, map, Observable } from 'rxjs'; +import { Observable } from 'rxjs'; import type { Readable } from 'stream'; -export function streamIntoObservable(readable: Readable): Observable<string> { - let lineBuffer = ''; +export function streamIntoObservable(readable: Readable): Observable<any> { + return new Observable<string>((subscriber) => { + const decodedStream = readable; - return from(readable).pipe( - map((chunk: Buffer) => chunk.toString('utf-8')), - map((part) => { - const lines = (lineBuffer + part).split('\n'); - lineBuffer = lines.pop() || ''; // Keep the last incomplete line for the next chunk - return lines; - }), - concatMap((lines) => lines), - filter((line) => line.trim() !== '') - ); + async function processStream() { + for await (const chunk of decodedStream) { + subscriber.next(chunk); + } + } + + processStream() + .then(() => { + subscriber.complete(); + }) + .catch((error) => { + subscriber.error(error); + }); + }); } diff --git a/x-pack/plugins/observability_ai_assistant/server/types.ts b/x-pack/plugins/observability_ai_assistant/server/types.ts index ea2d3ee39e426..21fcc21f39a65 100644 --- a/x-pack/plugins/observability_ai_assistant/server/types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/types.ts @@ -23,7 +23,8 @@ import type { } from '@kbn/data-views-plugin/server'; import type { MlPluginSetup, MlPluginStart } from '@kbn/ml-plugin/server'; import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/server'; -import { ObservabilityAIAssistantService } from './service'; +import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server'; +import type { ObservabilityAIAssistantService } from './service'; export interface ObservabilityAIAssistantPluginSetup { /** @@ -47,6 +48,7 @@ export interface ObservabilityAIAssistantPluginSetupDependencies { dataViews: DataViewsServerPluginSetup; ml: MlPluginSetup; licensing: LicensingPluginSetup; + cloud?: CloudSetup; } export interface ObservabilityAIAssistantPluginStartDependencies { actions: ActionsPluginStart; @@ -56,4 +58,5 @@ export interface ObservabilityAIAssistantPluginStartDependencies { dataViews: DataViewsServerPluginStart; ml: MlPluginStart; licensing: LicensingPluginStart; + cloud?: CloudStart; } diff --git a/x-pack/plugins/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_ai_assistant/tsconfig.json index f5a29c470fe7a..13af731fd49db 100644 --- a/x-pack/plugins/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_ai_assistant/tsconfig.json @@ -61,6 +61,8 @@ "@kbn/apm-synthtrace-client", "@kbn/apm-synthtrace", "@kbn/code-editor", + "@kbn/safer-lodash-set", + "@kbn/cloud-plugin", "@kbn/ui-actions-plugin", "@kbn/expressions-plugin", "@kbn/visualization-utils", diff --git a/x-pack/plugins/observability_shared/public/components/feature_feedback_button/feature_feedback_button.tsx b/x-pack/plugins/observability_shared/public/components/feature_feedback_button/feature_feedback_button.tsx index c8e684294dc0f..d10d8262ee3d6 100644 --- a/x-pack/plugins/observability_shared/public/components/feature_feedback_button/feature_feedback_button.tsx +++ b/x-pack/plugins/observability_shared/public/components/feature_feedback_button/feature_feedback_button.tsx @@ -23,26 +23,48 @@ const getDeploymentType = (isCloudEnv?: boolean, isServerlessEnv?: boolean): str return 'Self-Managed (you manage)'; }; -const getSurveyFeedbackURL = ( - formUrl: string, - kibanaVersion?: string, - deploymentType?: string, - sanitizedPath?: string -) => { +const getSurveyFeedbackURL = ({ + formUrl, + formConfig, + kibanaVersion, + deploymentType, + sanitizedPath, +}: { + formUrl: string; + formConfig?: FormConfig; + kibanaVersion?: string; + deploymentType?: string; + sanitizedPath?: string; +}) => { const url = new URL(formUrl); if (kibanaVersion) { - url.searchParams.append(KIBANA_VERSION_QUERY_PARAM, kibanaVersion); + url.searchParams.append( + formConfig?.kibanaVersionQueryParam || KIBANA_VERSION_QUERY_PARAM, + kibanaVersion + ); } if (deploymentType) { - url.searchParams.append(KIBANA_DEPLOYMENT_TYPE_PARAM, deploymentType); + url.searchParams.append( + formConfig?.kibanaDeploymentTypeQueryParam || KIBANA_DEPLOYMENT_TYPE_PARAM, + deploymentType + ); } if (sanitizedPath) { - url.searchParams.append(SANITIZED_PATH_PARAM, sanitizedPath); + url.searchParams.append( + formConfig?.sanitizedPathQueryParam || SANITIZED_PATH_PARAM, + sanitizedPath + ); } return url.href; }; +interface FormConfig { + kibanaVersionQueryParam?: string; + kibanaDeploymentTypeQueryParam?: string; + sanitizedPathQueryParam?: string; +} + interface FeatureFeedbackButtonProps { formUrl: string; 'data-test-subj': string; @@ -53,10 +75,12 @@ interface FeatureFeedbackButtonProps { isCloudEnv?: boolean; isServerlessEnv?: boolean; sanitizedPath?: string; + formConfig?: FormConfig; } export const FeatureFeedbackButton = ({ formUrl, + formConfig, 'data-test-subj': dts, onClickCapture, defaultButton, @@ -78,7 +102,13 @@ export const FeatureFeedbackButton = ({ return ( <EuiButton - href={getSurveyFeedbackURL(formUrl, kibanaVersion, deploymentType, sanitizedPath)} + href={getSurveyFeedbackURL({ + formUrl, + formConfig, + kibanaVersion, + deploymentType, + sanitizedPath, + })} target="_blank" color={defaultButton ? undefined : 'warning'} iconType={defaultButton ? undefined : 'editorComment'} diff --git a/x-pack/plugins/observability_solution/logs_explorer/common/constants.ts b/x-pack/plugins/observability_solution/logs_explorer/common/constants.ts index 37182444b8894..7839a5c0654cd 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/common/constants.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { SmartFieldGridColumnOptions } from './display_options'; + export const LOGS_EXPLORER_PROFILE_ID = 'logs-explorer'; // Fields constants @@ -34,6 +36,14 @@ export const ORCHESTRATOR_NAMESPACE_FIELD = 'orchestrator.namespace'; export const CONTAINER_NAME_FIELD = 'container.name'; export const CONTAINER_ID_FIELD = 'container.id'; +// Malformed Docs +export const MALFORMED_DOCS_FIELD = 'ignored_field_values'; + +// Error Stacktrace +export const ERROR_STACK_TRACE = 'error.stack_trace'; +export const ERROR_EXCEPTION_STACKTRACE = 'error.exception.stacktrace'; +export const ERROR_LOG_STACKTRACE = 'error.log.stacktrace'; + // Virtual column fields export const CONTENT_FIELD = 'content'; export const RESOURCE_FIELD = 'resource'; @@ -41,15 +51,26 @@ export const RESOURCE_FIELD = 'resource'; // Sizing export const DATA_GRID_COLUMN_WIDTH_SMALL = 240; export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320; +export const ACTIONS_COLUMN_WIDTH = 80; + +export const RESOURCE_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = { + type: 'smart-field', + smartField: RESOURCE_FIELD, + fallbackFields: [HOST_NAME_FIELD, SERVICE_NAME_FIELD], + width: DATA_GRID_COLUMN_WIDTH_MEDIUM, +}; + +export const CONTENT_FIELD_CONFIGURATION: SmartFieldGridColumnOptions = { + type: 'smart-field', + smartField: CONTENT_FIELD, + fallbackFields: [MESSAGE_FIELD], +}; + +export const SMART_FALLBACK_FIELDS = { + [CONTENT_FIELD]: CONTENT_FIELD_CONFIGURATION, + [RESOURCE_FIELD]: RESOURCE_FIELD_CONFIGURATION, +}; // UI preferences -export const DEFAULT_COLUMNS = [ - { - field: RESOURCE_FIELD, - width: DATA_GRID_COLUMN_WIDTH_MEDIUM, - }, - { - field: CONTENT_FIELD, - }, -]; +export const DEFAULT_COLUMNS = [RESOURCE_FIELD_CONFIGURATION, CONTENT_FIELD_CONFIGURATION]; export const DEFAULT_ROWS_PER_PAGE = 100; diff --git a/x-pack/plugins/observability_solution/logs_explorer/common/display_options/types.ts b/x-pack/plugins/observability_solution/logs_explorer/common/display_options/types.ts index b4c482d088a54..6251dee26c6ba 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/common/display_options/types.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/common/display_options/types.ts @@ -11,11 +11,21 @@ export interface ChartDisplayOptions { export type PartialChartDisplayOptions = Partial<ChartDisplayOptions>; -export interface GridColumnDisplayOptions { +export interface DocumentFieldGridColumnOptions { + type: 'document-field'; field: string; width?: number; } +export interface SmartFieldGridColumnOptions { + type: 'smart-field'; + smartField: 'content' | 'resource'; + fallbackFields: string[]; + width?: number; +} + +export type GridColumnDisplayOptions = DocumentFieldGridColumnOptions | SmartFieldGridColumnOptions; + export interface GridRowsDisplayOptions { rowHeight: number; rowsPerPage: number; diff --git a/x-pack/plugins/observability_solution/logs_explorer/common/document.ts b/x-pack/plugins/observability_solution/logs_explorer/common/document.ts index e9b97f1671f1e..778d8546c2d1c 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/common/document.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/common/document.ts @@ -33,6 +33,10 @@ export interface LogDocument extends DataTableRecord { 'log.file.path'?: string; 'data_stream.namespace': string; 'data_stream.dataset': string; + + 'error.stack_trace'?: string; + 'error.exception.stacktrace'?: string; + 'error.log.stacktrace'?: string; }; } @@ -74,3 +78,9 @@ export interface ResourceFields { 'container.id'?: string; 'cloud.instance.id'?: string; } + +export interface StackTraceFields { + 'error.stack_trace'?: string; + 'error.exception.stacktrace'?: string; + 'error.log.stacktrace'?: string; +} diff --git a/x-pack/plugins/observability_solution/logs_explorer/common/index.ts b/x-pack/plugins/observability_solution/logs_explorer/common/index.ts index 5466a00ae0caa..0f593cb8ad072 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/common/index.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/common/index.ts @@ -30,3 +30,10 @@ export type { PartialGridDisplayOptions, PartialGridRowsDisplayOptions, } from './display_options'; + +export { + CONTENT_FIELD, + CONTENT_FIELD_CONFIGURATION, + RESOURCE_FIELD_CONFIGURATION, + SMART_FALLBACK_FIELDS, +} from './constants'; diff --git a/x-pack/plugins/observability_solution/logs_explorer/common/locators/logs_explorer/logs_explorer_locator.ts b/x-pack/plugins/observability_solution/logs_explorer/common/locators/logs_explorer/logs_explorer_locator.ts index ec8b2df56cb79..c60b0ca74dddb 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/common/locators/logs_explorer/logs_explorer_locator.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/common/locators/logs_explorer/logs_explorer_locator.ts @@ -21,7 +21,7 @@ export class LogsExplorerLocatorDefinition implements LocatorDefinition<LogsExpl constructor(protected readonly deps: LogsExplorerLocatorDependencies) {} public readonly getLocation = (params: LogsExplorerLocatorParams) => { - const { dataset } = params; + const { dataset, columns } = params; const dataViewSpec: DataViewSpec | undefined = dataset ? { id: dataset, @@ -29,8 +29,13 @@ export class LogsExplorerLocatorDefinition implements LocatorDefinition<LogsExpl } : undefined; + const discoverColumns = columns?.map((column) => { + return column.type === 'document-field' ? column.field : column.smartField; + }); + return this.deps.discoverAppLocator?.getLocation({ ...params, + columns: discoverColumns, dataViewId: dataset, dataViewSpec, })!; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/common/log_level.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/common/log_level.tsx index 5cc883b1f40b4..9f5dfef23ce0d 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/common/log_level.tsx +++ b/x-pack/plugins/observability_solution/logs_explorer/public/components/common/log_level.tsx @@ -31,11 +31,13 @@ export function LogLevel({ level, dataTestSubj, renderInFlyout = false }: LogLev ? euiTheme.colors[LEVEL_DICT[level as keyof typeof LEVEL_DICT]] : null; + const truncatedLogLevel = level.length > 10 ? level.substring(0, 10) + '...' : level; + if (renderInFlyout) { return ( <ChipWithPopover property={constants.LOG_LEVEL_FIELD} - text={level} + text={truncatedLogLevel} borderColor={levelColor} style={{ width: 'none' }} dataTestSubj={dataTestSubj} @@ -50,7 +52,7 @@ export function LogLevel({ level, dataTestSubj, renderInFlyout = false }: LogLev text={level} rightSideIcon="arrowDown" borderColor={levelColor} - style={{ width: '80px' }} + style={{ width: '80px', marginTop: '-3px' }} /> ); } diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/common/popover_chip.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/common/popover_chip.tsx index 953c75bc480cc..2811bbf5480c4 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/common/popover_chip.tsx +++ b/x-pack/plugins/observability_solution/logs_explorer/public/components/common/popover_chip.tsx @@ -78,7 +78,8 @@ export function ChipWithPopover({ font-size: ${xsFontSize}; display: flex; justify-content: center; - margin-top: -3px; + ${shouldRenderPopover && `margin-right: 4px; margin-top: -3px;`} + cursor: pointer; `} style={style} > diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/common/translations.ts b/x-pack/plugins/observability_solution/logs_explorer/public/components/common/translations.tsx similarity index 69% rename from x-pack/plugins/observability_solution/logs_explorer/public/components/common/translations.ts rename to x-pack/plugins/observability_solution/logs_explorer/public/components/common/translations.tsx index 168ea6e2995cb..5debc38c3f25f 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/common/translations.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/public/components/common/translations.tsx @@ -5,7 +5,10 @@ * 2.0. */ +import React from 'react'; import { i18n } from '@kbn/i18n'; +import { EuiCode } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; export const flyoutContentLabel = i18n.translate('xpack.logsExplorer.flyoutDetail.label.message', { defaultMessage: 'Content breakdown', @@ -22,6 +25,17 @@ export const resourceLabel = i18n.translate( } ); +export const actionsLabel = i18n.translate('xpack.logsExplorer.dataTable.header.popover.actions', { + defaultMessage: 'Actions', +}); + +export const actionsLabelLowerCase = i18n.translate( + 'xpack.logsExplorer.dataTable.header.popover.actions.lowercase', + { + defaultMessage: 'actions', + } +); + export const flyoutServiceLabel = i18n.translate('xpack.logsExplorer.flyoutDetail.label.service', { defaultMessage: 'Service', }); @@ -208,17 +222,21 @@ export const closeCellActionPopoverText = i18n.translate( } ); -export const contentHeaderTooltipParagraph1 = i18n.translate( - 'xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1', - { - defaultMessage: "Fields that provide information on the document's source, such as:", - } +export const contentHeaderTooltipParagraph1 = ( + <FormattedMessage + id="xpack.logsExplorer.dataTable.header.content.tooltip.paragraph1" + defaultMessage="Displays the document's {logLevel} and {message} fields." + values={{ + logLevel: <strong>log.level</strong>, + message: <strong>message</strong>, + }} + /> ); export const contentHeaderTooltipParagraph2 = i18n.translate( 'xpack.logsExplorer.dataTable.header.content.tooltip.paragraph2', { - defaultMessage: 'When the message field is empty, one of the following is displayed', + defaultMessage: 'When the message field is empty, one of the following is displayed:', } ); @@ -228,3 +246,63 @@ export const resourceHeaderTooltipParagraph = i18n.translate( defaultMessage: "Fields that provide information on the document's source, such as:", } ); + +export const actionsHeaderTooltipParagraph = i18n.translate( + 'xpack.logsExplorer.dataTable.header.actions.tooltip.paragraph', + { + defaultMessage: 'Fields that provide actionable information, such as:', + } +); + +export const actionsHeaderTooltipExpandAction = i18n.translate( + 'xpack.logsExplorer.dataTable.header.actions.tooltip.expand', + { defaultMessage: 'Expand log details' } +); + +export const actionsHeaderTooltipMalformedAction = ( + <FormattedMessage + id="xpack.logsExplorer.dataTable.controlColumn.actions.button.malformedDoc" + defaultMessage="Access to malformed doc with {ignoredProperty} field" + values={{ + ignoredProperty: ( + <EuiCode language="json" transparentBackground> + _ignored + </EuiCode> + ), + }} + /> +); + +export const actionsHeaderTooltipStacktraceAction = i18n.translate( + 'xpack.logsExplorer.dataTable.header.actions.tooltip.stacktrace', + { defaultMessage: 'Access to available stacktraces based on:' } +); + +export const malformedDocButtonLabelWhenPresent = i18n.translate( + 'xpack.logsExplorer.dataTable.controlColumn.actions.button.malformedDocPresent', + { + defaultMessage: + "This document couldn't be parsed correctly. Not all fields are properly populated", + } +); + +export const malformedDocButtonLabelWhenNotPresent = i18n.translate( + 'xpack.logsExplorer.dataTable.controlColumn.actions.button.malformedDocNotPresent', + { + defaultMessage: 'All fields in this document were parsed correctly', + } +); + +export const stacktraceAvailableControlButton = i18n.translate( + 'xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.available', + { + defaultMessage: 'Stacktraces available', + } +); + +export const stacktraceNotAvailableControlButton = i18n.translate( + 'xpack.logsExplorer.dataTable.controlColumn.actions.button.stacktrace.notAvailable', + { + defaultMessage: 'Stacktraces not available', + } +); diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/column_tooltips/actions_column_tooltip.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/column_tooltips/actions_column_tooltip.tsx new file mode 100644 index 0000000000000..95c7a7b4c9468 --- /dev/null +++ b/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/column_tooltips/actions_column_tooltip.tsx @@ -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 React from 'react'; +import { css } from '@emotion/react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { + actionsHeaderTooltipExpandAction, + actionsHeaderTooltipMalformedAction, + actionsHeaderTooltipParagraph, + actionsHeaderTooltipStacktraceAction, + actionsLabel, + actionsLabelLowerCase, +} from '../../common/translations'; +import { HoverPopover } from '../../common/hover_popover'; +import { TooltipButtonComponent } from './tooltip_button'; +import * as constants from '../../../../common/constants'; +import { FieldWithToken } from './field_with_token'; + +const spacingCSS = css` + margin-bottom: ${euiThemeVars.euiSizeS}; +`; + +export const ActionsColumnTooltip = () => { + return ( + <HoverPopover + button={<TooltipButtonComponent displayText={actionsLabelLowerCase} />} + title={actionsLabel} + > + <div style={{ width: '230px' }}> + <EuiText size="s" css={spacingCSS}> + <p>{actionsHeaderTooltipParagraph}</p> + </EuiText> + <EuiFlexGroup + responsive={false} + alignItems="baseline" + justifyContent="flexStart" + gutterSize="s" + css={spacingCSS} + > + <EuiFlexItem grow={false}> + <EuiIcon type="expand" size="s" /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiText size="s"> + <p>{actionsHeaderTooltipExpandAction}</p> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup + responsive={false} + alignItems="baseline" + justifyContent="flexStart" + gutterSize="s" + css={spacingCSS} + > + <EuiFlexItem grow={false}> + <EuiIcon type="indexClose" size="s" color="danger" /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiText size="s"> + <p>{actionsHeaderTooltipMalformedAction}</p> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup + responsive={false} + alignItems="baseline" + justifyContent="flexStart" + gutterSize="s" + css={spacingCSS} + > + <EuiFlexItem grow={false}> + <EuiIcon type="apmTrace" size="s" /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiText size="s"> + <p>{actionsHeaderTooltipStacktraceAction}</p> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + <div style={{ marginLeft: '15px' }}> + {[ + constants.ERROR_STACK_TRACE, + constants.ERROR_EXCEPTION_STACKTRACE, + constants.ERROR_LOG_STACKTRACE, + ].map((field) => ( + <FieldWithToken field={field} key={field} /> + ))} + </div> + </div> + </HoverPopover> + ); +}; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/column_tooltips/resource_column_tooltip.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/column_tooltips/resource_column_tooltip.tsx index 6e6f62e274a9f..57e51097e391a 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/column_tooltips/resource_column_tooltip.tsx +++ b/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/column_tooltips/resource_column_tooltip.tsx @@ -42,7 +42,7 @@ export const ResourceColumnTooltip = ({ column, headerRowHeight }: CustomGridCol constants.HOST_NAME_FIELD, constants.CLOUD_INSTANCE_ID_FIELD, ].map((field) => ( - <FieldWithToken field={field} /> + <FieldWithToken field={field} key={field} /> ))} </div> </HoverPopover> diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/resource.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/resource.tsx index e64b9c81c65bc..7cecdeee5ce40 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/resource.tsx +++ b/x-pack/plugins/observability_solution/logs_explorer/public/components/virtual_columns/resource.tsx @@ -7,7 +7,6 @@ import React from 'react'; import type { DataGridCellValueElementProps } from '@kbn/unified-data-table'; -import { first } from 'lodash'; import { AgentName } from '@kbn/elastic-agent-utils'; import { dynamic } from '@kbn/shared-ux-utility'; import { ChipWithPopover } from '../common/popover_chip'; @@ -27,10 +26,12 @@ export const Resource = ({ row }: DataGridCellValueElementProps) => { text={resourceDoc[constants.SERVICE_NAME_FIELD] as string} rightSideIcon="arrowDown" leftSideIcon={ - <AgentIcon - agentName={first((resourceDoc[constants.AGENT_NAME_FIELD] ?? []) as AgentName[])} - size="m" - /> + resourceDoc[constants.AGENT_NAME_FIELD] && ( + <AgentIcon + agentName={resourceDoc[constants.AGENT_NAME_FIELD] as AgentName} + size="m" + /> + ) } /> )} diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/customizations/custom_control_column.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/custom_control_column.tsx new file mode 100644 index 0000000000000..5caee8b6e82e1 --- /dev/null +++ b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/custom_control_column.tsx @@ -0,0 +1,151 @@ +/* + * 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, { ComponentClass } from 'react'; +import { + OPEN_DETAILS, + SELECT_ROW, + type ControlColumnsProps, + DataTableRowControl, +} from '@kbn/unified-data-table'; +import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui'; +import type { DataTableRecord } from '@kbn/discover-utils/src/types'; +import { useActor } from '@xstate/react'; +import { LogsExplorerControllerStateService } from '../state_machines/logs_explorer_controller'; +import { + malformedDocButtonLabelWhenNotPresent, + malformedDocButtonLabelWhenPresent, + stacktraceAvailableControlButton, + stacktraceNotAvailableControlButton, +} from '../components/common/translations'; +import * as constants from '../../common/constants'; +import { getStacktraceFields } from '../utils/get_stack_trace'; +import { LogDocument } from '../../common/document'; +import { ActionsColumnTooltip } from '../components/virtual_columns/column_tooltips/actions_column_tooltip'; + +const ConnectedMalformedDocs = ({ + rowIndex, + service, +}: { + rowIndex: number; + service: LogsExplorerControllerStateService; +}) => { + const [state] = useActor(service); + + if (state.matches('initialized') && state.context.rows) { + const row = state.context.rows[rowIndex]; + return <MalformedDocs row={row} rowIndex={rowIndex} />; + } + + return null; +}; + +const ConnectedStacktraceDocs = ({ + rowIndex, + service, +}: { + rowIndex: number; + service: LogsExplorerControllerStateService; +}) => { + const [state] = useActor(service); + + if (state.matches('initialized') && state.context.rows) { + const row = state.context.rows[rowIndex]; + return <Stacktrace row={row} rowIndex={rowIndex} />; + } + + return null; +}; + +const MalformedDocs = ({ row, rowIndex }: { row: DataTableRecord; rowIndex: number }) => { + const isMalformedDocumentExists = !!row.raw[constants.MALFORMED_DOCS_FIELD]; + + return isMalformedDocumentExists ? ( + <DataTableRowControl> + <EuiToolTip content={malformedDocButtonLabelWhenPresent} delay="long"> + <EuiButtonIcon + id={`malformedDocExists_${rowIndex}`} + size="xs" + iconSize="s" + data-test-subj={'docTableMalformedDocExist'} + color={'danger'} + aria-label={malformedDocButtonLabelWhenPresent} + iconType={'indexClose'} + /> + </EuiToolTip> + </DataTableRowControl> + ) : ( + <DataTableRowControl> + <EuiToolTip content={malformedDocButtonLabelWhenNotPresent} delay="long"> + <EuiButtonIcon + id={`malformedDocExists_${rowIndex}`} + size="xs" + iconSize="s" + data-test-subj={'docTableMalformedDocDoesNotExist'} + color={'text'} + iconType={'pagesSelect'} + aria-label={malformedDocButtonLabelWhenNotPresent} + /> + </EuiToolTip> + </DataTableRowControl> + ); +}; + +const Stacktrace = ({ row, rowIndex }: { row: DataTableRecord; rowIndex: number }) => { + const stacktrace = getStacktraceFields(row as LogDocument); + const hasValue = Object.values(stacktrace).some((value) => value); + + return ( + <DataTableRowControl> + <EuiToolTip + content={hasValue ? stacktraceAvailableControlButton : stacktraceNotAvailableControlButton} + delay="long" + > + <EuiButtonIcon + id={`stacktrace_${rowIndex}`} + size="xs" + iconSize="s" + data-test-subj={hasValue ? 'docTableStacktraceExist' : 'docTableStacktraceDoesNotExist'} + color={'text'} + iconType={'apmTrace'} + aria-label={ + hasValue ? stacktraceAvailableControlButton : stacktraceNotAvailableControlButton + } + disabled={!hasValue} + /> + </EuiToolTip> + </DataTableRowControl> + ); +}; + +export const createCustomControlColumnsConfiguration = + (service: LogsExplorerControllerStateService) => + ({ controlColumns }: ControlColumnsProps) => { + const checkBoxColumn = controlColumns[SELECT_ROW]; + const openDetails = controlColumns[OPEN_DETAILS]; + const ExpandButton = + openDetails.rowCellRender as ComponentClass<EuiDataGridCellValueElementProps>; + const actionsColumn = { + id: 'actionsColumn', + width: constants.ACTIONS_COLUMN_WIDTH, + headerCellRender: ActionsColumnTooltip, + rowCellRender: ({ rowIndex, setCellProps, ...rest }: EuiDataGridCellValueElementProps) => { + return ( + <span> + <ExpandButton rowIndex={rowIndex} setCellProps={setCellProps} {...rest} /> + <ConnectedMalformedDocs rowIndex={rowIndex} service={service} /> + <ConnectedStacktraceDocs rowIndex={rowIndex} service={service} /> + </span> + ); + }, + }; + + return { + leadingControlColumns: [checkBoxColumn], + trailingControlColumns: [actionsColumn], + }; + }; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx index 54147baf4a221..faa8c8d4a51ee 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx +++ b/x-pack/plugins/observability_solution/logs_explorer/public/customizations/logs_explorer_profile.tsx @@ -87,6 +87,9 @@ export const createLogsExplorerProfileCustomizations = id: 'data_table', customCellRenderer: createCustomCellRenderer({ data }), customGridColumnsConfiguration: createCustomGridColumnsConfiguration(), + customControlColumnsConfiguration: await import('./custom_control_column').then((module) => + module.createCustomControlColumnsConfiguration(service) + ), }); /** diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/index.ts b/x-pack/plugins/observability_solution/logs_explorer/public/index.ts index d4febf6775416..6e452f8904828 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/index.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/public/index.ts @@ -25,6 +25,7 @@ export { getDiscoverColumnsFromDisplayOptions, getDiscoverGridFromDisplayOptions, getDiscoverFiltersFromState, + getDiscoverColumnsWithFallbackFieldsFromDisplayOptions, } from './utils/convert_discover_app_state'; export function plugin(context: PluginInitializerContext<LogsExplorerConfig>) { diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/defaults.ts b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/defaults.ts index 0030f45c638d1..a097d23ac7349 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/defaults.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/defaults.ts @@ -40,4 +40,5 @@ export const DEFAULT_CONTEXT: DefaultLogsExplorerControllerState = { from: 'now-15m/m', to: 'now', }, + rows: [], }; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/services/discover_service.ts b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/services/discover_service.ts index 4abc91ca47d8d..b4644cb635d4a 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/services/discover_service.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/services/discover_service.ts @@ -26,9 +26,9 @@ export const subscribeToDiscoverState = throw new Error('Failed to subscribe to the Discover state: no state container in context.'); } - const { appState } = context.discoverStateContainer; + const { appState, dataState } = context.discoverStateContainer; - const subscription = appState.state$.subscribe({ + const appStateSubscription = appState.state$.subscribe({ next: (newAppState) => { if (isEmpty(newAppState)) { return; @@ -41,8 +41,20 @@ export const subscribeToDiscoverState = }, }); + const dataStateSubscription = dataState.data$.documents$.subscribe({ + next: (newDataState) => { + if (!isEmpty(newDataState?.result)) { + send({ + type: 'RECEIVE_DISCOVER_DATA_STATE', + dataState: newDataState.result, + }); + } + }, + }); + return () => { - subscription.unsubscribe(); + appStateSubscription.unsubscribe(); + dataStateSubscription.unsubscribe(); }; }; @@ -71,6 +83,19 @@ export const updateContextFromDiscoverAppState = actions.assign< return {}; }); +export const updateContextFromDiscoverDataState = actions.assign< + LogsExplorerControllerContext, + LogsExplorerControllerEvent +>((context, event) => { + if ('dataState' in event && event.type === 'RECEIVE_DISCOVER_DATA_STATE') { + return { + rows: event.dataState, + }; + } + + return {}; +}); + export const updateDiscoverAppStateFromContext: ActionFunction< LogsExplorerControllerContext, LogsExplorerControllerEvent diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/state_machine.ts b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/state_machine.ts index b1cba428d4c4e..3c244a73bbd5b 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/state_machine.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/state_machine.ts @@ -25,6 +25,7 @@ import { createAndSetDataView } from './services/data_view_service'; import { subscribeToDiscoverState, updateContextFromDiscoverAppState, + updateContextFromDiscoverDataState, updateDiscoverAppStateFromContext, } from './services/discover_service'; import { validateSelection } from './services/selection_service'; @@ -103,6 +104,7 @@ export const createPureLogsExplorerControllerStateMachine = ( id: 'timefilterService', }, ], + entry: ['resetRows'], states: { datasetSelection: { initial: 'validatingSelection', @@ -196,6 +198,9 @@ export const createPureLogsExplorerControllerStateMachine = ( RECEIVE_DISCOVER_APP_STATE: { actions: ['updateContextFromDiscoverAppState'], }, + RECEIVE_DISCOVER_DATA_STATE: { + actions: ['updateContextFromDiscoverDataState'], + }, RECEIVE_QUERY_STATE: { actions: ['updateQueryStateFromQueryServiceState'], }, @@ -239,8 +244,12 @@ export const createPureLogsExplorerControllerStateMachine = ( } : {} ), + resetRows: actions.assign((_context, event) => ({ + rows: [], + })), notifyDataViewUpdate: raise('DATA_VIEW_UPDATED'), updateContextFromDiscoverAppState, + updateContextFromDiscoverDataState, updateDiscoverAppStateFromContext, updateContextFromTimefilter, }, diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/types.ts b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/types.ts index f5a5abd964482..5e7d617a1cd4e 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/types.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/types.ts @@ -7,8 +7,13 @@ import { ControlGroupAPI } from '@kbn/controls-plugin/public'; import { QueryState, RefreshInterval, TimeRange } from '@kbn/data-plugin/common'; -import { DiscoverAppState, DiscoverStateContainer } from '@kbn/discover-plugin/public'; +import type { + DiscoverAppState, + DiscoverStateContainer, + DataDocumentsMsg, +} from '@kbn/discover-plugin/public'; import { DoneInvokeEvent } from 'xstate'; +import type { DataTableRecord } from '@kbn/discover-utils/src/types'; import { ControlPanels, DisplayOptions } from '../../../../common'; import type { DatasetEncodingError, DatasetSelection } from '../../../../common/dataset_selection'; @@ -32,9 +37,14 @@ export interface WithDiscoverStateContainer { discoverStateContainer: DiscoverStateContainer; } +export interface WithDataTableRecord { + rows: DataTableRecord[]; +} + export type DefaultLogsExplorerControllerState = WithDatasetSelection & WithQueryState & - WithDisplayOptions; + WithDisplayOptions & + WithDataTableRecord; export type LogsExplorerControllerTypeState = | { @@ -59,6 +69,7 @@ export type LogsExplorerControllerTypeState = WithControlPanels & WithQueryState & WithDisplayOptions & + WithDataTableRecord & WithDiscoverStateContainer; } | { @@ -67,6 +78,7 @@ export type LogsExplorerControllerTypeState = WithControlPanels & WithQueryState & WithDisplayOptions & + WithDataTableRecord & WithDiscoverStateContainer; } | { @@ -75,6 +87,7 @@ export type LogsExplorerControllerTypeState = WithControlPanels & WithQueryState & WithDisplayOptions & + WithDataTableRecord & WithDiscoverStateContainer; } | { @@ -83,6 +96,7 @@ export type LogsExplorerControllerTypeState = WithControlPanels & WithQueryState & WithDisplayOptions & + WithDataTableRecord & WithDiscoverStateContainer; } | { @@ -91,6 +105,7 @@ export type LogsExplorerControllerTypeState = WithControlPanels & WithQueryState & WithDisplayOptions & + WithDataTableRecord & WithDiscoverStateContainer; } | { @@ -99,6 +114,7 @@ export type LogsExplorerControllerTypeState = WithControlPanels & WithQueryState & WithDisplayOptions & + WithDataTableRecord & WithDiscoverStateContainer; } | { @@ -108,6 +124,7 @@ export type LogsExplorerControllerTypeState = WithControlPanels & WithQueryState & WithDisplayOptions & + WithDataTableRecord & WithDiscoverStateContainer; } | { @@ -117,6 +134,7 @@ export type LogsExplorerControllerTypeState = WithControlPanels & WithQueryState & WithDisplayOptions & + WithDataTableRecord & WithDiscoverStateContainer; }; @@ -151,6 +169,10 @@ export type LogsExplorerControllerEvent = type: 'RECEIVE_DISCOVER_APP_STATE'; appState: DiscoverAppState; } + | { + type: 'RECEIVE_DISCOVER_DATA_STATE'; + dataState: DataDocumentsMsg['result']; + } | { type: 'RECEIVE_TIMEFILTER_TIME'; time: TimeRange; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/utils/convert_discover_app_state.ts b/x-pack/plugins/observability_solution/logs_explorer/public/utils/convert_discover_app_state.ts index 90d51f75e8c7c..71ca190627f34 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/utils/convert_discover_app_state.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/public/utils/convert_discover_app_state.ts @@ -10,21 +10,28 @@ import { DiscoverAppState } from '@kbn/discover-plugin/public'; import { ExistsFilter, Filter, FILTERS, PhrasesFilter } from '@kbn/es-query'; import { PhraseFilterValue } from '@kbn/es-query/src/filters/build_filters'; import { cloneDeep } from 'lodash'; +import { CONTENT_FIELD, RESOURCE_FIELD, SMART_FALLBACK_FIELDS } from '../../common/constants'; import { ChartDisplayOptions, DisplayOptions, GridColumnDisplayOptions, GridRowsDisplayOptions, } from '../../common'; -import { ControlOptions, OptionsListControlOption } from '../controller'; +import type { ControlOptions, OptionsListControl } from '../controller'; export const getGridColumnDisplayOptionsFromDiscoverAppState = ( discoverAppState: DiscoverAppState ): GridColumnDisplayOptions[] | undefined => - discoverAppState.columns?.map((field) => ({ - field, - width: discoverAppState.grid?.columns?.[field]?.width, - })); + discoverAppState.columns?.map((field) => { + if (field === CONTENT_FIELD || field === RESOURCE_FIELD) { + return SMART_FALLBACK_FIELDS[field]; + } + return { + type: 'document-field', + field, + width: discoverAppState.grid?.columns?.[field]?.width, + }; + }); export const getGridRowsDisplayOptionsFromDiscoverAppState = ( discoverAppState: DiscoverAppState @@ -58,18 +65,32 @@ export const getDiscoverAppStateFromContext = ( filters: cloneDeep(displayOptions.filters), }); +export const getDiscoverColumnsWithFallbackFieldsFromDisplayOptions = ( + displayOptions: DisplayOptions +): DiscoverAppState['columns'] => + displayOptions.grid.columns.flatMap((column) => { + return column.type === 'document-field' + ? column.field + : SMART_FALLBACK_FIELDS[column.smartField].fallbackFields; + }); + export const getDiscoverColumnsFromDisplayOptions = ( displayOptions: DisplayOptions -): DiscoverAppState['columns'] => displayOptions.grid.columns.map(({ field }) => field); +): DiscoverAppState['columns'] => + displayOptions.grid.columns.flatMap((column) => { + return column.type === 'document-field' ? column.field : column.smartField; + }); export const getDiscoverGridFromDisplayOptions = ( displayOptions: DisplayOptions ): DiscoverAppState['grid'] => ({ columns: displayOptions.grid.columns.reduce< NonNullable<NonNullable<DiscoverAppState['grid']>['columns']> - >((gridColumns, { field, width }) => { - if (width != null) { - gridColumns[field] = { width }; + >((gridColumns, column) => { + const key = column.type === 'document-field' ? column.field : column.smartField; + + if (column.width != null) { + gridColumns[key] = { width: column.width }; } return gridColumns; }, {}), @@ -79,55 +100,78 @@ const createDiscoverPhrasesFilter = ({ key, values, negate, + index, }: { - values: PhraseFilterValue[]; + index: string; key: string; + values: PhraseFilterValue[]; negate?: boolean; -}): PhrasesFilter => - ({ - meta: { - key, - negate, - type: FILTERS.PHRASES, - params: values, - }, - query: { - bool: { - should: values.map((value) => ({ match_phrase: { [key]: value.toString() } })), - minimum_should_match: 1, - }, +}): PhrasesFilter => ({ + meta: { + index, + type: FILTERS.PHRASES, + key, + params: values.map((value) => value.toString()), + negate, + }, + query: { + bool: { + should: values.map((value) => ({ match_phrase: { [key]: value.toString() } })), + minimum_should_match: 1, }, - } as PhrasesFilter); + }, +}); const createDiscoverExistsFilter = ({ + index, key, negate, }: { key: string; + index: string; negate?: boolean; }): ExistsFilter => ({ meta: { + index, + type: FILTERS.EXISTS, + value: FILTERS.EXISTS, // Required for the filter to be displayed correctly in FilterBadge key, negate, - type: FILTERS.EXISTS, }, query: { exists: { field: key } }, }); -export const getDiscoverFiltersFromState = (filters: Filter[] = [], controls?: ControlOptions) => [ - ...filters, - ...(controls - ? (Object.keys(controls) as Array<keyof ControlOptions>).map((key) => - controls[key as keyof ControlOptions]?.selection.type === 'exists' - ? createDiscoverExistsFilter({ - key, - negate: controls[key]?.mode === 'exclude', - }) - : createDiscoverPhrasesFilter({ - key, - values: (controls[key]?.selection as OptionsListControlOption).selectedOptions, - negate: controls[key]?.mode === 'exclude', - }) - ) - : []), -]; +export const getDiscoverFiltersFromState = ( + index: string, + filters: Filter[] = [], + controls?: ControlOptions +) => { + return [ + ...filters, + ...(controls + ? (Object.entries(controls) as Array<[keyof ControlOptions, OptionsListControl]>).reduce< + Filter[] + >((acc, [key, control]) => { + if (control.selection.type === 'exists') { + acc.push( + createDiscoverExistsFilter({ + index, + key, + negate: control.mode === 'exclude', + }) + ); + } else if (control.selection.selectedOptions.length > 0) { + acc.push( + createDiscoverPhrasesFilter({ + index, + key, + values: control.selection.selectedOptions, + negate: control.mode === 'exclude', + }) + ); + } + return acc; + }, []) + : []), + ]; +}; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/utils/get_field_from_flattened_doc.ts b/x-pack/plugins/observability_solution/logs_explorer/public/utils/get_field_from_flattened_doc.ts new file mode 100644 index 0000000000000..7d05d9ab61583 --- /dev/null +++ b/x-pack/plugins/observability_solution/logs_explorer/public/utils/get_field_from_flattened_doc.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogDocument } from '../../common/document'; + +type Field = keyof LogDocument['flattened']; + +export const getFieldFromDoc = <T extends Field>(doc: LogDocument, field: T) => { + const fieldValueArray = doc.flattened[field]; + return fieldValueArray && fieldValueArray.length ? fieldValueArray[0] : undefined; +}; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/utils/get_stack_trace.ts b/x-pack/plugins/observability_solution/logs_explorer/public/utils/get_stack_trace.ts new file mode 100644 index 0000000000000..58eb44a7744c9 --- /dev/null +++ b/x-pack/plugins/observability_solution/logs_explorer/public/utils/get_stack_trace.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 { LogDocument, StackTraceFields } from '../../common/document'; +import * as constants from '../../common/constants'; +import { getFieldFromDoc } from './get_field_from_flattened_doc'; + +export const getStacktraceFields = (doc: LogDocument): StackTraceFields => { + const errorStackTrace = getFieldFromDoc(doc, constants.ERROR_STACK_TRACE); + const errorExceptionStackTrace = getFieldFromDoc(doc, constants.ERROR_EXCEPTION_STACKTRACE); + const errorLogStackTrace = getFieldFromDoc(doc, constants.ERROR_LOG_STACKTRACE); + + return { + [constants.ERROR_STACK_TRACE]: errorStackTrace, + [constants.ERROR_EXCEPTION_STACKTRACE]: errorExceptionStackTrace, + [constants.ERROR_LOG_STACKTRACE]: errorLogStackTrace, + }; +}; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/utils/resource.ts b/x-pack/plugins/observability_solution/logs_explorer/public/utils/resource.ts index 11123d8030481..1b1f3dd078008 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/utils/resource.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/public/utils/resource.ts @@ -7,13 +7,7 @@ import { LogDocument, ResourceFields } from '../../common/document'; import * as constants from '../../common/constants'; - -type Field = keyof LogDocument['flattened']; - -const getFieldFromDoc = <T extends Field>(doc: LogDocument, field: T) => { - const fieldValueArray = doc.flattened[field]; - return fieldValueArray && fieldValueArray.length ? fieldValueArray[0] : undefined; -}; +import { getFieldFromDoc } from './get_field_from_flattened_doc'; export const getUnformattedResourceFields = (doc: LogDocument): ResourceFields => { const serviceName = getFieldFromDoc(doc, constants.SERVICE_NAME_FIELD); diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/locators.test.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/locators.test.ts index 0c4615ad110e0..9da7b798c50b3 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/locators.test.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/locators.test.ts @@ -92,9 +92,9 @@ describe('Observability Logs Explorer Locators', () => { }); }); - it('should allow specifiying columns', async () => { + it('should allow specifying columns', async () => { const params: AllDatasetsLocatorParams = { - columns: ['_source'], + columns: [{ field: '_source', type: 'document-field' }], }; const { allDatasetsLocator } = await setup(); @@ -102,7 +102,7 @@ describe('Observability Logs Explorer Locators', () => { expect(location).toMatchObject({ app: OBSERVABILITY_LOGS_EXPLORER_APP_ID, - path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selectionType:all),v:1)`, + path: '/?pageState=(columns:!((field:_source,type:document-field)),datasetSelection:(selectionType:all),v:1)', state: {}, }); }); @@ -214,7 +214,7 @@ describe('Observability Logs Explorer Locators', () => { const params: SingleDatasetLocatorParams = { integration, dataset, - columns: ['_source'], + columns: [{ field: '_source', type: 'document-field' }], }; const { singleDatasetLocator } = await setup(); @@ -222,7 +222,7 @@ describe('Observability Logs Explorer Locators', () => { expect(location).toMatchObject({ app: OBSERVABILITY_LOGS_EXPLORER_APP_ID, - path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`, + path: `/?pageState=(columns:!((field:_source,type:document-field)),datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`, state: {}, }); }); diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/utils/construct_locator_path.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/utils/construct_locator_path.ts index 20245c6f0a72d..949dc6b1fcafe 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/utils/construct_locator_path.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/utils/construct_locator_path.ts @@ -15,6 +15,7 @@ import { AvailableControlPanels, availableControlsPanels, DatasetSelectionPlain, + SMART_FALLBACK_FIELDS, } from '@kbn/logs-explorer-plugin/common'; import { OBSERVABILITY_LOGS_EXPLORER_APP_ID } from '@kbn/deeplinks-observability'; import { @@ -46,7 +47,9 @@ export const constructLocatorPath = async (params: LocatorPathConstructionParams query, refreshInterval, time: timeRange, - columns: columns?.map((field) => ({ field })), + columns: columns?.map((column) => { + return column.type === 'smart-field' ? SMART_FALLBACK_FIELDS[column.smartField] : column; + }), controls: getControlsPageStateFromFilterControlsParams(filterControls ?? {}), }) ); diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/url_schema_v1.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/url_schema_v1.ts index dbeb3ae1fd274..2cdbe422009f3 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/url_schema_v1.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/url_schema_v1.ts @@ -8,8 +8,15 @@ import { availableControlsPanels, datasetSelectionPlainRT } from '@kbn/logs-explorer-plugin/common'; import * as rt from 'io-ts'; -export const columnRT = rt.intersection([ +const allowedNamesRT = rt.keyof({ + content: null, + resource: null, +}); + +// Define the runtime type for DocumentFieldGridColumnOptions +const documentFieldColumnRT = rt.intersection([ rt.strict({ + type: rt.literal('document-field'), field: rt.string, }), rt.exact( @@ -19,6 +26,22 @@ export const columnRT = rt.intersection([ ), ]); +// Define the runtime type for SmartFieldGridColumnOptions +const smartFieldColumnRT = rt.intersection([ + rt.strict({ + type: rt.literal('smart-field'), + smartField: allowedNamesRT, + fallbackFields: rt.array(rt.string), + }), + rt.exact( + rt.partial({ + width: rt.number, + }) + ), +]); + +export const columnRT = rt.union([documentFieldColumnRT, smartFieldColumnRT]); + export const columnsRT = rt.array(columnRT); export const optionsListControlRT = rt.strict({ diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/kibana.jsonc b/x-pack/plugins/observability_solution/observability_logs_explorer/kibana.jsonc index 42d762820aaad..8f6e248557efa 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/kibana.jsonc @@ -23,7 +23,12 @@ "datasetQuality" ], "optionalPlugins": [ - "serverless" + "serverless", + "triggersActionsUi", + "unifiedSearch", + "dataViews", + "dataViewEditor", + "lens" ], "requiredBundles": [ "kibanaReact" diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_logs_explorer.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_logs_explorer.tsx index 5f6739a5dfe3d..cab3742c06f05 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_logs_explorer.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_logs_explorer.tsx @@ -6,6 +6,7 @@ */ import { CoreStart } from '@kbn/core/public'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { Route, Router, Routes } from '@kbn/shared-ux-router'; import React from 'react'; @@ -57,6 +58,7 @@ export const ObservabilityLogsExplorerApp = ({ plugins, pluginStart, }: ObservabilityLogsExplorerAppProps) => { + const isDarkMode = core.theme.getTheme().darkMode; const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider( core, plugins, @@ -69,10 +71,20 @@ export const ObservabilityLogsExplorerApp = ({ <KibanaContextProviderForPlugin> <KbnUrlStateStorageFromRouterProvider> <Router history={appParams.history}> - <Routes> - <Route path="/" exact={true} render={() => <ObservabilityLogsExplorerMainRoute />} /> - <Route path="/dataset-quality" exact={true} render={() => <DatasetQualityRoute />} /> - </Routes> + <EuiThemeProvider darkMode={isDarkMode}> + <Routes> + <Route + path="/" + exact={true} + render={() => <ObservabilityLogsExplorerMainRoute />} + /> + <Route + path="/dataset-quality" + exact={true} + render={() => <DatasetQualityRoute />} + /> + </Routes> + </EuiThemeProvider> </Router> </KbnUrlStateStorageFromRouterProvider> </KibanaContextProviderForPlugin> diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/alerts_popover.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/alerts_popover.tsx new file mode 100644 index 0000000000000..22f689010a2df --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/alerts_popover.tsx @@ -0,0 +1,142 @@ +/* + * 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 { EuiPopover, EuiButtonEmpty, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import React, { useMemo, useReducer } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { useActor } from '@xstate/react'; +import { hydrateDatasetSelection } from '@kbn/logs-explorer-plugin/common'; +import { getDiscoverFiltersFromState } from '@kbn/logs-explorer-plugin/public'; +import type { AlertParams } from '@kbn/observability-plugin/public/components/custom_threshold/types'; +import { useLinkProps } from '@kbn/observability-shared-plugin/public'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; +import { useObservabilityLogsExplorerPageStateContext } from '../state_machines/observability_logs_explorer/src'; + +type ThresholdRuleTypeParams = Pick<AlertParams, 'searchConfiguration'>; + +interface AlertsPopoverState { + isPopoverOpen: boolean; + isAddRuleFlyoutOpen: boolean; +} + +type AlertsPopoverAction = + | { + type: 'togglePopover'; + isOpen?: boolean; + } + | { + type: 'toggleAddRuleFlyout'; + isOpen?: boolean; + }; + +function alertsPopoverReducer(state: AlertsPopoverState, action: AlertsPopoverAction) { + switch (action.type) { + case 'togglePopover': + return { + isPopoverOpen: action.isOpen ?? !state.isPopoverOpen, + isAddRuleFlyoutOpen: state.isAddRuleFlyoutOpen, + }; + + case 'toggleAddRuleFlyout': + return { + isPopoverOpen: false, + isAddRuleFlyoutOpen: action.isOpen ?? !state.isAddRuleFlyoutOpen, + }; + + default: + return state; + } +} + +export const AlertsPopover = () => { + const { + services: { triggersActionsUi }, + } = useKibanaContextForPlugin(); + + const manageRulesLinkProps = useLinkProps({ app: 'observability', pathname: '/alerts/rules' }); + + const [pageState] = useActor(useObservabilityLogsExplorerPageStateContext()); + + const [state, dispatch] = useReducer(alertsPopoverReducer, { + isPopoverOpen: false, + isAddRuleFlyoutOpen: false, + }); + + const togglePopover = () => dispatch({ type: 'togglePopover' }); + const closePopover = () => dispatch({ type: 'togglePopover', isOpen: false }); + const openAddRuleFlyout = () => dispatch({ type: 'toggleAddRuleFlyout', isOpen: true }); + const closeAddRuleFlyout = () => dispatch({ type: 'toggleAddRuleFlyout', isOpen: false }); + + const addRuleFlyout = useMemo(() => { + if ( + state.isAddRuleFlyoutOpen && + triggersActionsUi && + pageState.matches({ initialized: 'validLogsExplorerState' }) + ) { + const { logsExplorerState } = pageState.context; + const index = hydrateDatasetSelection(logsExplorerState.datasetSelection).toDataviewSpec(); + + return triggersActionsUi.getAddRuleFlyout<ThresholdRuleTypeParams>({ + consumer: 'logs', + ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + canChangeTrigger: false, + initialValues: { + params: { + searchConfiguration: { + index, + query: logsExplorerState.query, + filter: getDiscoverFiltersFromState( + index.id, + logsExplorerState.filters, + logsExplorerState.controls + ), + }, + }, + }, + onClose: closeAddRuleFlyout, + }); + } + }, [triggersActionsUi, pageState, state.isAddRuleFlyoutOpen]); + + return ( + <> + {state.isAddRuleFlyoutOpen && addRuleFlyout} + <EuiPopover + button={ + <EuiButtonEmpty onClick={togglePopover} iconType="arrowDown" iconSide="right"> + <FormattedMessage + id="xpack.observabilityLogsExplorer.alertsPopover.buttonLabel" + defaultMessage="Alerts" + /> + </EuiButtonEmpty> + } + isOpen={state.isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + <EuiContextMenuPanel + items={[ + <EuiContextMenuItem key="createRule" icon="bell" onClick={openAddRuleFlyout}> + <FormattedMessage + id="xpack.observabilityLogsExplorer.alertsPopover.createRuleMenuItem" + defaultMessage="Create rule" + /> + </EuiContextMenuItem>, + <EuiContextMenuItem key="manageRules" icon="tableOfContents" {...manageRulesLinkProps}> + <FormattedMessage + id="xpack.observabilityLogsExplorer.alertsPopover.manageRulesMenuItem" + defaultMessage="Manage rules" + /> + </EuiContextMenuItem>, + ]} + /> + </EuiPopover> + </> + ); +}; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/discover_link.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/discover_link.tsx index b12390cc952a1..49a50458ba652 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/discover_link.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/discover_link.tsx @@ -10,7 +10,7 @@ import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { DiscoverStart } from '@kbn/discover-plugin/public'; import { hydrateDatasetSelection } from '@kbn/logs-explorer-plugin/common'; import { - getDiscoverColumnsFromDisplayOptions, + getDiscoverColumnsWithFallbackFieldsFromDisplayOptions, getDiscoverFiltersFromState, } from '@kbn/logs-explorer-plugin/public'; import { getRouterLinkProps } from '@kbn/router-utils'; @@ -53,18 +53,22 @@ export const DiscoverLinkForValidState = React.memo( discover: DiscoverStart; pageState: InitializedPageState; }) => { - const discoverLinkParams = useMemo<DiscoverAppLocatorParams>( - () => ({ + const discoverLinkParams = useMemo<DiscoverAppLocatorParams>(() => { + const index = hydrateDatasetSelection(logsExplorerState.datasetSelection).toDataviewSpec(); + return { breakdownField: logsExplorerState.chart.breakdownField ?? undefined, - columns: getDiscoverColumnsFromDisplayOptions(logsExplorerState), - filters: getDiscoverFiltersFromState(logsExplorerState.filters, logsExplorerState.controls), + columns: getDiscoverColumnsWithFallbackFieldsFromDisplayOptions(logsExplorerState), + filters: getDiscoverFiltersFromState( + index.id, + logsExplorerState.filters, + logsExplorerState.controls + ), query: logsExplorerState.query, refreshInterval: logsExplorerState.refreshInterval, timeRange: logsExplorerState.time, - dataViewSpec: hydrateDatasetSelection(logsExplorerState.datasetSelection).toDataviewSpec(), - }), - [logsExplorerState] - ); + dataViewSpec: index, + }; + }, [logsExplorerState]); return <DiscoverLink discover={discover} discoverLinkParams={discoverLinkParams} />; } diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx index 9c2ea0a5e4817..0f64f586ab3fd 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx @@ -26,6 +26,7 @@ import { useKibanaContextForPlugin } from '../utils/use_kibana'; import { ConnectedDiscoverLink } from './discover_link'; import { FeedbackLink } from './feedback_link'; import { ConnectedOnboardingLink } from './onboarding_link'; +import { AlertsPopover } from './alerts_popover'; export const LogsExplorerTopNavMenu = () => { const { @@ -67,6 +68,8 @@ const ServerlessTopNav = () => { <VerticalRule /> <FeedbackLink /> <VerticalRule /> + <AlertsPopover /> + <VerticalRule /> {ObservabilityAIAssistantActionMenuItem ? ( <ObservabilityAIAssistantActionMenuItem /> ) : null} @@ -143,6 +146,8 @@ const StatefulTopNav = () => { <EuiHeaderLinks gutterSize="xs"> <ConnectedDiscoverLink /> <VerticalRule /> + <AlertsPopover /> + <VerticalRule /> {ObservabilityAIAssistantActionMenuItem ? ( <ObservabilityAIAssistantActionMenuItem /> ) : null} diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts index c3f094033f697..96754cfdab021 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts @@ -15,6 +15,11 @@ import { AppMountParameters, ScopedHistory } from '@kbn/core/public'; import { LogsSharedClientStartExports } from '@kbn/logs-shared-plugin/public'; import { DatasetQualityPluginStart } from '@kbn/dataset-quality-plugin/public'; import { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public'; +import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; +import { LensPublicStart } from '@kbn/lens-plugin/public'; import { ObservabilityLogsExplorerLocators, ObservabilityLogsExplorerLocationState, @@ -41,6 +46,11 @@ export interface ObservabilityLogsExplorerStartDeps { observabilityAIAssistant: ObservabilityAIAssistantPluginStart; observabilityShared: ObservabilitySharedPluginStart; serverless?: ServerlessPluginStart; + triggersActionsUi?: TriggersAndActionsUIPublicPluginStart; + unifiedSearch?: UnifiedSearchPublicPluginStart; + dataViews?: DataViewsPublicPluginStart; + dataViewEditor?: DataViewEditorStart; + lens?: LensPublicStart; share: SharePluginStart; datasetQuality: DatasetQualityPluginStart; } diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json b/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json index 7192e3001a70b..c434a418c4246 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json @@ -38,6 +38,13 @@ "@kbn/xstate-utils", "@kbn/router-utils", "@kbn/observability-ai-assistant-plugin", + "@kbn/rule-data-utils", + "@kbn/observability-plugin", + "@kbn/triggers-actions-ui-plugin", + "@kbn/unified-search-plugin", + "@kbn/data-views-plugin", + "@kbn/data-view-editor-plugin", + "@kbn/lens-plugin", ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap index f9de5da1a1467..fbf5c14bd351d 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap @@ -8,13 +8,13 @@ exports[`<RolesGridPage /> renders permission denied if required 1`] = ` class="emotion-euiPageSection__content-l-center" > <div - class="euiPanel euiPanel--transparent euiEmptyPrompt euiEmptyPrompt--vertical euiEmptyPrompt--paddingLarge emotion-euiPanel-m-transparent" + class="euiPanel euiPanel--transparent euiEmptyPrompt emotion-euiPanel-m-transparent-euiEmptyPrompt-vertical" > <div - class="euiEmptyPrompt__main" + class="euiEmptyPrompt__main emotion-euiEmptyPrompt__main-vertical-l" > <div - class="euiEmptyPrompt__icon" + class="euiEmptyPrompt__icon emotion-euiEmptyPrompt__icon-vertical" > <span color="subdued" @@ -22,28 +22,24 @@ exports[`<RolesGridPage /> renders permission denied if required 1`] = ` /> </div> <div - class="euiEmptyPrompt__content" + class="euiEmptyPrompt__content emotion-euiEmptyPrompt__content-vertical" > + <h2 + class="euiTitle emotion-euiTitle-m" + > + You need permission to manage roles + </h2> + <div + class="euiSpacer euiSpacer--m emotion-euiSpacer-m" + /> <div - class="euiEmptyPrompt__contentInner" + class="euiText emotion-euiText-m-euiTextColor-subdued" > - <h2 - class="euiTitle emotion-euiTitle-m" - > - You need permission to manage roles - </h2> - <div - class="euiSpacer euiSpacer--m emotion-euiSpacer-m" - /> - <div - class="euiText emotion-euiText-m-euiTextColor-subdued" + <p + data-test-subj="permissionDeniedMessage" > - <p - data-test-subj="permissionDeniedMessage" - > - Contact your system administrator. - </p> - </div> + Contact your system administrator. + </p> </div> </div> </div> diff --git a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap index b6bb95e80744b..fe6552fa889af 100644 --- a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap +++ b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PromptPage renders as expected with additional scripts 1`] = `"<html lang=\\"en\\"><head><title>ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; +exports[`PromptPage renders as expected with additional scripts 1`] = `"ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; -exports[`PromptPage renders as expected without additional scripts 1`] = `"ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; +exports[`PromptPage renders as expected without additional scripts 1`] = `"ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; diff --git a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap index 8b78fb132c3f8..43f406782acc2 100644 --- a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UnauthenticatedPage renders as expected 1`] = `"ElasticMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; +exports[`UnauthenticatedPage renders as expected 1`] = `"ElasticMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; -exports[`UnauthenticatedPage renders as expected with custom title 1`] = `"My Company NameMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; +exports[`UnauthenticatedPage renders as expected with custom title 1`] = `"My Company NameMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap index a2217e1acf40a..80155b3fde5ac 100644 --- a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ResetSessionPage renders as expected 1`] = `"ElasticMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; +exports[`ResetSessionPage renders as expected 1`] = `"ElasticMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; -exports[`ResetSessionPage renders as expected with custom page title 1`] = `"My Company NameMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; +exports[`ResetSessionPage renders as expected with custom page title 1`] = `"My Company NameMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts index 908043ab7335e..2c58f0461967b 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.gen.ts @@ -19,6 +19,8 @@ import { z } from 'zod'; export type EntityAnalyticsPrivileges = z.infer; export const EntityAnalyticsPrivileges = z.object({ has_all_required: z.boolean(), + has_read_permissions: z.boolean().optional(), + has_write_permissions: z.boolean().optional(), privileges: z.object({ elasticsearch: z.object({ cluster: z diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml index b16729ccc38b3..9285503c322f0 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/common/common.schema.yaml @@ -3,7 +3,7 @@ info: title: Entity Analytics Common Schema description: Common schema for Entity Analytics version: 1.0.0 -paths: { } +paths: {} components: schemas: EntityAnalyticsPrivileges: @@ -11,6 +11,10 @@ components: properties: has_all_required: type: boolean + has_read_permissions: + type: boolean + has_write_permissions: + type: boolean privileges: type: object properties: @@ -20,19 +24,19 @@ components: cluster: type: object properties: - manage_index_templates: - type: boolean - manage_transform: - type: boolean + manage_index_templates: + type: boolean + manage_transform: + type: boolean index: type: object additionalProperties: - type: object - properties: - read: - type: boolean - write: - type: boolean + type: object + properties: + read: + type: boolean + write: + type: boolean required: - elasticsearch required: @@ -176,8 +180,6 @@ components: items: $ref: '#/components/schemas/RiskScoreInput' - - RiskScoreWeight: description: "Configuration used to tune risk scoring. Weights can be used to change the score contribution of risk inputs for hosts and users at both a global level and also for Risk Input categories (e.g. 'category_1')." type: object diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx index bd4a59414e114..37f4f4559b50b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx @@ -71,6 +71,7 @@ export const FieldValueCell = React.memo( eventId={eventId} fieldFormat={data.format} fieldName={data.field} + fieldFromBrowserField={fieldFromBrowserField} fieldType={data.type} isAggregatable={fieldFromBrowserField.aggregatable} isDraggable={isDraggable} diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/toggle_panel.tsx b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/toggle_panel.tsx index 45b18d046a601..43a5ccfdef5a7 100644 --- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/toggle_panel.tsx +++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/toggle_panel.tsx @@ -47,7 +47,7 @@ const TogglePanelComponent: React.FC<{ body={

{i18n.TOGGLE_PANEL_EMPTY_DESCRIPTION}

} css={css` padding: ${euiTheme.base * 5}px 0; - .euiEmptyPrompt__contentInner { + .euiEmptyPrompt__content { max-width: none; } `} diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap index e8bdc77c7c460..4e26d269ed8ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap @@ -41,6 +41,9 @@ Object { "isBucketed": false, "label": "Count of records", "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, "scale": "ratio", "sourceField": "___records___", }, @@ -50,6 +53,7 @@ Object { "label": "@timestamp", "operationType": "date_histogram", "params": Object { + "includeEmptyRows": true, "interval": "auto", }, "scale": "interval", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap index a48df2b2787e6..5e666cc63c3e9 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap @@ -50,6 +50,7 @@ Object { "label": "@timestamp", "operationType": "date_histogram", "params": Object { + "includeEmptyRows": true, "interval": "auto", }, "scale": "interval", @@ -60,6 +61,9 @@ Object { "isBucketed": false, "label": "Count of records", "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, "scale": "ratio", "sourceField": "___records___", }, @@ -233,6 +237,7 @@ Object { "label": "@timestamp", "operationType": "date_histogram", "params": Object { + "includeEmptyRows": true, "interval": "auto", }, "scale": "interval", @@ -243,6 +248,9 @@ Object { "isBucketed": false, "label": "Count of records", "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, "scale": "ratio", "sourceField": "___records___", }, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts index 571040b33b378..5fb0630bcca80 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts @@ -70,6 +70,7 @@ export const getAlertsHistogramLensAttributes: GetLensAttributes = ( scale: 'interval', params: { interval: 'auto', + includeEmptyRows: true, }, }, 'e09e0380-0740-4105-becc-0a4ca12e3944': { @@ -79,6 +80,7 @@ export const getAlertsHistogramLensAttributes: GetLensAttributes = ( isBucketed: false, scale: 'ratio', sourceField: '___records___', + params: { emptyAsNull: true }, }, '34919782-4546-43a5-b668-06ac934d3acd': { label: `Top values of ${stackByField}`, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts index baa267b9f17b8..8e238ca11b1d7 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts @@ -71,6 +71,7 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( scale: 'interval', params: { interval: 'auto', + includeEmptyRows: true, }, }, 'e09e0380-0740-4105-becc-0a4ca12e3944': { @@ -80,6 +81,7 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( isBucketed: false, scale: 'ratio', sourceField: '___records___', + params: { emptyAsNull: true }, }, '34919782-4546-43a5-b668-06ac934d3acd': { label: `Top values of ${stackByField}`, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts index 2aa3eab25d105..3baec52d3b8fb 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts @@ -91,6 +91,7 @@ export const getExternalAlertLensAttributes: GetLensAttributes = ( scale: 'interval', params: { interval: 'auto', + includeEmptyRows: true, }, }, '0a923af2-c880-4aa3-aa93-a0b9c2801f6d': { @@ -100,6 +101,7 @@ export const getExternalAlertLensAttributes: GetLensAttributes = ( isBucketed: false, scale: 'ratio', sourceField: '___records___', + params: { emptyAsNull: true }, }, '42334c6e-98d9-47a2-b4cb-a445abb44c93': { label: TOP_VALUE(`${stackByField}`), // could be event.category diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap index 5d7a36ab09e20..19a5eb4f45b71 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap @@ -26,6 +26,9 @@ Object { "isBucketed": false, "label": "Unique count of dns.question.name", "operationType": "unique_count", + "params": Object { + "emptyAsNull": true, + }, "scale": "ratio", "sourceField": "dns.question.name", }, @@ -35,6 +38,7 @@ Object { "label": "@timestamp", "operationType": "date_histogram", "params": Object { + "includeEmptyRows": true, "interval": "auto", }, "scale": "interval", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts index f39ad3c2ea30d..39f5d56a3d461 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts @@ -133,6 +133,7 @@ export const getDnsTopDomainsLensAttributes: GetLensAttributes = ( scale: 'interval', params: { interval: 'auto', + includeEmptyRows: true, }, }, '2a4d5e20-f570-48e4-b9ab-ff3068919377': { @@ -142,6 +143,7 @@ export const getDnsTopDomainsLensAttributes: GetLensAttributes = ( scale: 'ratio', sourceField: 'dns.question.name', isBucketed: false, + params: { emptyAsNull: true }, }, }, columnOrder: [ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx index 54f33e81e1bba..f13c8130bf740 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx @@ -8,6 +8,7 @@ import type { Dispatch, SetStateAction } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { EuiButton } from '@elastic/eui'; +import { useUserData } from '../../../../../detections/components/user_info'; import { useFetchPrebuiltRulesStatusQuery } from '../../../../rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query'; import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; import type { RuleSignatureId } from '../../../../../../common/api/detection_engine'; @@ -99,6 +100,8 @@ export const AddPrebuiltRulesTableContextProvider = ({ const [loadingRules, setLoadingRules] = useState([]); const [selectedRules, setSelectedRules] = useState([]); + const [{ loading: userInfoLoading, canUserCRUD }] = useUserData(); + const [filterOptions, setFilterOptions] = useState({ filter: '', tags: [], @@ -135,11 +138,13 @@ export const AddPrebuiltRulesTableContextProvider = ({ const filteredRules = useFilterPrebuiltRulesToInstall({ filterOptions, rules }); const { openRulePreview, closeRulePreview, previewedRule } = useRuleDetailsFlyout(filteredRules); - const canPreviewedRuleBeInstalled = Boolean( - (previewedRule?.rule_id && loadingRules.includes(previewedRule.rule_id)) || - isRefetching || - isUpgradingSecurityPackages - ); + + const isPreviewRuleLoading = + previewedRule?.rule_id && loadingRules.includes(previewedRule.rule_id); + const canPreviewedRuleBeInstalled = + !userInfoLoading && + canUserCRUD && + !(isPreviewRuleLoading || isRefetching || isUpgradingSecurityPackages); const installOneRule = useCallback( async (ruleId: RuleSignatureId) => { @@ -237,7 +242,7 @@ export const AddPrebuiltRulesTableContextProvider = ({ closeFlyout={closeRulePreview} ruleActions={ { installOneRule(previewedRule.rule_id ?? ''); closeRulePreview(); 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 1e0c7021929c9..05de5e9725399 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 @@ -67,11 +67,27 @@ const platinumBaseColumns = [ initialWidth: 450, }, { columnHeaderType: 'not-filtered', id: 'host.name' }, - { 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: 'host.asset.criticality' }, - { columnHeaderType: 'not-filtered', id: 'user.asset.criticality' }, + { + columnHeaderType: 'not-filtered', + id: 'host.risk.calculated_level', + displayAsText: 'Host Risk Level', + }, + { + columnHeaderType: 'not-filtered', + id: 'user.risk.calculated_level', + displayAsText: 'User Risk Level', + }, + { + columnHeaderType: 'not-filtered', + id: 'host.asset.criticality', + displayAsText: 'Host Criticality', + }, + { + columnHeaderType: 'not-filtered', + id: 'user.asset.criticality', + displayAsText: 'User 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/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index ae0c05d2bdb05..4a7ea8e77cc92 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -137,6 +137,34 @@ export const ALERTS_HEADERS_NEW_TERMS_FIELDS = i18n.translate( } ); +export const ALERTS_HEADERS_HOST_RISK_LEVEL = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.defaultHeaders.hostRiskLevel', + { + defaultMessage: 'Host Risk Level', + } +); + +export const ALERTS_HEADERS_USER_RISK_LEVEL = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.defaultHeaders.userRiskLevel', + { + defaultMessage: 'User Risk Level', + } +); + +export const ALERTS_HEADERS_HOST_CRITICALITY = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.defaultHeaders.hostCriticality', + { + defaultMessage: 'Host Criticality', + } +); + +export const ALERTS_HEADERS_USER_CRITICALITY = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.defaultHeaders.userCriticality', + { + defaultMessage: 'User Criticality', + } +); + export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts b/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts index 62772fa029e66..cfa004b7ce6b2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts +++ b/x-pack/plugins/security_solution/public/detections/components/endpoint_responder/use_responder_action_data.ts @@ -7,6 +7,7 @@ import type { ReactNode } from 'react'; import { useCallback, useMemo } from 'react'; import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { getSentinelOneAgentId } from '../../../common/utils/sentinelone_alert_check'; import type { ThirdPartyAgentInfo } from '../../../../common/types'; import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants'; @@ -74,17 +75,22 @@ export const useResponderActionData = ({ tooltip: ReactNode; } => { const isEndpointHost = agentType === 'endpoint'; + const isSentinelOneHost = agentType === 'sentinel_one'; const showResponseActionsConsole = useWithShowResponder(); + const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled( + 'responseActionsSentinelOneV1Enabled' + ); const { data: hostInfo, isFetching, error, - } = useGetEndpointDetails(endpointId, { enabled: Boolean(endpointId) }); + } = useGetEndpointDetails(endpointId, { enabled: Boolean(endpointId && isEndpointHost) }); const [isDisabled, tooltip]: [disabled: boolean, tooltip: ReactNode] = useMemo(() => { + // v8.13 disabled for third-party agent alerts if the feature flag is not enabled if (!isEndpointHost) { - return [false, undefined]; + return [isSentinelOneHost ? !isSentinelOneV1Enabled : true, undefined]; } // Still loading host info @@ -114,7 +120,14 @@ export const useResponderActionData = ({ } return [false, undefined]; - }, [isEndpointHost, isFetching, error, hostInfo?.host_status]); + }, [ + isEndpointHost, + isSentinelOneHost, + isSentinelOneV1Enabled, + isFetching, + error, + hostInfo?.host_status, + ]); const handleResponseActionsClick = useCallback(() => { if (!isEndpointHost) { 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 fc3f5afa897a2..6ca0a67244179 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 @@ -34,6 +34,18 @@ export const assigneesColumn: ColumnHeaderOptions = { initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, }; +export const hostRiskLevelColumn: ColumnHeaderOptions = { + columnHeaderType: defaultColumnHeaderType, + id: ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL, + displayAsText: i18n.ALERTS_HEADERS_HOST_RISK_LEVEL, +}; + +export const userRiskLevelColumn: ColumnHeaderOptions = { + columnHeaderType: defaultColumnHeaderType, + id: ALERT_USER_RISK_SCORE_CALCULATED_LEVEL, + displayAsText: i18n.ALERTS_HEADERS_USER_RISK_LEVEL, +}; + const getBaseColumns = ( license?: LicenseService ): Array< @@ -63,32 +75,24 @@ const getBaseColumns = ( columnHeaderType: defaultColumnHeaderType, id: 'host.name', }, - isPlatinumPlus - ? { - columnHeaderType: defaultColumnHeaderType, - id: ALERT_HOST_RISK_SCORE_CALCULATED_LEVEL, - } - : null, { columnHeaderType: defaultColumnHeaderType, id: 'user.name', }, - isPlatinumPlus - ? { - columnHeaderType: defaultColumnHeaderType, - id: ALERT_USER_RISK_SCORE_CALCULATED_LEVEL, - } - : null, + isPlatinumPlus ? hostRiskLevelColumn : null, + isPlatinumPlus ? userRiskLevelColumn : null, isPlatinumPlus ? { columnHeaderType: defaultColumnHeaderType, id: ALERT_HOST_CRITICALITY, + displayAsText: i18n.ALERTS_HEADERS_HOST_CRITICALITY, } : null, isPlatinumPlus ? { columnHeaderType: defaultColumnHeaderType, id: ALERT_USER_CRITICALITY, + displayAsText: i18n.ALERTS_HEADERS_USER_CRITICALITY, } : null, { diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_badge.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_badge.tsx index 003d693a0182e..a28e0e9e4df8a 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_badge.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_badge.tsx @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { CRITICALITY_LEVEL_TITLE, CRITICALITY_LEVEL_DESCRIPTION } from './translations'; import type { CriticalityLevel } from '../../../../common/entity_analytics/asset_criticality/types'; -const CRITICALITY_LEVEL_COLOR: Record = { +export const CRITICALITY_LEVEL_COLOR: Record = { very_important: '#E7664C', important: '#D6BF57', normal: '#54B399', @@ -73,7 +73,7 @@ export const AssetCriticalityBadgeAllowMissing: React.FC<{ ); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx index fa161ea239ac3..f6a0da49197c8 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx @@ -44,7 +44,7 @@ const AssetCriticalityComponent: React.FC = ({ entity }) => { const criticality = useAssetCriticalityData(entity, modal); const { euiTheme } = useEuiTheme(); - if (criticality.privileges.isLoading || !criticality.privileges.data?.has_all_required) { + if (criticality.privileges.isLoading || !criticality.privileges.data?.has_read_permissions) { return null; } @@ -87,27 +87,29 @@ const AssetCriticalityComponent: React.FC = ({ entity }) => { />
- - modal.toggle(true)} - > - {criticality.status === 'update' ? ( - - ) : ( - - )} - - + {criticality.privileges.data?.has_write_permissions && ( + + modal.toggle(true)} + > + {criticality.status === 'update' ? ( + + ) : ( + + )} + + + )}
)} diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.ts index 5ac7d176a7b44..893f157d045cf 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.ts @@ -40,7 +40,7 @@ export const useAssetCriticalityData = (entity: Entity, modal: ModalState): Stat queryKey: QUERY_KEYS.doc, queryFn: () => fetchAssetCriticality({ idField: `${entity.type}.name`, idValue: entity.name }), retry: (failureCount, error) => error.body.statusCode === 404 && failureCount > 0, - enabled: privileges.data?.has_all_required, + enabled: !!privileges.data?.has_read_permissions, }); const mutation = useMutation({ diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/contexts_table.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/contexts_table.test.tsx index 14c8f466cede9..cabe9c68a36e6 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/contexts_table.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/contexts_table.test.tsx @@ -74,6 +74,6 @@ describe('ContextsTable', () => { expect(screen.getByText('Asset Criticality Level')).toBeInTheDocument(); expect(screen.getByTestId('risk-inputs-asset-criticality-badge')).toBeInTheDocument(); - expect(screen.getByText('No criticality assigned')).toBeInTheDocument(); + expect(screen.getByText('Criticality Unassigned')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.test.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.test.ts index abd771d240cae..f02ccc68cce0e 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.test.ts @@ -28,14 +28,12 @@ describe('useManagedUserItems', () => { getValues: expect.any(Function), }, { - field: '@timestamp', label: 'First seen', - getValues: expect.any(Function), + render: expect.any(Function), }, { - field: '@timestamp', label: 'Last seen', - getValues: expect.any(Function), + render: expect.any(Function), }, { field: 'host.os.name', @@ -65,8 +63,8 @@ describe('useManagedUserItems', () => { [ ['1234', '321'], // id ['test domain', 'another test domain'], // domain - ['2023-02-23T20:03:17.489Z'], // First seen - ['2023-02-23T20:03:17.489Z'], // Last seen + undefined, // First seen doesn't implement getValues + undefined, // Last seen doesn't implement getValues ['testOs'], // OS name ['testFamily'], // os family ['10.0.0.1', '127.0.0.1'], // IP addresses diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.tsx similarity index 75% rename from x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.ts rename to x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.tsx index 7275b2ca55570..3dbc4fd1b507f 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.ts +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.tsx @@ -5,13 +5,15 @@ * 2.0. */ -import { useMemo } from 'react'; +import React, { useMemo } from 'react'; +import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; import type { UserItem } from '../../../../../common/search_strategy'; import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities'; import { getAnomaliesFields } from '../../shared/common'; import * as i18n from './translations'; import type { ObservedEntityData } from '../../shared/components/observed_entity/types'; import type { EntityTableRows } from '../../shared/components/entity_table/types'; +import { getEmptyTagValue } from '../../../../common/components/empty_value'; const basicUserFields: EntityTableRows> = [ { @@ -26,15 +28,21 @@ const basicUserFields: EntityTableRows> = [ }, { label: i18n.FIRST_SEEN, - getValues: (userData: ObservedEntityData) => - userData.firstSeen.date ? [userData.firstSeen.date] : undefined, - field: '@timestamp', + render: (userData: ObservedEntityData) => + userData.firstSeen.date ? ( + + ) : ( + getEmptyTagValue() + ), }, { label: i18n.LAST_SEEN, - getValues: (userData: ObservedEntityData) => - userData.lastSeen.date ? [userData.lastSeen.date] : undefined, - field: '@timestamp', + render: (userData: ObservedEntityData) => + userData.lastSeen.date ? ( + + ) : ( + getEmptyTagValue() + ), }, { label: i18n.OPERATING_SYSTEM_TITLE, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/asset_criticality_level.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/asset_criticality_level.test.tsx new file mode 100644 index 0000000000000..2a52c4509492a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/asset_criticality_level.test.tsx @@ -0,0 +1,35 @@ +/* + * 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 { TestProviders } from '../../../../../common/mock'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { AssetCriticalityLevel } from './asset_criticality_level'; + +jest.mock('../../../../../common/components/draggables', () => ({ + DefaultDraggable: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +const defaultProps = { + contextId: 'testContext', + eventId: 'testEvent', + fieldName: 'testField', + fieldType: 'testType', + isAggregatable: true, + isDraggable: true, + value: 'low', +}; + +describe('AssetCriticalityLevel', () => { + it('renders', () => { + const { getByTestId } = render(, { + wrapper: TestProviders, + }); + + expect(getByTestId('AssetCriticalityLevel-score-badge')).toHaveTextContent('low'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/asset_criticality_level.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/asset_criticality_level.tsx new file mode 100644 index 0000000000000..ac3fa8d977b61 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/asset_criticality_level.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBadge } from '@elastic/eui'; +import { isString } from 'lodash/fp'; +import type { CriticalityLevel } from '../../../../../../common/entity_analytics/asset_criticality/types'; +import { CRITICALITY_LEVEL_COLOR } from '../../../../../entity_analytics/components/asset_criticality'; +import { DefaultDraggable } from '../../../../../common/components/draggables'; + +interface Props { + contextId: string; + eventId: string; + fieldName: string; + fieldType: string; + isAggregatable: boolean; + isDraggable: boolean; + value: string | number | undefined | null; +} + +const AssetCriticalityLevelComponent: React.FC = ({ + contextId, + eventId, + fieldName, + fieldType, + isAggregatable, + isDraggable, + value, +}) => { + const color = isString(value) ? CRITICALITY_LEVEL_COLOR[value as CriticalityLevel] : 'normal'; + + const badge = ( + + {value} + + ); + + return isDraggable ? ( + + {badge} + + ) : ( + badge + ); +}; + +export const AssetCriticalityLevel = React.memo(AssetCriticalityLevelComponent); +AssetCriticalityLevel.displayName = 'AssetCriticalityLevel'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 7062fc7afbb78..944ee20034d0e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -13,6 +13,11 @@ import { isNumber, isEmpty } from 'lodash/fp'; import React from 'react'; import { css } from '@emotion/css'; +import type { BrowserField } from '../../../../../common/containers/source'; +import { + ALERT_HOST_CRITICALITY, + ALERT_USER_CRITICALITY, +} from '../../../../../../common/field_maps/field_names'; import { SENTINEL_ONE_AGENT_ID_FIELD } from '../../../../../common/utils/sentinelone_alert_check'; import { SentinelOneAgentStatus } from '../../../../../detections/components/host_isolation/sentinel_one_agent_status'; import { EndpointAgentStatusById } from '../../../../../common/components/endpoint/endpoint_agent_status'; @@ -45,6 +50,7 @@ import { RenderRuleName, renderEventModule, renderUrl } from './formatted_field_ import { RuleStatus } from './rule_status'; import { HostName } from './host_name'; import { UserName } from './user_name'; +import { AssetCriticalityLevel } from './asset_criticality_level'; // simple black-list to prevent dragging and dropping fields such as message name const columnNamesNotDraggable = [MESSAGE_FIELD_NAME]; @@ -65,6 +71,7 @@ const FormattedFieldValueComponent: React.FC<{ isAggregatable?: boolean; isObjectArray?: boolean; fieldFormat?: string; + fieldFromBrowserField?: BrowserField; fieldName: string; fieldType?: string; isButton?: boolean; @@ -84,6 +91,7 @@ const FormattedFieldValueComponent: React.FC<{ isAggregatable = false, fieldName, fieldType = '', + fieldFromBrowserField, isButton, isObjectArray = false, isDraggable = true, @@ -256,6 +264,23 @@ const FormattedFieldValueComponent: React.FC<{ iconSide={isButton ? 'right' : undefined} /> ); + } else if ( + fieldName === SENTINEL_ONE_AGENT_ID_FIELD || + fieldFromBrowserField?.name === SENTINEL_ONE_AGENT_ID_FIELD + ) { + return ; + } else if (fieldName === ALERT_HOST_CRITICALITY || fieldName === ALERT_USER_CRITICALITY) { + return ( + + ); } else if (fieldName === AGENT_STATUS_FIELD_NAME) { return ( ); - } else if (fieldName === SENTINEL_ONE_AGENT_ID_FIELD) { - return ; } else if ( [ RULE_REFERENCE_FIELD_NAME, diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx index 8191f800ff934..cc8bafa928180 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx @@ -18,6 +18,7 @@ import { assigneesColumn } from '../../../detections/configurations/security_sol import { ALERTS_TABLE_REGISTRY_CONFIG_IDS, VIEW_SELECTION } from '../../../../common/constants'; import type { DataTablesStorage } from './types'; import { useKibana } from '../../../common/lib/kibana'; +import { migrateEntityRiskLevelColumnTitle } from './migrates_risk_level_title'; export const LOCAL_STORAGE_TABLE_KEY = 'securityDataTable'; const LOCAL_STORAGE_TIMELINE_KEY_LEGACY = 'timelines'; @@ -295,6 +296,7 @@ export const getDataTablesInStorageByIds = (storage: Storage, tableIds: TableIdL migrateAlertTableStateToTriggerActionsState(storage, allDataTables); migrateTriggerActionsVisibleColumnsAlertTable88xTo89(storage); addAssigneesSpecsToSecurityDataTableIfNeeded(storage, allDataTables); + migrateEntityRiskLevelColumnTitle(storage, allDataTables); return tableIds.reduce((acc, tableId) => { const tableModel = allDataTables[tableId]; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrates_risk_level_title.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrates_risk_level_title.test.tsx new file mode 100644 index 0000000000000..a43e0a860e7ac --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrates_risk_level_title.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + LOCAL_STORAGE_MIGRATION_KEY, + migrateEntityRiskLevelColumnTitle, +} from './migrates_risk_level_title'; +import type { DataTableState } from '@kbn/securitysolution-data-table'; +import { + hostRiskLevelColumn, + userRiskLevelColumn, +} from '../../../detections/configurations/security_solution_detections/columns'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { localStorageMock } from '../../../common/mock/mock_local_storage'; +import { LOCAL_STORAGE_TABLE_KEY } from '.'; + +const getColumnsBeforeMigration = () => [ + { ...userRiskLevelColumn, displayAsText: undefined }, + { ...hostRiskLevelColumn, displayAsText: undefined }, +]; + +let storage: Storage; + +describe('migrateEntityRiskLevelColumnTitle', () => { + beforeEach(() => { + storage = new Storage(localStorageMock()); + }); + + it('does NOT migrate `columns` when `columns` is not an array', () => { + const dataTableState = { + 'alerts-page': {}, + } as unknown as DataTableState['dataTable']['tableById']; + + migrateEntityRiskLevelColumnTitle(storage, dataTableState); + + expect(dataTableState['alerts-page'].columns).toStrictEqual(undefined); + }); + + it('does not migrates columns if if it has already run once', () => { + storage.set(LOCAL_STORAGE_MIGRATION_KEY, true); + const dataTableState = { + 'alerts-page': { + columns: getColumnsBeforeMigration(), + }, + } as unknown as DataTableState['dataTable']['tableById']; + + migrateEntityRiskLevelColumnTitle(storage, dataTableState); + + expect(dataTableState['alerts-page'].columns).toStrictEqual(getColumnsBeforeMigration()); + }); + + it('migrates columns saved to localStorage', () => { + const dataTableState = { + 'alerts-page': { + columns: getColumnsBeforeMigration(), + }, + } as unknown as DataTableState['dataTable']['tableById']; + + migrateEntityRiskLevelColumnTitle(storage, dataTableState); + + // assert that it mutates the table model + expect(dataTableState['alerts-page'].columns).toStrictEqual([ + userRiskLevelColumn, + hostRiskLevelColumn, + ]); + // assert that it updates the migration flag on storage + expect(storage.get(LOCAL_STORAGE_MIGRATION_KEY)).toEqual(true); + // assert that it updates the table inside local storage + expect(storage.get(LOCAL_STORAGE_TABLE_KEY)['alerts-page'].columns).toStrictEqual([ + userRiskLevelColumn, + hostRiskLevelColumn, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrates_risk_level_title.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrates_risk_level_title.tsx new file mode 100644 index 0000000000000..8cbae007b3a1f --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrates_risk_level_title.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DataTableState, TableId } from '@kbn/securitysolution-data-table'; +import { tableEntity, TableEntityType } from '@kbn/securitysolution-data-table'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; +import { LOCAL_STORAGE_TABLE_KEY } from '.'; +import { + hostRiskLevelColumn, + userRiskLevelColumn, +} from '../../../detections/configurations/security_solution_detections/columns'; + +export const LOCAL_STORAGE_MIGRATION_KEY = + 'securitySolution.dataTable.entityRiskLevelColumnTitleMigration'; + +export const migrateEntityRiskLevelColumnTitle = ( + storage: Storage, + dataTableState: DataTableState['dataTable']['tableById'] +) => { + // Set/Get a flag to prevent migration from running more than once + const hasAlreadyMigrated: boolean = storage.get(LOCAL_STORAGE_MIGRATION_KEY); + if (hasAlreadyMigrated) { + return; + } + storage.set(LOCAL_STORAGE_MIGRATION_KEY, true); + + let updatedTableModel = false; + + for (const [tableId, tableModel] of Object.entries(dataTableState)) { + // Only updates the title for alerts tables + if (tableEntity[tableId as TableId] === TableEntityType.alert) { + // In order to show correct column title after user upgrades to 8.13 we need update the stored table model with the new title. + const columns = tableModel.columns; + if (Array.isArray(columns)) { + columns.forEach((col) => { + if (col.id === userRiskLevelColumn.id) { + col.displayAsText = userRiskLevelColumn.displayAsText; + updatedTableModel = true; + } + + if (col.id === hostRiskLevelColumn.id) { + col.displayAsText = hostRiskLevelColumn.displayAsText; + updatedTableModel = true; + } + }); + } + } + } + if (updatedTableModel) { + storage.set(LOCAL_STORAGE_TABLE_KEY, dataTableState); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts index 211466e17c8f7..acf506b0304c9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_event_signal.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { firstValueFrom } from 'rxjs'; - import { buildThreatMappingFilter } from './build_threat_mapping_filter'; import { getFilter } from '../../utils/get_filter'; import { searchAfterAndBulkCreate } from '../../utils/search_after_bulk_create'; @@ -56,7 +54,8 @@ export const createEventSignal = async ({ inputIndexFields, threatIndexFields, completeRule, - licensing, + sortOrder = 'desc', + isAlertSuppressionActive, }: CreateEventSignalOptions): Promise => { const threatFiltersFromEvents = buildThreatMappingFilter({ threatMapping, @@ -65,9 +64,6 @@ export const createEventSignal = async ({ allowedFieldsForTermsQuery, }); - const license = await firstValueFrom(licensing.license$); - const hasPlatinumLicense = license.hasAtLeast('platinum'); - if (!threatFiltersFromEvents.query || threatFiltersFromEvents.query?.bool.should.length === 0) { // empty event list and we do not want to return everything as being // a hit so opt to return the existing result. @@ -134,10 +130,6 @@ export const createEventSignal = async ({ threatSearchParams, }); - const isAlertSuppressionEnabled = Boolean( - completeRule.ruleParams.alertSuppression?.groupBy?.length - ); - let createResult: SearchAfterAndBulkCreateReturnType; const searchAfterBulkCreateParams = { buildReasonMessage: buildReasonMessageForThreatMatchAlert, @@ -151,7 +143,7 @@ export const createEventSignal = async ({ pageSize: searchAfterSize, ruleExecutionLogger, services, - sortOrder: 'desc' as const, + sortOrder, trackTotalHits: false, tuple, wrapHits, @@ -160,11 +152,7 @@ export const createEventSignal = async ({ secondaryTimestamp, }; - if ( - isAlertSuppressionEnabled && - runOpts.experimentalFeatures?.alertSuppressionForIndicatorMatchRuleEnabled && - hasPlatinumLicense - ) { + if (isAlertSuppressionActive) { createResult = await searchAfterAndBulkCreateSuppressedAlerts({ ...searchAfterBulkCreateParams, wrapSuppressedHits, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts index c5071605841f5..7740bed7777bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signal.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { firstValueFrom } from 'rxjs'; - import { buildThreatMappingFilter } from './build_threat_mapping_filter'; import { getFilter } from '../../utils/get_filter'; import { searchAfterAndBulkCreate } from '../../utils/search_after_bulk_create'; @@ -54,7 +52,8 @@ export const createThreatSignal = async ({ allowedFieldsForTermsQuery, inputIndexFields, threatIndexFields, - licensing, + sortOrder = 'desc', + isAlertSuppressionActive, }: CreateThreatSignalOptions): Promise => { const threatFilter = buildThreatMappingFilter({ threatMapping, @@ -63,9 +62,6 @@ export const createThreatSignal = async ({ allowedFieldsForTermsQuery, }); - const license = await firstValueFrom(licensing.license$); - const hasPlatinumLicense = license.hasAtLeast('platinum'); - if (!threatFilter.query || threatFilter.query?.bool.should.length === 0) { // empty threat list and we do not want to return everything as being // a hit so opt to return the existing result. @@ -107,10 +103,6 @@ export const createThreatSignal = async ({ threatIndexFields, }); - const isAlertSuppressionEnabled = Boolean( - completeRule.ruleParams.alertSuppression?.groupBy?.length - ); - let result: SearchAfterAndBulkCreateReturnType; const searchAfterBulkCreateParams = { buildReasonMessage: buildReasonMessageForThreatMatchAlert, @@ -124,7 +116,7 @@ export const createThreatSignal = async ({ pageSize: searchAfterSize, ruleExecutionLogger, services, - sortOrder: 'desc' as const, + sortOrder, trackTotalHits: false, tuple, wrapHits, @@ -133,11 +125,7 @@ export const createThreatSignal = async ({ secondaryTimestamp, }; - if ( - isAlertSuppressionEnabled && - runOpts.experimentalFeatures?.alertSuppressionForIndicatorMatchRuleEnabled && - hasPlatinumLicense - ) { + if (isAlertSuppressionActive) { result = await searchAfterAndBulkCreateSuppressedAlerts({ ...searchAfterBulkCreateParams, wrapSuppressedHits, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts index 5cb9dddc9b42e..aa108e5ea9bea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/create_threat_signals.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { firstValueFrom } from 'rxjs'; + import type { OpenPointInTimeResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { uniq, chunk } from 'lodash/fp'; @@ -216,6 +218,22 @@ export const createThreatSignals = async ({ } }; + const license = await firstValueFrom(licensing.license$); + const hasPlatinumLicense = license.hasAtLeast('platinum'); + const isAlertSuppressionConfigured = Boolean( + completeRule.ruleParams.alertSuppression?.groupBy?.length + ); + + const isAlertSuppressionActive = + isAlertSuppressionConfigured && + Boolean(runOpts.experimentalFeatures?.alertSuppressionForIndicatorMatchRuleEnabled) && + hasPlatinumLicense; + + // alert suppression needs to be performed on results searched in ascending order, so alert's suppression boundaries would be set correctly + // at the same time, there are concerns on performance of IM rule when sorting is set to asc, as it may lead to longer rule runs, since it will + // first go through alerts that might ve been processed in earlier executions, when look back interval set to large values (it can't be larger than 24h) + const sortOrder = isAlertSuppressionConfigured ? 'asc' : 'desc'; + if (eventCount < threatListCount) { await createSignals({ totalDocumentCount: eventCount, @@ -236,6 +254,7 @@ export const createThreatSignals = async ({ exceptionFilter, eventListConfig, indexFields: inputIndexFields, + sortOrder, }), createSignal: (slicedChunk) => @@ -278,7 +297,8 @@ export const createThreatSignals = async ({ inputIndexFields, threatIndexFields, runOpts, - licensing, + sortOrder, + isAlertSuppressionActive, }), }); } else { @@ -342,7 +362,8 @@ export const createThreatSignals = async ({ inputIndexFields, threatIndexFields, runOpts, - licensing, + sortOrder, + isAlertSuppressionActive, }), }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts index 214c7d3f13075..c74424f65d514 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_event_count.ts @@ -29,6 +29,7 @@ export const getEventList = async ({ exceptionFilter, eventListConfig, indexFields, + sortOrder = 'desc', }: EventsOptions): Promise> => { const calculatedPerPage = perPage ?? MAX_PER_PAGE; if (calculatedPerPage > 10000) { @@ -59,7 +60,7 @@ export const getEventList = async ({ filter: queryFilter, primaryTimestamp, secondaryTimestamp, - sortOrder: 'desc', + sortOrder, trackTotalHits: false, runtimeMappings, overrideBody: eventListConfig, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts index ae9548090ea64..d8a6a97bbc644 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/types.ts @@ -120,7 +120,8 @@ export interface CreateThreatSignalOptions { inputIndexFields: DataViewFieldBase[]; threatIndexFields: DataViewFieldBase[]; runOpts: RunOpts; - licensing: LicensingPluginSetup; + sortOrder?: SortOrderOrUndefined; + isAlertSuppressionActive: boolean; } export interface CreateEventSignalOptions { @@ -163,7 +164,8 @@ export interface CreateEventSignalOptions { inputIndexFields: DataViewFieldBase[]; threatIndexFields: DataViewFieldBase[]; runOpts: RunOpts; - licensing: LicensingPluginSetup; + sortOrder?: SortOrderOrUndefined; + isAlertSuppressionActive: boolean; } type EntryKey = 'field' | 'value'; @@ -312,6 +314,7 @@ export interface EventsOptions { exceptionFilter: Filter | undefined; eventListConfig?: OverrideBodyQuery; indexFields: DataViewFieldBase[]; + sortOrder?: SortOrderOrUndefined; } export interface EventDoc { diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/check_and_format_privileges.test.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/check_and_format_privileges.test.ts index 0001dfdac4143..04f4e95272116 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/check_and_format_privileges.test.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/check_and_format_privileges.test.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { _formatPrivileges } from './check_and_format_privileges'; +import { ASSET_CRITICALITY_INDEX_PATTERN } from '../../../../common/entity_analytics/asset_criticality/constants'; +import { _formatPrivileges, hasReadWritePermissions } from './check_and_format_privileges'; describe('_formatPrivileges', () => { it('should correctly format elasticsearch index privileges', () => { @@ -146,4 +147,60 @@ describe('_formatPrivileges', () => { }, }); }); + + it('should correctly extract read and write permissions from elasticsearch cluster privileges', () => { + const privileges = { + elasticsearch: { + cluster: [ + { + privilege: 'read', + authorized: true, + }, + { + privilege: 'write', + authorized: false, + }, + ], + index: {}, + }, + kibana: [], + }; + + const result = hasReadWritePermissions(privileges.elasticsearch); + + expect(result).toEqual({ + has_read_permissions: true, + has_write_permissions: false, + }); + }); + it('should correctly extract read and write permissions from elasticsearch index privileges', () => { + const privileges = { + elasticsearch: { + cluster: [], + index: { + [ASSET_CRITICALITY_INDEX_PATTERN]: [ + { + privilege: 'read', + authorized: true, + }, + { + privilege: 'write', + authorized: false, + }, + ], + }, + }, + kibana: [], + }; + + const result = hasReadWritePermissions( + privileges.elasticsearch, + ASSET_CRITICALITY_INDEX_PATTERN + ); + + expect(result).toEqual({ + has_read_permissions: true, + has_write_permissions: false, + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/check_and_format_privileges.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/check_and_format_privileges.ts index 52348ad1be879..0c9ac9036fad2 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/check_and_format_privileges.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/check_and_format_privileges.ts @@ -11,6 +11,7 @@ import type { CheckPrivilegesResponse, SecurityPluginStart, } from '@kbn/security-plugin/server'; +import { ASSET_CRITICALITY_INDEX_PATTERN } from '../../../../common/entity_analytics/asset_criticality/constants'; import type { EntityAnalyticsPrivileges } from '../../../../common/api/entity_analytics/common'; const groupPrivilegesByName = ( privileges: Array<{ @@ -69,5 +70,21 @@ export async function checkAndFormatPrivileges({ return { privileges: _formatPrivileges(privileges), has_all_required: hasAllRequested, + ...hasReadWritePermissions(privileges.elasticsearch, ASSET_CRITICALITY_INDEX_PATTERN), }; } + +export const hasReadWritePermissions = ( + { index, cluster }: CheckPrivilegesResponse['privileges']['elasticsearch'], + indexKey = '' +) => { + const has = + (type: string) => + ({ privilege, authorized }: { privilege: string; authorized: boolean }) => + privilege === type && authorized; + return { + has_read_permissions: index[indexKey]?.some(has('read')) || cluster.some(has('read')), + + has_write_permissions: index[indexKey]?.some(has('write')) || cluster.some(has('write')), + }; +}; diff --git a/x-pack/plugins/serverless_search/public/application/components/overview.tsx b/x-pack/plugins/serverless_search/public/application/components/overview.tsx index 2a2313564c2e2..1cb337e6584b9 100644 --- a/x-pack/plugins/serverless_search/public/application/components/overview.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/overview.tsx @@ -250,6 +250,7 @@ export const ElasticsearchOverview = () => { assetBasePath={assetBasePath} docLinks={docLinks} application={application} + consolePlugin={consolePlugin} sharePlugin={share} additionalIngestionPanel={} /> @@ -277,6 +278,7 @@ export const ElasticsearchOverview = () => { setSelectedLanguage={setSelectedLanguage} assetBasePath={assetBasePath} application={application} + consolePlugin={consolePlugin} sharePlugin={share} /> } diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx index 7e3aa104f5a60..fd9a63f5547e0 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx @@ -116,6 +116,7 @@ export const EsqlQueryExpression: React.FC< from: new Date(now - timeWindow).toISOString(), to: new Date(now).toISOString(), }, + undefined, // create a data view with the timefield to pass into the query new DataView({ spec: { timeFieldName: timeField }, @@ -219,7 +220,7 @@ export const EsqlQueryExpression: React.FC< }, 1000)} expandCodeEditor={() => true} isCodeEditorExpanded={true} - onTextLangQuerySubmit={() => {}} + onTextLangQuerySubmit={async () => {}} detectTimestamp={detectTimestamp} hideMinimizeButton={true} hideRunQueryText={true} diff --git a/x-pack/plugins/stack_alerts/public/rule_types/threshold/visualization.tsx b/x-pack/plugins/stack_alerts/public/rule_types/threshold/visualization.tsx index c8996d9fcaa4b..918c87c5d13a8 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/threshold/visualization.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/threshold/visualization.tsx @@ -269,7 +269,11 @@ export const ThresholdVisualization: React.FunctionComponent = ({ baseTheme={chartsBaseTheme} xDomain={domain} showLegend={!!termField} - showLegendExtra + // Please double check if the data passed to the chart contains all the buckets, even the empty ones. + // the showLegendExtra will display the last element of the data array as the default legend value + // and if empty buckets are filtered out you can probably see a value that doesn't correspond + // to the value in the last time bucket visualized. + // showLegendExtra legendPosition={Position.Bottom} locale={i18n.getLocale()} /> diff --git a/x-pack/plugins/stack_connectors/common/bedrock/constants.ts b/x-pack/plugins/stack_connectors/common/bedrock/constants.ts index 242447d505218..ea3fb7af72fa9 100644 --- a/x-pack/plugins/stack_connectors/common/bedrock/constants.ts +++ b/x-pack/plugins/stack_connectors/common/bedrock/constants.ts @@ -23,6 +23,6 @@ export enum SUB_ACTION { } export const DEFAULT_TOKEN_LIMIT = 8191; -export const DEFAULT_BEDROCK_MODEL = 'anthropic.claude-v2'; +export const DEFAULT_BEDROCK_MODEL = 'anthropic.claude-v2:1'; export const DEFAULT_BEDROCK_URL = `https://bedrock-runtime.us-east-1.amazonaws.com` as const; diff --git a/x-pack/plugins/stack_connectors/common/bedrock/schema.ts b/x-pack/plugins/stack_connectors/common/bedrock/schema.ts index 057780a803560..c26ce8c1e88c3 100644 --- a/x-pack/plugins/stack_connectors/common/bedrock/schema.ts +++ b/x-pack/plugins/stack_connectors/common/bedrock/schema.ts @@ -32,6 +32,8 @@ export const InvokeAIActionParamsSchema = schema.object({ }) ), model: schema.maybe(schema.string()), + temperature: schema.maybe(schema.number()), + stopSequences: schema.maybe(schema.arrayOf(schema.string())), }); export const InvokeAIActionResponseSchema = schema.object({ diff --git a/x-pack/plugins/stack_connectors/public/connector_types/bedrock/params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/bedrock/params.tsx index 7678f52321dd3..0ccd8c1d08023 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/bedrock/params.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/bedrock/params.tsx @@ -102,7 +102,7 @@ const BedrockParamsFields: React.FunctionComponent { editSubActionParams({ model: ev.target.value }); diff --git a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.test.ts index 3cd2ad2061ffd..3b1cb3bc96ec8 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.test.ts @@ -73,7 +73,7 @@ describe('BedrockConnector', () => { 'Content-Type': 'application/json', }, host: 'bedrock-runtime.us-east-1.amazonaws.com', - path: '/model/anthropic.claude-v2/invoke', + path: '/model/anthropic.claude-v2:1/invoke', service: 'bedrock', }, { accessKeyId: '123', secretAccessKey: 'secret' } @@ -137,7 +137,7 @@ describe('BedrockConnector', () => { 'x-amzn-bedrock-accept': '*/*', }, host: 'bedrock-runtime.us-east-1.amazonaws.com', - path: '/model/anthropic.claude-v2/invoke-with-response-stream', + path: '/model/anthropic.claude-v2:1/invoke-with-response-stream', service: 'bedrock', }, { accessKeyId: '123', secretAccessKey: 'secret' } @@ -165,14 +165,53 @@ describe('BedrockConnector', () => { it('formats messages from user, assistant, and system', async () => { await connector.invokeStream({ messages: [ + { + role: 'system', + content: 'Be a good chatbot', + }, { role: 'user', content: 'Hello world', }, + { + role: 'assistant', + content: 'Hi, I am a good chatbot', + }, + { + role: 'user', + content: 'What is 2+2?', + }, + ], + }); + expect(mockRequest).toHaveBeenCalledWith({ + signed: true, + responseType: 'stream', + url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke-with-response-stream`, + method: 'post', + responseSchema: StreamingResponseSchema, + data: JSON.stringify({ + prompt: + 'Be a good chatbot\n\nHuman:Hello world\n\nAssistant:Hi, I am a good chatbot\n\nHuman:What is 2+2? \n\nAssistant:', + max_tokens_to_sample: DEFAULT_TOKEN_LIMIT, + temperature: 0.5, + stop_sequences: ['\n\nHuman:'], + }), + }); + }); + + it('formats the system message as a user message for claude<2.1', async () => { + const modelOverride = 'anthropic.claude-v2'; + + await connector.invokeStream({ + messages: [ { role: 'system', content: 'Be a good chatbot', }, + { + role: 'user', + content: 'Hello world', + }, { role: 'assistant', content: 'Hi, I am a good chatbot', @@ -182,16 +221,17 @@ describe('BedrockConnector', () => { content: 'What is 2+2?', }, ], + model: modelOverride, }); expect(mockRequest).toHaveBeenCalledWith({ signed: true, responseType: 'stream', - url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke-with-response-stream`, + url: `${DEFAULT_BEDROCK_URL}/model/${modelOverride}/invoke-with-response-stream`, method: 'post', responseSchema: StreamingResponseSchema, data: JSON.stringify({ prompt: - '\n\nHuman:Hello world\n\nHuman:Be a good chatbot\n\nAssistant:Hi, I am a good chatbot\n\nHuman:What is 2+2? \n\nAssistant:', + '\n\nHuman:Be a good chatbot\n\nHuman:Hello world\n\nAssistant:Hi, I am a good chatbot\n\nHuman:What is 2+2? \n\nAssistant:', max_tokens_to_sample: DEFAULT_TOKEN_LIMIT, temperature: 0.5, stop_sequences: ['\n\nHuman:'], @@ -244,14 +284,14 @@ describe('BedrockConnector', () => { it('formats messages from user, assistant, and system', async () => { const response = await connector.invokeAI({ messages: [ - { - role: 'user', - content: 'Hello world', - }, { role: 'system', content: 'Be a good chatbot', }, + { + role: 'user', + content: 'Hello world', + }, { role: 'assistant', content: 'Hi, I am a good chatbot', @@ -271,7 +311,7 @@ describe('BedrockConnector', () => { responseSchema: RunActionResponseSchema, data: JSON.stringify({ prompt: - '\n\nHuman:Hello world\n\nHuman:Be a good chatbot\n\nAssistant:Hi, I am a good chatbot\n\nHuman:What is 2+2? \n\nAssistant:', + 'Be a good chatbot\n\nHuman:Hello world\n\nAssistant:Hi, I am a good chatbot\n\nHuman:What is 2+2? \n\nAssistant:', max_tokens_to_sample: DEFAULT_TOKEN_LIMIT, temperature: 0.5, stop_sequences: ['\n\nHuman:'], diff --git a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts index f70a592509776..3fdbaae1d702a 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts @@ -26,7 +26,11 @@ import type { InvokeAIActionResponse, StreamActionParams, } from '../../../common/bedrock/types'; -import { SUB_ACTION, DEFAULT_TOKEN_LIMIT } from '../../../common/bedrock/constants'; +import { + SUB_ACTION, + DEFAULT_TOKEN_LIMIT, + DEFAULT_BEDROCK_MODEL, +} from '../../../common/bedrock/constants'; import { DashboardActionParams, DashboardActionResponse, @@ -233,9 +237,14 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B * @param messages An array of messages to be sent to the API * @param model Optional model to be used for the API request. If not provided, the default model from the connector will be used. */ - public async invokeStream({ messages, model }: InvokeAIActionParams): Promise { + public async invokeStream({ + messages, + model, + stopSequences, + temperature, + }: InvokeAIActionParams): Promise { const res = (await this.streamApi({ - body: JSON.stringify(formatBedrockBody({ messages })), + body: JSON.stringify(formatBedrockBody({ messages, model, stopSequences, temperature })), model, })) as unknown as IncomingMessage; return res; @@ -250,20 +259,43 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B messages, model, }: InvokeAIActionParams): Promise { - const res = await this.runApi({ body: JSON.stringify(formatBedrockBody({ messages })), model }); + const res = await this.runApi({ + body: JSON.stringify(formatBedrockBody({ messages, model })), + model, + }); return { message: res.completion.trim() }; } } const formatBedrockBody = ({ + model = DEFAULT_BEDROCK_MODEL, messages, + stopSequences = ['\n\nHuman:'], + temperature = 0.5, }: { + model?: string; messages: Array<{ role: string; content: string }>; + stopSequences?: string[]; + temperature?: number; }) => { const combinedMessages = messages.reduce((acc: string, message) => { const { role, content } = message; - // Bedrock only has Assistant and Human, so 'system' and 'user' will be converted to Human - const bedrockRole = role === 'assistant' ? '\n\nAssistant:' : '\n\nHuman:'; + const [, , modelName, majorVersion, minorVersion] = + (model || '').match(/(\w+)\.(.*)-v(\d+)(?::(\d+))?/) || []; + // Claude only has Assistant and Human, so 'user' will be converted to Human + let bedrockRole: string; + + if ( + role === 'system' && + modelName === 'claude' && + Number(majorVersion) >= 2 && + Number(minorVersion) >= 1 + ) { + bedrockRole = ''; + } else { + bedrockRole = role === 'assistant' ? '\n\nAssistant:' : '\n\nHuman:'; + } + return `${acc}${bedrockRole}${content}`; }, ''); @@ -271,8 +303,8 @@ const formatBedrockBody = ({ // end prompt in "Assistant:" to avoid the model starting its message with "Assistant:" prompt: `${combinedMessages} \n\nAssistant:`, max_tokens_to_sample: DEFAULT_TOKEN_LIMIT, - temperature: 0.5, + temperature, // prevent model from talking to itself - stop_sequences: ['\n\nHuman:'], + stop_sequences: stopSequences, }; }; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/index.ts index 5f295b8c39367..688148d51ed63 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/index.ts @@ -10,7 +10,10 @@ import { SubActionConnectorType, ValidatorType, } from '@kbn/actions-plugin/server/sub_action_framework/types'; -import { GenerativeAIForSecurityConnectorFeatureId } from '@kbn/actions-plugin/common'; +import { + GenerativeAIForObservabilityConnectorFeatureId, + GenerativeAIForSecurityConnectorFeatureId, +} from '@kbn/actions-plugin/common'; import { urlAllowListValidator } from '@kbn/actions-plugin/server'; import { ValidatorServices } from '@kbn/actions-plugin/server/types'; import { assertURL } from '@kbn/actions-plugin/server/sub_action_framework/helpers/validators'; @@ -29,7 +32,10 @@ export const getConnectorType = (): SubActionConnectorType => ( secrets: SecretsSchema, }, validators: [{ type: ValidatorType.CONFIG, validator: configValidator }], - supportedFeatureIds: [GenerativeAIForSecurityConnectorFeatureId], + supportedFeatureIds: [ + GenerativeAIForSecurityConnectorFeatureId, + GenerativeAIForObservabilityConnectorFeatureId, + ], minimumLicenseRequired: 'enterprise' as const, renderParameterTemplates, }); diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index 9d85c216546f8..097dcb9b32199 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -42,6 +42,9 @@ describe('config validation', () => { }, "poll_interval": 3000, "request_capacity": 1000, + "request_timeouts": Object { + "update_by_query": 30000, + }, "requeue_invalid_tasks": Object { "delay": 3000, "enabled": false, @@ -102,6 +105,9 @@ describe('config validation', () => { }, "poll_interval": 3000, "request_capacity": 1000, + "request_timeouts": Object { + "update_by_query": 30000, + }, "requeue_invalid_tasks": Object { "delay": 3000, "enabled": false, @@ -165,6 +171,9 @@ describe('config validation', () => { }, "poll_interval": 3000, "request_capacity": 1000, + "request_timeouts": Object { + "update_by_query": 30000, + }, "requeue_invalid_tasks": Object { "delay": 3000, "enabled": false, diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index 3be8b341c939e..0e30dd16b8841 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -62,6 +62,11 @@ const requeueInvalidTasksConfig = schema.object({ max_attempts: schema.number({ defaultValue: 100, min: 1, max: 500 }), }); +const requestTimeoutsConfig = schema.object({ + /* The request timeout config for task manager's updateByQuery default:30s, min:10s, max:10m */ + update_by_query: schema.number({ defaultValue: 1000 * 30, min: 1000 * 10, max: 1000 * 60 * 10 }), +}); + export const configSchema = schema.object( { allow_reading_invalid_state: schema.boolean({ defaultValue: true }), @@ -156,6 +161,7 @@ export const configSchema = schema.object( min: 1, }), claim_strategy: schema.string({ defaultValue: CLAIM_STRATEGY_DEFAULT }), + request_timeouts: requestTimeoutsConfig, }, { validate: (config) => { @@ -179,3 +185,4 @@ export type RequeueInvalidTasksConfig = TypeOf export type TaskManagerConfig = TypeOf; export type TaskExecutionFailureThreshold = TypeOf; export type EventLoopDelayConfig = TypeOf; +export type RequestTimeoutsConfig = TypeOf; diff --git a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts index 6ae1d00a62243..98465e88e5466 100644 --- a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts @@ -86,6 +86,9 @@ describe('EphemeralTaskLifecycle', () => { }, metrics_reset_interval: 3000, claim_strategy: 'default', + request_timeouts: { + update_by_query: 1000, + }, ...config, }, elasticsearchAndSOAvailability$, diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts index 2fd4ceb74ca74..408771d0a55a6 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -81,6 +81,9 @@ describe('managed configuration', () => { }, metrics_reset_interval: 3000, claim_strategy: 'default', + request_timeouts: { + update_by_query: 1000, + }, }); logger = context.logger.get('taskManager'); diff --git a/x-pack/plugins/task_manager/server/integration_tests/task_state_validation.test.ts b/x-pack/plugins/task_manager/server/integration_tests/task_state_validation.test.ts index 716e1f8dcb83f..a4ffa7514e02c 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/task_state_validation.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/task_state_validation.test.ts @@ -6,14 +6,11 @@ */ import { v4 as uuidV4 } from 'uuid'; -import { - type TestElasticsearchUtils, - type TestKibanaUtils, -} from '@kbn/core-test-helpers-kbn-server'; +import type { TestElasticsearchUtils, TestKibanaUtils } from '@kbn/core-test-helpers-kbn-server'; import { schema } from '@kbn/config-schema'; import { TaskStatus } from '../task'; -import { type TaskPollingLifecycleOpts } from '../polling_lifecycle'; -import { type TaskClaimingOpts } from '../queries/task_claiming'; +import type { TaskPollingLifecycleOpts } from '../polling_lifecycle'; +import type { TaskClaimingOpts } from '../queries/task_claiming'; import { TaskManagerPlugin, type TaskManagerStartContract } from '../plugin'; import { injectTask, setupTestServers, retry } from './lib'; @@ -308,6 +305,7 @@ describe('task state validation', () => { it('should fail the task run when setting allow_reading_invalid_state:false and reading an invalid state', async () => { const logSpy = jest.spyOn(pollingLifecycleOpts.logger, 'warn'); + const updateSpy = jest.spyOn(pollingLifecycleOpts.taskStore, 'bulkUpdate'); const id = uuidV4(); await injectTask(kibanaServer.coreStart.elasticsearch.client.asInternalUser, { @@ -331,8 +329,9 @@ describe('task state validation', () => { expect(logSpy.mock.calls[0][0]).toBe( `Task (fooType/${id}) has a validation error: [foo]: expected value of type [string] but got [boolean]` ); - expect(logSpy.mock.calls[1][0]).toBe( - `Task fooType \"${id}\" failed in attempt to run: [foo]: expected value of type [string] but got [boolean]` + expect(updateSpy).toHaveBeenCalledWith( + expect.arrayContaining([expect.objectContaining({ id, taskType: 'fooType' })]), + { validate: false } ); }); }); diff --git a/x-pack/plugins/task_manager/server/lib/calculate_health_status.test.ts b/x-pack/plugins/task_manager/server/lib/calculate_health_status.test.ts index 228d94ed87daf..df3ffdcc75cfe 100644 --- a/x-pack/plugins/task_manager/server/lib/calculate_health_status.test.ts +++ b/x-pack/plugins/task_manager/server/lib/calculate_health_status.test.ts @@ -58,6 +58,9 @@ const config = { }, metrics_reset_interval: 3000, claim_strategy: 'default', + request_timeouts: { + update_by_query: 1000, + }, }; const getStatsWithTimestamp = ({ diff --git a/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts b/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts index afcd4884f7777..349f0f73a4215 100644 --- a/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts +++ b/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts @@ -74,6 +74,9 @@ const config: TaskManagerConfig = { version_conflict_threshold: 80, worker_utilization_running_average_window: 5, claim_strategy: 'default', + request_timeouts: { + update_by_query: 1000, + }, }; describe('createAggregator', () => { diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts index 543707bf940b2..25b6f604ce305 100644 --- a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts @@ -54,6 +54,9 @@ describe('Configuration Statistics Aggregator', () => { }, metrics_reset_interval: 3000, claim_strategy: 'default', + request_timeouts: { + update_by_query: 1000, + }, }; const managedConfig = { diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts index 7dc3460d98388..2b3ec904d52b6 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts @@ -59,6 +59,9 @@ describe('createMonitoringStatsStream', () => { }, metrics_reset_interval: 3000, claim_strategy: 'default', + request_timeouts: { + update_by_query: 1000, + }, }; it('returns the initial config used to configure Task Manager', async () => { diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 5e178db3b99ad..4905dc09fb5c2 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -79,6 +79,9 @@ const pluginInitializerContextParams = { }, metrics_reset_interval: 3000, claim_strategy: 'default', + request_timeouts: { + update_by_query: 1000, + }, }; describe('TaskManagerPlugin', () => { diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 2fbabc1fe4d67..0baedbaf2bf54 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -241,6 +241,7 @@ export class TaskManagerPlugin adHocTaskCounter: this.adHocTaskCounter, allowReadingInvalidState: this.config.allow_reading_invalid_state, logger: this.logger, + requestTimeouts: this.config.request_timeouts, }); const managedConfiguration = createManagedConfiguration({ diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index 24898d3c99385..56675ac868195 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -84,6 +84,9 @@ describe('TaskPollingLifecycle', () => { }, metrics_reset_interval: 3000, claim_strategy: 'default', + request_timeouts: { + update_by_query: 1000, + }, }, taskStore: mockTaskStore, logger: taskManagerLogger, diff --git a/x-pack/plugins/task_manager/server/task.ts b/x-pack/plugins/task_manager/server/task.ts index c71f8b42185ca..0d064153859a5 100644 --- a/x-pack/plugins/task_manager/server/task.ts +++ b/x-pack/plugins/task_manager/server/task.ts @@ -50,6 +50,7 @@ export type SuccessfulRunResult = { state: Record; taskRunError?: DecoratedError; skipAttempts?: number; + shouldValidate?: boolean; } & ( | // ensure a SuccessfulRunResult can either specify a new `runAt` or a new `schedule`, but not both { diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts index 8a96405abfed6..6735b3c0602b8 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts @@ -1082,6 +1082,7 @@ describe('TaskManagerRunner', () => { await runner.run(); expect(store.update).toHaveBeenCalledTimes(1); + expect(store.update).toHaveBeenCalledWith(expect.any(Object), { validate: true }); const instance = store.update.mock.calls[0][0]; expect(instance.runAt.getTime()).toEqual(nextRetry.getTime()); @@ -1113,6 +1114,8 @@ describe('TaskManagerRunner', () => { await runner.run(); expect(store.update).toHaveBeenCalledTimes(1); + expect(store.update).toHaveBeenCalledWith(expect.any(Object), { validate: true }); + const instance = store.update.mock.calls[0][0]; const minRunAt = Date.now(); @@ -1179,6 +1182,8 @@ describe('TaskManagerRunner', () => { await runner.run(); expect(store.update).toHaveBeenCalledTimes(1); + expect(store.update).toHaveBeenCalledWith(expect.any(Object), { validate: true }); + sinon.assert.notCalled(getRetryStub); const instance = store.update.mock.calls[0][0]; @@ -1252,6 +1257,7 @@ describe('TaskManagerRunner', () => { new Date(Date.now() + intervalSeconds * 1000).getTime() ); expect(instance.enabled).not.toBeDefined(); + expect(store.update).toHaveBeenCalledWith(expect.any(Object), { validate: true }); }); test('throws error when the task has invalid state', async () => { @@ -1266,7 +1272,7 @@ describe('TaskManagerRunner', () => { stateVersion: 4, }; - const { runner, logger } = await readyToRunStageSetup({ + const { runner, logger, store } = await readyToRunStageSetup({ instance: mockTaskInstance, definitions: { bar: { @@ -1308,13 +1314,19 @@ describe('TaskManagerRunner', () => { }, }); - expect(() => runner.run()).rejects.toMatchInlineSnapshot( - `[Error: [foo]: expected value of type [string] but got [boolean]]` - ); + expect(await runner.run()).toEqual({ + error: { + error: new Error('[foo]: expected value of type [string] but got [boolean]'), + shouldValidate: false, + state: { bar: 'test', baz: 'test', foo: true }, + }, + tag: 'err', + }); expect(logger.warn).toHaveBeenCalledTimes(1); expect(logger.warn).toHaveBeenCalledWith( 'Task (bar/foo) has a validation error: [foo]: expected value of type [string] but got [boolean]' ); + expect(store.update).toHaveBeenCalledWith(expect.any(Object), { validate: false }); }); test('does not throw error and runs when the task has invalid state and allowReadingInvalidState = true', async () => { diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.ts index ab86d83e99310..faea2bfb7e446 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.ts @@ -314,16 +314,30 @@ export class TaskManagerRunner implements TaskRunner { const apmTrans = apm.startTransaction(this.taskType, TASK_MANAGER_RUN_TRANSACTION_TYPE, { childOf: this.instance.task.traceparent, }); + const stopTaskTimer = startTaskTimerWithEventLoopMonitoring(this.eventLoopDelayConfig); // Validate state - const validatedTaskInstance = this.validateTaskState(this.instance.task); + const stateValidationResult = this.validateTaskState(this.instance.task); + + if (stateValidationResult.error) { + const processedResult = await withSpan({ name: 'process result', type: 'task manager' }, () => + this.processResult( + asErr({ + error: stateValidationResult.error, + state: stateValidationResult.taskInstance.state, + shouldValidate: false, + }), + stopTaskTimer() + ) + ); + if (apmTrans) apmTrans.end('failure'); + return processedResult; + } const modifiedContext = await this.beforeRun({ - taskInstance: validatedTaskInstance, + taskInstance: stateValidationResult.taskInstance, }); - const stopTaskTimer = startTaskTimerWithEventLoopMonitoring(this.eventLoopDelayConfig); - this.onTaskEvent( asTaskManagerStatEvent( 'runDelay', @@ -411,11 +425,12 @@ export class TaskManagerRunner implements TaskRunner { private validateTaskState(taskInstance: ConcreteTaskInstance) { const { taskType, id } = taskInstance; try { - const validatedTask = this.taskValidator.getValidatedTaskInstanceFromReading(taskInstance); - return validatedTask; + const validatedTaskInstance = + this.taskValidator.getValidatedTaskInstanceFromReading(taskInstance); + return { taskInstance: validatedTaskInstance, error: null }; } catch (error) { this.logger.warn(`Task (${taskType}/${id}) has a validation error: ${error.message}`); - throw error; + return { taskInstance, error }; } } @@ -723,6 +738,7 @@ export class TaskManagerRunner implements TaskRunner { this.instance = asRan(this.instance.task); await this.removeTask(); } else { + const { shouldValidate = true } = unwrap(result); this.instance = asRan( await this.bufferedTaskStore.update( defaults( @@ -735,7 +751,7 @@ export class TaskManagerRunner implements TaskRunner { }, taskWithoutEnabled(this.instance.task) ), - { validate: true } + { validate: shouldValidate } ) ); } diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 933410be29b0c..a712cc5814a7d 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -25,6 +25,7 @@ import { TaskTypeDictionary } from './task_type_dictionary'; import { mockLogger } from './test_utils'; import { AdHocTaskCounter } from './lib/adhoc_task_counter'; import { asErr } from './lib/result_type'; +import { UpdateByQueryResponse } from '@elastic/elasticsearch/lib/api/types'; const mockGetValidatedTaskInstanceFromReading = jest.fn(); const mockGetValidatedTaskInstanceForUpdating = jest.fn(); @@ -108,6 +109,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); }); @@ -280,6 +284,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); }); @@ -351,6 +358,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); }); @@ -459,6 +469,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); }); @@ -610,6 +623,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); }); @@ -693,6 +709,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); }); @@ -729,6 +748,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); }); @@ -768,6 +790,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); }); @@ -828,6 +853,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); }); @@ -923,6 +951,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); expect(await store.getLifecycle(task.id)).toEqual(status); @@ -945,6 +976,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); expect(await store.getLifecycle(randomId())).toEqual(TaskLifecycleResult.NotFound); @@ -965,6 +999,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); return expect(store.getLifecycle(randomId())).rejects.toThrow('Bad Request'); @@ -985,6 +1022,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); }); @@ -1155,6 +1195,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, }); expect(jest.requireMock('./task_validator').TaskValidator).toHaveBeenCalledWith({ @@ -1177,6 +1220,9 @@ describe('TaskStore', () => { savedObjectsRepository: savedObjectsClient, adHocTaskCounter, allowReadingInvalidState: true, + requestTimeouts: { + update_by_query: 1000, + }, }); expect(jest.requireMock('./task_validator').TaskValidator).toHaveBeenCalledWith({ @@ -1186,4 +1232,41 @@ describe('TaskStore', () => { }); }); }); + + describe('updateByQuery', () => { + let store: TaskStore; + let esClient: ReturnType['asInternalUser']; + let childEsClient: ReturnType< + typeof elasticsearchServiceMock.createClusterClient + >['asInternalUser']; + + beforeAll(() => { + esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + childEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + esClient.child.mockReturnValue(childEsClient as unknown as Client); + store = new TaskStore({ + logger: mockLogger(), + index: 'tasky', + taskManagerId: '', + serializer, + esClient, + definitions: taskDefinitions, + savedObjectsRepository: savedObjectsClient, + adHocTaskCounter, + allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, + }); + }); + test('should pass requestTimeout', async () => { + childEsClient.updateByQuery.mockResponse({ + hits: { hits: [], total: 0, updated: 100, version_conflicts: 0 }, + } as UpdateByQueryResponse); + await store.updateByQuery({ script: '' }, { max_docs: 10 }); + expect(childEsClient.updateByQuery).toHaveBeenCalledWith(expect.any(Object), { + requestTimeout: 1000, + }); + }); + }); }); diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index dc3ce1459f760..45bcb6589ab26 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -24,6 +24,7 @@ import { ElasticsearchClient, } from '@kbn/core/server'; +import { RequestTimeoutsConfig } from './config'; import { asOk, asErr, Result } from './lib/result_type'; import { @@ -48,6 +49,7 @@ export interface StoreOpts { adHocTaskCounter: AdHocTaskCounter; allowReadingInvalidState: boolean; logger: Logger; + requestTimeouts: RequestTimeoutsConfig; } export interface SearchOpts { @@ -108,6 +110,7 @@ export class TaskStore { private savedObjectsRepository: ISavedObjectsRepository; private serializer: ISavedObjectsSerializer; private adHocTaskCounter: AdHocTaskCounter; + private requestTimeouts: RequestTimeoutsConfig; /** * Constructs a new TaskStore. @@ -136,6 +139,7 @@ export class TaskStore { // The poller doesn't need retry logic because it will try again at the next polling cycle maxRetries: 0, }); + this.requestTimeouts = opts.requestTimeouts; } /** @@ -492,17 +496,20 @@ export class TaskStore { const { query } = ensureQueryOnlyReturnsTaskObjects(opts); try { const // eslint-disable-next-line @typescript-eslint/naming-convention - { total, updated, version_conflicts } = await this.esClientWithoutRetries.updateByQuery({ - index: this.index, - ignore_unavailable: true, - refresh: true, - conflicts: 'proceed', - body: { - ...opts, - max_docs, - query, + { total, updated, version_conflicts } = await this.esClientWithoutRetries.updateByQuery( + { + index: this.index, + ignore_unavailable: true, + refresh: true, + conflicts: 'proceed', + body: { + ...opts, + max_docs, + query, + }, }, - }); + { requestTimeout: this.requestTimeouts.update_by_query } + ); const conflictsCorrectedForContinuation = correctVersionConflictsForContinuation( updated, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c812c47f9aa48..0fda2e3915518 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -140,8 +140,6 @@ "coloring.colorMapping.assignments.autoAssignedTermAriaLabel": "Cette couleur sera automatiquement affectée au premier terme qui ne correspond pas à toutes les autres affectations", "coloring.colorMapping.assignments.autoAssignedTermPlaceholder": "Affecté automatiquement", "coloring.colorMapping.assignments.deleteAssignmentButtonLabel": "Supprimer cette affectation", - "coloring.colorMapping.assignments.unassignedAriaLabel": "Affecter cette couleur à tout terme non affecté qui n'est pas décrit dans la liste d'affectation", - "coloring.colorMapping.assignments.unassignedPlaceholder": "Termes non affectés", "coloring.colorMapping.colorChangesModal.categoricalModeDescription": "Basculer en mode de catégorie conduira à l'abandon de toutes vos modifications de couleurs personnalisées", "coloring.colorMapping.colorChangesModal.discardButton": "Abandonner les modifications", "coloring.colorMapping.colorChangesModal.discardButtonLabel": "Abandonner les modifications", @@ -159,14 +157,11 @@ "coloring.colorMapping.colorPicker.removeGradientColorButtonLabel": "Supprimer l'étape couleur", "coloring.colorMapping.colorPicker.themeAwareColorsLabel": "Couleurs neutres", "coloring.colorMapping.colorPicker.themeAwareColorsTooltip": "Les couleurs neutres fournies se conforment au thème et s'adapteront en fonction du basculement entre les thèmes clair et sombre", - "coloring.colorMapping.container.addAssignmentButtonLabel": "Ajouter une affectation", - "coloring.colorMapping.container.autoAssignLabel": "Affectation automatique", "coloring.colorMapping.container.invertGradientButtonLabel": "Inverser le gradient", "coloring.colorMapping.container.mappingAssignmentHeader": "Mapping des affectations", "coloring.colorMapping.paletteSelector.categoricalLabel": "De catégorie", "coloring.colorMapping.paletteSelector.paletteLabel": "Palette de couleurs", "coloring.colorMapping.paletteSelector.scaleLabel": "Scaling", - "coloring.colorMapping.paletteSelector.sequentialLabel": "Séquentiel", "coloring.dynamicColoring.customPalette.addColor": "Ajouter une couleur", "coloring.dynamicColoring.customPalette.addColorAriaLabel": "Ajouter une couleur", "coloring.dynamicColoring.customPalette.colorStopsHelpPercentage": "Les types de valeurs en pourcentage sont relatifs à la plage complète des valeurs de données disponibles.", @@ -870,9 +865,9 @@ "core.euiToast.dismissToast": "Rejeter le toast", "core.euiToast.newNotification": "Une nouvelle notification apparaît.", "core.euiToast.notification": "Notification", - "core.euiTourStep.closeTour": "Fermer la visite", - "core.euiTourStep.endTour": "Terminer la visite", - "core.euiTourStep.skipTour": "Ignorer la visite", + "core.euiTourFooter.closeTour": "Fermer la visite", + "core.euiTourFooter.endTour": "Terminer la visite", + "core.euiTourFooter.skipTour": "Ignorer la visite", "core.euiTourStepIndicator.isActive": "active", "core.euiTourStepIndicator.isComplete": "terminée", "core.euiTourStepIndicator.isIncomplete": "incomplète", @@ -11004,7 +10999,6 @@ "xpack.cases.allCases.comments": "Commentaires", "xpack.cases.allCases.noCategoriesAvailable": "Pas de catégories disponibles", "xpack.cases.allCases.noTagsAvailable": "Aucune balise disponible", - "xpack.cases.allCasesView.filterAssignees.clearFilters": "Effacer les filtres", "xpack.cases.allCasesView.filterAssignees.noAssigneesLabel": "Aucun utilisateur affecté", "xpack.cases.allCasesView.filterAssigneesAriaLabel": "cliquer pour filtrer les utilisateurs affectés", "xpack.cases.allCasesView.showLessAvatars": "afficher moins", @@ -12174,7 +12168,6 @@ "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterSValidationErrorMessage": "{length, plural, one { {lg} } many { Le groupe de lettres {lg} } other { Le groupe de lettres {lg} }} en {format} n'est pas compatible, car il n'est pas précédé de ss ni d'un séparateur de {sep}", "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterValidationErrorMessage": "{length, plural, one { {lg} } many { Le groupe de lettres {lg} } other { Le groupe de lettres {lg} }} en {format} n'est pas compatible", "xpack.dataVisualizer.file.editFlyout.overrides.timestampQuestionMarkValidationErrorMessage": "Le format d'horodatage {timestampFormat} n'est pas compatible, car il contient un point d'interrogation ({fieldPlaceholder})", - "xpack.dataVisualizer.file.fileContents.firstLinesDescription": "{numberOfLines, plural, one {# ligne} many {# lignes} other {# lignes}} premier(s)", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeByDiffFormatErrorMessage": "La taille du fichier que vous avez sélectionné pour le chargement dépasse la taille maximale autorisée de {maxFileSizeFormatted} de {diffFormatted}", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeErrorMessage": "La taille du fichier que vous avez sélectionné pour le chargement est de {fileSizeFormatted}, ce qui dépasse la taille maximale autorisée de {maxFileSizeFormatted}", "xpack.dataVisualizer.file.importSummary.documentsCouldNotBeImportedDescription": "Impossible d'importer {importFailuresLength} document(s) sur {docCount}. Cela peut être dû au manque de correspondance entre les lignes et le modèle Grok.", @@ -12772,7 +12765,6 @@ "xpack.enterpriseSearch.content.indices.connectorScheduling.page.description": "Votre connecteur est désormais déployé. Vous pouvez planifier du contenu récurrent et accéder aux synchronisations de contrôle ici. Si vous souhaitez exécuter un test rapide, lancez une synchronisation unique à l’aide du bouton {sync}.", "xpack.enterpriseSearch.content.indices.connectorScheduling.schedulePanel.documentLevelSecurity.dlsDisabledCallout.text": "{link} pour ce connecteur afin d'activer ces options.", "xpack.enterpriseSearch.content.indices.deleteIndex.successToast.title": "Votre index {indexName} et toute configuration d'ingestion associée ont été supprimés avec succès", - "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.description": "Aucun de vos modèles entraînés de Machine Learning ne peut être utilisé par un pipeline d'inférence. {documentationLink}", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipeline.missingSourceFieldsDescription": "Champs manquants dans cet index : {commaSeparatedMissingSourceFields}", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText": "Les noms de pipeline sont uniques dans un déploiement, et ils peuvent uniquement contenir des lettres, des chiffres, des traits de soulignement et des traits d'union. Cela créera un pipeline nommé {pipelineName}.", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.fields.descriptionReview": "Vérifiez les mappings des champs du pipeline que vous avez choisi afin de vous assurer que les champs source et cible correspondent à votre cas d'utilisation spécifique. {notEditable}", @@ -14196,8 +14188,6 @@ "xpack.enterpriseSearch.content.indices.extractionRules.editRule.url.urlFiltersLink": "En savoir plus sur les filtres d'URL", "xpack.enterpriseSearch.content.indices.extractionRules.editRule.urlLabel": "URL", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.createErrors": "Erreur lors de la création d'un pipeline", - "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.esDocs.link": "Découvrir comment ajouter un modèle entraîné", - "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.imageAlt": "Illustration d'absence de modèles de Machine Learning", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.description": "Créez ou réutilisez un pipeline enfant qui servira de processeur dans votre pipeline principal.", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.emptyValueError": "Champ obligatoire.", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipelineLabel": "Sélectionner un pipeline d'inférence existant", @@ -42906,4 +42896,4 @@ "xpack.serverlessObservability.nav.projectSettings": "Paramètres de projet", "xpack.serverlessObservability.nav.visualizations": "Visualisations" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 10bbca87a9cad..a18a4202fc850 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -140,8 +140,6 @@ "coloring.colorMapping.assignments.autoAssignedTermAriaLabel": "この色は、他のすべての割り当てと一致しない最初の用語に自動的に割り当てられます。", "coloring.colorMapping.assignments.autoAssignedTermPlaceholder": "自動割り当て済み", "coloring.colorMapping.assignments.deleteAssignmentButtonLabel": "この割り当てを削除", - "coloring.colorMapping.assignments.unassignedAriaLabel": "割り当てリストに記載されていない未割り当てのすべての色にこの色を割り当てます。", - "coloring.colorMapping.assignments.unassignedPlaceholder": "割り当てられていない用語", "coloring.colorMapping.colorChangesModal.categoricalModeDescription": "分類モードに切り替えると、カスタム色の変更はすべて破棄されます。", "coloring.colorMapping.colorChangesModal.discardButton": "変更を破棄", "coloring.colorMapping.colorChangesModal.discardButtonLabel": "変更を破棄", @@ -159,14 +157,11 @@ "coloring.colorMapping.colorPicker.removeGradientColorButtonLabel": "色ステップを削除", "coloring.colorMapping.colorPicker.themeAwareColorsLabel": "中間色", "coloring.colorMapping.colorPicker.themeAwareColorsTooltip": "提供されている中間色はテーマを意識しており、明るいテーマと暗いテーマを切り替えると適切に変化します。", - "coloring.colorMapping.container.addAssignmentButtonLabel": "割り当てを追加", - "coloring.colorMapping.container.autoAssignLabel": "自動割り当て", "coloring.colorMapping.container.invertGradientButtonLabel": "グラデーションを反転", "coloring.colorMapping.container.mappingAssignmentHeader": "マッピング割り当て", "coloring.colorMapping.paletteSelector.categoricalLabel": "分類", "coloring.colorMapping.paletteSelector.paletteLabel": "カラーパレット", "coloring.colorMapping.paletteSelector.scaleLabel": "スケール", - "coloring.colorMapping.paletteSelector.sequentialLabel": "連続", "coloring.dynamicColoring.customPalette.addColor": "色を追加", "coloring.dynamicColoring.customPalette.addColorAriaLabel": "色を追加", "coloring.dynamicColoring.customPalette.colorStopsHelpPercentage": "割合値は使用可能なデータ値の全範囲に対して相対的です。", @@ -884,9 +879,9 @@ "core.euiToast.dismissToast": "トーストを閉じる", "core.euiToast.newNotification": "新しい通知が表示されます", "core.euiToast.notification": "通知", - "core.euiTourStep.closeTour": "ツアーを閉じる", - "core.euiTourStep.endTour": "ツアーを終了", - "core.euiTourStep.skipTour": "ツアーをスキップ", + "core.euiTourFooter.closeTour": "ツアーを閉じる", + "core.euiTourFooter.endTour": "ツアーを終了", + "core.euiTourFooter.skipTour": "ツアーをスキップ", "core.euiTourStepIndicator.isActive": "アクティブ", "core.euiTourStepIndicator.isComplete": "完了", "core.euiTourStepIndicator.isIncomplete": "未完了", @@ -11018,7 +11013,6 @@ "xpack.cases.allCases.comments": "コメント", "xpack.cases.allCases.noCategoriesAvailable": "カテゴリがありません", "xpack.cases.allCases.noTagsAvailable": "利用可能なタグがありません", - "xpack.cases.allCasesView.filterAssignees.clearFilters": "フィルターを消去", "xpack.cases.allCasesView.filterAssignees.noAssigneesLabel": "担当者なし", "xpack.cases.allCasesView.filterAssigneesAriaLabel": "クリックすると、担当者でフィルタリングします", "xpack.cases.allCasesView.showLessAvatars": "縮小表示", @@ -12187,7 +12181,6 @@ "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterSValidationErrorMessage": "{format}の文字{length, plural, other { グループ{lg} }}は、前にssと{sep}の区切り文字が付いていないため、サポートされません", "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterValidationErrorMessage": "{format}の文字{length, plural, other { グループ{lg} }}はサポートされていません", "xpack.dataVisualizer.file.editFlyout.overrides.timestampQuestionMarkValidationErrorMessage": "タイムスタンプフォーマット {timestampFormat} は、疑問符({fieldPlaceholder})が含まれているためサポートされていません", - "xpack.dataVisualizer.file.fileContents.firstLinesDescription": "最初の{numberOfLines, plural, other {#行}}件", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeByDiffFormatErrorMessage": "アップロードするよう選択されたファイルのサイズが {diffFormatted} に許可された最大サイズの {maxFileSizeFormatted} を超えています", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeErrorMessage": "アップロードするよう選択されたファイルのサイズは {fileSizeFormatted} で、許可された最大サイズの {maxFileSizeFormatted} を超えています", "xpack.dataVisualizer.file.importSummary.documentsCouldNotBeImportedDescription": "{docCount}件中{importFailuresLength}件のドキュメントをインポートできません。行が Grok パターンと一致していないことが原因の可能性があります。", @@ -12785,7 +12778,6 @@ "xpack.enterpriseSearch.content.indices.connectorScheduling.page.description": "コネクターがデプロイされました。ここで、繰り返しコンテンツとアクセス制御同期をスケジュールします。簡易テストを実行する場合は、{sync}ボタンを使用してワンタイム同期を実行します。", "xpack.enterpriseSearch.content.indices.connectorScheduling.schedulePanel.documentLevelSecurity.dlsDisabledCallout.text": "これらのオプションを有効にするには、このコネクターの{link}。", "xpack.enterpriseSearch.content.indices.deleteIndex.successToast.title": "インデックス{indexName}と関連付けられたすべての統合構成が正常に削除されました", - "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.description": "推論パイプラインで使用できる学習済み機械学習モデルがありません。{documentationLink}", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipeline.missingSourceFieldsDescription": "このインデックスで欠落しているフィールド:{commaSeparatedMissingSourceFields}", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText": "パイプライン名はデプロイ内で一意であり、文字、数字、アンダースコア、ハイフンのみを使用できます。これにより、{pipelineName}という名前のパイプラインが作成されます。", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.fields.descriptionReview": "選択したパイプラインのフィールドマッピングを調べ、ソースフィールドとターゲットフィールドが特定のユースケースに適合していることを確認します。{notEditable}", @@ -14209,8 +14201,6 @@ "xpack.enterpriseSearch.content.indices.extractionRules.editRule.url.urlFiltersLink": "URLフィルターの詳細をご覧ください", "xpack.enterpriseSearch.content.indices.extractionRules.editRule.urlLabel": "URL", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.createErrors": "パイプラインの作成エラー", - "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.esDocs.link": "学習されたモデルの追加方法の詳細", - "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.imageAlt": "機械学習モデル例がありません", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.description": "メインパイプラインでプロセッサーとして使用される子パイプラインを作成または再利用します。", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.emptyValueError": "フィールドが必要です。", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipelineLabel": "既存の推論パイプラインを選択", @@ -42898,4 +42888,4 @@ "xpack.serverlessObservability.nav.projectSettings": "プロジェクト設定", "xpack.serverlessObservability.nav.visualizations": "ビジュアライゼーション" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bd37c0d9f3fb6..601cef213948a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -140,8 +140,6 @@ "coloring.colorMapping.assignments.autoAssignedTermAriaLabel": "会将此颜色自动分配给第一个与所有其他分配均不匹配的词", "coloring.colorMapping.assignments.autoAssignedTermPlaceholder": "已自动分配", "coloring.colorMapping.assignments.deleteAssignmentButtonLabel": "删除此分配", - "coloring.colorMapping.assignments.unassignedAriaLabel": "将此颜色分配给分配列表中未描述的每个未分配项", - "coloring.colorMapping.assignments.unassignedPlaceholder": "未分配的词", "coloring.colorMapping.colorChangesModal.categoricalModeDescription": "切换到分类模式将丢弃您的所有定制颜色更改", "coloring.colorMapping.colorChangesModal.discardButton": "放弃更改", "coloring.colorMapping.colorChangesModal.discardButtonLabel": "放弃更改", @@ -159,14 +157,11 @@ "coloring.colorMapping.colorPicker.removeGradientColorButtonLabel": "移除色阶", "coloring.colorMapping.colorPicker.themeAwareColorsLabel": "中性色", "coloring.colorMapping.colorPicker.themeAwareColorsTooltip": "提供的中性色能够感知主题,在浅色主题与深色主题之间切换时会做出相应更改", - "coloring.colorMapping.container.addAssignmentButtonLabel": "添加分配", - "coloring.colorMapping.container.autoAssignLabel": "自动分配", "coloring.colorMapping.container.invertGradientButtonLabel": "反向渐变", "coloring.colorMapping.container.mappingAssignmentHeader": "映射分配", "coloring.colorMapping.paletteSelector.categoricalLabel": "分类", "coloring.colorMapping.paletteSelector.paletteLabel": "调色板", "coloring.colorMapping.paletteSelector.scaleLabel": "比例", - "coloring.colorMapping.paletteSelector.sequentialLabel": "顺序", "coloring.dynamicColoring.customPalette.addColor": "添加颜色", "coloring.dynamicColoring.customPalette.addColorAriaLabel": "添加颜色", "coloring.dynamicColoring.customPalette.colorStopsHelpPercentage": "百分比值是相对于全范围可用数据值的类型。", @@ -884,9 +879,9 @@ "core.euiToast.dismissToast": "关闭 Toast", "core.euiToast.newNotification": "新通知出现", "core.euiToast.notification": "通知", - "core.euiTourStep.closeTour": "关闭教程", - "core.euiTourStep.endTour": "结束教程", - "core.euiTourStep.skipTour": "跳过教程", + "core.euiTourFooter.closeTour": "关闭教程", + "core.euiTourFooter.endTour": "结束教程", + "core.euiTourFooter.skipTour": "跳过教程", "core.euiTourStepIndicator.isActive": "活动", "core.euiTourStepIndicator.isComplete": "已完成", "core.euiTourStepIndicator.isIncomplete": "未完成", @@ -11112,7 +11107,6 @@ "xpack.cases.allCases.comments": "注释", "xpack.cases.allCases.noCategoriesAvailable": "没有可用类别", "xpack.cases.allCases.noTagsAvailable": "没有可用标签", - "xpack.cases.allCasesView.filterAssignees.clearFilters": "清除筛选", "xpack.cases.allCasesView.filterAssignees.noAssigneesLabel": "无被分配人", "xpack.cases.allCasesView.filterAssigneesAriaLabel": "单击以筛选被分配人", "xpack.cases.allCasesView.showLessAvatars": "显示更少", @@ -12281,7 +12275,6 @@ "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterSValidationErrorMessage": "{format}的字母 {length, plural, other { 组 {lg} }} 不受支持,因为其未前置 ss 和 {sep} 中的分隔符", "xpack.dataVisualizer.file.editFlyout.overrides.timestampLetterValidationErrorMessage": "{format}的字母 {length, plural, other { 组 {lg} }} 不受支持", "xpack.dataVisualizer.file.editFlyout.overrides.timestampQuestionMarkValidationErrorMessage": "时间戳格式 {timestampFormat} 不受支持,因为其包含问号字符 ({fieldPlaceholder})", - "xpack.dataVisualizer.file.fileContents.firstLinesDescription": "前 {numberOfLines, plural, other {# 行}}", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeByDiffFormatErrorMessage": "您选择用于上传的文件大小超过上限值 {maxFileSizeFormatted} 的 {diffFormatted}", "xpack.dataVisualizer.file.fileErrorCallouts.fileSizeExceedsAllowedSizeErrorMessage": "您选择用于上传的文件大小为 {fileSizeFormatted},超过上限值 {maxFileSizeFormatted}", "xpack.dataVisualizer.file.importSummary.documentsCouldNotBeImportedDescription": "无法导入 {importFailuresLength} 个文档(共 {docCount} 个)。这可能是由于行与 Grok 模式不匹配。", @@ -12879,7 +12872,6 @@ "xpack.enterpriseSearch.content.indices.connectorScheduling.page.description": "现已部署您的连接器。在此处计划重复内容和访问控制同步。如果要运行快速测试,请使用 {sync} 按钮启动一次性同步。", "xpack.enterpriseSearch.content.indices.connectorScheduling.schedulePanel.documentLevelSecurity.dlsDisabledCallout.text": "此连接器的 {link},用于激活这些选项。", "xpack.enterpriseSearch.content.indices.deleteIndex.successToast.title": "您的索引 {indexName} 和任何关联的采集配置已成功删除", - "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.description": "您没有可供推理管道使用的已训练 Machine Learning 模型。{documentationLink}", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipeline.missingSourceFieldsDescription": "此索引中缺少字段:{commaSeparatedMissingSourceFields}", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText": "管道名称在部署内唯一,并且只能包含字母、数字、下划线和连字符。这会创建名为 {pipelineName} 的管道。", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.fields.descriptionReview": "检查您选择的管道的字段映射,确保源和目标字段符合您的特定用例。{notEditable}", @@ -14303,8 +14295,6 @@ "xpack.enterpriseSearch.content.indices.extractionRules.editRule.url.urlFiltersLink": "详细了解 URL 筛选", "xpack.enterpriseSearch.content.indices.extractionRules.editRule.urlLabel": "URL", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.createErrors": "创建管道时出错", - "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.esDocs.link": "了解如何添加已训练模型", - "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.noModels.imageAlt": "无 Machine Learning 模型图示", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.description": "构建或重复使用将在您的主管道中用作处理器的子管道。", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.emptyValueError": "“字段”必填。", "xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.existingPipelineLabel": "选择现有推理管道", @@ -42878,4 +42868,4 @@ "xpack.serverlessObservability.nav.projectSettings": "项目设置", "xpack.serverlessObservability.nav.visualizations": "可视化" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_disable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_disable.ts index 4e322d494d7df..437d2ae016293 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_disable.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_disable.ts @@ -5,16 +5,18 @@ * 2.0. */ import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -import { BulkOperationResponse, BulkOperationAttributes } from '../../../types'; +import { BulkOperationResponse, BulkDisableParams } from '../../../types'; export const bulkDisableRules = async ({ filter, ids, http, -}: BulkOperationAttributes): Promise => { + untrack, +}: BulkDisableParams): Promise => { try { const body = JSON.stringify({ ids: ids?.length ? ids : undefined, + untrack, ...(filter ? { filter: JSON.stringify(filter) } : {}), }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx index 2031c9a1f3fe2..2619ef2b25258 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/alert_summary_widget.tsx @@ -37,9 +37,9 @@ export const AlertSummaryWidget = ({ useEffect(() => { if (!isLoading && onLoaded) { - onLoaded(); + onLoaded({ activeAlertCount, recoveredAlertCount }); } - }, [isLoading, onLoaded]); + }, [activeAlertCount, isLoading, onLoaded, recoveredAlertCount]); if (isLoading) return ; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts index 48a49acf5ad7c..1bb02adff0653 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_summary_widget/types.ts @@ -30,6 +30,11 @@ export interface ChartProps { onBrushEnd?: BrushEndListener; } +interface AlertsCount { + activeAlertCount: number; + recoveredAlertCount: number; +} + export interface AlertSummaryWidgetProps { featureIds?: ValidFeatureId[]; filter?: estypes.QueryDslQueryContainer; @@ -38,5 +43,5 @@ export interface AlertSummaryWidgetProps { timeRange: AlertSummaryTimeRange; chartProps: ChartProps; hideChart?: boolean; - onLoaded?: () => void; + onLoaded?: (alertsCount?: AlertsCount) => void; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx index fa7fe6bae44d5..9327733f3b5ef 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { KueryNode } from '@kbn/es-query'; -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; @@ -18,6 +18,7 @@ import { } from './with_bulk_rule_api_operations'; import './rule_quick_edit_buttons.scss'; import { useKibana } from '../../../../common/lib/kibana'; +import { UntrackAlertsModal } from './untrack_alerts_modal'; export type ComponentOpts = { selectedItems: RuleTableItem[]; @@ -29,7 +30,7 @@ export type ComponentOpts = { isEnablingRules?: boolean; isDisablingRules?: boolean; isBulkEditing?: boolean; - onDisable: () => Promise; + onDisable: (untrack: boolean) => Promise; onEnable: () => Promise; updateRulesToBulkEdit: (props: UpdateRulesToBulkEditProps) => void; } & BulkOperationsComponentOpts; @@ -52,6 +53,8 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ notifications: { toasts }, } = useKibana().services; + const [isUntrackAlertsModalOpen, setIsUntrackAlertsModalOpen] = useState(false); + const isPerformingAction = isEnablingRules || isDisablingRules || isBulkEditing; const hasDisabledByLicenseRuleTypes = useMemo(() => { @@ -229,125 +232,146 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ } } + const onDisableClick = useCallback(() => { + setIsUntrackAlertsModalOpen(true); + }, []); + + const onModalClose = useCallback(() => { + setIsUntrackAlertsModalOpen(false); + }, []); + + const onModalConfirm = useCallback( + (untrack: boolean) => { + onModalClose(); + onDisable(untrack); + }, + [onModalClose, onDisable] + ); + return ( - - {!isAllSelected && ( - <> - - - - - - - - - - - - - - - - - - - - - + <> + + {!isAllSelected && ( + <> + + + + + + + + + + + + + + + + + + + + + + )} + + + + + + + + + + + + + + + + + + + + + + {isUntrackAlertsModalOpen && ( + )} - - - - - - - - - - - - - - - - - - - - - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.test.tsx new file mode 100644 index 0000000000000..78d06175c580c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; + +import { UntrackAlertsModal } from './untrack_alerts_modal'; + +const onConfirmMock = jest.fn(); + +const onCancelMock = jest.fn(); + +describe('Untrack alerts modal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render correctly', () => { + render(); + + expect(screen.getByTestId('untrackAlertsModal')).toBeInTheDocument(); + }); + + it('should track alerts', () => { + render(); + + fireEvent.click(screen.getByTestId('untrackAlertsModalSwitch')); + + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + expect(onConfirmMock).toHaveBeenCalledWith(true); + }); + + it('should untrack alerts', () => { + render(); + + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + expect(onConfirmMock).toHaveBeenCalledWith(false); + }); + + it('should close if cancel is clicked', () => { + render(); + + fireEvent.click(screen.getByTestId('confirmModalCancelButton')); + + expect(onCancelMock).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.tsx new file mode 100644 index 0000000000000..f3ad676e5fc21 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/untrack_alerts_modal.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; + +const UNTRACK_ORPHANED_ALERTS_TITLE = i18n.translate( + 'xpack.triggersActionsUI.sections.untrackAlertsModal.title', + { + defaultMessage: 'Disable rule', + } +); + +const UNTRACK_ORPHANED_ALERTS_CONFIRM_BUTTON_TEXT = i18n.translate( + 'xpack.triggersActionsUI.sections.untrackAlertsModal.confirmButtonText', + { + defaultMessage: 'Disable rule', + } +); + +const UNTRACK_ORPHANED_ALERTS_CANCEL_BUTTON_TEXT = i18n.translate( + 'xpack.triggersActionsUI.sections.untrackAlertsModal.cancelButtonText', + { + defaultMessage: 'cancel', + } +); + +const UNTRACK_ORPHANED_ALERTS_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.untrackAlertsModal.toggleLabel', + { + defaultMessage: + 'When disabling, all alerts related to this rule will be updated to "Untracked"', + } +); + +export interface UntrackAlertsModalProps { + onCancel: () => void; + onConfirm: (untrack: boolean) => void; +} + +export const UntrackAlertsModal = (props: UntrackAlertsModalProps) => { + const { onCancel, onConfirm } = props; + + const [isUntrack, setIsUntrack] = useState(false); + + const onChange = useCallback((e: EuiSwitchEvent) => { + setIsUntrack(e.target.checked); + }, []); + + return ( + onConfirm(isUntrack)} + confirmButtonText={UNTRACK_ORPHANED_ALERTS_CONFIRM_BUTTON_TEXT} + cancelButtonText={UNTRACK_ORPHANED_ALERTS_CANCEL_BUTTON_TEXT} + > + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx index 4fba7e7027d8c..5a283ff60ec72 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx @@ -139,7 +139,11 @@ describe('with_bulk_rule_api_operations', () => { it('disableRule calls the disableRule api', () => { const { http } = useKibanaMock().services; const ComponentToExtend = ({ bulkDisableRules, rule }: ComponentOpts & { rule: Rule }) => { - return ; + return ( + + ); }; const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); @@ -148,7 +152,7 @@ describe('with_bulk_rule_api_operations', () => { component.find('button').simulate('click'); expect(bulkDisableRules).toHaveBeenCalledTimes(1); - expect(bulkDisableRules).toHaveBeenCalledWith({ ids: [rule.id], http }); + expect(bulkDisableRules).toHaveBeenCalledWith({ ids: [rule.id], http, untrack: true }); }); // bulk rules @@ -212,7 +216,9 @@ describe('with_bulk_rule_api_operations', () => { const { http } = useKibanaMock().services; const ComponentToExtend = ({ bulkDisableRules, rules }: ComponentOpts & { rules: Rule[] }) => { return ( - ); @@ -227,6 +233,7 @@ describe('with_bulk_rule_api_operations', () => { expect(bulkDisableRules).toHaveBeenCalledWith({ ids: [rules[0].id, rules[1].id], http, + untrack: true, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx index ecb431ad3ba93..81a4f27ef5e1c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx @@ -23,6 +23,7 @@ import { BulkEditResponse, BulkOperationResponse, BulkOperationAttributesWithoutHttp, + BulkDisableParamsWithoutHttp, } from '../../../../types'; import type { LoadExecutionLogAggregationsProps, @@ -92,7 +93,7 @@ export interface ComponentOpts { cloneRule: (ruleId: string) => Promise; bulkDeleteRules: (props: BulkOperationAttributesWithoutHttp) => Promise; bulkEnableRules: (props: BulkOperationAttributesWithoutHttp) => Promise; - bulkDisableRules: (props: BulkOperationAttributesWithoutHttp) => Promise; + bulkDisableRules: (props: BulkDisableParamsWithoutHttp) => Promise; } export type PropsWithOptionalApiHandlers = Omit & Partial; @@ -199,7 +200,7 @@ export function withBulkRuleOperations( bulkEnableRules={async (bulkEnableProps: BulkOperationAttributesWithoutHttp) => { return await bulkEnableRules({ http, ...bulkEnableProps }); }} - bulkDisableRules={async (bulkDisableProps: BulkOperationAttributesWithoutHttp) => { + bulkDisableRules={async (bulkDisableProps: BulkDisableParamsWithoutHttp) => { return await bulkDisableRules({ http, ...bulkDisableProps }); }} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index 3bd45741714a0..7d292c5dee3cb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -772,8 +772,16 @@ describe('rule_details', () => { disableButton.simulate('click'); + const modal = wrapper.find('[data-test-subj="untrackAlertsModal"]'); + expect(modal.exists()).toBeTruthy(); + + modal.find('[data-test-subj="confirmModalConfirmButton"]').last().simulate('click'); + expect(mockRuleApis.bulkDisableRules).toHaveBeenCalledTimes(1); - expect(mockRuleApis.bulkDisableRules).toHaveBeenCalledWith({ ids: [rule.id] }); + expect(mockRuleApis.bulkDisableRules).toHaveBeenCalledWith({ + ids: [rule.id], + untrack: false, + }); }); it('should enable the rule when clicked', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index 6bae4f720c032..a7f46a8e554c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -70,6 +70,7 @@ import { } from '../../rules_list/translations'; import { useBulkOperationToast } from '../../../hooks/use_bulk_operation_toast'; import { RefreshToken } from './types'; +import { UntrackAlertsModal } from '../../common/components/untrack_alerts_modal'; export type RuleDetailsProps = { rule: Rule; @@ -115,6 +116,7 @@ export const RuleDetails: React.FunctionComponent = ({ const [rulesToDelete, setRulesToDelete] = useState([]); const [rulesToUpdateAPIKey, setRulesToUpdateAPIKey] = useState([]); + const [isUntrackAlertsModalOpen, setIsUntrackAlertsModalOpen] = useState(false); const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState(false); @@ -288,11 +290,39 @@ export const RuleDetails: React.FunctionComponent = ({ setRulesToDelete([]); goToRulesList(); }; + const onDeleteCancel = () => { setIsDeleteModalVisibility(false); setRulesToDelete([]); }; + const onDisableModalOpen = () => { + setIsUntrackAlertsModalOpen(true); + }; + + const onDisableModalClose = () => { + setIsUntrackAlertsModalOpen(false); + }; + + const onEnable = async () => { + await bulkEnableRules({ ids: [rule.id] }); + requestRefresh(); + }; + + const onDisable = async (untrack: boolean) => { + onDisableModalClose(); + await bulkDisableRules({ ids: [rule.id], untrack }); + requestRefresh(); + }; + + const onEnableDisable = (enable: boolean) => { + if (enable) { + onEnable(); + } else { + onDisableModalOpen(); + } + }; + return ( <> {isDeleteModalFlyoutVisible && ( @@ -311,6 +341,9 @@ export const RuleDetails: React.FunctionComponent = ({ )} /> )} + {isUntrackAlertsModalOpen && ( + + )} { setRulesToUpdateAPIKey([]); @@ -400,14 +433,7 @@ export const RuleDetails: React.FunctionComponent = ({ onApiKeyUpdate={(ruleId) => { setRulesToUpdateAPIKey([ruleId]); }} - onEnableDisable={async (enable) => { - if (enable) { - await bulkEnableRules({ ids: [rule.id] }); - } else { - await bulkDisableRules({ ids: [rule.id] }); - } - requestRefresh(); - }} + onEnableDisable={onEnableDisable} onRunRule={onRunRule} />, editButton, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.test.tsx index 69dab6587d6f0..ebe28a2636378 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.test.tsx @@ -6,6 +6,14 @@ */ import React from 'react'; +import { + render, + screen, + waitFor, + waitForElementToBeRemoved, + fireEvent, +} from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { act } from 'react-dom/test-utils'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; @@ -103,35 +111,33 @@ describe('rule status panel', () => { it('should disable the rule when picking disable in the dropdown', async () => { const rule = mockRule({ enabled: true }); const bulkDisableRules = jest.fn(); - const wrapper = mountWithIntl( - + render( + + + ); - const actionsElem = wrapper - .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') - .first(); - actionsElem.simulate('click'); - await act(async () => { - await nextTick(); - wrapper.update(); - }); + if (screen.queryByTestId('centerJustifiedSpinner')) { + await waitForElementToBeRemoved(() => screen.queryByTestId('centerJustifiedSpinner')); + } - await act(async () => { - const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); - const actionsMenuItemElem = actionsMenuElem.first().find('button.euiContextMenuItem'); - actionsMenuItemElem.at(1).simulate('click'); - await nextTick(); - }); + fireEvent.click(screen.getByTestId('ruleStatusDropdownBadge')); + + fireEvent.click(screen.getByTestId('statusDropdownDisabledItem')); + + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); - expect(bulkDisableRules).toHaveBeenCalledTimes(1); + expect(screen.queryByRole('progressbar')).toBeInTheDocument(); + + await waitFor(() => expect(bulkDisableRules).toHaveBeenCalledTimes(1)); }); it('if rule is already disabled should do nothing when picking disable in the dropdown', async () => { @@ -233,55 +239,4 @@ describe('rule status panel', () => { expect(bulkEnableRules).toHaveBeenCalledTimes(0); }); - - it('should show the loading spinner when the rule enabled switch was clicked and the server responded with some delay', async () => { - const rule = mockRule({ - enabled: true, - }); - - const bulkDisableRules = jest.fn(async () => { - await new Promise((resolve) => setTimeout(resolve, 6000)); - }) as any; - - const wrapper = mountWithIntl( - - ); - - const actionsElem = wrapper - .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') - .first(); - actionsElem.simulate('click'); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - await act(async () => { - const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); - const actionsMenuItemElem = actionsMenuElem.first().find('button.euiContextMenuItem'); - actionsMenuItemElem.at(1).simulate('click'); - }); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - await act(async () => { - expect(bulkDisableRules).toHaveBeenCalled(); - expect( - wrapper.find('[data-test-subj="statusDropdown"] .euiBadge__childButton .euiLoadingSpinner') - .length - ).toBeGreaterThan(0); - }); - }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx index a7b87cc722530..2419993f12670 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_status_panel.tsx @@ -102,6 +102,13 @@ export const RuleStatusPanel: React.FC = ({ requestRefresh(); }, [requestRefresh, loadEventLogs]); + const onDisableRule = useCallback( + (untrack: boolean) => { + return bulkDisableRules({ ids: [rule.id], untrack }); + }, + [bulkDisableRules, rule.id] + ); + useEffect(() => { if (isInitialized.current) { loadEventLogs(); @@ -126,7 +133,7 @@ export const RuleStatusPanel: React.FC = ({ bulkDisableRules({ ids: [rule.id] })} + disableRule={onDisableRule} enableRule={() => bulkEnableRules({ ids: [rule.id] })} snoozeRule={async () => {}} unsnoozeRule={async () => {}} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/index.tsx index dfab594febf10..1a99e346ed808 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/index.tsx @@ -7,6 +7,13 @@ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; +import type { RuleAddComponent } from './rule_add'; +import type { RuleEditComponent } from './rule_edit'; -export const RuleAdd = suspendedComponentWithProps(lazy(() => import('./rule_add'))); -export const RuleEdit = suspendedComponentWithProps(lazy(() => import('./rule_edit'))); +export const RuleAdd = suspendedComponentWithProps( + lazy(() => import('./rule_add')) +) as RuleAddComponent; // `React.lazy` is not typed correctly to support generics so casting back to imported component + +export const RuleEdit = suspendedComponentWithProps( + lazy(() => import('./rule_edit')) +) as RuleEditComponent; // `React.lazy` is not typed correctly to support generics so casting back to imported component diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index 07264709dd544..19eb8da4bf0d3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -15,6 +15,7 @@ import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common import { Rule, RuleTypeParams, + RuleTypeMetaData, RuleUpdates, RuleFlyoutCloseReason, IErrorObject, @@ -49,7 +50,12 @@ const defaultCreateRuleErrorMessage = i18n.translate( } ); -const RuleAdd = ({ +export type RuleAddComponent = typeof RuleAdd; + +const RuleAdd = < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +>({ consumer, ruleTypeRegistry, actionTypeRegistry, @@ -67,7 +73,7 @@ const RuleAdd = ({ useRuleProducer, initialSelectedConsumer, ...props -}: RuleAddProps) => { +}: RuleAddProps) => { const onSaveHandler = onSave ?? reloadRules; const [metadata, setMetadata] = useState(initialMetadata); const onChangeMetaData = useCallback((newMetadata) => setMetadata(newMetadata), []); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx index 0aebaaaa29882..975881e516e45 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx @@ -34,6 +34,8 @@ import { RuleEditProps, IErrorObject, RuleType, + RuleTypeParams, + RuleTypeMetaData, TriggersActionsUiConfig, RuleNotifyWhenType, } from '../../../types'; @@ -81,7 +83,12 @@ const cloneAndMigrateRule = (initialRule: Rule) => { return clonedRule; }; -export const RuleEdit = ({ +export type RuleEditComponent = typeof RuleEdit; + +export const RuleEdit = < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +>({ initialRule, onClose, reloadRules, @@ -91,7 +98,7 @@ export const RuleEdit = ({ actionTypeRegistry, metadata: initialMetadata, ...props -}: RuleEditProps) => { +}: RuleEditProps) => { const onSaveHandler = onSave ?? reloadRules; const [{ rule }, dispatch] = useReducer(ruleReducer as ConcreteRuleReducer, { rule: cloneAndMigrateRule(initialRule), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx index 8d5b7952b5f00..9bcab7c092421 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx @@ -224,11 +224,20 @@ describe('CollapsedItemActions', () => { wrapper.update(); }); wrapper.find('button[data-test-subj="disableButton"]').simulate('click'); + + const modal = wrapper.find('[data-test-subj="untrackAlertsModal"]'); + expect(modal.exists()).toBeTruthy(); + + modal.find('[data-test-subj="confirmModalConfirmButton"]').last().simulate('click'); + await act(async () => { await tick(10); wrapper.update(); }); - expect(bulkDisableRules).toHaveBeenCalled(); + expect(bulkDisableRules).toHaveBeenCalledWith({ + ids: ['1'], + untrack: false, + }); }); test('handles case when rule is unmuted and disabled and enable is clicked', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx index 7df59161dffeb..5a6217f31f83f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx @@ -34,6 +34,7 @@ import { SNOOZE_SUCCESS_MESSAGE, UNSNOOZE_SUCCESS_MESSAGE, } from './notify_badge'; +import { UntrackAlertsModal } from '../../common/components/untrack_alerts_modal'; export type ComponentOpts = { item: RuleTableItem; @@ -70,6 +71,8 @@ export const CollapsedItemActions: React.FunctionComponent = ({ const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isDisabled, setIsDisabled] = useState(!item.enabled); + const [isUntrackAlertsModalOpen, setIsUntrackAlertsModalOpen] = useState(false); + useEffect(() => { setIsDisabled(!item.enabled); }, [item.enabled]); @@ -179,6 +182,42 @@ export const CollapsedItemActions: React.FunctionComponent = ({ ]; }, [isDisabled, item, snoozedButtonText]); + const onDisableModalOpen = useCallback(() => { + setIsUntrackAlertsModalOpen(true); + }, []); + + const onDisableModalClose = useCallback(() => { + setIsUntrackAlertsModalOpen(false); + }, []); + + const onEnable = useCallback(async () => { + asyncScheduler.schedule(async () => { + await bulkEnableRules({ ids: [item.id] }); + onRuleChanged(); + }, 10); + setIsDisabled(false); + setIsPopoverOpen(false); + }, [bulkEnableRules, onRuleChanged, item.id]); + + const onDisable = useCallback( + async (untrack: boolean) => { + onDisableModalClose(); + await bulkDisableRules({ ids: [item.id], untrack }); + onRuleChanged(); + setIsDisabled(true); + setIsPopoverOpen(false); + }, + [onDisableModalClose, bulkDisableRules, onRuleChanged, item.id] + ); + + const onDisableClick = useCallback(() => { + if (isDisabled) { + onEnable(); + } else { + onDisableModalOpen(); + } + }, [isDisabled, onEnable, onDisableModalOpen]); + const panels = [ { id: 0, @@ -191,19 +230,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ { disabled: !item.isEditable || !item.enabledInLicense, 'data-test-subj': 'disableButton', - onClick: async () => { - const enabled = !isDisabled; - asyncScheduler.schedule(async () => { - if (enabled) { - await bulkDisableRules({ ids: [item.id] }); - } else { - await bulkEnableRules({ ids: [item.id] }); - } - onRuleChanged(); - }, 10); - setIsDisabled(!isDisabled); - setIsPopoverOpen(!isPopoverOpen); - }, + onClick: onDisableClick, name: isDisabled ? i18n.translate( 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.enableTitle', @@ -310,22 +337,27 @@ export const CollapsedItemActions: React.FunctionComponent = ({ ]; return ( - setIsPopoverOpen(false)} - ownFocus - panelPaddingSize="none" - data-test-subj="collapsedItemActions" - > - - + <> + setIsPopoverOpen(false)} + ownFocus + panelPaddingSize="none" + data-test-subj="collapsedItemActions" + > + + + {isUntrackAlertsModalOpen && ( + + )} + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx index 145fda4e4addd..1470fe2606107 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx @@ -27,6 +27,7 @@ import { SnoozePanel } from './rule_snooze'; import { isRuleSnoozed } from '../../../lib'; import { Rule, SnoozeSchedule, BulkOperationResponse } from '../../../../types'; import { ToastWithCircuitBreakerContent } from '../../../components/toast_with_circuit_breaker_content'; +import { UntrackAlertsModal } from '../../common/components/untrack_alerts_modal'; export type SnoozeUnit = 'm' | 'h' | 'd' | 'w' | 'M'; const SNOOZE_END_TIME_FORMAT = 'LL @ LT'; @@ -40,7 +41,7 @@ export interface ComponentOpts { rule: DropdownRuleRecord; onRuleChanged: () => void; enableRule: () => Promise; - disableRule: () => Promise; + disableRule: (untrack: boolean) => Promise; snoozeRule: (snoozeSchedule: SnoozeSchedule) => Promise; unsnoozeRule: (scheduleIds?: string[]) => Promise; isEditable: boolean; @@ -74,6 +75,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ }, [rule, hideSnoozeOption]); const [isUpdating, setIsUpdating] = useState(false); const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isUntrackAlertsModalOpen, setIsUntrackAlertsModalOpen] = useState(false); const onClickBadge = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), [setIsPopoverOpen]); const onClosePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); @@ -97,25 +99,59 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ throw new Error(); }, [enableRule, toasts]); - const onChangeEnabledStatus = useCallback( - async (enable: boolean) => { - if (rule.enabled === enable) { - return; - } + const onEnable = useCallback(async () => { + setIsUpdating(true); + try { + await enableRuleInternal(); + setIsEnabled(true); + onRuleChanged(); + } finally { + setIsUpdating(false); + } + }, [onRuleChanged, enableRuleInternal]); + + const onDisable = useCallback( + async (untrack: boolean) => { setIsUpdating(true); try { - if (enable) { - await enableRuleInternal(); - } else { - await disableRule(); - } - setIsEnabled(!isEnabled); + await disableRule(untrack); + setIsEnabled(false); onRuleChanged(); } finally { setIsUpdating(false); } }, - [rule.enabled, isEnabled, onRuleChanged, enableRuleInternal, disableRule] + [onRuleChanged, disableRule] + ); + + const onDisableModalOpen = useCallback(() => { + setIsUntrackAlertsModalOpen(true); + }, []); + + const onDisableModalClose = useCallback(() => { + setIsUntrackAlertsModalOpen(false); + }, []); + + const onModalConfirm = useCallback( + (untrack: boolean) => { + onDisableModalClose(); + onDisable(untrack); + }, + [onDisableModalClose, onDisable] + ); + + const onChangeEnabledStatus = useCallback( + async (enable: boolean) => { + if (rule.enabled === enable) { + return; + } + if (enable) { + await onEnable(); + } else { + onDisableModalOpen(); + } + }, + [rule.enabled, onEnable, onDisableModalOpen] ); const onSnoozeRule = useCallback( @@ -168,6 +204,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ const editableBadge = ( = ({ ); return ( - - - {isEditable ? ( - - - - ) : ( - nonEditableBadge - )} - - - {remainingSnoozeTime} - - + <> + + + {isEditable ? ( + + + + ) : ( + nonEditableBadge + )} + + + {remainingSnoozeTime} + + + {isUntrackAlertsModalOpen && ( + + )} + ); }; @@ -260,6 +302,7 @@ const RuleStatusMenu: React.FunctionComponent = ({ } onClosePopover(); }, [onChangeEnabledStatus, onClosePopover, unsnoozeRule, isSnoozed]); + const disableRule = useCallback(() => { onChangeEnabledStatus(false); onClosePopover(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 67d475ae2689e..9b66a65949674 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -555,8 +555,8 @@ export const RulesList = ({ }; const onDisableRule = useCallback( - (rule: RuleTableItem) => { - return bulkDisableRules({ http, ids: [rule.id] }); + (rule: RuleTableItem, untrack: boolean) => { + return bulkDisableRules({ http, ids: [rule.id], untrack }); }, [bulkDisableRules] ); @@ -701,12 +701,12 @@ export const RulesList = ({ onClearSelection(); }; - const onDisable = async () => { + const onDisable = async (untrack: boolean) => { setIsDisablingRules(true); const { errors, total } = isAllSelected - ? await bulkDisableRules({ http, filter: getFilter() }) - : await bulkDisableRules({ http, ids: selectedIds }); + ? await bulkDisableRules({ http, filter: getFilter(), untrack }) + : await bulkDisableRules({ http, ids: selectedIds, untrack }); setIsDisablingRules(false); showToast({ action: 'DISABLE', errors, total }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx index 4110ea34d3c95..40aad0464e4f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_disable.test.tsx @@ -7,10 +7,10 @@ import * as React from 'react'; import { IToasts } from '@kbn/core/public'; import { - act, render, screen, cleanup, + waitFor, waitForElementToBeRemoved, fireEvent, } from '@testing-library/react'; @@ -180,10 +180,16 @@ describe('Rules list Bulk Disable', () => { }); it('can bulk disable', async () => { - await act(async () => { - fireEvent.click(screen.getByTestId('bulkDisable')); + fireEvent.click(screen.getByTestId('bulkDisable')); + + await waitFor(() => { + expect(screen.getByTestId('untrackAlertsModal')).toBeInTheDocument(); }); + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + await waitForElementToBeRemoved(() => screen.queryByTestId('bulkDisable')); + const filter = bulkDisableRules.mock.calls[0][0].filter; expect(filter.function).toEqual('and'); @@ -192,23 +198,25 @@ describe('Rules list Bulk Disable', () => { expect(filter.arguments[1].arguments[0].arguments[0].value).toEqual('alert.id'); expect(filter.arguments[1].arguments[0].arguments[1].value).toEqual('alert:2'); - expect(bulkDisableRules).toHaveBeenCalledWith( - expect.not.objectContaining({ - ids: [], - }) - ); + expect(bulkDisableRules).toHaveBeenCalled(); + expect(screen.getByTestId('checkboxSelectRow-1').closest('tr')).not.toHaveClass( 'euiTableRow-isSelected' ); - expect(screen.queryByTestId('bulkDisable')).not.toBeInTheDocument(); }); describe('Toast', () => { it('should have success toast message', async () => { - await act(async () => { - fireEvent.click(screen.getByTestId('bulkDisable')); + fireEvent.click(screen.getByTestId('bulkDisable')); + + await waitFor(() => { + expect(screen.getByTestId('untrackAlertsModal')).toBeInTheDocument(); }); + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + await waitForElementToBeRemoved(() => screen.queryByTestId('bulkDisable')); + expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); expect(useKibanaMock().services.notifications.toasts.addSuccess).toHaveBeenCalledWith( 'Disabled 10 rules' @@ -229,10 +237,16 @@ describe('Rules list Bulk Disable', () => { total: 10, }); - await act(async () => { - fireEvent.click(screen.getByTestId('bulkDisable')); + fireEvent.click(screen.getByTestId('bulkDisable')); + + await waitFor(() => { + expect(screen.getByTestId('untrackAlertsModal')).toBeInTheDocument(); }); + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + await waitForElementToBeRemoved(() => screen.queryByTestId('bulkDisable')); + expect(useKibanaMock().services.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); expect(useKibanaMock().services.notifications.toasts.addWarning).toHaveBeenCalledWith( expect.objectContaining({ @@ -255,10 +269,16 @@ describe('Rules list Bulk Disable', () => { total: 1, }); - await act(async () => { - fireEvent.click(screen.getByTestId('bulkDisable')); + fireEvent.click(screen.getByTestId('bulkDisable')); + + await waitFor(() => { + expect(screen.getByTestId('untrackAlertsModal')).toBeInTheDocument(); }); + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + + await waitForElementToBeRemoved(() => screen.queryByTestId('bulkDisable')); + expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledTimes(1); expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx index 9933a52e3ac1f..0f1f42b475a35 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_table.tsx @@ -127,7 +127,7 @@ export interface RulesListTableProps { onPercentileOptionsChange?: (options: EuiSelectableOption[]) => void; onRuleChanged: () => Promise; onEnableRule: (rule: RuleTableItem) => Promise; - onDisableRule: (rule: RuleTableItem) => Promise; + onDisableRule: (rule: RuleTableItem, untrack: boolean) => Promise; onSnoozeRule: (rule: RuleTableItem, snoozeSchedule: SnoozeSchedule) => Promise; onUnsnoozeRule: (rule: RuleTableItem, scheduleIds?: string[]) => Promise; onSelectAll: () => void; @@ -281,12 +281,19 @@ export const RulesListTable = (props: RulesListTableProps) => { [ruleTypeRegistry] ); + const onDisableRuleInternal = useCallback( + (rule: RuleTableItem) => (untrack: boolean) => { + return onDisableRule(rule, untrack); + }, + [onDisableRule] + ); + const renderRuleStatusDropdown = useCallback( (rule: RuleTableItem) => { return ( await onDisableRule(rule)} + disableRule={onDisableRuleInternal(rule)} enableRule={async () => await onEnableRule(rule)} snoozeRule={async () => {}} unsnoozeRule={async () => {}} @@ -296,7 +303,7 @@ export const RulesListTable = (props: RulesListTableProps) => { /> ); }, - [isRuleTypeEditableInContext, onDisableRule, onEnableRule, onRuleChanged] + [isRuleTypeEditableInContext, onDisableRuleInternal, onEnableRule, onRuleChanged] ); const selectionColumn = useMemo(() => { diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_add_rule_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_add_rule_flyout.tsx index 23f751201d1be..c6a79b4c6e82d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_add_rule_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_add_rule_flyout.tsx @@ -9,11 +9,14 @@ import React from 'react'; import { QueryClientProvider } from '@tanstack/react-query'; import { ConnectorProvider } from '../application/context/connector_context'; import { RuleAdd } from '../application/sections/rule_form'; -import type { ConnectorServices, RuleAddProps } from '../types'; +import type { ConnectorServices, RuleAddProps, RuleTypeParams, RuleTypeMetaData } from '../types'; import { queryClient } from '../application/query_client'; -export const getAddRuleFlyoutLazy = ( - props: RuleAddProps & { connectorServices: ConnectorServices } +export const getAddRuleFlyoutLazy = < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +>( + props: RuleAddProps & { connectorServices: ConnectorServices } ) => { return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx index 2d99e3911a168..f3fbccce267c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx @@ -9,11 +9,14 @@ import React from 'react'; import { QueryClientProvider } from '@tanstack/react-query'; import { ConnectorProvider } from '../application/context/connector_context'; import { RuleEdit } from '../application/sections/rule_form'; -import type { ConnectorServices, RuleEditProps as AlertEditProps } from '../types'; +import type { ConnectorServices, RuleEditProps, RuleTypeParams, RuleTypeMetaData } from '../types'; import { queryClient } from '../application/query_client'; -export const getEditRuleFlyoutLazy = ( - props: AlertEditProps & { connectorServices: ConnectorServices } +export const getEditRuleFlyoutLazy = < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +>( + props: RuleEditProps & { connectorServices: ConnectorServices } ) => { return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index 223a54205cb48..48691c15ed62f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -15,8 +15,6 @@ import { getEditRuleFlyoutLazy } from './common/get_edit_rule_flyout'; import { TypeRegistry } from './application/type_registry'; import { ActionTypeModel, - RuleAddProps, - RuleEditProps, RuleTypeModel, AlertsTableProps, FieldBrowserProps, @@ -73,7 +71,7 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { connectorServices, }); }, - getAddRuleFlyout: (props: Omit) => { + getAddRuleFlyout: (props) => { return getAddRuleFlyoutLazy({ ...props, actionTypeRegistry, @@ -81,7 +79,7 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { connectorServices, }); }, - getEditRuleFlyout: (props: Omit) => { + getEditRuleFlyout: (props) => { return getEditRuleFlyoutLazy({ ...props, actionTypeRegistry, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 872528a9a5f85..bcd639e21a2ff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -62,6 +62,8 @@ import type { RuleAddProps, RuleEditProps, RuleTypeModel, + RuleTypeParams, + RuleTypeMetaData, AlertsTableProps, RuleStatusDropdownProps, RuleTagFilterProps, @@ -115,12 +117,18 @@ export interface TriggersAndActionsUIPublicPluginStart { getEditConnectorFlyout: ( props: Omit ) => ReactElement; - getAddRuleFlyout: ( - props: Omit - ) => ReactElement; - getEditRuleFlyout: ( - props: Omit - ) => ReactElement; + getAddRuleFlyout: < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData + >( + props: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'> + ) => ReactElement>; + getEditRuleFlyout: < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData + >( + props: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'> + ) => ReactElement>; getAlertsTable: (props: AlertsTableProps) => ReactElement; getAlertsTableDefaultAlertActions:

( props: P @@ -403,7 +411,7 @@ export class Plugin connectorServices: this.connectorServices!, }); }, - getAddRuleFlyout: (props: Omit) => { + getAddRuleFlyout: (props) => { return getAddRuleFlyoutLazy({ ...props, actionTypeRegistry: this.actionTypeRegistry, @@ -411,9 +419,7 @@ export class Plugin connectorServices: this.connectorServices!, }); }, - getEditRuleFlyout: ( - props: Omit - ) => { + getEditRuleFlyout: (props) => { return getEditRuleFlyoutLazy({ ...props, actionTypeRegistry: this.actionTypeRegistry, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index b47d80a0839e5..ba5afb74ecfd8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -51,6 +51,7 @@ import { AlertingFrameworkHealth, RuleNotifyWhenType, RuleTypeParams, + RuleTypeMetaData, ActionVariable, RuleLastRun, MaintenanceWindow, @@ -127,6 +128,7 @@ export type { AlertingFrameworkHealth, RuleNotifyWhenType, RuleTypeParams, + RuleTypeMetaData, ResolvedRule, SanitizedRule, RuleStatusDropdownProps, @@ -219,6 +221,14 @@ export type BulkOperationAttributes = BulkOperationAttributesWithoutHttp & { http: HttpSetup; }; +export type BulkDisableParamsWithoutHttp = BulkOperationAttributesWithoutHttp & { + untrack: boolean; +}; + +export type BulkDisableParams = BulkDisableParamsWithoutHttp & { + http: HttpSetup; +}; + export interface ActionParamsProps { actionParams: Partial; index: number; @@ -412,8 +422,11 @@ export enum EditConnectorTabs { Test = 'test', } -export interface RuleEditProps> { - initialRule: Rule; +export interface RuleEditProps< + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +> { + initialRule: Rule; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; onClose: (reason: RuleFlyoutCloseReason, metadata?: MetaData) => void; @@ -425,14 +438,27 @@ export interface RuleEditProps> { ruleType?: RuleType; } -export interface RuleAddProps> { +export interface RuleAddProps< + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +> { + /** + * ID of the feature this rule should be created for. + * + * Notes: + * - The feature needs to be registered using `featuresPluginSetup.registerKibanaFeature()` API during your plugin's setup phase. + * - The user needs to have permission to access the feature in order to create the rule. + * */ consumer: string; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; onClose: (reason: RuleFlyoutCloseReason, metadata?: MetaData) => void; ruleTypeId?: string; + /** + * Determines whether the user should be able to change the rule type in the UI. + */ canChangeTrigger?: boolean; - initialValues?: Partial; + initialValues?: Partial>; /** @deprecated use `onSave` as a callback after an alert is saved*/ reloadRules?: () => Promise; hideGrouping?: boolean; @@ -445,8 +471,8 @@ export interface RuleAddProps> { useRuleProducer?: boolean; initialSelectedConsumer?: RuleCreationValidConsumer | null; } -export interface RuleDefinitionProps { - rule: Rule; +export interface RuleDefinitionProps { + rule: Rule; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; onEditRule: () => Promise; diff --git a/x-pack/plugins/uptime/public/legacy_uptime/components/common/charts/duration_chart.tsx b/x-pack/plugins/uptime/public/legacy_uptime/components/common/charts/duration_chart.tsx index 8e5da4fa970ab..d85654b195c6d 100644 --- a/x-pack/plugins/uptime/public/legacy_uptime/components/common/charts/duration_chart.tsx +++ b/x-pack/plugins/uptime/public/legacy_uptime/components/common/charts/duration_chart.tsx @@ -106,7 +106,11 @@ export const DurationChartComponent = ({ objectRemover.removeAll()); + afterEach(async () => { + await es.deleteByQuery({ + index: alertAsDataIndex, + query: { + match_all: {}, + }, + conflicts: 'proceed', + }); + await objectRemover.removeAll(); + }); async function getScheduledTask(id: string): Promise { const scheduledTask = await es.get({ @@ -172,6 +185,54 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex }); }); + it('should not untrack alerts if untrack is false', async () => { + const { body: createdRule } = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.always-firing-alert-as-data', + schedule: { interval: '24h' }, + throttle: undefined, + notify_when: undefined, + params: { + index: ES_TEST_INDEX_NAME, + reference: 'test', + }, + }) + ) + .expect(200); + + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + + await retry.try(async () => { + const { + hits: { hits: activeAlerts }, + } = await es.search({ + index: alertAsDataIndex, + body: { query: { match_all: {} } }, + }); + + expect(activeAlerts.length).eql(2); + activeAlerts.forEach((activeAlert: any) => { + expect(activeAlert._source[ALERT_STATUS]).eql('active'); + }); + }); + + await ruleUtils.getDisableRequest(createdRule.id, false); + + const { + hits: { hits: untrackedAlerts }, + } = await es.search({ + index: alertAsDataIndex, + body: { query: { match_all: {} } }, + }); + expect(untrackedAlerts.length).eql(2); + untrackedAlerts.forEach((untrackedAlert: any) => { + expect(untrackedAlert._source[ALERT_STATUS]).eql('active'); + }); + }); + it('should disable rule even if associated task manager document is missing', async () => { const { body: createdRule } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_disable.ts new file mode 100644 index 0000000000000..e5692e73a15ab --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_disable.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; +import { ALERT_STATUS } from '@kbn/rule-data-utils'; +import { Spaces } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; + +const alertAsDataIndex = '.internal.alerts-observability.test.alerts.alerts-default-000001'; + +// eslint-disable-next-line import/no-default-export +export default function createDisableRuleTests({ getService }: FtrProviderContext) { + const es = getService('es'); + const retry = getService('retry'); + const supertest = getService('supertest'); + + describe('bulkDisable', () => { + const objectRemover = new ObjectRemover(supertest); + + const createRule = async () => { + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.always-firing-alert-as-data', + schedule: { interval: '24h' }, + throttle: undefined, + notify_when: undefined, + params: { + index: ES_TEST_INDEX_NAME, + reference: 'test', + }, + }) + ) + .expect(200); + + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + return createdRule.id; + }; + + const getAlerts = async () => { + const { + hits: { hits: alerts }, + } = await es.search({ + index: alertAsDataIndex, + body: { query: { match_all: {} } }, + }); + + return alerts; + }; + + afterEach(async () => { + await es.deleteByQuery({ + index: alertAsDataIndex, + query: { + match_all: {}, + }, + conflicts: 'proceed', + }); + await objectRemover.removeAll(); + }); + + it('should bulk disable and untrack', async () => { + const createdRule1 = await createRule(); + const createdRule2 = await createRule(); + + await retry.try(async () => { + const alerts = await getAlerts(); + + expect(alerts.length).eql(4); + alerts.forEach((activeAlert: any) => { + expect(activeAlert._source[ALERT_STATUS]).eql('active'); + }); + }); + + await supertest + .patch(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_bulk_disable`) + .set('kbn-xsrf', 'foo') + .send({ + ids: [createdRule1, createdRule2], + untrack: true, + }) + .expect(200); + + const alerts = await getAlerts(); + + expect(alerts.length).eql(4); + alerts.forEach((untrackedAlert: any) => { + expect(untrackedAlert._source[ALERT_STATUS]).eql('untracked'); + }); + }); + + it('should bulk disable and not untrack if untrack is false', async () => { + const createdRule1 = await createRule(); + const createdRule2 = await createRule(); + + await retry.try(async () => { + const alerts = await getAlerts(); + + expect(alerts.length).eql(4); + alerts.forEach((activeAlert: any) => { + expect(activeAlert._source[ALERT_STATUS]).eql('active'); + }); + }); + + await supertest + .patch(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_bulk_disable`) + .set('kbn-xsrf', 'foo') + .send({ + ids: [createdRule1, createdRule2], + untrack: false, + }) + .expect(200); + + const alerts = await getAlerts(); + + expect(alerts.length).eql(4); + alerts.forEach((activeAlert: any) => { + expect(activeAlert._source[ALERT_STATUS]).eql('active'); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts index 15084a47f4d86..86c239250d109 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/index.ts @@ -23,6 +23,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./snooze')); loadTestFile(require.resolve('./unsnooze')); loadTestFile(require.resolve('./bulk_edit')); + loadTestFile(require.resolve('./bulk_disable')); loadTestFile(require.resolve('./capped_action_type')); loadTestFile(require.resolve('./scheduled_task_id')); loadTestFile(require.resolve('./run_soon')); diff --git a/x-pack/test/api_integration/apis/management/index_management/lib/templates.api.ts b/x-pack/test/api_integration/apis/management/index_management/lib/templates.api.ts index 6e8fbffbe0416..e929cbff2f188 100644 --- a/x-pack/test/api_integration/apis/management/index_management/lib/templates.api.ts +++ b/x-pack/test/api_integration/apis/management/index_management/lib/templates.api.ts @@ -36,9 +36,9 @@ export function templatesApi(getService: FtrProviderContext['getService']) { .send(payload); // Delete all templates created during tests - const cleanUpTemplates = async () => { + const cleanUpTemplates = async (additionalRequestHeaders: object = {}) => { try { - await deleteTemplates(templatesCreated); + await deleteTemplates(templatesCreated).set(additionalRequestHeaders); templatesCreated = []; } catch (e) { // Silently swallow errors diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.ts b/x-pack/test/api_integration/apis/management/index_management/templates.ts index a3b2ce50e04b5..6d0e79993d040 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.ts +++ b/x-pack/test/api_integration/apis/management/index_management/templates.ts @@ -96,6 +96,7 @@ export default function ({ getService }: FtrProviderContext) { 'hasMappings', 'priority', 'composedOf', + 'ignoreMissingComponentTemplates', 'version', '_kbnMeta', ].sort(); @@ -119,6 +120,7 @@ export default function ({ getService }: FtrProviderContext) { 'version', '_kbnMeta', 'composedOf', + 'ignoreMissingComponentTemplates', ].sort(); expect(Object.keys(legacyTemplateFound).sort()).to.eql(expectedLegacyKeys); @@ -139,6 +141,7 @@ export default function ({ getService }: FtrProviderContext) { 'hasMappings', 'priority', 'composedOf', + 'ignoreMissingComponentTemplates', 'dataStream', 'version', '_kbnMeta', @@ -162,6 +165,7 @@ export default function ({ getService }: FtrProviderContext) { 'hasMappings', 'priority', 'composedOf', + 'ignoreMissingComponentTemplates', 'version', '_kbnMeta', ].sort(); @@ -183,6 +187,7 @@ export default function ({ getService }: FtrProviderContext) { 'indexPatterns', 'template', 'composedOf', + 'ignoreMissingComponentTemplates', 'priority', 'version', '_kbnMeta', @@ -207,6 +212,7 @@ export default function ({ getService }: FtrProviderContext) { 'version', '_kbnMeta', 'composedOf', + 'ignoreMissingComponentTemplates', ].sort(); const expectedTemplateKeys = ['aliases', 'mappings', 'settings'].sort(); diff --git a/x-pack/test/api_integration/apis/maps/maps_telemetry.ts b/x-pack/test/api_integration/apis/maps/maps_telemetry.ts index 92ae21c7c09c0..8f5c9ae95f8af 100644 --- a/x-pack/test/api_integration/apis/maps/maps_telemetry.ts +++ b/x-pack/test/api_integration/apis/maps/maps_telemetry.ts @@ -60,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) { es_point_to_point: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 }, es_top_hits: { min: 1, max: 1, total: 2, avg: 0.07142857142857142 }, es_agg_heatmap: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 }, - esql: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 }, + esql: { min: 1, max: 1, total: 2, avg: 0.07142857142857142 }, kbn_tms_raster: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 }, ems_basemap: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 }, ems_region: { min: 1, max: 1, total: 1, avg: 0.03571428571428571 }, @@ -81,8 +81,8 @@ export default function ({ getService }: FtrProviderContext) { min: 0, }, dataSourcesCount: { - avg: 1.1785714285714286, - max: 6, + avg: 1.2142857142857142, + max: 7, min: 1, }, emsVectorLayersCount: { @@ -104,8 +104,8 @@ export default function ({ getService }: FtrProviderContext) { min: 1, }, GEOJSON_VECTOR: { - avg: 0.8214285714285714, - max: 5, + avg: 0.8571428571428571, + max: 6, min: 1, }, HEATMAP: { @@ -125,8 +125,8 @@ export default function ({ getService }: FtrProviderContext) { }, }, layersCount: { - avg: 1.2142857142857142, - max: 7, + avg: 1.25, + max: 8, min: 1, }, }, diff --git a/x-pack/test/cases_api_integration/common/lib/api/index.ts b/x-pack/test/cases_api_integration/common/lib/api/index.ts index 4d7d64bbc4487..0b7f9c542a6d0 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/index.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/index.ts @@ -37,6 +37,7 @@ import { Case, Cases, CaseStatuses, + CaseCustomField, } from '@kbn/cases-plugin/common/types/domain'; import { AlertResponse, @@ -45,6 +46,7 @@ import { CasesFindResponse, CasesPatchRequest, CasesStatusResponse, + CustomFieldPutRequest, GetRelatedCasesByAlertResponse, } from '@kbn/cases-plugin/common/types/api'; import { User } from '../authentication/types'; @@ -808,3 +810,38 @@ export const searchCases = async ({ return res; }; + +export const replaceCustomField = async ({ + supertest, + caseId, + customFieldId, + params, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, + headers = {}, +}: { + supertest: SuperTest.SuperTest; + caseId: string; + customFieldId: string; + params: CustomFieldPutRequest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null } | null; + headers?: Record; +}): Promise => { + const apiCall = supertest.put( + `${getSpaceUrlPrefix( + auth?.space + )}${CASES_INTERNAL_URL}/${caseId}/custom_fields/${customFieldId}` + ); + + setupAuth({ apiCall, headers, auth }); + + const { body: theCustomField } = await apiCall + .set('kbn-xsrf', 'true') + .set('x-elastic-internal-origin', 'foo') + .set(headers) + .send(params) + .expect(expectedHttpCode); + + return theCustomField; +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index cc49ddf44bdcc..3a965b73004ef 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -1149,6 +1149,74 @@ export default ({ getService }: FtrProviderContext): void => { ]); }); + it('patches a case with missing optional custom fields to their default values', async () => { + await createConfiguration( + supertest, + getConfigurationRequest({ + overrides: { + customFields: [ + { + key: 'text_custom_field', + label: 'text', + type: CustomFieldTypes.TEXT, + defaultValue: 'default value', + required: false, + }, + { + key: 'toggle_custom_field', + label: 'toggle', + type: CustomFieldTypes.TOGGLE, + defaultValue: false, + required: false, + }, + ], + }, + }) + ); + + const originalValues = [ + { + key: 'text_custom_field', + type: CustomFieldTypes.TEXT, + value: 'hello', + }, + { + key: 'toggle_custom_field', + type: CustomFieldTypes.TOGGLE, + value: true, + }, + ] as CaseCustomFields; + + const postedCase = await createCase(supertest, { + ...postCaseReq, + customFields: originalValues, + }); + + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + customFields: [ + { + key: 'toggle_custom_field', + type: CustomFieldTypes.TOGGLE, + value: false, + }, + ], + }, + ], + }, + }); + + expect(patchedCases[0].customFields).to.eql([ + { ...originalValues[1], value: false }, + { ...originalValues[0], value: 'default value' }, + ]); + }); + it('400s trying to patch a case with missing required custom fields if they dont have default values', async () => { await createConfiguration( supertest, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 5049c04b060a8..411932212f242 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -318,6 +318,53 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); }); + + it('creates a case with missing optional custom fields and default values', async () => { + const customFieldsConfiguration = [ + { + key: 'text_custom_field', + label: 'text', + type: CustomFieldTypes.TEXT, + required: false, + defaultValue: 'default value', + }, + { + key: 'toggle_custom_field', + label: 'toggle', + type: CustomFieldTypes.TOGGLE, + defaultValue: false, + required: false, + }, + ]; + + await createConfiguration( + supertest, + getConfigurationRequest({ + overrides: { + customFields: customFieldsConfiguration, + }, + }) + ); + const createdCase = await createCase( + supertest, + getPostCaseRequest({ + customFields: [], + }) + ); + + expect(createdCase.customFields).to.eql([ + { + key: customFieldsConfiguration[0].key, + type: customFieldsConfiguration[0].type, + value: 'default value', + }, + { + key: customFieldsConfiguration[1].key, + type: customFieldsConfiguration[1].type, + value: false, + }, + ]); + }); }); describe('unhappy path', () => { diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts index d5b6e931ca671..c8e0f092edf3a 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts @@ -64,7 +64,7 @@ export default ({ getService }: FtrProviderContext): void => { required: true, }, { - key: 'toggle_field', + key: 'toggle_field_1', label: '#2', type: CustomFieldTypes.TOGGLE, required: false, @@ -76,6 +76,13 @@ export default ({ getService }: FtrProviderContext): void => { required: true, defaultValue: 'foobar', }, + { + key: 'toggle_field_2', + label: '#4', + type: CustomFieldTypes.TOGGLE, + required: false, + defaultValue: true, + }, ] as ConfigurationPatchRequest['customFields']; const configuration = await createConfiguration(supertest); const newConfiguration = await updateConfiguration(supertest, configuration.id, { diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts index 15efb00444993..a7461d5f1fc18 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts @@ -63,20 +63,27 @@ export default ({ getService }: FtrProviderContext): void => { it('should create a configuration with customFields', async () => { const customFields = { customFields: [ - { key: 'hello', label: 'text', type: CustomFieldTypes.TEXT, required: false }, + { key: 'text_1', label: 'text 1', type: CustomFieldTypes.TEXT, required: false }, { - key: 'goodbye', - label: 'toggle', + key: 'toggle_1', + label: 'toggle 1', type: CustomFieldTypes.TOGGLE, required: true, defaultValue: false, }, { - key: 'hello_again', - label: 'text', + key: 'text_2', + label: 'text 2', type: CustomFieldTypes.TEXT, required: true, }, + { + key: 'toggle_2', + label: 'toggle 2', + type: CustomFieldTypes.TOGGLE, + required: false, + defaultValue: true, + }, ], }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts index 636c166169808..e731e0101bdc0 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts @@ -56,6 +56,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./internal/user_actions_get_users')); loadTestFile(require.resolve('./internal/bulk_delete_file_attachments')); loadTestFile(require.resolve('./internal/search_cases')); + loadTestFile(require.resolve('./internal/replace_custom_field')); /** * Attachments framework diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/replace_custom_field.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/replace_custom_field.ts new file mode 100644 index 0000000000000..b76079f81c62f --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/replace_custom_field.ts @@ -0,0 +1,464 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { CustomFieldTypes } from '@kbn/cases-plugin/common/types/domain'; + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { postCaseReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + createConfiguration, + getConfigurationRequest, + replaceCustomField, +} from '../../../../common/lib/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('replace_custom_field', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + describe('basic tests', () => { + it('should replace a text customField', async () => { + await createConfiguration( + supertest, + getConfigurationRequest({ + overrides: { + customFields: [ + { + key: 'test_custom_field_1', + label: 'text', + type: CustomFieldTypes.TEXT, + required: false, + }, + { + key: 'test_custom_field_2', + label: 'toggle', + type: CustomFieldTypes.TOGGLE, + required: true, + }, + ], + }, + }) + ); + + const postedCase = await createCase(supertest, { + ...postCaseReq, + customFields: [ + { + key: 'test_custom_field_1', + type: CustomFieldTypes.TEXT, + value: 'text field value', + }, + { + key: 'test_custom_field_2', + type: CustomFieldTypes.TOGGLE, + value: true, + }, + ], + }); + const replacedCustomField = await replaceCustomField({ + supertest, + caseId: postedCase.id, + customFieldId: 'test_custom_field_1', + params: { + value: 'this is updated text field value', + caseVersion: postedCase.version, + }, + }); + + expect(replacedCustomField).to.eql({ + key: 'test_custom_field_1', + type: CustomFieldTypes.TEXT, + value: 'this is updated text field value', + }); + }); + + it('should patch a toggle customField', async () => { + await createConfiguration( + supertest, + getConfigurationRequest({ + overrides: { + customFields: [ + { + key: 'test_custom_field_1', + label: 'text', + type: CustomFieldTypes.TEXT, + required: false, + }, + { + key: 'test_custom_field_2', + label: 'toggle', + type: CustomFieldTypes.TOGGLE, + required: true, + }, + ], + }, + }) + ); + + const postedCase = await createCase(supertest, { + ...postCaseReq, + customFields: [ + { + key: 'test_custom_field_1', + type: CustomFieldTypes.TEXT, + value: 'text field value', + }, + { + key: 'test_custom_field_2', + type: CustomFieldTypes.TOGGLE, + value: true, + }, + ], + }); + const replacedCustomField = await replaceCustomField({ + supertest, + caseId: postedCase.id, + customFieldId: 'test_custom_field_2', + params: { + value: false, + caseVersion: postedCase.version, + }, + }); + + expect(replacedCustomField).to.eql({ + key: 'test_custom_field_2', + type: CustomFieldTypes.TOGGLE, + value: false, + }); + }); + + it('does not throw error when updating an optional custom field with a null value', async () => { + await createConfiguration( + supertest, + getConfigurationRequest({ + overrides: { + customFields: [ + { + key: 'test_custom_field', + label: 'text', + type: CustomFieldTypes.TEXT, + required: false, + }, + ], + }, + }) + ); + + const postedCase = await createCase(supertest, { + ...postCaseReq, + customFields: [ + { + key: 'test_custom_field', + type: CustomFieldTypes.TEXT, + value: 'hello', + }, + ], + }); + + await replaceCustomField({ + supertest, + caseId: postedCase.id, + customFieldId: 'test_custom_field', + params: { + caseVersion: postedCase.version, + value: null, + }, + expectedHttpCode: 200, + }); + }); + }); + + describe('errors', () => { + it('400s when trying to patch with a custom field key that does not exist', async () => { + await createConfiguration( + supertest, + getConfigurationRequest({ + overrides: { + customFields: [ + { + key: 'test_custom_field', + label: 'text', + type: CustomFieldTypes.TEXT, + required: false, + }, + ], + }, + }) + ); + const postedCase = await createCase(supertest, postCaseReq); + + await replaceCustomField({ + supertest, + caseId: postedCase.id, + customFieldId: 'random_key', + params: { + caseVersion: postedCase.version, + value: 'this is updated text field value', + }, + expectedHttpCode: 400, + }); + }); + + it('400s when trying to patch a case with a required custom field with null value', async () => { + await createConfiguration( + supertest, + getConfigurationRequest({ + overrides: { + customFields: [ + { + key: 'test_custom_field', + label: 'text', + type: CustomFieldTypes.TEXT, + required: true, + }, + ], + }, + }) + ); + + const postedCase = await createCase(supertest, { + ...postCaseReq, + customFields: [ + { + key: 'test_custom_field', + type: CustomFieldTypes.TEXT, + value: 'hello', + }, + ], + }); + + await replaceCustomField({ + supertest, + caseId: postedCase.id, + customFieldId: 'test_custom_field', + params: { + caseVersion: postedCase.version, + value: null, + }, + expectedHttpCode: 400, + }); + }); + + it('400s when trying to patch a case with a custom field with the wrong type', async () => { + await createConfiguration( + supertest, + getConfigurationRequest({ + overrides: { + customFields: [ + { + key: 'test_custom_field', + label: 'text', + type: CustomFieldTypes.TEXT, + required: false, + }, + ], + }, + }) + ); + const postedCase = await createCase(supertest, postCaseReq); + + await replaceCustomField({ + supertest, + caseId: postedCase.id, + customFieldId: 'test_custom_field', + params: { + caseVersion: postedCase.version, + value: true, + }, + expectedHttpCode: 400, + }); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should replace the custom field when the user has the correct permissions', async () => { + await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest({ + overrides: { + customFields: [ + { + key: 'test_custom_field', + label: 'text', + type: CustomFieldTypes.TEXT, + required: false, + }, + ], + }, + }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, { + user: secOnly, + space: 'space1', + }); + + await replaceCustomField({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + customFieldId: 'test_custom_field', + params: { + caseVersion: postedCase.version, + + value: 'this is updated text field value', + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 200, + }); + }); + + it('should not replace a custom field when the user does not have the correct ownership', async () => { + await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest({ + overrides: { + owner: 'observabilityFixture', + customFields: [ + { + key: 'test_custom_field', + label: 'text', + type: CustomFieldTypes.TEXT, + required: false, + }, + ], + }, + }), + 200, + { + user: obsOnly, + space: 'space1', + } + ); + + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ + owner: 'observabilityFixture', + customFields: [ + { + key: 'test_custom_field', + type: CustomFieldTypes.TEXT, + value: 'hello', + }, + ], + }), + 200, + { user: obsOnly, space: 'space1' } + ); + + await replaceCustomField({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + customFieldId: 'test_custom_field', + params: { + caseVersion: postedCase.version, + value: 'this is updated text field value', + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT replace a custom field`, async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: superUser, + space: 'space1', + } + ); + + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await replaceCustomField({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + customFieldId: 'test_custom_field', + params: { + caseVersion: postedCase.version, + value: 'this is updated text field value', + }, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should NOT replace a custom field in a space with no permissions', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: superUser, + space: 'space2', + } + ); + + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await replaceCustomField({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + customFieldId: 'test_custom_field', + params: { + caseVersion: postedCase.version, + value: 'this is updated text field value', + }, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/cloud_security_posture_functional/pages/vulnerabilities_grouping.ts b/x-pack/test/cloud_security_posture_functional/pages/vulnerabilities_grouping.ts index b0eacf3ff7297..aa9ece3bbf9af 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/vulnerabilities_grouping.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/vulnerabilities_grouping.ts @@ -5,8 +5,8 @@ * 2.0. */ -import expect from '@kbn/expect'; import { asyncForEach } from '@kbn/std'; +import expect from '@kbn/expect'; import type { FtrProviderContext } from '../ftr_provider_context'; import { vulnerabilitiesLatestMock } from '../mocks/vulnerabilities_latest_mock'; @@ -44,9 +44,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - const groupSelector = await findings.groupSelector(); - await groupSelector.openDropDown(); - await groupSelector.setValue('None'); await findings.vulnerabilitiesIndex.remove(); }); @@ -95,6 +92,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('groups vulnerabilities by CVE and sort by number of vulnerabilities desc', async () => { const groupSelector = findings.groupSelector(); await groupSelector.openDropDown(); + await groupSelector.setValue('None'); + await groupSelector.openDropDown(); await groupSelector.setValue('CVE'); const grouping = await findings.findingsGrouping(); @@ -133,6 +132,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('groups vulnerabilities by resource and sort by number of vulnerabilities desc', async () => { const groupSelector = findings.groupSelector(); await groupSelector.openDropDown(); + await groupSelector.setValue('None'); + await groupSelector.openDropDown(); await groupSelector.setValue('Resource'); const grouping = await findings.findingsGrouping(); @@ -172,7 +173,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('SearchBar', () => { it('add filter', async () => { + const groupSelector = await findings.groupSelector(); + await groupSelector.openDropDown(); + await groupSelector.setValue('None'); + await groupSelector.openDropDown(); + await groupSelector.setValue('Resource'); + // Filter bar uses the field's customLabel in the DataView + await filterBar.addFilter({ field: 'Resource Name', operation: 'is', diff --git a/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts b/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts index 06a67a13e425c..a257ff97933d9 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts @@ -41,43 +41,46 @@ export default function (providerContext: FtrProviderContext) { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + const writeMetricsDoc = (namespace: string) => + es.transport.request( + { + method: 'POST', + path: `/${metricsTemplateName}-${namespace}/_doc?refresh=true`, + body: { + '@timestamp': new Date().toISOString(), + logs_test_name: 'test', + data_stream: { + dataset: `${pkgName}.test_metrics`, + namespace, + type: 'metrics', + }, + }, + }, + { meta: true } + ); + + const writeLogsDoc = (namespace: string) => + es.transport.request( + { + method: 'POST', + path: `/${logsTemplateName}-${namespace}/_doc?refresh=true`, + body: { + '@timestamp': new Date().toISOString(), + logs_test_name: 'test', + data_stream: { + dataset: `${pkgName}.test_logs`, + namespace, + type: 'logs', + }, + }, + }, + { meta: true } + ); beforeEach(async () => { await installPackage(pkgName, pkgVersion); await Promise.all( namespaces.map(async (namespace) => { - const createLogsRequest = es.transport.request( - { - method: 'POST', - path: `/${logsTemplateName}-${namespace}/_doc`, - body: { - '@timestamp': '2015-01-01', - logs_test_name: 'test', - data_stream: { - dataset: `${pkgName}.test_logs`, - namespace, - type: 'logs', - }, - }, - }, - { meta: true } - ); - const createMetricsRequest = es.transport.request( - { - method: 'POST', - path: `/${metricsTemplateName}-${namespace}/_doc`, - body: { - '@timestamp': '2015-01-01', - logs_test_name: 'test', - data_stream: { - dataset: `${pkgName}.test_metrics`, - namespace, - type: 'metrics', - }, - }, - }, - { meta: true } - ); - return Promise.all([createLogsRequest, createMetricsRequest]); + return Promise.all([writeLogsDoc(namespace), writeMetricsDoc(namespace)]); }) ); }); @@ -141,7 +144,11 @@ export default function (providerContext: FtrProviderContext) { it('after update, it should have rolled over logs datastream because mappings are not compatible and not metrics', async function () { await installPackage(pkgName, pkgUpdateVersion); + await asyncForEach(namespaces, async (namespace) => { + // write doc as rollover is lazy + await writeLogsDoc(namespace); + await writeMetricsDoc(namespace); const resLogsDatastream = await es.transport.request( { method: 'GET', @@ -266,6 +273,8 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); + // Write a doc to trigger lazy rollover + await writeLogsDoc('default'); // Datastream should have been rolled over expect(await getLogsDefaultBackingIndicesLength()).to.be(2); }); @@ -303,26 +312,29 @@ export default function (providerContext: FtrProviderContext) { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); - beforeEach(async () => { - await installPackage(pkgName, pkgVersion); - - // Create a sample document so the data stream is created - await es.transport.request( + const writeMetricDoc = (body: any = {}) => + es.transport.request( { method: 'POST', - path: `/${metricsTemplateName}-${namespace}/_doc`, + path: `/${metricsTemplateName}-${namespace}/_doc?refresh=true`, body: { - '@timestamp': '2015-01-01', + '@timestamp': new Date().toISOString(), logs_test_name: 'test', data_stream: { dataset: `${pkgName}.test_logs`, namespace, type: 'logs', }, + ...body, }, }, { meta: true } ); + beforeEach(async () => { + await installPackage(pkgName, pkgVersion); + + // Create a sample document so the data stream is created + await writeMetricDoc(); }); afterEach(async () => { @@ -340,6 +352,10 @@ export default function (providerContext: FtrProviderContext) { it('rolls over data stream when index_mode: time_series is set in the updated package version', async () => { await installPackage(pkgName, pkgUpdateVersion); + // Write a doc so lazy rollover can happen + await writeMetricDoc({ + some_field: 'test', + }); const resMetricsDatastream = await es.transport.request( { method: 'GET', diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts index c34413aaae2c2..ac741e10db1e1 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts @@ -207,7 +207,7 @@ export default function (providerContext: FtrProviderContext) { .send(buf) .expect(400); expect(res.error.text).to.equal( - '{"statusCode":400,"error":"Bad Request","message":"Package at top-level directory apache-0.1.4 must contain a top-level manifest.yml file."}' + '{"statusCode":400,"error":"Bad Request","message":"Manifest file apache-0.1.4/manifest.yml not found in paths."}' ); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_hidden_datastreams.ts b/x-pack/test/fleet_api_integration/apis/epm/install_hidden_datastreams.ts index 2fe976352944a..2ec6fb92000e3 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_hidden_datastreams.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_hidden_datastreams.ts @@ -34,46 +34,50 @@ export default function (providerContext: FtrProviderContext) { .send({ force: true }) .expect(200); - await es.index({ - index: 'metrics-apm.service_summary.10m-default', - document: { - '@timestamp': '2023-05-30T07:50:00.000Z', - agent: { - name: 'go', - }, - data_stream: { - dataset: 'apm.service_summary.10m', - namespace: 'default', - type: 'metrics', - }, - ecs: { - version: '8.6.0-dev', - }, - event: { - agent_id_status: 'missing', - ingested: '2023-05-30T07:57:12Z', - }, - metricset: { - interval: '10m', - name: 'service_summary', - }, - observer: { - hostname: '047e282994fb', - type: 'apm-server', - version: '8.7.0', - }, - processor: { - event: 'metric', - name: 'metric', - }, - service: { - language: { + const writeDoc = () => + es.index({ + refresh: true, + index: 'metrics-apm.service_summary.10m-default', + document: { + '@timestamp': '2023-05-30T07:50:00.000Z', + agent: { name: 'go', }, - name: '___main_elastic_cloud_87_ilm_fix', + data_stream: { + dataset: 'apm.service_summary.10m', + namespace: 'default', + type: 'metrics', + }, + ecs: { + version: '8.6.0-dev', + }, + event: { + agent_id_status: 'missing', + ingested: '2023-05-30T07:57:12Z', + }, + metricset: { + interval: '10m', + name: 'service_summary', + }, + observer: { + hostname: '047e282994fb', + type: 'apm-server', + version: '8.7.0', + }, + processor: { + event: 'metric', + name: 'metric', + }, + service: { + language: { + name: 'go', + }, + name: '___main_elastic_cloud_87_ilm_fix', + }, }, - }, - }); + }); + + await writeDoc(); await supertest .post(`/api/fleet/epm/packages/apm/8.8.0`) @@ -81,6 +85,8 @@ export default function (providerContext: FtrProviderContext) { .send({ force: true }) .expect(200); + // Rollover are lazy need to write a new doc + await writeDoc(); const ds = await es.indices.get({ index: 'metrics-apm.service_summary*', expand_wildcards: ['open', 'hidden'], diff --git a/x-pack/test/functional/apps/index_management/index_template_wizard.ts b/x-pack/test/functional/apps/index_management/index_template_wizard.ts index 8776c6de06e40..2a16849f2f5de 100644 --- a/x-pack/test/functional/apps/index_management/index_template_wizard.ts +++ b/x-pack/test/functional/apps/index_management/index_template_wizard.ts @@ -117,13 +117,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.setValue('indexPatternsField', 'test-index-pattern'); // Go to Mappings step - await pageObjects.indexManagement.clickNextButton(); - expect(await testSubjects.getVisibleText('stepTitle')).to.be( - 'Component templates (optional)' - ); - await pageObjects.indexManagement.clickNextButton(); - expect(await testSubjects.getVisibleText('stepTitle')).to.be('Index settings (optional)'); - await pageObjects.indexManagement.clickNextButton(); + await testSubjects.click('formWizardStep-3'); expect(await testSubjects.getVisibleText('stepTitle')).to.be('Mappings (optional)'); }); diff --git a/x-pack/test/functional/apps/infra/node_details.ts b/x-pack/test/functional/apps/infra/node_details.ts index 172ac410eb427..42147652f34a5 100644 --- a/x-pack/test/functional/apps/infra/node_details.ts +++ b/x-pack/test/functional/apps/infra/node_details.ts @@ -204,6 +204,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.assetDetails.overviewAlertsTitleExists(); }); + it('should show / hide alerts section with no alerts and show / hide closed section content', async () => { + await pageObjects.assetDetails.alertsSectionCollapsibleExist(); + // Collapsed by default + await pageObjects.assetDetails.alertsSectionClosedContentNoAlertsExist(); + // Expand + await pageObjects.assetDetails.alertsSectionCollapsibleClick(); + await pageObjects.assetDetails.alertsSectionClosedContentNoAlertsMissing(); + }); + it('shows the CPU Profiling prompt if UI setting for Profiling integration is enabled', async () => { await setInfrastructureProfilingIntegrationUiSetting(true); await pageObjects.assetDetails.cpuProfilingPromptExists(); @@ -213,6 +222,42 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await setInfrastructureProfilingIntegrationUiSetting(false); await pageObjects.assetDetails.cpuProfilingPromptMissing(); }); + + describe('Alerts Section with alerts', () => { + before(async () => { + await navigateToNodeDetails('demo-stack-apache-01', 'demo-stack-apache-01'); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await pageObjects.timePicker.setAbsoluteRange( + START_HOST_ALERTS_DATE.format(DATE_PICKER_FORMAT), + END_HOST_ALERTS_DATE.format(DATE_PICKER_FORMAT) + ); + + await pageObjects.assetDetails.clickOverviewTab(); + }); + + after(async () => { + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'Jennys-MBP.fritz.box'); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await pageObjects.timePicker.setAbsoluteRange( + START_HOST_PROCESSES_DATE.format(DATE_PICKER_FORMAT), + END_HOST_PROCESSES_DATE.format(DATE_PICKER_FORMAT) + ); + }); + + it('should show / hide alerts section with active alerts and show / hide closed section content', async () => { + await pageObjects.assetDetails.alertsSectionCollapsibleExist(); + // Expanded by default + await pageObjects.assetDetails.alertsSectionClosedContentMissing(); + // Collapse + await pageObjects.assetDetails.alertsSectionCollapsibleClick(); + await pageObjects.assetDetails.alertsSectionClosedContentExist(); + // Expand + await pageObjects.assetDetails.alertsSectionCollapsibleClick(); + await pageObjects.assetDetails.alertsSectionClosedContentMissing(); + }); + }); }); describe('Metadata Tab', () => { diff --git a/x-pack/test/functional/apps/managed_content/managed_content.ts b/x-pack/test/functional/apps/managed_content/managed_content.ts index 545d7ad1c4dee..959bddb9f8268 100644 --- a/x-pack/test/functional/apps/managed_content/managed_content.ts +++ b/x-pack/test/functional/apps/managed_content/managed_content.ts @@ -16,12 +16,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'common', 'discover', 'maps', + 'visualize', 'dashboard', ]); const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); const dashboardAddPanel = getService('dashboardAddPanel'); + const listingTable = getService('listingTable'); + const log = getService('log'); describe('Managed Content', () => { before(async () => { @@ -32,6 +35,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/managed_content'); + kibanaServer.importExport.savedObjects.clean({ types: ['dashboard'] }); // we do create a new dashboard in this test }); describe('preventing the user from overwriting managed content', () => { @@ -122,11 +126,51 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); + describe('library views', () => { + const assertInspectorReadonly = async (name: string) => { + log.debug(`making sure table list inspector for ${name} is read-only`); + await listingTable.searchForItemWithName(name); + await listingTable.waitUntilTableIsLoaded(); + await listingTable.inspectVisualization(); + expect(await listingTable.inspectorFieldsReadonly()).to.be(true); + await listingTable.closeInspector(); + }; + + it('visualize library: managed content is read-only', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + + const deletableItems = await listingTable.getAllSelectableItemsNames(); + + expect(deletableItems).to.eql([ + 'Unmanaged lens vis', + 'Unmanaged legacy visualization', + 'Unmanaged map', + ]); + + await assertInspectorReadonly('Managed lens vis'); + await assertInspectorReadonly('Managed legacy visualization'); + await assertInspectorReadonly('Managed map'); + }); + + it('dashboard library: managed content is read-only', async () => { + await PageObjects.dashboard.gotoDashboardListingURL(); + + const deletableItems = await listingTable.getAllSelectableItemsNames(); + + expect(deletableItems).to.eql([]); + + await assertInspectorReadonly('Managed dashboard'); + }); + }); + describe('managed panels in dashboards', () => { it('inlines panels when managed dashboard cloned', async () => { await PageObjects.common.navigateToActualUrl( 'dashboard', - 'view/c44c86f9-b105-4a9c-9a24-449a58a827f3' + 'view/c44c86f9-b105-4a9c-9a24-449a58a827f3', + // for some reason the URL didn't always match the expected, so I turned off this check + // URL doesn't matter as long as we get the dashboard app + { ensureCurrentUrl: false } ); await PageObjects.dashboard.waitForRenderComplete(); diff --git a/x-pack/test/functional/apps/maps/group4/layer_errors.js b/x-pack/test/functional/apps/maps/group4/layer_errors.js index e47c0e582c8f4..9f8a570a46d96 100644 --- a/x-pack/test/functional/apps/maps/group4/layer_errors.js +++ b/x-pack/test/functional/apps/maps/group4/layer_errors.js @@ -18,6 +18,20 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.loadSavedMap('layer with errors'); }); + describe('Layer with invalid descriptor', () => { + const INVALID_LAYER_NAME = 'fff76ebb-57a6-4067-a373-1d191b9bd1a3'; + + it('should diplay error icon in legend', async () => { + await PageObjects.maps.hasErrorIconExistsOrFail(INVALID_LAYER_NAME); + }); + + it('should allow deletion of layer', async () => { + await PageObjects.maps.removeLayer(INVALID_LAYER_NAME); + const exists = await PageObjects.maps.doesLayerExist(INVALID_LAYER_NAME); + expect(exists).to.be(false); + }); + }); + describe('Layer with EsError', () => { after(async () => { await inspector.close(); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index 24437e02e6907..3a859249fe234 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { results: { title: 'artificial_server_log', - numberOfFields: 4, + highlightedText: true, }, metricFields: [ { @@ -104,7 +104,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { results: { title: 'geo_file.csv', - numberOfFields: 3, + highlightedText: false, }, metricFields: [], nonMetricFields: [ @@ -146,7 +146,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { results: { title: 'missing_end_of_file_newline.csv', - numberOfFields: 3, + highlightedText: false, }, metricFields: [ { @@ -217,6 +217,13 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the components of the file details page'); await ml.dataVisualizerFileBased.assertFileTitle(testData.expected.results.title); await ml.dataVisualizerFileBased.assertFileContentPanelExists(); + await ml.dataVisualizerFileBased.assertFileContentHighlightingSwitchExists( + testData.expected.results.highlightedText + ); + await ml.dataVisualizerFileBased.assertFileContentHighlighting( + testData.expected.results.highlightedText, + testData.expected.totalFieldsCount - 1 // -1 for the message field + ); await ml.dataVisualizerFileBased.assertSummaryPanelExists(); await ml.dataVisualizerFileBased.assertFileStatsPanelExists(); diff --git a/x-pack/test/functional/apps/observability_logs_explorer/columns_selection.ts b/x-pack/test/functional/apps/observability_logs_explorer/columns_selection.ts index e6291e4d8fc01..d61ce9e734eba 100644 --- a/x-pack/test/functional/apps/observability_logs_explorer/columns_selection.ts +++ b/x-pack/test/functional/apps/observability_logs_explorer/columns_selection.ts @@ -58,9 +58,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { mode: 'absolute', }, columns: [ - { field: 'resource' }, - { field: 'content' }, - { field: 'data_stream.namespace' }, + { + smartField: 'resource', + type: 'smart-field', + fallbackFields: ['host.name', 'service.name'], + }, + { + smartField: 'content', + type: 'smart-field', + fallbackFields: ['message'], + }, + { field: 'data_stream.namespace', type: 'document-field' }, ], }, }); @@ -77,7 +85,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('render content virtual column properly', async () => { it('should render log level and log message when present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElement(0, 3); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('A sample log')).to.be(true); @@ -86,7 +94,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render log message when present and skip log level when missing', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(1, 4); + const cellElement = await dataGrid.getCellElement(1, 3); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(false); expect(cellValue.includes('A sample log')).to.be(true); @@ -95,7 +103,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render message from error object when top level message not present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(2, 4); + const cellElement = await dataGrid.getCellElement(2, 3); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('error.message')).to.be(true); @@ -105,7 +113,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render message from event.original when top level message and error.message not present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(3, 4); + const cellElement = await dataGrid.getCellElement(3, 3); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('event.original')).to.be(true); @@ -115,7 +123,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the whole JSON when neither message, error.message and event.original are present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(4, 4); + const cellElement = await dataGrid.getCellElement(4, 3); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); @@ -131,7 +139,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('on cell expansion with no message field should open JSON Viewer', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - await dataGrid.clickCellExpandButton(4, 4); + await dataGrid.clickCellExpandButton(4, 3); await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover'); }); }); @@ -139,7 +147,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('on cell expansion with message field should open regular popover', async () => { await navigateToLogsExplorer(); await retry.tryForTime(TEST_TIMEOUT, async () => { - await dataGrid.clickCellExpandButton(3, 4); + await dataGrid.clickCellExpandButton(3, 3); await testSubjects.existOrFail('euiDataGridExpansionPopover'); }); }); @@ -148,7 +156,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('render resource virtual column properly', async () => { it('should render service name and host name when present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 3); + const cellElement = await dataGrid.getCellElement(0, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('synth-service')).to.be(true); expect(cellValue.includes('synth-host')).to.be(true); @@ -162,7 +170,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should render a popover with cell actions when a chip on content column is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElement(0, 3); const logLevelChip = await cellElement.findByTestSubject( 'dataTablePopoverChip_log.level' ); @@ -178,7 +186,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where log.level value is info when filter in action is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElement(0, 3); const logLevelChip = await cellElement.findByTestSubject( 'dataTablePopoverChip_log.level' ); @@ -198,7 +206,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where log.level value is not info when filter out action is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElement(0, 3); const logLevelChip = await cellElement.findByTestSubject( 'dataTablePopoverChip_log.level' ); @@ -216,7 +224,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where service.name value is selected', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 3); + const cellElement = await dataGrid.getCellElement(0, 2); const serviceNameChip = await cellElement.findByTestSubject( 'dataTablePopoverChip_service.name' ); diff --git a/x-pack/test/functional/apps/observability_logs_explorer/custom_control_columns.ts b/x-pack/test/functional/apps/observability_logs_explorer/custom_control_columns.ts new file mode 100644 index 0000000000000..94bb7a4552c77 --- /dev/null +++ b/x-pack/test/functional/apps/observability_logs_explorer/custom_control_columns.ts @@ -0,0 +1,189 @@ +/* + * 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 { log, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import moment from 'moment'; +import { FtrProviderContext } from './config'; + +const MORE_THAN_1024_CHARS = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const PageObjects = getPageObjects(['discover', 'observabilityLogsExplorer']); + const synthtrace = getService('logSynthtraceEsClient'); + const dataGrid = getService('dataGrid'); + const from = '2024-02-06T10:24:14.035Z'; + const to = '2024-02-06T10:25:14.091Z'; + const TEST_TIMEOUT = 10 * 1000; // 10 secs + + const navigateToLogsExplorer = () => + PageObjects.observabilityLogsExplorer.navigateTo({ + pageState: { + time: { + from, + to, + mode: 'absolute', + }, + }, + }); + + describe('When the logs explorer loads', () => { + before(async () => { + await synthtrace.index(generateLogsData({ to })); + await navigateToLogsExplorer(); + }); + + after(async () => { + await synthtrace.clean(); + }); + + describe('should render custom control columns properly', async () => { + it('should render control column with proper header', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + // First control column has no title, so empty string, last control column has title + expect(await dataGrid.getControlColumnHeaderFields()).to.eql(['', 'actions']); + }); + }); + + it('should render the expand icon in the last control column', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(0, 4); + const expandButton = await cellElement.findByTestSubject('docTableExpandToggleColumn'); + expect(expandButton).to.not.be.empty(); + }); + }); + + it('should render the malformed icon in the last control column if malformed doc exists', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(1, 4); + const malformedButton = await cellElement.findByTestSubject('docTableMalformedDocExist'); + expect(malformedButton).to.not.be.empty(); + }); + }); + + it('should render the disabled malformed icon in the last control column when malformed doc does not exists', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(0, 4); + const malformedDisableButton = await cellElement.findByTestSubject( + 'docTableMalformedDocDoesNotExist' + ); + expect(malformedDisableButton).to.not.be.empty(); + }); + }); + + it('should render the stacktrace icon in the last control column when stacktrace exists', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(4, 4); + const stacktraceButton = await cellElement.findByTestSubject('docTableStacktraceExist'); + expect(stacktraceButton).to.not.be.empty(); + }); + }); + + it('should render the stacktrace icon disabled in the last control column when stacktrace does not exists', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(1, 4); + const stacktraceButton = await cellElement.findByTestSubject( + 'docTableStacktraceDoesNotExist' + ); + expect(stacktraceButton).to.not.be.empty(); + }); + }); + }); + }); +} + +function generateLogsData({ to, count = 1 }: { to: string; count?: number }) { + const logs = timerange(moment(to).subtract(1, 'second'), moment(to)) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log + .create() + .message('A sample log') + .logLevel('info') + .timestamp(timestamp) + .defaults({ 'service.name': 'synth-service' }); + }) + ); + + const malformedDocs = timerange( + moment(to).subtract(2, 'second'), + moment(to).subtract(1, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log + .create() + .message('A malformed doc') + .logLevel(MORE_THAN_1024_CHARS) + .timestamp(timestamp) + .defaults({ 'service.name': 'synth-service' }); + }) + ); + + const logsWithErrorMessage = timerange( + moment(to).subtract(3, 'second'), + moment(to).subtract(2, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().logLevel('info').timestamp(timestamp).defaults({ + 'error.stack_trace': 'Error message in error.stack_trace', + 'service.name': 'node-service', + }); + }) + ); + + const logsWithErrorException = timerange( + moment(to).subtract(4, 'second'), + moment(to).subtract(3, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().logLevel('info').timestamp(timestamp).defaults({ + 'error.exception.stacktrace': 'Error message in error.exception.stacktrace', + 'service.name': 'node-service', + }); + }) + ); + + const logsWithErrorInLog = timerange( + moment(to).subtract(5, 'second'), + moment(to).subtract(4, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().logLevel('info').timestamp(timestamp).defaults({ + 'error.log.stacktrace': 'Error message in error.log.stacktrace', + 'service.name': 'node-service', + }); + }) + ); + + return [logs, malformedDocs, logsWithErrorMessage, logsWithErrorException, logsWithErrorInLog]; +} diff --git a/x-pack/test/functional/apps/observability_logs_explorer/flyout.ts b/x-pack/test/functional/apps/observability_logs_explorer/flyout.ts index 8e478755794a6..c3ef409546b31 100644 --- a/x-pack/test/functional/apps/observability_logs_explorer/flyout.ts +++ b/x-pack/test/functional/apps/observability_logs_explorer/flyout.ts @@ -66,30 +66,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should mount the flyout customization content', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutDetail'); }); it('should display a timestamp badge', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutLogTimestamp'); }); it('should display a log level badge when available', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutLogLevel'); await dataGrid.closeFlyout(); - await dataGrid.clickRowToggle({ rowIndex: 1 }); + await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); await testSubjects.missingOrFail('logsExplorerFlyoutLogLevel'); }); it('should display a message code block when available', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutLogMessage'); await dataGrid.closeFlyout(); - await dataGrid.clickRowToggle({ rowIndex: 1 }); + await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); await testSubjects.missingOrFail('logsExplorerFlyoutLogMessage'); }); }); diff --git a/x-pack/test/functional/apps/observability_logs_explorer/flyout_highlights.ts b/x-pack/test/functional/apps/observability_logs_explorer/flyout_highlights.ts index 678c76bde9f2f..238d456b5ec54 100644 --- a/x-pack/test/functional/apps/observability_logs_explorer/flyout_highlights.ts +++ b/x-pack/test/functional/apps/observability_logs_explorer/flyout_highlights.ts @@ -87,7 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the service container with all fields', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); await testSubjects.existOrFail('logsExplorerFlyoutService'); await testSubjects.existOrFail('logsExplorerFlyoutTrace'); @@ -98,7 +98,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the service container even when 1 field is missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1 }); + await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); await testSubjects.missingOrFail('logsExplorerFlyoutService'); await testSubjects.existOrFail('logsExplorerFlyoutTrace'); @@ -109,7 +109,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should not load the service container if all fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 2 }); + await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 }); await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); await dataGrid.closeFlyout(); }); @@ -155,7 +155,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the cloud container with all fields', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud'); await testSubjects.existOrFail('logsExplorerFlyoutCloudProvider'); await testSubjects.existOrFail('logsExplorerFlyoutCloudRegion'); @@ -166,7 +166,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the cloud container even when some fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1 }); + await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud'); await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider'); @@ -179,7 +179,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should not load the cloud container if all fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 2 }); + await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 }); await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionCloud'); await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider'); await testSubjects.missingOrFail('logsExplorerFlyoutCloudRegion'); @@ -225,7 +225,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the other container with all fields', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther'); await testSubjects.existOrFail('logsExplorerFlyoutLogPathFile'); await testSubjects.existOrFail('logsExplorerFlyoutNamespace'); @@ -235,7 +235,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the other container even when some fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1 }); + await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther'); await testSubjects.missingOrFail('logsExplorerFlyoutLogPathFile'); diff --git a/x-pack/test/functional/apps/observability_logs_explorer/header_menu.ts b/x-pack/test/functional/apps/observability_logs_explorer/header_menu.ts index 9bc4e090fb6cc..f7c45395dd758 100644 --- a/x-pack/test/functional/apps/observability_logs_explorer/header_menu.ts +++ b/x-pack/test/functional/apps/observability_logs_explorer/header_menu.ts @@ -46,7 +46,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await discoverLink.isDisplayed()).to.be(true); }); - it('should navigate to discover keeping the current columns/filters/query/time/data view', async () => { + it('should navigate to discover keeping the current filters/query/time/data view and use fallback columns for virtual columns', async () => { await retry.try(async () => { await testSubjects.existOrFail('superDatePickerstartDatePopoverButton'); await testSubjects.existOrFail('superDatePickerendDatePopoverButton'); @@ -69,8 +69,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { expect(await PageObjects.discover.getColumnHeaders()).to.eql([ '@timestamp', - 'resource', - 'content', + 'host.name', + 'service.name', + 'message', ]); }); diff --git a/x-pack/test/functional/apps/observability_logs_explorer/index.ts b/x-pack/test/functional/apps/observability_logs_explorer/index.ts index e8114a8d14bfd..c56a898b6d0c0 100644 --- a/x-pack/test/functional/apps/observability_logs_explorer/index.ts +++ b/x-pack/test/functional/apps/observability_logs_explorer/index.ts @@ -17,5 +17,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./flyout')); loadTestFile(require.resolve('./header_menu')); loadTestFile(require.resolve('./flyout_highlights.ts')); + loadTestFile(require.resolve('./custom_control_columns.ts')); }); } diff --git a/x-pack/test/functional/fixtures/kbn_archiver/maps.json b/x-pack/test/functional/fixtures/kbn_archiver/maps.json index 74f27e360cfa1..b7fef81c49bfb 100644 --- a/x-pack/test/functional/fixtures/kbn_archiver/maps.json +++ b/x-pack/test/functional/fixtures/kbn_archiver/maps.json @@ -747,7 +747,7 @@ "version": "WzU1LDFd", "attributes": { "description": "", - "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"KIBANA_TILEMAP\"},\"id\":\"ap0ys\",\"label\":\"Custom_TMS\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{},\"previousStyle\":null},\"type\":\"RASTER_TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"idThatDoesNotExitForEMSTile\",\"lightModeDefault\":\"road_map\"},\"temporary\":false,\"id\":\"plw9l\",\"label\":\"EMS_tiles\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{},\"previousStyle\":null},\"type\":\"EMS_VECTOR_TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"idThatDoesNotExitForEMSFileSource\"},\"temporary\":false,\"id\":\"2gro0\",\"label\":\"EMS_vector_shapes\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"f67fe707-95dd-46d6-89b8-82617b251b61\",\"geoField\":\"geo.coordinates\",\"requestType\":\"grid\",\"resolution\":\"COARSE\",\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_3_source_index_pattern\"},\"temporary\":false,\"id\":\"pl5qd\",\"label\":\"\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Blues\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32,\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"id\":\"a07072bb-3a92-4320-bd37-250ef6d04db7\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_4_source_index_pattern\"},\"temporary\":false,\"id\":\"9bw8h\",\"label\":\"\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_5_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.shape_name\",\"name\":\"__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"origin\":\"join\"},\"color\":\"Blues\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"type\":\"ES_TERM_SOURCE\",\"indexPatternRefName\":\"layer_5_join_0_index_pattern\"}}]},{\"sourceDescriptor\":{\"geoField\":\"destination\",\"scalingType\":\"LIMIT\",\"id\":\"ed01aac3-c0be-491e-98c9-f1cb6e37f185\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"applyForceRefresh\":true,\"filterByMapBounds\":true,\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"topHitsGroupByTimeseries\":false,\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"indexPatternRefName\":\"layer_6_source_index_pattern\"},\"id\":\"1cfaa7fa-dc73-419d-b362-7238e2270a1c\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":0}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"GEOJSON_VECTOR\",\"joins\":[],\"disableTooltips\":false}]", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"KIBANA_TILEMAP\"},\"id\":\"ap0ys\",\"label\":\"Custom_TMS\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{},\"previousStyle\":null},\"type\":\"RASTER_TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"idThatDoesNotExitForEMSTile\",\"lightModeDefault\":\"road_map\"},\"temporary\":false,\"id\":\"plw9l\",\"label\":\"EMS_tiles\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{},\"previousStyle\":null},\"type\":\"EMS_VECTOR_TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"idThatDoesNotExitForEMSFileSource\"},\"temporary\":false,\"id\":\"2gro0\",\"label\":\"EMS_vector_shapes\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"f67fe707-95dd-46d6-89b8-82617b251b61\",\"geoField\":\"geo.coordinates\",\"requestType\":\"grid\",\"resolution\":\"COARSE\",\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_3_source_index_pattern\"},\"temporary\":false,\"id\":\"pl5qd\",\"label\":\"\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Blues\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32,\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"id\":\"a07072bb-3a92-4320-bd37-250ef6d04db7\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_4_source_index_pattern\"},\"temporary\":false,\"id\":\"9bw8h\",\"label\":\"\",\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\"},{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_5_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.shape_name\",\"name\":\"__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"origin\":\"join\"},\"color\":\"Blues\",\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3}}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"GEOJSON_VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"type\":\"ES_TERM_SOURCE\",\"indexPatternRefName\":\"layer_5_join_0_index_pattern\"}}]},{\"sourceDescriptor\":{\"geoField\":\"destination\",\"scalingType\":\"LIMIT\",\"id\":\"ed01aac3-c0be-491e-98c9-f1cb6e37f185\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"applyForceRefresh\":true,\"filterByMapBounds\":true,\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"topHitsGroupByTimeseries\":false,\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"indexPatternRefName\":\"layer_6_source_index_pattern\"},\"id\":\"1cfaa7fa-dc73-419d-b362-7238e2270a1c\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":0}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"GEOJSON_VECTOR\",\"joins\":[],\"disableTooltips\":false},{\"sourceDescriptor\":{\"id\":\"d4d6d4cf-58ee-4a0d-a792-532c0711fa2a\",\"type\":\"ESQL\"},\"id\":\"fff76ebb-57a6-4067-a373-1d191b9bd1a3\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#6092C0\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#4379aa\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"GEOJSON_VECTOR\",\"joins\":[],\"disableTooltips\":false}]", "mapStateJSON": "{\"adHocDataViews\":[],\"zoom\":3.38,\"center\":{\"lon\":76.34937,\"lat\":-77.25604},\"timeFilters\":{\"from\":\"now-7d\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"index\":\"561253e0-f731-11e8-8487-11b9dd924f96\",\"type\":\"custom\",\"disabled\":false,\"negate\":false,\"alias\":\"connections shard failure\",\"key\":\"query\",\"value\":\"{\\\"error_query\\\":{\\\"indices\\\":[{\\\"error_type\\\":\\\"exception\\\",\\\"message\\\":\\\"simulated shard failure\\\",\\\"name\\\":\\\"connections\\\"}]}}\"},\"$state\":{\"store\":\"appState\"},\"query\":{\"error_query\":{\"indices\":[{\"error_type\":\"exception\",\"message\":\"simulated shard failure\",\"name\":\"connections\"}]}}}],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"customIcons\":[],\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"keydownScrollZoom\":false,\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"showTimesliderToggleButton\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", "title": "layer with errors", "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[\"1cfaa7fa-dc73-419d-b362-7238e2270a1c\"]}" @@ -1181,8 +1181,8 @@ "attributes": { "title": "esql example", "description": "", - "mapStateJSON": "{\"adHocDataViews\":[],\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"customIcons\":[],\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"keydownScrollZoom\":false,\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"showTimesliderToggleButton\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", - "layerListJSON": "[{\"sourceDescriptor\":{\"columns\":[{\"name\":\"geo.coordinates\",\"type\":\"geo_point\"}],\"dateField\":\"@timestamp\",\"esql\":\"from logstash-* | KEEP geo.coordinates | limit 10000\",\"id\":\"fad0e2eb-9278-415c-bdc8-1189a46eac0b\",\"type\":\"ESQL\",\"narrowByGlobalSearch\":true,\"narrowByMapBounds\":true,\"applyForceRefresh\":true,\"geoField\":\"geo.coordinates\",\"narrowByGlobalTime\":true},\"id\":\"59ca05b3-e3be-4fb4-ab4d-56c17b8bd589\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"GEOJSON_VECTOR\",\"joins\":[],\"disableTooltips\":false}]", + "mapStateJSON": "{\"adHocDataViews\":[{\"id\":\"624b9c7c17e840dd2161e64c37fe4696eb042ca3afbba2c390c742e3f576a8c9\",\"title\":\"logstash-*\",\"sourceFilters\":[],\"fieldFormats\":{},\"runtimeFieldMap\":{},\"fieldAttrs\":{},\"allowNoIndex\":false,\"name\":\"logstash-*\",\"allowHidden\":false}],\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"customIcons\":[],\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"keydownScrollZoom\":false,\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"showTimesliderToggleButton\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "layerListJSON": "[{\"sourceDescriptor\":{\"columns\":[{\"name\":\"geo.coordinates\",\"type\":\"geo_point\"}],\"dataViewId\":\"624b9c7c17e840dd2161e64c37fe4696eb042ca3afbba2c390c742e3f576a8c9\",\"dateField\":\"@timestamp\",\"esql\":\"from logstash-* | KEEP geo.coordinates | limit 10000\",\"id\":\"fad0e2eb-9278-415c-bdc8-1189a46eac0b\",\"type\":\"ESQL\",\"narrowByGlobalSearch\":true,\"narrowByMapBounds\":true,\"applyForceRefresh\":true,\"geoField\":\"geo.coordinates\",\"narrowByGlobalTime\":true},\"id\":\"59ca05b3-e3be-4fb4-ab4d-56c17b8bd589\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"GEOJSON_VECTOR\",\"joins\":[],\"disableTooltips\":false}]", "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" }, "references": [], diff --git a/x-pack/test/functional/page_objects/asset_details.ts b/x-pack/test/functional/page_objects/asset_details.ts index 5e1ea574f8a81..cd34d9c2ca10b 100644 --- a/x-pack/test/functional/page_objects/asset_details.ts +++ b/x-pack/test/functional/page_objects/asset_details.ts @@ -89,6 +89,24 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) { return await testSubjects.existOrFail('infraAssetDetailsMetricsCollapsible'); }, + async alertsSectionCollapsibleClick() { + return await testSubjects.click('infraAssetDetailsAlertsCollapsible'); + }, + + async alertsSectionClosedContentExist() { + return await testSubjects.existOrFail('infraAssetDetailsAlertsClosedContentWithAlerts'); + }, + async alertsSectionClosedContentMissing() { + return await testSubjects.missingOrFail('infraAssetDetailsAlertsClosedContentWithAlerts'); + }, + + async alertsSectionClosedContentNoAlertsExist() { + return await testSubjects.existOrFail('infraAssetDetailsAlertsClosedContentNoAlerts'); + }, + async alertsSectionClosedContentNoAlertsMissing() { + return await testSubjects.missingOrFail('infraAssetDetailsAlertsClosedContentNoAlerts'); + }, + // Metadata async clickMetadataTab() { return testSubjects.click('infraAssetDetailsMetadataTab'); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index c159fb17b4db7..c5018bda39ffa 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -1934,8 +1934,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.existOrFail('lns-indexPattern-dimensionContainerClose'); }); await testSubjects.click('lns_colorEditing_trigger'); - // disable autoAssign - await testSubjects.setEuiSwitch('lns-colorMapping-autoAssignSwitch', 'uncheck'); + + // assign all + await testSubjects.click('lns-colorMapping-assignmentsPromptAddAll'); await testSubjects.click(`lns-colorMapping-colorSwatch-${colorSwatchIndex}`); diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts index 03d1078ccec2c..f6ec13a492318 100644 --- a/x-pack/test/functional/services/cases/list.ts +++ b/x-pack/test/functional/services/cases/list.ts @@ -5,6 +5,9 @@ * 2.0. */ +import deepEqual from 'react-fast-compare'; +import expect from '@kbn/expect'; +import rison from '@kbn/rison'; import { CaseSeverity, CaseStatuses } from '@kbn/cases-plugin/common/types/domain'; import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -100,6 +103,15 @@ export function CasesTableServiceProvider( await header.waitUntilLoadingHasFinished(); }, + async waitForNthToBeListed(numberOfCases: number) { + await retry.try(async () => { + await this.refreshTable(); + await this.validateCasesTableHasNthRows(numberOfCases); + }); + + await header.waitUntilLoadingHasFinished(); + }, + async waitForCasesToBeDeleted() { await retry.waitFor('the cases table to be empty', async () => { await this.refreshTable(); @@ -133,6 +145,13 @@ export function CasesTableServiceProvider( return rows[index] ?? null; }, + async verifyCase(caseId: string, index: number) { + const theCaseById = await this.getCaseById(caseId); + const theCaseByIndex = await this.getCaseByIndex(index); + + return (await theCaseById._webElement.getId()) === (await theCaseByIndex._webElement.getId()); + }, + async filterByTag(tag: string) { await common.clickAndValidate( 'options-filter-popover-button-tags', @@ -437,5 +456,54 @@ export function CasesTableServiceProvider( // closes the popover await browser.pressKeys(browser.keys.ESCAPE); }, + + async clearFilters() { + if (await testSubjects.exists('all-cases-clear-filters-link-icon')) { + await testSubjects.click('all-cases-clear-filters-link-icon'); + await header.waitUntilLoadingHasFinished(); + } + }, + + async setAllCasesStateInLocalStorage(state: Record) { + await browser.setLocalStorageItem('management.cases.list.state', JSON.stringify(state)); + + const currentState = JSON.parse( + (await browser.getLocalStorageItem('management.cases.list.state')) ?? '{}' + ); + + expect(deepEqual(currentState, state)).to.be(true); + }, + + async getAllCasesStateInLocalStorage() { + const currentState = JSON.parse( + (await browser.getLocalStorageItem('management.cases.list.state')) ?? '{}' + ); + + return currentState; + }, + + async setFiltersConfigurationInLocalStorage(state: Array<{ key: string; isActive: boolean }>) { + await browser.setLocalStorageItem( + 'management.cases.list.tableFiltersConfig', + JSON.stringify(state) + ); + + const currentState = JSON.parse( + (await browser.getLocalStorageItem('management.cases.list.tableFiltersConfig')) ?? '{}' + ); + + expect(deepEqual(currentState, state)).to.be(true); + }, + + async expectFiltersToBeActive(filters: string[]) { + for (const filter of filters) { + await testSubjects.existOrFail(`options-filter-popover-button-${filter}`); + } + }, + + async setStateToUrlAndNavigate(state: Record) { + const encodedUrlParams = rison.encode(state); + await common.navigateToApp('cases', { search: `cases=${encodedUrlParams}` }); + }, }; } diff --git a/x-pack/test/functional/services/cases/navigation.ts b/x-pack/test/functional/services/cases/navigation.ts index f0d4fb52ba5e4..5b827c0287a0f 100644 --- a/x-pack/test/functional/services/cases/navigation.ts +++ b/x-pack/test/functional/services/cases/navigation.ts @@ -12,13 +12,13 @@ export function CasesNavigationProvider({ getPageObject, getService }: FtrProvid const testSubjects = getService('testSubjects'); return { - async navigateToApp(app: string = 'cases', appSelector: string = 'cases-app') { - await common.navigateToApp(app); + async navigateToApp(app: string = 'cases', appSelector: string = 'cases-app', search?: string) { + await common.navigateToApp(app, { search }); await testSubjects.existOrFail(appSelector); }, - async navigateToConfigurationPage(app: string = 'cases', appSelector: string = 'cases-app') { - await this.navigateToApp(app, appSelector); + async navigateToConfigurationPage(app: string = 'cases') { + await this.navigateToApp(app, 'cases-app'); await common.clickAndValidate('configure-case-button', 'case-configure-title'); }, diff --git a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts index 0b8effd17cbb0..df95eddd957f8 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts @@ -49,6 +49,32 @@ export function MachineLearningDataVisualizerFileBasedProvider( await testSubjects.existOrFail('dataVisualizerFileFileContentPanel'); }, + async assertFileContentHighlightingSwitchExists(exist: boolean) { + const tabs = await testSubjects.findAll('dataVisualizerFileContentsHighlightingSwitch'); + expect(tabs.length).to.eql( + exist ? 1 : 0, + `Expected file content highlighting switch to ${exist ? 'exist' : 'not exist'}, but found ${ + tabs.length + }` + ); + }, + + async assertFileContentHighlighting(highlighted: boolean, numberOfFields: number) { + const lines = await testSubjects.findAll('dataVisualizerHighlightedLine', 1000); + const linesExist = lines.length > 0; + expect(linesExist).to.eql( + highlighted, + `Expected file content highlighting to be '${highlighted ? 'enabled' : 'disabled'}'` + ); + const expectedNumberOfFields = highlighted ? numberOfFields : 0; + const foundFields = (await lines[0]?.findAllByTestSubject('dataVisualizerFieldBadge')) ?? []; + + expect(foundFields.length).to.eql( + expectedNumberOfFields, + `Expected ${expectedNumberOfFields} fields to be highlighted, but found ${foundFields.length}` + ); + }, + async assertSummaryPanelExists() { await testSubjects.existOrFail('dataVisualizerFileSummaryPanel'); }, diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/list_view.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/list_view.ts index 32a1ef6125b7b..906e571941ff2 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/list_view.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/list_view.ts @@ -6,7 +6,12 @@ */ import expect from '@kbn/expect'; -import { CaseSeverity, CaseStatuses } from '@kbn/cases-plugin/common/types/domain'; +import rison from '@kbn/rison'; +import { + CaseSeverity, + CaseStatuses, + CustomFieldTypes, +} from '@kbn/cases-plugin/common/types/domain'; import { UserProfile } from '@kbn/user-profile-components'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { @@ -24,6 +29,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { describe('cases list', () => { before(async () => { + await cases.api.deleteAllCases(); await cases.navigation.navigateToApp(); }); @@ -280,13 +286,19 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { describe('filtering', () => { const caseTitle = 'matchme'; - const caseIds: string[] = []; - - before(async () => { - await createUsersAndRoles(getService, users, roles); - await cases.api.activateUserProfiles([casesAllUser, casesAllUser2]); - - const profiles = await cases.api.suggestUserProfiles({ name: 'all', owners: ['cases'] }); + let caseIds: string[] = []; + const profiles: UserProfile[] = []; + const customFields = [ + { + key: 'my_field_01', + label: 'My field', + type: CustomFieldTypes.TOGGLE, + required: false, + }, + ]; + + const createCases = async () => { + caseIds = []; const case1 = await cases.api.createCase({ title: caseTitle, @@ -309,18 +321,30 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { caseIds.push(case2.id); caseIds.push(case3.id); caseIds.push(case4.id); + }; + + before(async () => { + await cases.api.deleteAllCases(); + await createUsersAndRoles(getService, users, roles); + await cases.api.activateUserProfiles([casesAllUser, casesAllUser2]); + + profiles.push(...(await cases.api.suggestUserProfiles({ name: 'all', owners: ['cases'] }))); await header.waitUntilLoadingHasFinished(); - await cases.casesTable.waitForCasesToBeListed(); }); beforeEach(async () => { - /** - * There is no easy way to clear the filtering. - * Refreshing the page seems to be easier. - */ await browser.clearLocalStorage(); - await cases.navigation.navigateToApp(); + await cases.api.createConfigWithCustomFields({ customFields, owner: 'cases' }); + await createCases(); + await header.waitUntilLoadingHasFinished(); + await cases.casesTable.waitForCasesToBeListed(); + }); + + afterEach(async () => { + await cases.casesTable.clearFilters(); + await cases.api.deleteAllCases(); + await cases.casesTable.waitForCasesToBeDeleted(); }); after(async () => { @@ -500,6 +524,234 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.casesTable.validateCasesTableHasNthRows(2); }); + it('clears the filters correctly', async () => { + // filter by status first + await cases.casesTable.filterByTag('one'); + await cases.casesTable.validateCasesTableHasNthRows(1); + + await cases.casesTable.clearFilters(); + await cases.casesTable.validateCasesTableHasNthRows(caseIds.length); + }); + + it('loads the state from the local storage when the URL is empty', async () => { + await cases.casesTable.validateCasesTableHasNthRows(caseIds.length); + const lsState = { + filterOptions: { + search: '', + searchFields: ['title', 'description'], + severity: [], + assignees: [], + reporters: [], + status: [], + // filter by tags + tags: ['one'], + owner: [], + category: [], + customFields: {}, + }, + queryParams: { page: 1, perPage: 10, sortField: 'createdAt', sortOrder: 'desc' }, + }; + + await cases.casesTable.setAllCasesStateInLocalStorage(lsState); + + /** + * Clicking to the navigation bar (sidebar) clears out any query params + * added by the cases app. + */ + await testSubjects.click('cases'); + await cases.casesTable.validateCasesTableHasNthRows(1); + await cases.casesTable.verifyCase(caseIds[0], 0); + }); + + it('loads the state from the URL with empty filter configuration in local storage', async () => { + const theCase = await cases.api.createCase({ + title: 'url-testing', + assignees: [{ uid: profiles[0].uid }], + description: 'url testing', + category: 'url-testing', + tags: ['url'], + severity: CaseSeverity.CRITICAL, + customFields: [{ key: customFields[0].key, type: CustomFieldTypes.TOGGLE, value: true }], + }); + + const lsState = [ + { key: 'status', isActive: false }, + { key: 'severity', isActive: false }, + { key: 'tags', isActive: false }, + { key: 'assignees', isActive: false }, + { key: 'category', isActive: false }, + { key: `cf_${customFields[0].key}`, isActive: false }, + ]; + + await cases.casesTable.setFiltersConfigurationInLocalStorage(lsState); + await cases.casesTable.waitForNthToBeListed(caseIds.length + 1); + + const casesState = { + search: theCase.title, + severity: [theCase.severity], + status: [theCase.status], + tags: theCase.tags, + assignees: [profiles[0].uid], + category: [theCase.category], + customFields: { [customFields[0].key]: ['on'] }, + }; + + await cases.casesTable.setStateToUrlAndNavigate(casesState); + await cases.casesTable.validateCasesTableHasNthRows(1); + await cases.casesTable.verifyCase(theCase.id, 0); + + const currentUrl = decodeURIComponent(await browser.getCurrentUrl()); + expect(new URL(currentUrl).search).to.be(`?cases=${rison.encode(casesState)}`); + + await cases.casesTable.expectFiltersToBeActive([ + 'status', + 'severity', + 'tags', + 'category', + 'assignees', + customFields[0].key, + ]); + + const searchBar = await testSubjects.find('search-cases'); + expect(await searchBar.getAttribute('value')).to.be(casesState.search); + }); + + it('loads the state from the URL with filter configuration in local storage', async () => { + const theCase = await cases.api.createCase({ + title: 'url-testing', + assignees: [{ uid: profiles[0].uid }], + description: 'url testing', + category: 'url-testing', + tags: ['url'], + severity: CaseSeverity.CRITICAL, + customFields: [{ key: customFields[0].key, type: CustomFieldTypes.TOGGLE, value: true }], + }); + + await cases.casesTable.waitForNthToBeListed(caseIds.length + 1); + + const casesState = { + search: theCase.title, + severity: [theCase.severity], + status: [theCase.status], + tags: theCase.tags, + assignees: [profiles[0].uid], + category: [theCase.category], + customFields: { [customFields[0].key]: ['on'] }, + }; + + await cases.casesTable.setStateToUrlAndNavigate(casesState); + await cases.casesTable.validateCasesTableHasNthRows(1); + await cases.casesTable.verifyCase(theCase.id, 0); + + const currentUrl = decodeURIComponent(await browser.getCurrentUrl()); + expect(new URL(currentUrl).search).to.be(`?cases=${rison.encode(casesState)}`); + + await cases.casesTable.expectFiltersToBeActive([ + 'status', + 'severity', + 'tags', + 'category', + 'assignees', + customFields[0].key, + ]); + + const searchBar = await testSubjects.find('search-cases'); + expect(await searchBar.getAttribute('value')).to.be(casesState.search); + }); + + it('updates the local storage correctly when navigating to a URL', async () => { + const theCase = await cases.api.createCase({ + title: 'url-testing', + assignees: [{ uid: profiles[0].uid }], + description: 'url testing', + category: 'url-testing', + tags: ['url'], + severity: CaseSeverity.CRITICAL, + customFields: [{ key: customFields[0].key, type: CustomFieldTypes.TOGGLE, value: true }], + }); + + await cases.casesTable.waitForNthToBeListed(caseIds.length + 1); + + const casesState = { + search: theCase.title, + severity: [theCase.severity], + status: [theCase.status], + tags: theCase.tags, + assignees: [profiles[0].uid], + category: [theCase.category], + customFields: { [customFields[0].key]: ['on'] }, + }; + + await cases.casesTable.setStateToUrlAndNavigate(casesState); + await cases.casesTable.validateCasesTableHasNthRows(1); + await cases.casesTable.verifyCase(theCase.id, 0); + + const currentState = await cases.casesTable.getAllCasesStateInLocalStorage(); + + expect(currentState).to.eql({ + queryParams: { page: 1, perPage: 10, sortField: 'createdAt', sortOrder: 'desc' }, + filterOptions: { + search: theCase.title, + searchFields: ['title', 'description'], + severity: [theCase.severity], + assignees: [profiles[0].uid], + reporters: [], + status: [theCase.status], + tags: theCase.tags, + owner: [], + category: [theCase.category], + customFields: { my_field_01: { type: CustomFieldTypes.TOGGLE, options: ['on'] } }, + }, + }); + }); + + it('loads the state from a legacy URL', async () => { + const theCase = await cases.api.createCase({ + title: 'url-testing', + description: 'url testing', + severity: CaseSeverity.CRITICAL, + }); + + await cases.casesTable.waitForNthToBeListed(caseIds.length + 1); + + const search = `severity=${theCase.severity}&status=${theCase.status}&page=1&perPage=1sortField=createdAt&sortOrder=desc`; + + await cases.navigation.navigateToApp('cases', 'cases-app', search); + await cases.casesTable.validateCasesTableHasNthRows(1); + await cases.casesTable.verifyCase(theCase.id, 0); + + const currentUrl = decodeURIComponent(await browser.getCurrentUrl()); + expect(new URL(currentUrl).search).to.be(`?${search}`); + + await cases.casesTable.expectFiltersToBeActive([ + 'status', + 'severity', + 'tags', + 'category', + 'assignees', + ]); + }); + + it('navigating between pages keeps the state', async () => { + await cases.casesTable.filterByTag('one'); + await cases.casesTable.validateCasesTableHasNthRows(1); + await cases.casesTable.verifyCase(caseIds[0], 0); + await cases.casesTable.goToFirstListedCase(); + + await header.waitUntilLoadingHasFinished(); + await testSubjects.click('backToCases'); + + await header.waitUntilLoadingHasFinished(); + await cases.casesTable.waitForCasesToBeListed(); + await cases.casesTable.validateCasesTableHasNthRows(1); + await cases.casesTable.verifyCase(caseIds[0], 0); + }); + + it('loads the initial state correctly', async () => { + await cases.casesTable.validateCasesTableHasNthRows(caseIds.length); + expect(await testSubjects.exists('all-cases-clear-filters-link-icon')).to.be(false); + }); + describe('assignees filtering', () => { it('filters cases by the first cases all user assignee', async () => { await cases.casesTable.filterByAssignee('all'); @@ -554,12 +806,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.casesTable.waitForCasesToBeListed(); }); - beforeEach(async () => { - /** - * There is no easy way to clear the filtering. - * Refreshing the page seems to be easier. - */ - await cases.navigation.navigateToApp(); + afterEach(async () => { + await cases.casesTable.clearFilters(); }); after(async () => { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index c7dc220e96723..0d7539c566e7e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -200,6 +200,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await actionsMenuItemElem.at(1)?.click(); + await testSubjects.click('confirmModalConfirmButton'); + await pageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async () => { expect(await actionsDropdown.getVisibleText()).to.eql('Disabled'); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts index 1a169c3bd69bf..7838ec24c50a3 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rules_list/rules_list.ts @@ -17,13 +17,15 @@ import { } from '../../../lib/alert_api_actions'; import { ObjectRemover } from '../../../lib/object_remover'; import { generateUniqueKey } from '../../../lib/get_test_data'; +import { getTestAlertData } from '../../../lib/get_test_data'; -export default ({ getPageObjects, getService }: FtrProviderContext) => { +export default ({ getPageObjects, getPageObject, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); const find = getService('find'); const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const supertest = getService('supertest'); const retry = getService('retry'); + const header = getPageObject('header'); const objectRemover = new ObjectRemover(supertest); async function refreshAlertsList() { @@ -39,6 +41,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('rulesTab'); } + const getAlertSummary = async (ruleId: string) => { + const { body: summary } = await supertest + .get(`/internal/alerting/rule/${encodeURIComponent(ruleId)}/_alert_summary`) + .expect(200); + return summary; + }; + describe('rules list', function () { const assertRulesLength = async (length: number) => { return await retry.try(async () => { @@ -185,6 +194,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('disableButton'); + await testSubjects.click('confirmModalConfirmButton'); + await refreshAlertsList(); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); @@ -195,12 +206,93 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); }); + it('should untrack disable rule if untrack switch is true', async () => { + const { body: createdRule } = await supertest + .post(`/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.always-firing', + schedule: { interval: '24h' }, + params: { + instances: [{ id: 'alert-id' }], + }, + }) + ) + .expect(200); + + objectRemover.add(createdRule.id, 'alert', 'alerts'); + + await retry.try(async () => { + const { alerts: alertInstances } = await getAlertSummary(createdRule.id); + expect(Object.keys(alertInstances).length).to.eql(1); + expect(alertInstances['alert-id'].tracked).to.eql(true); + }); + + await refreshAlertsList(); + await pageObjects.triggersActionsUI.searchAlerts(createdRule.name); + + await testSubjects.click('collapsedItemActions'); + + await testSubjects.click('disableButton'); + + await testSubjects.click('untrackAlertsModalSwitch'); + + await testSubjects.click('confirmModalConfirmButton'); + + await header.waitUntilLoadingHasFinished(); + + await retry.try(async () => { + const { alerts: alertInstances } = await getAlertSummary(createdRule.id); + expect(alertInstances['alert-id'].tracked).to.eql(false); + }); + }); + + it('should not untrack disable rule if untrack switch if false', async () => { + const { body: createdRule } = await supertest + .post(`/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.always-firing', + schedule: { interval: '24h' }, + params: { + instances: [{ id: 'alert-id' }], + }, + }) + ) + .expect(200); + + objectRemover.add(createdRule.id, 'alert', 'alerts'); + + await retry.try(async () => { + const { alerts: alertInstances } = await getAlertSummary(createdRule.id); + expect(Object.keys(alertInstances).length).to.eql(1); + expect(alertInstances['alert-id'].tracked).to.eql(true); + }); + + await refreshAlertsList(); + await pageObjects.triggersActionsUI.searchAlerts(createdRule.name); + + await testSubjects.click('collapsedItemActions'); + + await testSubjects.click('disableButton'); + + await testSubjects.click('confirmModalConfirmButton'); + + await header.waitUntilLoadingHasFinished(); + + await retry.try(async () => { + const { alerts: alertInstances } = await getAlertSummary(createdRule.id); + expect(alertInstances['alert-id'].tracked).to.eql(true); + }); + }); + it('should re-enable single alert', async () => { const createdAlert = await createAlert({ supertest, objectRemover, }); - await disableAlert({ supertest, alertId: createdAlert.id }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); @@ -212,8 +304,25 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('disableButton'); - await refreshAlertsList(); - await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await testSubjects.click('confirmModalConfirmButton'); + + await header.waitUntilLoadingHasFinished(); + + await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied( + createdAlert.name, + 'statusDropdown', + 'disabled' + ); + + await testSubjects.click('collapsedItemActions'); + + await retry.waitForWithTimeout('disable button to show up', 30000, async () => { + return await testSubjects.isDisplayed('disableButton'); + }); + + await testSubjects.click('disableButton'); + + await header.waitUntilLoadingHasFinished(); await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied( createdAlert.name, @@ -255,6 +364,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('bulkAction'); await testSubjects.click('bulkDisable'); + await testSubjects.click('confirmModalConfirmButton'); + + await header.waitUntilLoadingHasFinished(); + await retry.try(async () => { const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql('Disabled 1 rule'); diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/create_llm_proxy.ts b/x-pack/test/observability_ai_assistant_api_integration/common/create_llm_proxy.ts index 3aaaf982c3597..aead4e6276c56 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/common/create_llm_proxy.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/common/create_llm_proxy.ts @@ -65,7 +65,8 @@ export class LlmProxy { } } - throw new Error('No interceptors found to handle request'); + response.writeHead(500, 'No interceptors found to handle request: ' + request.url); + response.end(); }) .listen(port); } @@ -111,7 +112,7 @@ export class LlmProxy { }), next: (msg) => { const chunk = createOpenAiChunk(msg); - return write(`data: ${JSON.stringify(chunk)}\n`); + return write(`data: ${JSON.stringify(chunk)}\n\n`); }, rawWrite: (chunk: string) => { return write(chunk); @@ -120,11 +121,11 @@ export class LlmProxy { await end(); }, complete: async () => { - await write('data: [DONE]'); + await write('data: [DONE]\n\n'); await end(); }, error: async (error) => { - await write(`data: ${JSON.stringify({ error })}`); + await write(`data: ${JSON.stringify({ error })}\n\n`); await end(); }, }; diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts index c56af40a6ab29..82ad5b6dd1224 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts @@ -104,7 +104,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const chunk = JSON.stringify(createOpenAiChunk('Hello')); await simulator.rawWrite(`data: ${chunk.substring(0, 10)}`); - await simulator.rawWrite(`${chunk.substring(10)}\n`); + await simulator.rawWrite(`${chunk.substring(10)}\n\n`); await simulator.complete(); await new Promise((resolve) => passThrough.on('end', () => resolve())); @@ -146,15 +146,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { const titleInterceptor = proxy.intercept( 'title', (body) => - (JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming).messages - .length === 1 + ( + JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming + ).functions?.find((fn) => fn.name === 'title_conversation') !== undefined ); const conversationInterceptor = proxy.intercept( 'conversation', (body) => - (JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming).messages - .length !== 1 + ( + JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming + ).functions?.find((fn) => fn.name === 'title_conversation') === undefined ); const responsePromise = new Promise((resolve, reject) => { diff --git a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts index ce9fc050b5e09..c0b2b36dfc029 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts @@ -148,15 +148,17 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte const titleInterceptor = proxy.intercept( 'title', (body) => - (JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming) - .messages.length === 1 + ( + JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming + ).functions?.find((fn) => fn.name === 'title_conversation') !== undefined ); const conversationInterceptor = proxy.intercept( 'conversation', (body) => - (JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming) - .messages.length !== 1 + ( + JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming + ).functions?.find((fn) => fn.name === 'title_conversation') === undefined ); await testSubjects.setValue(ui.pages.conversations.chatInput, 'hello'); diff --git a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts index e9b87e203fad8..ca1a3f5c622df 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts @@ -270,6 +270,10 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await testSubjects.existOrFail('rulesList'); await observability.alerts.rulesPage.clickRuleStatusDropDownMenu(); await observability.alerts.rulesPage.clickDisableFromDropDownMenu(); + + await testSubjects.click('confirmModalConfirmButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.waitFor('The rule to be disabled', async () => { const tableRows = await find.allByCssSelector('.euiTableRow'); const rows = await getRulesList(tableRows); 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_alert_suppression.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_alert_suppression.ts index 9d66f49c8558b..3f53e6d8a7100 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_alert_suppression.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_alert_suppression.ts @@ -1126,6 +1126,133 @@ export default ({ getService }: FtrProviderContext) => { }); }); + // large number of documents gets processed in batches of 9,000 + // rule should correctly go through them and suppress + // that can be an issue when search results returning in desc order + // this test is added to verify suppression works fine for this cases + it('should suppress alerts on large number of documents, more than 9,000', async () => { + const id = uuidv4(); + const firstTimestamp = '2020-10-28T05:45:00.000Z'; + const secondTimestamp = '2020-10-28T06:10:00.000Z'; + + await eventsFiller({ + id, + count: 10000 * eventsCount, + timestamp: [firstTimestamp, secondTimestamp], + }); + await threatsFiller({ id, count: 10000 * threatsCount, timestamp: firstTimestamp }); + + await indexGeneratedSourceDocuments({ + docsCount: 60000, + interval: [firstTimestamp, '2020-10-28T05:35:50.000Z'], + seed: (index, _, timestamp) => ({ + id, + '@timestamp': timestamp, + host: { + name: `host-${index}`, + }, + agent: { name: 'agent-a' }, + }), + }); + + await indexGeneratedSourceDocuments({ + docsCount: 60000, + interval: [secondTimestamp, '2020-10-28T06:20:50.000Z'], + seed: (index, _, timestamp) => ({ + id, + '@timestamp': timestamp, + host: { + name: `host-${index}`, + }, + agent: { name: 'agent-a' }, + }), + }); + + await addThreatDocuments({ + id, + timestamp: firstTimestamp, + fields: { + host: { + name: 'host-80', + }, + }, + count: 1, + }); + + await addThreatDocuments({ + id, + timestamp: firstTimestamp, + fields: { + host: { + name: 'host-14000', + }, + }, + count: 1, + }); + + await addThreatDocuments({ + id, + timestamp: firstTimestamp, + fields: { + host: { + name: 'host-36000', + }, + }, + count: 1, + }); + + await addThreatDocuments({ + id, + timestamp: firstTimestamp, + fields: { + host: { + name: 'host-5700', + }, + }, + count: 1, + }); + + const rule: ThreatMatchRuleCreateProps = { + ...indicatorMatchRule(id), + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'suppress', + duration: { + value: 300, + unit: 'm', + }, + }, + from: 'now-35m', + interval: '30m', + }; + + const { previewId } = await previewRule({ + supertest, + rule, + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + invocationCount: 2, + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + sort: ['agent.name', ALERT_ORIGINAL_TIME], + }); + expect(previewAlerts.length).toEqual(1); + expect(previewAlerts[0]._source).toEqual({ + ...previewAlerts[0]._source, + [ALERT_SUPPRESSION_TERMS]: [ + { + field: 'agent.name', + value: ['agent-a'], + }, + ], + // There 4 documents in threats index, each matches one document in source index on each of 2 rule executions + // In total it gives 8 potential alerts. With suppression enabled 1 is created, the rest 7 are suppressed + [ALERT_SUPPRESSION_DOCS_COUNT]: 7, + }); + }); + describe('rule execution only', () => { it('should suppress alerts during rule execution only', async () => { const id = uuidv4(); @@ -2064,6 +2191,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], [ALERT_SUPPRESSION_DOCS_COUNT]: 499, + [ALERT_SUPPRESSION_START]: '2020-10-28T06:50:00.000Z', }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/install_latest_bundled_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/install_latest_bundled_prebuilt_rules.ts index 32eb1d3dbdf01..dcb2561b1f3e8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/install_latest_bundled_prebuilt_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/install_latest_bundled_prebuilt_rules.ts @@ -61,7 +61,8 @@ export default ({ getService }: FtrProviderContext): void => { es, supertest, '99.0.0', - retry + retry, + log ); // As opposed to "registry" diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/prerelease_packages.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/prerelease_packages.ts index dcafdf8eaf1a7..3ed663f7ecc66 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/prerelease_packages.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/bundled_prebuilt_rules_package/trial_license_complete_tier/prerelease_packages.ts @@ -49,7 +49,8 @@ export default ({ getService }: FtrProviderContext): void => { const fleetPackageInstallationResponse = await installPrebuiltRulesPackageViaFleetAPI( es, supertest, - retry + retry, + log ); expect(fleetPackageInstallationResponse.items.length).toBe(1); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/fleet_integration.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/fleet_integration.ts index 1233af5c33f6a..1a8394a3b5144 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/fleet_integration.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/fleet_integration.ts @@ -49,6 +49,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest, overrideExistingPackage: true, retryService: retry, + log, }); // Verify that status is updated after package installation diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts index 11577bec1b5c7..ffba2bd01d988 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts @@ -107,7 +107,8 @@ export default ({ getService }: FtrProviderContext): void => { es, supertest, previousVersion, - retry + retry, + log ); expect(installPreviousPackageResponse._meta.install_source).toBe('registry'); @@ -160,7 +161,8 @@ export default ({ getService }: FtrProviderContext): void => { es, supertest, currentVersion, - retry + retry, + log ); expect(installLatestPackageResponse.items.length).toBeGreaterThanOrEqual(0); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry.ts index dafd16aaa9f5f..3007448ed895f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry.ts @@ -6,7 +6,7 @@ */ import { RetryService } from '@kbn/ftr-common-functional-services'; - +import type { ToolingLog } from '@kbn/tooling-log'; /** * Retry wrapper for async supertests, with a maximum number of retries. * You can pass in a function that executes a supertest test, and make assertions @@ -44,15 +44,19 @@ import { RetryService } from '@kbn/ftr-common-functional-services'; export const retry = async ({ test, retryService, + utilityName, retries = 2, timeout = 30000, retryDelay = 200, + log, }: { test: () => Promise; + utilityName: string; retryService: RetryService; retries?: number; timeout?: number; retryDelay?: number; + log: ToolingLog; }): Promise => { let retryAttempt = 0; const response = await retryService.tryForTime( @@ -61,12 +65,23 @@ export const retry = async ({ if (retryAttempt > retries) { // Log error message if we reached the maximum number of retries // but don't throw an error, return it to break the retry loop. - return new Error('Reached maximum number of retries for test.'); + const errorMessage = `Reached maximum number of retries for test: ${ + retryAttempt - 1 + }/${retries}`; + log?.error(errorMessage); + return new Error(JSON.stringify(errorMessage)); } retryAttempt = retryAttempt + 1; - return test(); + // Catch the error thrown by the test and log it, then throw it again + // to cause `tryForTime` to retry. + try { + return await test(); + } catch (error) { + log.error(`Retrying ${utilityName}: ${error}`); + throw error; + } }, undefined, retryDelay diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package_by_url.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package_by_url.ts index 988d73660d0ee..2839795ab1976 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package_by_url.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_fleet_package_by_url.ts @@ -9,6 +9,7 @@ import type SuperTest from 'supertest'; import { InstallPackageResponse } from '@kbn/fleet-plugin/common/types'; import { epmRouteService } from '@kbn/fleet-plugin/common'; import { RetryService } from '@kbn/ftr-common-functional-services'; +import type { ToolingLog } from '@kbn/tooling-log'; import expect from 'expect'; import { retry } from '../../retry'; import { refreshSavedObjectIndices } from '../../refresh_index'; @@ -28,7 +29,8 @@ const ATTEMPT_TIMEOUT = 120000; export const installPrebuiltRulesPackageViaFleetAPI = async ( es: Client, supertest: SuperTest.SuperTest, - retryService: RetryService + retryService: RetryService, + log: ToolingLog ): Promise => { const fleetResponse = await retry({ test: async () => { @@ -44,9 +46,11 @@ export const installPrebuiltRulesPackageViaFleetAPI = async ( return testResponse.body; }, + utilityName: installPrebuiltRulesPackageViaFleetAPI.name, retryService, retries: MAX_RETRIES, timeout: ATTEMPT_TIMEOUT, + log, }); await refreshSavedObjectIndices(es); @@ -67,7 +71,8 @@ export const installPrebuiltRulesPackageByVersion = async ( es: Client, supertest: SuperTest.SuperTest, version: string, - retryService: RetryService + retryService: RetryService, + log: ToolingLog ): Promise => { const fleetResponse = await retry({ test: async () => { @@ -83,9 +88,11 @@ export const installPrebuiltRulesPackageByVersion = async ( return testResponse.body; }, + utilityName: installPrebuiltRulesPackageByVersion.name, retryService, retries: MAX_RETRIES, timeout: ATTEMPT_TIMEOUT, + log, }); await refreshSavedObjectIndices(es); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_prebuilt_rules_fleet_package.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_prebuilt_rules_fleet_package.ts index 592406e8c3398..770d966f50a59 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_prebuilt_rules_fleet_package.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_prebuilt_rules_fleet_package.ts @@ -15,6 +15,7 @@ import { InstallPackageResponse } from '@kbn/fleet-plugin/common/types'; import type SuperTest from 'supertest'; import { RetryService } from '@kbn/ftr-common-functional-services'; import expect from 'expect'; +import { ToolingLog } from '@kbn/tooling-log'; import { retry } from '../../retry'; import { refreshSavedObjectIndices } from '../../refresh_index'; @@ -35,12 +36,14 @@ export const installPrebuiltRulesFleetPackage = async ({ version, overrideExistingPackage, retryService, + log, }: { es: Client; supertest: SuperTest.SuperTest; version?: string; overrideExistingPackage: boolean; retryService: RetryService; + log: ToolingLog; }): Promise => { if (version) { // Install a specific version @@ -59,8 +62,10 @@ export const installPrebuiltRulesFleetPackage = async ({ return testResponse.body; }, retryService, + utilityName: installPrebuiltRulesFleetPackage.name, retries: MAX_RETRIES, timeout: ATTEMPT_TIMEOUT, + log, }); await refreshSavedObjectIndices(es); @@ -91,8 +96,10 @@ export const installPrebuiltRulesFleetPackage = async ({ return body; }, retryService, + utilityName: installPrebuiltRulesFleetPackage.name, retries: MAX_RETRIES, timeout: ATTEMPT_TIMEOUT, + log, }); await refreshSavedObjectIndices(es); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts index acb38b4dcefff..a080c4494833f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts @@ -63,8 +63,8 @@ describe('Enrichment', { tags: ['@ess', '@serverless'] }, () => { }); it('Should has enrichment fields from legacy risk', function () { - cy.get(HOST_RISK_HEADER_COLUMN).contains('host.risk.calculated_level'); - cy.get(USER_RISK_HEADER_COLUMN).contains('user.risk.calculated_level'); + cy.get(HOST_RISK_HEADER_COLUMN).contains('Host Risk Level'); + cy.get(USER_RISK_HEADER_COLUMN).contains('User Risk Level'); scrollAlertTableColumnIntoView(HOST_RISK_COLUMN); cy.get(HOST_RISK_COLUMN).contains('Low'); scrollAlertTableColumnIntoView(USER_RISK_COLUMN); @@ -103,8 +103,8 @@ describe('Enrichment', { tags: ['@ess', '@serverless'] }, () => { }); it('Should has enrichment fields from legacy risk', function () { - cy.get(HOST_RISK_HEADER_COLUMN).contains('host.risk.calculated_level'); - cy.get(USER_RISK_HEADER_COLUMN).contains('user.risk.calculated_level'); + cy.get(HOST_RISK_HEADER_COLUMN).contains('Host Risk Level'); + cy.get(USER_RISK_HEADER_COLUMN).contains('User Risk Level'); scrollAlertTableColumnIntoView(HOST_RISK_COLUMN); cy.get(HOST_RISK_COLUMN).contains('Critical'); scrollAlertTableColumnIntoView(USER_RISK_COLUMN); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/new_entity_flyout.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/new_entity_flyout.cy.ts index ad0b8b9c06b64..5817d338627ff 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/new_entity_flyout.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/new_entity_flyout.cy.ts @@ -112,7 +112,7 @@ describe( 'Asset Criticality' ); - cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_BUTTON).should('have.text', 'Create'); + cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_BUTTON).should('have.text', 'Assign'); }); it('should display asset criticality modal', () => { @@ -193,7 +193,7 @@ describe( 'Asset Criticality' ); - cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_BUTTON).should('have.text', 'Create'); + cy.get(ENTITY_DETAILS_FLYOUT_ASSET_CRITICALITY_BUTTON).should('have.text', 'Assign'); }); it('should display asset criticality modal', () => { diff --git a/x-pack/test_serverless/README.md b/x-pack/test_serverless/README.md index f90f89a8b3b46..289366dbfc016 100644 --- a/x-pack/test_serverless/README.md +++ b/x-pack/test_serverless/README.md @@ -103,6 +103,29 @@ tests that should run in a serverless environment have to be added to the Tests in this area should be clearly designed for the serverless environment, particularly when it comes to timing for API requests and UI interaction. +### Roles-based testing + +Each serverless project has its own set of SAML roles with [specfic permissions defined in roles.yml](https://github.com/elastic/kibana/blob/main/packages/kbn-es/src/serverless_resources/project_roles) +and in oder to properly test Kibana functionality, UI tests design requires to login with +a project-supported SAML role. + +Some recommendations: +- in each test file top level `describe` suite should start with `loginWithRole` in `before` hook +- use the minimal required role to access tested functionality +- when feature logic depends on both project type & role, make sure to add separate tests +- avoid using basic authentication, unless it is the actual test case +- no need to log out, you can change role by calling `loginWithRole` again. + +``` +describe("my test suite", async function() { + before(async () => { + await PageObjects.svlCommonPage.loginWithRole('viewer'); + await esArchiver.load(...); + await PageObjects.dashboard.navigateToApp(); + }); +}); +``` + ### Testing with feature flags **tl;dr:** Tests specific to functionality behind a feature flag need special diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts index 0b94803bcd765..82fd6dd057ae5 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts @@ -23,6 +23,7 @@ export default function ({ getService }: FtrProviderContext) { let updateTemplate: typeof indexManagementService['templates']['api']['updateTemplate']; let deleteTemplates: typeof indexManagementService['templates']['api']['deleteTemplates']; let simulateTemplate: typeof indexManagementService['templates']['api']['simulateTemplate']; + let cleanUpTemplates: typeof indexManagementService['templates']['api']['cleanUpTemplates']; let getRandomString: () => string; describe('Index templates', function () { @@ -30,12 +31,22 @@ export default function ({ getService }: FtrProviderContext) { ({ templates: { helpers: { getTemplatePayload, catTemplate, getSerializedTemplate }, - api: { createTemplate, updateTemplate, deleteTemplates, simulateTemplate }, + api: { + createTemplate, + updateTemplate, + deleteTemplates, + simulateTemplate, + cleanUpTemplates, + }, }, } = indexManagementService); getRandomString = () => randomness.string({ casing: 'lower', alpha: true }); }); + after(async () => { + await cleanUpTemplates({ 'x-elastic-internal-origin': 'xxx' }); + }); + describe('get', () => { let templateName: string; @@ -93,6 +104,7 @@ export default function ({ getService }: FtrProviderContext) { 'hasMappings', '_kbnMeta', 'composedOf', + 'ignoreMissingComponentTemplates', ].sort(); expect(Object.keys(indexTemplateFound).sort()).to.eql(expectedKeys); @@ -113,6 +125,7 @@ export default function ({ getService }: FtrProviderContext) { 'template', '_kbnMeta', 'composedOf', + 'ignoreMissingComponentTemplates', ].sort(); expect(body.name).to.eql(templateName); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts index 4c8353487adce..da71d2ad4858a 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/authentication.ts @@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const svlCommonApi = getService('svlCommonApi'); const supertest = getService('supertest'); + const config = getService('config'); describe('security/authentication', function () { describe('route access', () => { @@ -144,8 +145,7 @@ export default function ({ getService }: FtrProviderContext) { metadata: {}, operator: true, roles: ['superuser'], - // We use `elastic` for MKI, and `elastic_serverless` for any other testing environment. - username: expect.stringContaining('elastic'), + username: config.get('servers.kibana.username'), }); expect(status).toBe(200); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/avg_pct_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/avg_pct_fired.ts index 32dd3e263998b..47ed06c1ad440 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/avg_pct_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/avg_pct_fired.ts @@ -191,7 +191,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open'); expect(resp.hits.hits[0]._source).property('event.kind', 'signal'); expect(resp.hits.hits[0]._source).property('event.action', 'open'); - + expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([0.5]); expect(resp.hits.hits[0]._source) .property('kibana.alert.rule.parameters') .eql({ diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts index 95d9608d0a841..8b6d76a380748 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/custom_eq_avg_bytes_fired.ts @@ -200,7 +200,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open'); expect(resp.hits.hits[0]._source).property('event.kind', 'signal'); expect(resp.hits.hits[0]._source).property('event.action', 'open'); - + expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([0.9]); expect(resp.hits.hits[0]._source) .property('kibana.alert.rule.parameters') .eql({ diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts index 519c0329e6390..15c34602e0a78 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts @@ -191,7 +191,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open'); expect(resp.hits.hits[0]._source).property('event.kind', 'signal'); expect(resp.hits.hits[0]._source).property('event.action', 'open'); - + expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([1, 2]); expect(resp.hits.hits[0]._source) .property('kibana.alert.rule.parameters') .eql({ diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts index ccd2aa6edaeaa..264057cacff1c 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/group_by_fired.ts @@ -217,7 +217,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.hits.hits[0]._source).property('container.id', 'container-0'); expect(resp.hits.hits[0]._source).property('container.name', 'container-name'); expect(resp.hits.hits[0]._source).not.property('container.cpu'); - + expect(resp.hits.hits[0]._source).property('kibana.alert.evaluation.threshold').eql([0.2]); expect(resp.hits.hits[0]._source) .property('kibana.alert.rule.parameters') .eql({ diff --git a/x-pack/test_serverless/functional/page_objects/svl_common_page.ts b/x-pack/test_serverless/functional/page_objects/svl_common_page.ts index 681f024b8f837..d8ba679128410 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_common_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_common_page.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function SvlCommonPageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const find = getService('find'); const config = getService('config'); const pageObjects = getPageObjects(['security', 'common']); const retry = getService('retry'); @@ -24,28 +25,56 @@ export function SvlCommonPageProvider({ getService, getPageObjects }: FtrProvide setTimeout(resolve, ms); }); + /** + * Delete browser cookies, clear session and local storages + */ + const cleanBrowserState = async () => { + // we need to load kibana host to delete/add cookie + const noAuthRequiredUrl = deployment.getHostPort() + '/bootstrap-anonymous.js'; + log.debug(`browser: navigate to /bootstrap-anonymous.js`); + await browser.get(noAuthRequiredUrl); + // previous test might left unsaved changes and alert will show up on url change + const alert = await browser.getAlert(); + if (alert) { + log.debug(`browser: closing alert`); + await alert.accept(); + } + log.debug(`browser: wait for resource page to be loaded`); + await find.byCssSelector('body > pre', 5000); + log.debug(`browser: delete all the cookies`); + await retry.waitForWithTimeout('Browser cookies are deleted', 10000, async () => { + await browser.deleteAllCookies(); + await pageObjects.common.sleep(1000); + const cookies = await browser.getCookies(); + return cookies.length === 0; + }); + log.debug(`browser: clearing session & local storages`); + await browser.clearSessionStorage(); + await browser.clearLocalStorage(); + await pageObjects.common.sleep(700); + }; + return { async loginWithRole(role: string) { + log.debug(`Fetch the cookie for '${role}' role`); + const sidCookie = await svlUserManager.getSessionCookieForRole(role); await retry.waitForWithTimeout( `Logging in by setting browser cookie for '${role}' role`, 30_000, async () => { - log.debug(`Delete all the cookies in the current browser context`); - await browser.deleteAllCookies(); - log.debug(`Setting the cookie for '${role}' role`); - const sidCookie = await svlUserManager.getSessionCookieForRole(role); - // Loading bootstrap.js in order to be on the domain that the cookie will be set for. - await browser.get(deployment.getHostPort() + '/bootstrap.js'); - await browser.setCookie('sid', sidCookie); + await cleanBrowserState(); + log.debug(`browser: set the new cookie`); + await retry.waitForWithTimeout('New cookie is added', 10000, async () => { + await browser.setCookie('sid', sidCookie); + await pageObjects.common.sleep(1000); + const cookies = await browser.getCookies(); + return cookies.length === 1; + }); // Cookie should be already set in the browsing context, navigating to the Home page + log.debug(`browser: refresh the page`); + await browser.refresh(); + log.debug(`browser: load base url and validate the cookie`); await browser.get(deployment.getHostPort()); - // Verifying that we are logged in - if (await testSubjects.exists('userMenuButton', { timeout: 10_000 })) { - log.debug('userMenuButton found, login passed'); - } else { - throw new Error(`Failed to login with cookie for '${role}' role`); - } - // Validating that the new cookie in the browser is set for the correct user const browserCookies = await browser.getCookies(); if (browserCookies.length === 0) { @@ -60,16 +89,31 @@ export function SvlCommonPageProvider({ getService, getPageObjects }: FtrProvide // email returned from API call must match the email for the specified role if (body.email === userData.email) { log.debug(`The new cookie is properly set for '${role}' role`); - return true; } else { + log.debug(`API response body: ${JSON.stringify(body)}`); throw new Error( `Cookie is not set properly, expected email is '${userData.email}', but found '${body.email}'` ); } + // Verifying that we are logged in + if (await testSubjects.exists('userMenuButton', { timeout: 10_000 })) { + log.debug('userMenuButton found, login passed'); + return true; + } else { + throw new Error(`Failed to login with cookie for '${role}' role`); + } } ); }, + async loginAsAdmin() { + await this.loginWithRole('admin'); + }, + + async loginWithPrivilegedRole() { + await this.loginWithRole(svlUserManager.DEFAULT_ROLE); + }, + async navigateToLoginForm() { const url = deployment.getHostPort() + '/login'; await browser.get(url); @@ -79,8 +123,38 @@ export function SvlCommonPageProvider({ getService, getPageObjects }: FtrProvide }); }, + async forceLogout() { + log.debug('SvlCommonPage.forceLogout'); + if (await find.existsByDisplayedByCssSelector('.login-form', 100)) { + log.debug('Already on the login page, not forcing anything'); + return; + } + + await cleanBrowserState(); + + log.debug(`Navigating to ${deployment.getHostPort()}/logout to force the logout`); + await browser.get(deployment.getHostPort() + '/logout'); + + // After logging out, the user can be redirected to various locations depending on the context. By default, we + // expect the user to be redirected to the login page. However, if the login page is not available for some reason, + // we should simply wait until the user is redirected *elsewhere*. + // Timeout has been doubled here in attempt to quiet the flakiness + await retry.waitForWithTimeout('URL redirects to finish', 40000, async () => { + const urlBefore = await browser.getCurrentUrl(); + delay(1000); + const urlAfter = await browser.getCurrentUrl(); + log.debug(`Expecting before URL '${urlBefore}' to equal after URL '${urlAfter}'`); + return urlAfter === urlBefore; + }); + + const currentUrl = await browser.getCurrentUrl(); + + // Logout might trigger multiple redirects, but in the end we expect the Cloud login page + return currentUrl.includes('/login') || currentUrl.includes('/projects'); + }, + async login() { - await pageObjects.security.forceLogout({ waitForLoginPage: false }); + await this.forceLogout(); // adding sleep to settle down logout await pageObjects.common.sleep(2500); @@ -135,11 +209,6 @@ export function SvlCommonPageProvider({ getService, getPageObjects }: FtrProvide log.debug('Logged in successfully'); }, - async forceLogout() { - await pageObjects.security.forceLogout({ waitForLoginPage: false }); - log.debug('Logged out successfully'); - }, - async assertProjectHeaderExists() { await testSubjects.existOrFail('kibanaProjectHeader'); }, diff --git a/x-pack/test_serverless/functional/test_suites/common/console/console.ts b/x-pack/test_serverless/functional/test_suites/common/console/console.ts index d754abdf340bb..8cfa93e8e8572 100644 --- a/x-pack/test_serverless/functional/test_suites/common/console/console.ts +++ b/x-pack/test_serverless/functional/test_suites/common/console/console.ts @@ -26,9 +26,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['svlCommonPage', 'common', 'console', 'header']); describe('console app', function describeIndexTests() { - this.tags('includeFirefox'); before(async () => { - await PageObjects.svlCommonPage.login(); + // TODO: https://github.com/elastic/kibana/issues/176582 + // this test scenario requires roles definition check: + // Search & Oblt projects 'viewer' role has access to Console, but for + // Security project only 'admin' role has access + await PageObjects.svlCommonPage.loginWithRole('admin'); log.debug('navigateTo console'); await PageObjects.common.navigateToApp('dev_tools', { hash: '/console' }); }); @@ -37,10 +40,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.console.closeHelpIfExists(); }); - after(async () => { - await PageObjects.svlCommonPage.forceLogout(); - }); - it('should show the default request', async () => { await retry.try(async () => { const actualRequest = await PageObjects.console.getRequest(); diff --git a/x-pack/test_serverless/functional/test_suites/common/context/_context_navigation.ts b/x-pack/test_serverless/functional/test_suites/common/context/_context_navigation.ts index f23ebd55f1072..8da97f7e39f4b 100644 --- a/x-pack/test_serverless/functional/test_suites/common/context/_context_navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/common/context/_context_navigation.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataGrid = getService('dataGrid'); const PageObjects = getPageObjects([ 'common', + 'svlCommonPage', 'header', 'context', 'discover', @@ -47,6 +48,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.update({ defaultIndex: 'logstash-*', }); + // TODO: Serverless tests require login first + await PageObjects.svlCommonPage.loginWithRole('viewer'); await PageObjects.common.navigateToApp('discover'); await PageObjects.header.waitUntilLoadingHasFinished(); for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { diff --git a/x-pack/test_serverless/functional/test_suites/common/context/_discover_navigation.ts b/x-pack/test_serverless/functional/test_suites/common/context/_discover_navigation.ts index 296a8245f9d1d..d53759ddc4e29 100644 --- a/x-pack/test_serverless/functional/test_suites/common/context/_discover_navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/common/context/_discover_navigation.ts @@ -20,6 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const PageObjects = getPageObjects([ 'common', + 'svlCommonPage', 'discover', 'timePicker', 'settings', @@ -40,6 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.update({ defaultIndex: 'logstash-*', }); + await PageObjects.svlCommonPage.loginWithPrivilegedRole(); await PageObjects.common.navigateToApp('discover'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test_serverless/functional/test_suites/common/context/_filters.ts b/x-pack/test_serverless/functional/test_suites/common/context/_filters.ts index c27998a658899..47f864787e6c5 100644 --- a/x-pack/test_serverless/functional/test_suites/common/context/_filters.ts +++ b/x-pack/test_serverless/functional/test_suites/common/context/_filters.ts @@ -21,7 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common', 'context']); + const PageObjects = getPageObjects(['common', 'context', 'svlCommonPage']); const testSubjects = getService('testSubjects'); describe('context filters', function contextSize() { @@ -29,6 +29,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.update({ 'discover:rowHeightOption': 0, // to have more grid rows visible at once }); + await PageObjects.svlCommonPage.loginWithRole('viewer'); + await PageObjects.common.navigateToApp('discover'); }); beforeEach(async function () { diff --git a/x-pack/test_serverless/functional/test_suites/common/context/_size.ts b/x-pack/test_serverless/functional/test_suites/common/context/_size.ts index 2daebf6ed0f82..1592e183a9faf 100644 --- a/x-pack/test_serverless/functional/test_suites/common/context/_size.ts +++ b/x-pack/test_serverless/functional/test_suites/common/context/_size.ts @@ -19,7 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataGrid = getService('dataGrid'); const browser = getService('browser'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['context']); + const PageObjects = getPageObjects(['context', 'svlCommonPage']); let expectedRowLength = 2 * TEST_DEFAULT_CONTEXT_SIZE + 1; describe('context size', function contextSize() { @@ -29,6 +29,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, 'context:step': `${TEST_STEP_SIZE}`, }); + await PageObjects.svlCommonPage.loginWithRole('viewer'); await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/context/index.ts b/x-pack/test_serverless/functional/test_suites/common/context/index.ts index 9ed486999e9f6..5d619b1824031 100644 --- a/x-pack/test_serverless/functional/test_suites/common/context/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/context/index.ts @@ -7,27 +7,17 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ - getService, - getPageObjects, - loadTestFile, - getPageObject, -}: FtrProviderContext) { +export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['common']); const kibanaServer = getService('kibanaServer'); - const svlCommonPage = getPageObject('svlCommonPage'); describe('context app', function () { before(async () => { await browser.setWindowSize(1200, 800); - // TODO: Serverless tests require login first - await svlCommonPage.login(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); - await PageObjects.common.navigateToApp('discover'); }); after(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts b/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts index 1747d6a517c5f..6a908ce4e0fe8 100644 --- a/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts +++ b/x-pack/test_serverless/functional/test_suites/common/dev_tools/search_profiler.ts @@ -21,15 +21,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Search Profiler Editor', () => { before(async () => { - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); await PageObjects.common.navigateToApp('searchProfiler'); expect(await PageObjects.searchProfiler.editorExists()).to.be(true); }); - after(async () => { - await PageObjects.svlCommonPage.forceLogout(); - }); - it('supports pre-configured search query', async () => { const query = { query: { diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/_saved_search_embeddable.ts b/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/_saved_search_embeddable.ts index 5b95dace01a6d..726246776f2ec 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/_saved_search_embeddable.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/_saved_search_embeddable.ts @@ -19,10 +19,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'timePicker', 'discover']); + const PageObjects = getPageObjects([ + 'common', + 'svlCommonPage', + 'dashboard', + 'header', + 'timePicker', + 'discover', + ]); describe('discover saved search embeddable', () => { before(async () => { + await browser.setWindowSize(1300, 800); + await PageObjects.svlCommonPage.loginWithPrivilegedRole(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); await kibanaServer.savedObjects.cleanStandardList(); @@ -39,6 +48,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.unload('test/functional/fixtures/es_archiver/dashboard/current/data'); await kibanaServer.savedObjects.cleanStandardList(); await PageObjects.common.unsetTime(); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/index.ts b/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/index.ts index c27f827e74ff2..805112976c653 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/index.ts @@ -7,22 +7,8 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; -export default function ({ getService, loadTestFile, getPageObject }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const browser = getService('browser'); - const svlCommonPage = getPageObject('svlCommonPage'); - +export default function ({ loadTestFile }: FtrProviderContext) { describe('discover/embeddable', function () { - before(async function () { - await browser.setWindowSize(1300, 800); - // TODO: Serverless tests require login first - await svlCommonPage.login(); - }); - - after(async function unloadMakelogs() { - await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); - }); - loadTestFile(require.resolve('./_saved_search_embeddable')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover.ts index e301266dcd168..1b5b3c8f6ff52 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover.ts @@ -20,6 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects([ 'common', + 'svlCommonPage', 'discover', 'header', 'timePicker', @@ -37,6 +38,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // and load a set of makelogs data await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.svlCommonPage.loginWithPrivilegedRole(); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts index cf581ad3edb51..0d7825caffe66 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_discover_histogram.ts @@ -17,10 +17,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ 'timePicker', 'dashboard', - 'settings', 'discover', 'common', 'header', + 'svlCommonPage', ]); const defaultSettings = { defaultIndex: 'long-window-logstash-*', @@ -42,6 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await security.testUser.setRoles(['kibana_admin', 'long_window_logstash']); await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.svlCommonPage.loginAsAdmin(); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_url_state.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_url_state.ts index 0f8c9ffd54744..242dfedde74ef 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group1/_url_state.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group1/_url_state.ts @@ -19,11 +19,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects([ 'common', + 'svlCommonPage', 'discover', 'header', 'timePicker', - 'unifiedFieldList', - 'visualize', 'svlCommonNavigation', ]); @@ -38,6 +37,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // and load a set of makelogs data await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.svlCommonPage.loginWithRole('viewer'); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group1/index.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group1/index.ts index 0365d037e8f32..4ad60320df38b 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group1/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group1/index.ts @@ -7,16 +7,13 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; -export default function ({ getService, loadTestFile, getPageObject }: FtrProviderContext) { +export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const svlCommonPage = getPageObject('svlCommonPage'); describe('discover/group1', function () { before(async function () { await browser.setWindowSize(1300, 800); - // TODO: Serverless tests require login first - await svlCommonPage.login(); }); after(async function unloadMakelogs() { diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group2/_adhoc_data_views.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group2/_adhoc_data_views.ts index 03dd58892e5cf..b27001a396bb6 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group2/_adhoc_data_views.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group2/_adhoc_data_views.ts @@ -22,10 +22,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const PageObjects = getPageObjects([ 'common', - 'unifiedSearch', + 'svlCommonPage', 'discover', 'timePicker', - 'settings', 'header', 'context', 'dashboard', @@ -45,7 +44,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - + await PageObjects.svlCommonPage.loginWithPrivilegedRole(); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group2/_data_grid_doc_navigation.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group2/_data_grid_doc_navigation.ts index d690efea7693b..6fc07fe38cbef 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group2/_data_grid_doc_navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group2/_data_grid_doc_navigation.ts @@ -12,7 +12,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const dataGrid = getService('dataGrid'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'context']); + const PageObjects = getPageObjects(['common', 'svlCommonPage', 'discover', 'timePicker']); const esArchiver = getService('esArchiver'); const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await PageObjects.svlCommonPage.loginWithRole('viewer'); }); after(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group2/_data_grid_doc_table.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group2/_data_grid_doc_table.ts index fdb0660727129..2ca4c5f856937 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group2/_data_grid_doc_table.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group2/_data_grid_doc_table.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardAddPanel = getService('dashboardAddPanel'); const PageObjects = getPageObjects([ 'common', + 'svlCommonPage', 'discover', 'header', 'timePicker', @@ -41,6 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace(defaultSettings); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.svlCommonPage.loginWithPrivilegedRole(); }); beforeEach(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group2/index.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group2/index.ts index 72243eaa24047..c579eca3bb7bd 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group2/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group2/index.ts @@ -7,16 +7,13 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; -export default function ({ getService, loadTestFile, getPageObject }: FtrProviderContext) { +export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const svlCommonPage = getPageObject('svlCommonPage'); describe('discover/group2', function () { before(async function () { await browser.setWindowSize(1600, 1200); - // TODO: Serverless tests require login first - await svlCommonPage.login(); }); after(async function unloadMakelogs() { diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts index 8e373bae57ad6..e69dcb361722d 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_request_counts.ts @@ -13,11 +13,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects([ 'common', + 'svlCommonPage', 'discover', 'timePicker', 'header', - 'unifiedSearch', - 'settings', ]); const testSubjects = getService('testSubjects'); const browser = getService('browser'); @@ -27,6 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('discover request counts', function describeIndexTests() { before(async function () { + await PageObjects.svlCommonPage.loginAsAdmin(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts index 0504049f89ed5..270abef04517e 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_sidebar.ts @@ -13,10 +13,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects([ 'common', + 'svlCommonPage', 'discover', 'timePicker', 'header', - 'unifiedSearch', 'unifiedFieldList', ]); const testSubjects = getService('testSubjects'); @@ -31,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('discover sidebar', function describeIndexTests() { before(async function () { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await PageObjects.svlCommonPage.loginAsAdmin(); }); beforeEach(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_unsaved_changes_badge.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_unsaved_changes_badge.ts index 1f6f89a7bb33b..45fcd18d6cb26 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group3/_unsaved_changes_badge.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group3/_unsaved_changes_badge.ts @@ -18,12 +18,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataGrid = getService('dataGrid'); const filterBar = getService('filterBar'); const PageObjects = getPageObjects([ - 'settings', 'common', + 'svlCommonPage', 'discover', 'header', 'timePicker', - 'dashboard', 'unifiedFieldList', ]); const security = getService('security'); @@ -37,6 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await PageObjects.svlCommonPage.loginWithPrivilegedRole(); }); after(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/group3/index.ts b/x-pack/test_serverless/functional/test_suites/common/discover/group3/index.ts index 75b2e6c9cd252..9f322013d986b 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/group3/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/group3/index.ts @@ -7,16 +7,13 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; -export default function ({ getService, loadTestFile, getPageObject }: FtrProviderContext) { +export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const svlCommonPage = getPageObject('svlCommonPage'); describe('discover/group3', function () { before(async function () { await browser.setWindowSize(1300, 800); - // TODO: Serverless tests require login first - await svlCommonPage.login(); }); after(async function unloadMakelogs() { diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/index.ts b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/index.ts index 96fad2c47d099..6d717fa082850 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/index.ts @@ -7,15 +7,8 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; -export default function ({ loadTestFile, getPageObject }: FtrProviderContext) { - const svlCommonPage = getPageObject('svlCommonPage'); - +export default function ({ loadTestFile }: FtrProviderContext) { describe('discover', function () { - before(async function () { - // TODO: Serverless tests require login first - await svlCommonPage.login(); - }); - loadTestFile(require.resolve('./reporting')); loadTestFile(require.resolve('./visualize_field')); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts index e2a491de828d9..620bf876cb9a9 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/reporting.ts @@ -18,7 +18,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); const retry = getService('retry'); - const PageObjects = getPageObjects(['reporting', 'common', 'discover', 'timePicker', 'share']); + const PageObjects = getPageObjects([ + 'reporting', + 'common', + 'svlCommonPage', + 'discover', + 'timePicker', + 'share', + ]); const filterBar = getService('filterBar'); const find = getService('find'); const testSubjects = getService('testSubjects'); @@ -56,6 +63,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Discover CSV Export', () => { describe('Check Available', () => { before(async () => { + await PageObjects.svlCommonPage.loginAsAdmin(); // TODO: emptyKibanaIndex fails in Serverless with // "index_not_found_exception: no such index [.kibana_ingest]", // so it was switched to `savedObjects.cleanStandardList()` diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/visualize_field.ts b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/visualize_field.ts index e412fea58df57..6027f3baed69d 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/visualize_field.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/x_pack/visualize_field.ts @@ -18,13 +18,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects([ 'common', - 'error', + 'svlCommonPage', 'discover', 'timePicker', - 'unifiedSearch', 'lens', - 'security', - 'spaceSelector', 'header', 'unifiedFieldList', ]); @@ -36,11 +33,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { } describe('discover field visualize button', () => { - beforeEach(async () => { + before(async () => { + // Security project requires admin role, search/oblt project passes with developer/editor. + await PageObjects.svlCommonPage.loginAsAdmin(); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.load( 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' ); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + }); + }); + + beforeEach(async () => { await PageObjects.common.navigateToApp('discover'); await setDiscoverTimeRange(); }); @@ -50,6 +55,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' ); + await kibanaServer.uiSettings.replace({}); }); it('shows "visualize" field button', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/index.ts b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/index.ts index 253effe65b460..f4f8c8c550cb2 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/index.ts @@ -6,15 +6,8 @@ */ import { FtrProviderContext } from '../../../../ftr_provider_context'; -export default ({ loadTestFile, getPageObject }: FtrProviderContext) => { - const svlCommonPage = getPageObject('svlCommonPage'); - +export default ({ loadTestFile }: FtrProviderContext) => { describe('Discover alerting', function () { - before(async function () { - // TODO: Serverless tests require login first - await svlCommonPage.login(); - }); - loadTestFile(require.resolve('./search_source_alert')); }); }; diff --git a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts index d2e8f863d5fe9..25ae3eb08af48 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const monacoEditor = getService('monacoEditor'); const PageObjects = getPageObjects([ 'settings', + 'svlCommonPage', 'common', 'header', 'discover', @@ -343,6 +344,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Search source Alert', () => { before(async () => { await security.testUser.setRoles(['discover_alert']); + await PageObjects.svlCommonPage.loginAsAdmin(); log.debug('create source indices'); await createSourceIndex(); diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example/index.ts b/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example/index.ts index f8f96c0bbdd20..ded70c1c77f29 100644 --- a/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/examples/data_view_field_editor_example/index.ts @@ -25,8 +25,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid describe('data view field editor example', function () { before(async () => { - // TODO: Serverless tests require login first - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); // TODO: emptyKibanaIndex fails in Serverless with // "index_not_found_exception: no such index [.kibana_ingest]", // so it was switched to `savedObjects.cleanStandardList()` diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/customizations.ts b/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/customizations.ts index d653c6f8505ba..e538ead1ce64b 100644 --- a/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/customizations.ts +++ b/x-pack/test_serverless/functional/test_suites/common/examples/discover_customization_examples/customizations.ts @@ -22,8 +22,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { describe('Customizations', () => { before(async () => { - // TODO: Serverless tests require login first - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/field_formats/index.ts b/x-pack/test_serverless/functional/test_suites/common/examples/field_formats/index.ts index 9da271eee5941..d6df9ec7f07c8 100644 --- a/x-pack/test_serverless/functional/test_suites/common/examples/field_formats/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/examples/field_formats/index.ts @@ -14,8 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Field formats example', function () { before(async () => { - // TODO: Serverless tests require login first - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); await PageObjects.common.navigateToApp('fieldFormatsExample'); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/partial_results/index.ts b/x-pack/test_serverless/functional/test_suites/common/examples/partial_results/index.ts index 88298145edeb3..c5a5bb2995217 100644 --- a/x-pack/test_serverless/functional/test_suites/common/examples/partial_results/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/examples/partial_results/index.ts @@ -15,8 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/167643 describe.skip('Partial Results Example', function () { before(async () => { - // TODO: Serverless tests require login first - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); await PageObjects.common.navigateToApp('partialResultsExample'); const element = await testSubjects.find('example-help'); diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/search/warnings.ts b/x-pack/test_serverless/functional/test_suites/common/examples/search/warnings.ts index 43ec250ff9967..43b6c6c086717 100644 --- a/x-pack/test_serverless/functional/test_suites/common/examples/search/warnings.ts +++ b/x-pack/test_serverless/functional/test_suites/common/examples/search/warnings.ts @@ -45,8 +45,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }; before(async () => { - // TODO: Serverless tests require login first - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); // create rollup data log.info(`loading ${testIndex} index...`); await esArchiver.loadIfNeeded(testArchive); diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/partial_results_example.ts b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/partial_results_example.ts index e4c86a4a64faf..6e8fbb465bb08 100644 --- a/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/partial_results_example.ts +++ b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/partial_results_example.ts @@ -15,15 +15,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Partial results example', () => { before(async () => { - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); await PageObjects.common.navigateToApp('searchExamples'); await testSubjects.click('/search'); }); - after(async () => { - await PageObjects.svlCommonPage.forceLogout(); - }); - it('should update a progress bar', async () => { await testSubjects.click('responseTab'); const progressBar = await testSubjects.find('progressBar'); diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/search_example.ts b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/search_example.ts index b62987007b1c8..a07e823b57972 100644 --- a/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/search_example.ts +++ b/x-pack/test_serverless/functional/test_suites/common/examples/search_examples/search_example.ts @@ -17,8 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Search example', () => { before(async () => { - // TODO: Serverless tests require login first - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); }); describe('with bfetch', () => { 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 1b3bf8a63caa2..4725b39bd9db1 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 @@ -65,7 +65,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await kibanaServer.importExport.load( 'test/api_integration/fixtures/kbn_archiver/index_patterns/constant_keyword.json' ); - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); await PageObjects.common.navigateToApp('unifiedFieldListExamples'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('combobox is ready', async () => { @@ -90,7 +90,6 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { ); await PageObjects.unifiedFieldList.cleanSidebarLocalStorage(); await kibanaServer.savedObjects.cleanStandardList(); - await PageObjects.svlCommonPage.forceLogout(); }); // FLAKY: https://github.com/elastic/kibana/issues/172781 diff --git a/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/field_stats.ts b/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/field_stats.ts index 47bbefa0dc8d6..a1c72e7c46182 100644 --- a/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/field_stats.ts +++ b/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples/field_stats.ts @@ -29,8 +29,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { describe('Field stats', () => { before(async () => { - // TODO: Serverless tests require login first - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); await kibanaServer.savedObjects.cleanStandardList(); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.load( diff --git a/x-pack/test_serverless/functional/test_suites/common/grok_debugger/grok_debugger.ts b/x-pack/test_serverless/functional/test_suites/common/grok_debugger/grok_debugger.ts index c3547fe790303..96f7c61cfb618 100644 --- a/x-pack/test_serverless/functional/test_suites/common/grok_debugger/grok_debugger.ts +++ b/x-pack/test_serverless/functional/test_suites/common/grok_debugger/grok_debugger.ts @@ -22,7 +22,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // fold. Otherwise it can't be clicked by the browser driver. await browser.setWindowSize(1600, 1000); await security.testUser.setRoles(['global_devtools_read', 'ingest_pipelines_user']); - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); await PageObjects.common.navigateToApp('dev_tools', { hash: '/grokdebugger' }); await retry.waitFor('Grok Debugger Header to be visible', async () => { return testSubjects.isDisplayed('grokDebuggerContainer'); @@ -30,7 +30,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); after(async () => { - await PageObjects.svlCommonPage.forceLogout(); await security.testUser.restoreDefaults(); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/home_page/home_page.ts b/x-pack/test_serverless/functional/test_suites/common/home_page/home_page.ts index 228a8b431ad32..cbf1bb5b933b9 100644 --- a/x-pack/test_serverless/functional/test_suites/common/home_page/home_page.ts +++ b/x-pack/test_serverless/functional/test_suites/common/home_page/home_page.ts @@ -13,11 +13,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { describe('home page', function () { before(async () => { - await svlCommonPage.login(); - }); - - after(async () => { - await svlCommonPage.forceLogout(); + await svlCommonPage.loginWithRole('viewer'); }); it('has project header', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/home_page/sample_data.ts b/x-pack/test_serverless/functional/test_suites/common/home_page/sample_data.ts index 803e0edeba789..4fd5a47f0b903 100644 --- a/x-pack/test_serverless/functional/test_suites/common/home_page/sample_data.ts +++ b/x-pack/test_serverless/functional/test_suites/common/home_page/sample_data.ts @@ -14,12 +14,11 @@ export default function ({ getPageObjects }: FtrProviderContext) { // Failing - should be fixed with https://github.com/elastic/kibana/pull/164052 describe.skip('Sample data in serverless', function () { before(async () => { - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginWithPrivilegedRole(); }); after(async () => { await pageObjects.home.removeSampleDataSet('ecommerce'); - await pageObjects.svlCommonPage.forceLogout(); }); it('Sample data loads', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/management/advanced_settings.ts b/x-pack/test_serverless/functional/test_suites/common/management/advanced_settings.ts index 3b07f05fa9191..55221b1451d2b 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/advanced_settings.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/advanced_settings.ts @@ -46,7 +46,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await kibanaServer.uiSettings.update({ 'csv:quoteValues': true, }); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('settings'); }); @@ -55,7 +55,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await kibanaServer.uiSettings.update({ 'csv:quoteValues': INITIAL_CSV_QUOTE_VALUES_SETTING_VALUE, }); - await pageObjects.svlCommonPage.forceLogout(); }); it('renders the page', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/management/data_views/_runtime_fields.ts b/x-pack/test_serverless/functional/test_suites/common/management/data_views/_runtime_fields.ts index b7bc99d9aecb1..5657b1aebfee5 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/data_views/_runtime_fields.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/data_views/_runtime_fields.ts @@ -18,8 +18,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Failing: See https://github.com/elastic/kibana/issues/173558 describe.skip('runtime fields', function () { - this.tags(['skipFirefox']); - before(async function () { await browser.setWindowSize(1200, 800); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); diff --git a/x-pack/test_serverless/functional/test_suites/common/management/data_views/_runtime_fields_composite.ts b/x-pack/test_serverless/functional/test_suites/common/management/data_views/_runtime_fields_composite.ts index a047a4f0e85a5..74bd6312807a0 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/data_views/_runtime_fields_composite.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/data_views/_runtime_fields_composite.ts @@ -17,8 +17,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); describe('runtime fields', function () { - this.tags(['skipFirefox']); - before(async function () { await browser.setWindowSize(1200, 800); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); diff --git a/x-pack/test_serverless/functional/test_suites/common/management/data_views/index.ts b/x-pack/test_serverless/functional/test_suites/common/management/data_views/index.ts index 69fec71b2ad22..dd33cb266d618 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/data_views/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/data_views/index.ts @@ -13,8 +13,7 @@ export default ({ getService, loadTestFile, getPageObject }: FtrProviderContext) const svlCommonPage = getPageObject('svlCommonPage'); before(async () => { - // TODO: Serverless tests require login first - await svlCommonPage.login(); + await svlCommonPage.loginAsAdmin(); await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/makelogs'); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/management/data_views/serverless.ts b/x-pack/test_serverless/functional/test_suites/common/management/data_views/serverless.ts index 896c95b6dc2bd..6e5831ee30beb 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/data_views/serverless.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/data_views/serverless.ts @@ -20,9 +20,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); describe('Serverless tests', function () { - this.beforeAll(async () => { - await PageObjects.svlCommonPage.login(); - }); describe('disables scripted fields', function () { let dataViewId = ''; diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/component_templates.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/component_templates.ts index e70470582b2b5..d72aadd9dbb1a 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/component_templates.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/component_templates.ts @@ -22,8 +22,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Index component templates', function () { before(async () => { await security.testUser.setRoles(['index_management_user']); - // Navigate to the index management page - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); }); beforeEach(async () => { @@ -33,10 +32,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.header.waitUntilLoadingHasFinished(); }); - after(async () => { - await pageObjects.svlCommonPage.forceLogout(); - }); - it('renders the component templates tab', async () => { const url = await browser.getCurrentUrl(); expect(url).to.contain(`/component_templates`); diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/create_enrich_policy.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/create_enrich_policy.ts index 60753a0ad9bd9..d5cd5e450019e 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/create_enrich_policy.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/create_enrich_policy.ts @@ -46,7 +46,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } log.debug('Navigating to the enrich policies tab'); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('indexManagement'); // Navigate to the enrich policies tab @@ -64,8 +64,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } catch (e) { log.debug('[Teardown error] Error deleting test policy'); throw e; - } finally { - await pageObjects.svlCommonPage.forceLogout(); } }); diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/data_streams.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/data_streams.ts index 4bd839e2c9ebb..4a92df7d90f39 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/data_streams.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/data_streams.ts @@ -21,7 +21,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Data Streams', function () { before(async () => { - await log.debug('Creating required data stream'); + log.debug('Creating required data stream'); try { await es.cluster.putComponentTemplate({ name: `${TEST_DS_NAME}_mapping`, @@ -56,8 +56,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } await security.testUser.setRoles(['index_management_user']); - // Navigate to the index management page - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('indexManagement'); // Navigate to the indices tab await pageObjects.indexManagement.changeTabs('data_streamsTab'); @@ -65,7 +64,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); after(async () => { - await log.debug('Cleaning up created data stream'); + log.debug('Cleaning up created data stream'); try { await es.indices.deleteDataStream({ name: TEST_DS_NAME }); diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/enrich_policies.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/enrich_policies.ts index 66819c1e6d233..5c8bd2f45fef9 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/enrich_policies.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/enrich_policies.ts @@ -53,7 +53,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } log.debug('Navigating to the enrich policies tab'); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('indexManagement'); // Navigate to the enrich policies tab diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_templates.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_templates.ts index 7d591ade32c3c..147ce1126ca33 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_templates.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_templates.ts @@ -23,7 +23,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Index Templates', function () { before(async () => { - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); }); beforeEach(async () => { @@ -42,8 +42,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { log.debug('[Setup error] Error creating test policy'); throw e; } - - await pageObjects.svlCommonPage.forceLogout(); }); it('renders the index templates tab', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/indices.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/indices.ts index efcddefda84e1..c0994de088513 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/indices.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/indices.ts @@ -16,8 +16,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Indices', function () { before(async () => { await security.testUser.setRoles(['index_management_user']); - // Navigate to the index management page - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('indexManagement'); // Navigate to the indices tab await pageObjects.indexManagement.changeTabs('indicesTab'); diff --git a/x-pack/test_serverless/functional/test_suites/common/management/ingest_pipelines.ts b/x-pack/test_serverless/functional/test_suites/common/management/ingest_pipelines.ts index c9a5b730cc00e..5a7c13c6a4b56 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/ingest_pipelines.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/ingest_pipelines.ts @@ -29,14 +29,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Ingest Pipelines', function () { this.tags('smoke'); before(async () => { - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); }); beforeEach(async () => { await pageObjects.common.navigateToApp('ingestPipelines'); }); - after(async () => { - await pageObjects.svlCommonPage.forceLogout(); - }); it('Loads the app', async () => { log.debug('Checking for section heading to say Ingest Pipelines.'); diff --git a/x-pack/test_serverless/functional/test_suites/common/management/landing_page.ts b/x-pack/test_serverless/functional/test_suites/common/management/landing_page.ts index e3271ec1009c2..b3c98e61e7d2a 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/landing_page.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/landing_page.ts @@ -17,15 +17,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Management landing page', function () { this.tags('smoke'); before(async () => { - // Navigate to the index management page - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('management'); }); - after(async () => { - await pageObjects.svlCommonPage.forceLogout(); - }); - it('renders the page', async () => { await retry.waitFor('page to be visible', async () => { return await testSubjects.exists('cards-navigation-page'); diff --git a/x-pack/test_serverless/functional/test_suites/common/management/transforms/search_bar_features.ts b/x-pack/test_serverless/functional/test_suites/common/management/transforms/search_bar_features.ts index 124fe461d5306..2add9bcc4476e 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/transforms/search_bar_features.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/transforms/search_bar_features.ts @@ -16,11 +16,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { describe('Search bar features', () => { before(async () => { - await PageObjects.svlCommonPage.login(); - }); - - after(async () => { - await PageObjects.svlCommonPage.forceLogout(); + await PageObjects.svlCommonPage.loginAsAdmin(); }); describe('list features', () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/management/transforms/transform_list.ts b/x-pack/test_serverless/functional/test_suites/common/management/transforms/transform_list.ts index 453db13c33c28..a7b0a83d56af2 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/transforms/transform_list.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/transforms/transform_list.ts @@ -17,7 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Transform List', function () { before(async () => { await security.testUser.setRoles(['transform_user']); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); // For this test to work, make sure there are no pre-existing transform present. // For example, solutions might set up transforms automatically. diff --git a/x-pack/test_serverless/functional/test_suites/common/painless_lab/painless_lab.ts b/x-pack/test_serverless/functional/test_suites/common/painless_lab/painless_lab.ts index e781808189100..2f385967149ee 100644 --- a/x-pack/test_serverless/functional/test_suites/common/painless_lab/painless_lab.ts +++ b/x-pack/test_serverless/functional/test_suites/common/painless_lab/painless_lab.ts @@ -27,17 +27,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Painless lab', function describeIndexTests() { before(async () => { - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); await PageObjects.common.navigateToApp('dev_tools', { hash: '/painless_lab' }); await retry.waitFor('Wait for editor to be visible', async () => { return testSubjects.isDisplayed('painless_lab'); }); }); - after(async () => { - await PageObjects.svlCommonPage.forceLogout(); - }); - it('should show the editor and preview panels', async () => { const editor = await testSubjects.find('kibanaCodeEditor'); const preview = await testSubjects.find('painlessTabs'); diff --git a/x-pack/test_serverless/functional/test_suites/common/platform_security/api_keys.ts b/x-pack/test_serverless/functional/test_suites/common/platform_security/api_keys.ts index 986e6d31c942e..265be2469ac90 100644 --- a/x-pack/test_serverless/functional/test_suites/common/platform_security/api_keys.ts +++ b/x-pack/test_serverless/functional/test_suites/common/platform_security/api_keys.ts @@ -33,12 +33,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // TimeoutError: Waiting for element to be located By(css selector, [data-test-subj="apiKeysCreatePromptButton"]) Wait timed out after 10028ms this.tags(['failsOnMKI']); before(async () => { - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); }); after(async () => { await clearAllApiKeys(es, log); - await pageObjects.svlCommonPage.forceLogout(); }); it('should create and delete API keys correctly', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/platform_security/navigation/avatar_menu.ts b/x-pack/test_serverless/functional/test_suites/common/platform_security/navigation/avatar_menu.ts index ca693e2b966c8..8b2dd1b227b9e 100644 --- a/x-pack/test_serverless/functional/test_suites/common/platform_security/navigation/avatar_menu.ts +++ b/x-pack/test_serverless/functional/test_suites/common/platform_security/navigation/avatar_menu.ts @@ -13,11 +13,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { describe('Avatar menu', function () { before(async () => { - await svlCommonPage.login(); - }); - - after(async () => { - await svlCommonPage.forceLogout(); + await svlCommonPage.loginWithRole('viewer'); }); it('is displayed', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/platform_security/user_profiles/user_profiles.ts b/x-pack/test_serverless/functional/test_suites/common/platform_security/user_profiles/user_profiles.ts index 57ef995a4f7a6..2045f172d3db0 100644 --- a/x-pack/test_serverless/functional/test_suites/common/platform_security/user_profiles/user_profiles.ts +++ b/x-pack/test_serverless/functional/test_suites/common/platform_security/user_profiles/user_profiles.ts @@ -13,9 +13,14 @@ export default function ({ getPageObjects }: FtrProviderContext) { describe('User Profile Page', async () => { before(async () => { + // TODO: migrate to SAML role when profile page displays the data await pageObjects.svlCommonPage.login(); }); + after(async () => { + await pageObjects.svlCommonPage.forceLogout(); + }); + describe('Theme', async () => { it('should change theme based on the User Profile Theme control', async () => { await pageObjects.common.navigateToApp('security_account'); diff --git a/x-pack/test_serverless/functional/test_suites/common/reporting/management.ts b/x-pack/test_serverless/functional/test_suites/common/reporting/management.ts index 90be1b602441a..6d23ef5512b76 100644 --- a/x-pack/test_serverless/functional/test_suites/common/reporting/management.ts +++ b/x-pack/test_serverless/functional/test_suites/common/reporting/management.ts @@ -24,7 +24,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const navigateToReportingManagement = async () => { log.debug(`navigating to reporting management app`); await retry.tryForTime(60 * 1000, async () => { - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); await PageObjects.common.navigateToApp('reportingManagement'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('reportingPageHeader', { timeout: 2000 }); diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/bulk_get.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/bulk_get.ts index 78d4f41033388..92e1a4a69c205 100644 --- a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/bulk_get.ts +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/bulk_get.ts @@ -26,7 +26,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await kibanaServer.importExport.load( 'x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_saved_objects' ); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('management'); await testSubjects.click('app-card-objects'); await pageObjects.savedObjects.waitTableIsLoaded(); @@ -40,7 +40,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_saved_objects' ); await kibanaServer.savedObjects.cleanStandardList(); - await pageObjects.svlCommonPage.forceLogout(); }); const URL = '/api/kibana/management/saved_objects/_bulk_get'; diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/export_transform.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/export_transform.ts index dea6b1118b0f1..80de679ca7b49 100644 --- a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/export_transform.ts +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/export_transform.ts @@ -28,7 +28,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/export_transform' ); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('management'); await testSubjects.click('app-card-objects'); await pageObjects.savedObjects.waitTableIsLoaded(); @@ -39,7 +39,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'test/functional/fixtures/es_archiver/saved_objects_management/export_transform' ); await kibanaServer.savedObjects.cleanStandardList(); - await pageObjects.svlCommonPage.forceLogout(); }); it('allows to mutate the objects during an export', async () => { @@ -159,7 +158,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform' ); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('management'); await testSubjects.click('app-card-objects'); await pageObjects.savedObjects.waitTableIsLoaded(); @@ -170,7 +169,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform' ); await kibanaServer.savedObjects.cleanStandardList(); - await pageObjects.svlCommonPage.forceLogout(); }); it('execute export transforms for reference objects', async () => { @@ -211,7 +209,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion' ); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('management'); await testSubjects.click('app-card-objects'); await pageObjects.savedObjects.waitTableIsLoaded(); @@ -222,7 +220,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion' ); await kibanaServer.savedObjects.cleanStandardList(); - await pageObjects.svlCommonPage.forceLogout(); }); it('should only export objects returning `true` for `isExportable`', async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/find.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/find.ts index e2787135b093a..986479518bfd4 100644 --- a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/find.ts +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/find.ts @@ -25,7 +25,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await kibanaServer.importExport.load( 'x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_saved_objects' ); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('management'); await testSubjects.click('app-card-objects'); await pageObjects.savedObjects.waitTableIsLoaded(); @@ -42,7 +42,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // "index_not_found_exception: no such index [.kibana_ingest]", // so it was switched to `savedObjects.cleanStandardList() await kibanaServer.savedObjects.cleanStandardList(); - await pageObjects.svlCommonPage.forceLogout(); }); it('returns saved objects with importableAndExportable types', async () => diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_from_http_apis.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_from_http_apis.ts index 404773e584a2a..d5edfb0e73bf2 100644 --- a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_from_http_apis.ts +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_from_http_apis.ts @@ -29,7 +29,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await kbnServer.importExport.load( 'test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_from_http_apis' ); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('management'); await testSubjects.click('app-card-objects'); await pageObjects.savedObjects.waitTableIsLoaded(); @@ -42,7 +42,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.unload( 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_from_http_apis' ); - await pageObjects.svlCommonPage.forceLogout(); }); describe('APIS', () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_types.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_types.ts index 767e833ecc20f..9a48fd898d3c7 100644 --- a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_types.ts +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/hidden_types.ts @@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_types' ); - await PageObjects.svlCommonPage.login(); + await PageObjects.svlCommonPage.loginAsAdmin(); await PageObjects.common.navigateToApp('management'); await testSubjects.click('app-card-objects'); await PageObjects.savedObjects.waitTableIsLoaded(); @@ -38,11 +38,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'test/functional/fixtures/es_archiver/saved_objects_management/hidden_types' ); await kibanaServer.savedObjects.cleanStandardList(); - await PageObjects.svlCommonPage.forceLogout(); }); beforeEach(async () => { - // await PageObjects.svlCommonPage.login(); await PageObjects.common.navigateToApp('management'); await testSubjects.click('app-card-objects'); await PageObjects.savedObjects.waitTableIsLoaded(); diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/import_warnings.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/import_warnings.ts index ca3d81f2c4551..a469010f09f71 100644 --- a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/import_warnings.ts +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/import_warnings.ts @@ -21,10 +21,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // "index_not_found_exception: no such index [.kibana_ingest]", // so it was switched to `savedObjects.cleanStandardList() await kibanaServer.savedObjects.cleanStandardList(); + await PageObjects.svlCommonPage.loginAsAdmin(); }); beforeEach(async () => { - await PageObjects.svlCommonPage.login(); await PageObjects.common.navigateToApp('management'); await testSubjects.click('app-card-objects'); await PageObjects.savedObjects.waitTableIsLoaded(); diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/scroll_count.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/scroll_count.ts index d6d173b646563..1ae8afebcc354 100644 --- a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/scroll_count.ts +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/scroll_count.ts @@ -26,7 +26,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await kibanaServer.importExport.load( 'x-pack/test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_saved_objects' ); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('management'); await testSubjects.click('app-card-objects'); await pageObjects.savedObjects.waitTableIsLoaded(); diff --git a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/visible_in_management.ts b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/visible_in_management.ts index f0e4486a5254b..f4e1b28682b40 100644 --- a/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/visible_in_management.ts +++ b/x-pack/test_serverless/functional/test_suites/common/saved_objects_management/visible_in_management.ts @@ -28,7 +28,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.load( 'test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management' ); - await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonPage.loginAsAdmin(); await pageObjects.common.navigateToApp('management'); await testSubjects.click('app-card-objects'); await pageObjects.savedObjects.waitTableIsLoaded(); @@ -38,7 +38,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.unload( 'test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management' ); - await pageObjects.svlCommonPage.forceLogout(); }); describe('export', () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/index.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/index.ts index 3b8db07a689ac..d5f782fc7ee6c 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/index.ts @@ -59,7 +59,6 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext }); await kibanaServer.importExport.load(fixtureDirs.lensBasic); await kibanaServer.importExport.load(fixtureDirs.lensDefault); - await PageObjects.svlCommonPage.login(); // changing the timepicker default here saves us from having to set it in Discover (~8s) await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); }); @@ -67,7 +66,6 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext after(async () => { await esArchiver.unload(esArchive); await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); - await PageObjects.svlCommonPage.forceLogout(); await kibanaServer.importExport.unload(fixtureDirs.lensBasic); await kibanaServer.importExport.unload(fixtureDirs.lensDefault); await kibanaServer.savedObjects.cleanStandardList(); diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/smokescreen.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/smokescreen.ts index d4337d16db4ea..027afe3c2ebe5 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/smokescreen.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/smokescreen.ts @@ -10,7 +10,7 @@ import { range } from 'lodash'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header', 'svlCommonPage']); const find = getService('find'); const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); @@ -20,6 +20,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const config = getService('config'); describe('lens smokescreen tests', () => { + before(async () => { + await PageObjects.svlCommonPage.loginWithPrivilegedRole(); + }); + it('should allow creation of lens xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/tsdb.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/tsdb.ts index 96b1d0125c955..99633e01940c1 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/tsdb.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/tsdb.ts @@ -238,7 +238,13 @@ function sumFirstNValues(n: number, bars: Array<{ y: number }>): number { } export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'timePicker', 'lens', 'dashboard']); + const PageObjects = getPageObjects([ + 'common', + 'timePicker', + 'lens', + 'dashboard', + 'svlCommonPage', + ]); const testSubjects = getService('testSubjects'); const find = getService('find'); const kibanaServer = getService('kibanaServer'); @@ -319,6 +325,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const toTime = 'Jun 16, 2023 @ 00:00:00.000'; before(async () => { + await PageObjects.svlCommonPage.loginAsAdmin(); log.info(`loading ${tsdbIndex} index...`); await esArchiver.loadIfNeeded(tsdbEsArchive); log.info(`creating a data view for "${tsdbDataView}"...`); diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/vega_chart.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/vega_chart.ts index e38c188a73096..347304b4b9f19 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/vega_chart.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group1/vega_chart.ts @@ -32,6 +32,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'visChart', 'visEditor', 'vegaChart', + 'svlCommonPage', ]); const filterBar = getService('filterBar'); const inspector = getService('inspector'); @@ -42,6 +43,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('vega chart in visualize app', () => { before(async () => { + await PageObjects.svlCommonPage.loginWithPrivilegedRole(); await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/index.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/index.ts index 4b7e3588669d1..d63cbd6e60dbd 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/index.ts @@ -7,19 +7,8 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; -export default ({ loadTestFile, getPageObject }: FtrProviderContext) => { - const svlCommonPage = getPageObject('svlCommonPage'); - - // FLAKY: https://github.com/elastic/kibana/issues/168985 - describe.skip('Visualizations - Group 2', function () { - before(async () => { - await svlCommonPage.login(); - }); - - after(async () => { - await svlCommonPage.forceLogout(); - }); - +export default ({ loadTestFile }: FtrProviderContext) => { + describe('Visualizations - Group 2', function () { loadTestFile(require.resolve('./open_in_lens/agg_based')); }); }; diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/gauge.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/gauge.ts index d13f2e5b2aed1..8315a8e2a7eda 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/gauge.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/gauge.ts @@ -9,7 +9,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { lens, timePicker, dashboard } = getPageObjects(['lens', 'timePicker', 'dashboard']); + const { svlCommonPage, lens, timePicker, dashboard } = getPageObjects([ + 'svlCommonPage', + 'lens', + 'timePicker', + 'dashboard', + ]); const testSubjects = getService('testSubjects'); const find = getService('find'); @@ -22,6 +27,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await kibanaServer.importExport.load(fixture); + await svlCommonPage.loginWithPrivilegedRole(); }); after(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/goal.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/goal.ts index 01f655af00a1f..d1c73fc4dc45f 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/goal.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/goal.ts @@ -9,7 +9,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { lens, timePicker, dashboard } = getPageObjects(['lens', 'timePicker', 'dashboard']); + const { svlCommonPage, lens, timePicker, dashboard } = getPageObjects([ + 'svlCommonPage', + 'lens', + 'timePicker', + 'dashboard', + ]); const testSubjects = getService('testSubjects'); const panelActions = getService('dashboardPanelActions'); @@ -21,6 +26,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await kibanaServer.importExport.load(fixture); + await svlCommonPage.loginWithPrivilegedRole(); }); after(async () => { @@ -51,6 +57,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '140.05%', color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingBar: true, showingTrendline: false, }, @@ -78,6 +85,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '131,040,360.81%', color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingBar: true, showingTrendline: false, }, @@ -106,6 +114,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '14.37%', color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingBar: true, showingTrendline: false, }, @@ -134,6 +143,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '13,228,964,670.613', color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingTrendline: false, showingBar: true, }, @@ -143,6 +153,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '13,186,695,551.251', color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingTrendline: false, showingBar: true, }, @@ -152,6 +163,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '13,073,190,186.423', color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingTrendline: false, showingBar: true, }, @@ -161,6 +173,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '13,031,579,645.108', color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingTrendline: false, showingBar: true, }, @@ -170,6 +183,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '13,009,497,206.823', color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingTrendline: false, showingBar: true, }, @@ -178,7 +192,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { subtitle: undefined, extraText: undefined, value: undefined, - color: 'rgba(0, 0, 0, 0)', + color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingTrendline: false, showingBar: true, }, diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/heatmap.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/heatmap.ts index 6e486d6d34e5d..836dceaee8b4e 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/heatmap.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/heatmap.ts @@ -9,7 +9,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { lens, timePicker, dashboard } = getPageObjects(['lens', 'timePicker', 'dashboard']); + const { svlCommonPage, lens, timePicker, dashboard } = getPageObjects([ + 'svlCommonPage', + 'lens', + 'timePicker', + 'dashboard', + ]); const panelActions = getService('dashboardPanelActions'); const kibanaServer = getService('kibanaServer'); @@ -20,6 +25,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await kibanaServer.importExport.load(fixture); + await svlCommonPage.loginWithPrivilegedRole(); }); after(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/metric.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/metric.ts index 9bd990484cc81..d767efbe11f10 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/metric.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/metric.ts @@ -9,7 +9,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { lens, timePicker, dashboard } = getPageObjects(['lens', 'timePicker', 'dashboard']); + const { svlCommonPage, lens, timePicker, dashboard } = getPageObjects([ + 'svlCommonPage', + 'lens', + 'timePicker', + 'dashboard', + ]); const testSubjects = getService('testSubjects'); const panelActions = getService('dashboardPanelActions'); @@ -21,6 +26,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await kibanaServer.importExport.load(fixture); + await svlCommonPage.loginWithPrivilegedRole(); }); after(async () => { @@ -47,6 +53,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '14,005', color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingBar: false, showingTrendline: false, }, @@ -73,6 +80,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '13,104,036,080.615', color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingBar: false, showingTrendline: false, }, @@ -100,6 +108,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '1,437', color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingBar: false, showingTrendline: false, }, @@ -131,6 +140,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '13,228,964,670.613', color: 'rgba(165, 0, 38, 1)', + trendlineColor: undefined, showingBar: false, showingTrendline: false, }, @@ -140,6 +150,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '13,186,695,551.251', color: 'rgba(253, 191, 111, 1)', + trendlineColor: undefined, showingBar: false, showingTrendline: false, }, @@ -149,6 +160,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '13,073,190,186.423', color: 'rgba(183, 224, 117, 1)', + trendlineColor: undefined, showingBar: false, showingTrendline: false, }, @@ -158,6 +170,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '13,031,579,645.108', color: 'rgba(183, 224, 117, 1)', + trendlineColor: undefined, showingBar: false, showingTrendline: false, }, @@ -167,6 +180,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { extraText: '', value: '13,009,497,206.823', color: 'rgba(183, 224, 117, 1)', + trendlineColor: undefined, showingBar: false, showingTrendline: false, }, @@ -175,7 +189,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { subtitle: undefined, extraText: undefined, value: undefined, - color: 'rgba(0, 0, 0, 0)', + color: 'rgba(255, 255, 255, 1)', + trendlineColor: undefined, showingBar: false, showingTrendline: false, }, diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/pie.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/pie.ts index d1aa46064e573..bdd51d65b703d 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/pie.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/pie.ts @@ -9,7 +9,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { lens, timePicker, dashboard } = getPageObjects(['lens', 'timePicker', 'dashboard']); + const { svlCommonPage, lens, timePicker, dashboard } = getPageObjects([ + 'svlCommonPage', + 'lens', + 'timePicker', + 'dashboard', + ]); const pieChart = getService('pieChart'); const testSubjects = getService('testSubjects'); @@ -22,6 +27,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await kibanaServer.importExport.load(fixture); + await svlCommonPage.loginWithPrivilegedRole(); }); after(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/table.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/table.ts index 5b5d31a842607..7fa380951a12d 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/table.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/table.ts @@ -9,11 +9,17 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { lens, timePicker, dashboard } = getPageObjects(['lens', 'timePicker', 'dashboard']); + const { svlCommonPage, lens, timePicker, dashboard } = getPageObjects([ + 'svlCommonPage', + 'lens', + 'timePicker', + 'dashboard', + ]); const testSubjects = getService('testSubjects'); const panelActions = getService('dashboardPanelActions'); const kibanaServer = getService('kibanaServer'); + const comboBox = getService('comboBox'); describe('Table', function describeIndexTests() { const fixture = @@ -21,6 +27,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await kibanaServer.importExport.load(fixture); + await svlCommonPage.loginWithPrivilegedRole(); }); after(async () => { @@ -67,8 +74,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await dimensions[0].getVisibleText()).to.be('Average machine.ram'); await lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger'); - const summaryRowFunction = await testSubjects.find('lnsDatatable_summaryrow_function'); - expect(await summaryRowFunction.getVisibleText()).to.be('Sum'); + expect(await comboBox.getComboBoxSelectedOptions('lnsDatatable_summaryrow_function')).to.eql([ + 'Sum', + ]); }); it('should convert sibling pipeline aggregation', async () => { @@ -132,8 +140,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const percentageColumnText = await lens.getDimensionTriggerText('lnsDatatable_metrics', 1); await lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger', 0, 1); - const format = await testSubjects.find('indexPattern-dimension-format'); - expect(await format.getVisibleText()).to.be('Percent'); + expect(await comboBox.getComboBoxSelectedOptions('indexPattern-dimension-format')).to.eql([ + 'Percent', + ]); const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); expect(dimensions).to.have.length(2); diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/xy.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/xy.ts index 08da72b5e66fb..299142e7cf9b5 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/xy.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/xy.ts @@ -9,7 +9,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { lens, timePicker, dashboard } = getPageObjects(['lens', 'timePicker', 'dashboard']); + const { svlCommonPage, lens, timePicker, dashboard } = getPageObjects([ + 'svlCommonPage', + 'lens', + 'timePicker', + 'dashboard', + ]); const testSubjects = getService('testSubjects'); const retry = getService('retry'); @@ -22,6 +27,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await kibanaServer.importExport.load(fixture); + await svlCommonPage.loginWithPrivilegedRole(); }); after(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/index.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/index.ts index 4dbf361074deb..6bf20a6968d99 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/index.ts @@ -12,11 +12,7 @@ export default ({ loadTestFile, getPageObject }: FtrProviderContext) => { describe('Visualizations - Group 3', function () { before(async () => { - await svlCommonPage.login(); - }); - - after(async () => { - await svlCommonPage.forceLogout(); + await svlCommonPage.loginWithPrivilegedRole(); }); loadTestFile(require.resolve('./open_in_lens/tsvb')); diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/table.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/table.ts index 864e205e3ff5f..e5b4174435e05 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/table.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/table.ts @@ -22,7 +22,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const retry = getService('retry'); const panelActions = getService('dashboardPanelActions'); const kibanaServer = getService('kibanaServer'); - const comboBox = getService('comboBox'); describe('Table', function describeIndexTests() { const fixture = @@ -84,9 +83,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger'); await testSubjects.click('indexPattern-advanced-accordion'); - expect( - await comboBox.getComboBoxSelectedOptions('indexPattern-dimension-reducedTimeRange') - ).to.eql(['1 minute (1m)']); + const reducedTimeRange = await testSubjects.find( + 'indexPattern-dimension-reducedTimeRange > comboBoxSearchInput' + ); + expect(await reducedTimeRange.getAttribute('value')).to.be('1 minute (1m)'); await retry.try(async () => { const layerCount = await lens.getLayerCount(); diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/top_n.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/top_n.ts index 4085e7faad08c..e9872a6b776d3 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/top_n.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group3/open_in_lens/tsvb/top_n.ts @@ -17,7 +17,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const queryBar = getService('queryBar'); const panelActions = getService('dashboardPanelActions'); const kibanaServer = getService('kibanaServer'); - const comboBox = getService('comboBox'); describe('Top N', function describeIndexTests() { const fixture = @@ -102,8 +101,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger'); await testSubjects.click('indexPattern-advanced-accordion'); - const reducedTimeRange = await testSubjects.find('indexPattern-dimension-reducedTimeRange'); - await comboBox.isOptionSelected(reducedTimeRange, '1 minute (1m)'); + const reducedTimeRange = await testSubjects.find( + 'indexPattern-dimension-reducedTimeRange > comboBoxSearchInput' + ); + expect(await reducedTimeRange.getAttribute('value')).to.be('1 minute (1m)'); await retry.try(async () => { const layerCount = await lens.getLayerCount(); expect(layerCount).to.be(1); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/columns_selection.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/columns_selection.ts index a719b1ae854ce..beb1adc25e70c 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/columns_selection.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/columns_selection.ts @@ -60,9 +60,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { mode: 'absolute', }, columns: [ - { field: 'resource' }, - { field: 'content' }, - { field: 'data_stream.namespace' }, + { + smartField: 'resource', + type: 'smart-field', + fallbackFields: ['host.name', 'service.name'], + }, + { + smartField: 'content', + type: 'smart-field', + fallbackFields: ['message'], + }, + { field: 'data_stream.namespace', type: 'document-field' }, ], }, }); @@ -79,7 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('render content virtual column properly', async () => { it('should render log level and log message when present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElement(0, 3); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('A sample log')).to.be(true); @@ -88,7 +96,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render log message when present and skip log level when missing', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(1, 4); + const cellElement = await dataGrid.getCellElement(1, 3); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(false); expect(cellValue.includes('A sample log')).to.be(true); @@ -97,7 +105,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render message from error object when top level message not present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(2, 4); + const cellElement = await dataGrid.getCellElement(2, 3); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('error.message')).to.be(true); @@ -107,7 +115,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render message from event.original when top level message and error.message not present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(3, 4); + const cellElement = await dataGrid.getCellElement(3, 3); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); expect(cellValue.includes('event.original')).to.be(true); @@ -117,7 +125,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the whole JSON when neither message, error.message and event.original are present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(4, 4); + const cellElement = await dataGrid.getCellElement(4, 3); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('info')).to.be(true); @@ -133,7 +141,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('on cell expansion with no message field should open JSON Viewer', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - await dataGrid.clickCellExpandButton(4, 4); + await dataGrid.clickCellExpandButton(4, 3); await testSubjects.existOrFail('dataTableExpandCellActionJsonPopover'); }); }); @@ -141,7 +149,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('on cell expansion with message field should open regular popover', async () => { await navigateToLogsExplorer(); await retry.tryForTime(TEST_TIMEOUT, async () => { - await dataGrid.clickCellExpandButton(3, 4); + await dataGrid.clickCellExpandButton(3, 3); await testSubjects.existOrFail('euiDataGridExpansionPopover'); }); }); @@ -150,7 +158,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('render resource virtual column properly', async () => { it('should render service name and host name when present', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 3); + const cellElement = await dataGrid.getCellElement(0, 2); const cellValue = await cellElement.getVisibleText(); expect(cellValue.includes('synth-service')).to.be(true); expect(cellValue.includes('synth-host')).to.be(true); @@ -164,7 +172,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should render a popover with cell actions when a chip on content column is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElement(0, 3); const logLevelChip = await cellElement.findByTestSubject( 'dataTablePopoverChip_log.level' ); @@ -180,7 +188,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where log.level value is info when filter in action is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElement(0, 3); const logLevelChip = await cellElement.findByTestSubject( 'dataTablePopoverChip_log.level' ); @@ -200,7 +208,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where log.level value is not info when filter out action is clicked', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 4); + const cellElement = await dataGrid.getCellElement(0, 3); const logLevelChip = await cellElement.findByTestSubject( 'dataTablePopoverChip_log.level' ); @@ -218,7 +226,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render the table filtered where service.name value is selected', async () => { await retry.tryForTime(TEST_TIMEOUT, async () => { - const cellElement = await dataGrid.getCellElement(0, 3); + const cellElement = await dataGrid.getCellElement(0, 2); const serviceNameChip = await cellElement.findByTestSubject( 'dataTablePopoverChip_service.name' ); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/custom_control_columns.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/custom_control_columns.ts new file mode 100644 index 0000000000000..232a5bd20c001 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/custom_control_columns.ts @@ -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 { log, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import moment from 'moment'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const MORE_THAN_1024_CHARS = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const PageObjects = getPageObjects(['discover', 'observabilityLogsExplorer', 'svlCommonPage']); + const synthtrace = getService('svlLogsSynthtraceClient'); + const dataGrid = getService('dataGrid'); + const from = '2024-02-06T10:24:14.035Z'; + const to = '2024-02-06T10:25:14.091Z'; + const TEST_TIMEOUT = 10 * 1000; // 10 secs + + const navigateToLogsExplorer = () => + PageObjects.observabilityLogsExplorer.navigateTo({ + pageState: { + time: { + from, + to, + mode: 'absolute', + }, + }, + }); + + describe('When the logs explorer loads', () => { + before(async () => { + await synthtrace.index(generateLogsData({ to })); + await PageObjects.svlCommonPage.login(); + await navigateToLogsExplorer(); + }); + + after(async () => { + await synthtrace.clean(); + await PageObjects.svlCommonPage.forceLogout(); + }); + + describe('should render custom control columns properly', async () => { + it('should render control column with proper header', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + // First control column has no title, so empty string, last control column has title + expect(await dataGrid.getControlColumnHeaderFields()).to.eql(['', 'actions']); + }); + }); + + it('should render the expand icon in the last control column', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(0, 4); + const expandButton = await cellElement.findByTestSubject('docTableExpandToggleColumn'); + expect(expandButton).to.not.be.empty(); + }); + }); + + it('should render the malformed icon in the last control column if malformed doc exists', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(1, 4); + const malformedButton = await cellElement.findByTestSubject('docTableMalformedDocExist'); + expect(malformedButton).to.not.be.empty(); + }); + }); + + it('should render the disabled malformed icon in the last control column when malformed doc does not exists', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(0, 4); + const malformedDisableButton = await cellElement.findByTestSubject( + 'docTableMalformedDocDoesNotExist' + ); + expect(malformedDisableButton).to.not.be.empty(); + }); + }); + + it('should render the stacktrace icon in the last control column when stacktrace exists', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(4, 4); + const stacktraceButton = await cellElement.findByTestSubject('docTableStacktraceExist'); + expect(stacktraceButton).to.not.be.empty(); + }); + }); + + it('should render the stacktrace icon disabled in the last control column when stacktrace does not exists', async () => { + await retry.tryForTime(TEST_TIMEOUT, async () => { + const cellElement = await dataGrid.getCellElement(1, 4); + const stacktraceButton = await cellElement.findByTestSubject( + 'docTableStacktraceDoesNotExist' + ); + expect(stacktraceButton).to.not.be.empty(); + }); + }); + }); + }); +} + +function generateLogsData({ to, count = 1 }: { to: string; count?: number }) { + const logs = timerange(moment(to).subtract(1, 'second'), moment(to)) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log + .create() + .message('A sample log') + .logLevel('info') + .timestamp(timestamp) + .defaults({ 'service.name': 'synth-service' }); + }) + ); + + const malformedDocs = timerange( + moment(to).subtract(2, 'second'), + moment(to).subtract(1, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log + .create() + .message('A malformed doc') + .logLevel(MORE_THAN_1024_CHARS) + .timestamp(timestamp) + .defaults({ 'service.name': 'synth-service' }); + }) + ); + + const logsWithErrorMessage = timerange( + moment(to).subtract(3, 'second'), + moment(to).subtract(2, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().logLevel('info').timestamp(timestamp).defaults({ + 'error.stack_trace': 'Error message in error.stack_trace', + 'service.name': 'node-service', + }); + }) + ); + + const logsWithErrorException = timerange( + moment(to).subtract(4, 'second'), + moment(to).subtract(3, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().logLevel('info').timestamp(timestamp).defaults({ + 'error.exception.stacktrace': 'Error message in error.exception.stacktrace', + 'service.name': 'node-service', + }); + }) + ); + + const logsWithErrorInLog = timerange( + moment(to).subtract(5, 'second'), + moment(to).subtract(4, 'second') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => + Array(count) + .fill(0) + .map(() => { + return log.create().logLevel('info').timestamp(timestamp).defaults({ + 'error.log.stacktrace': 'Error message in error.log.stacktrace', + 'service.name': 'node-service', + }); + }) + ); + + return [logs, malformedDocs, logsWithErrorMessage, logsWithErrorException, logsWithErrorInLog]; +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout.ts index f8087eff743c3..e952294c2cda1 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout.ts @@ -68,30 +68,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should mount the flyout customization content', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutDetail'); }); it('should display a timestamp badge', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutLogTimestamp'); }); it('should display a log level badge when available', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutLogLevel'); await dataGrid.closeFlyout(); - await dataGrid.clickRowToggle({ rowIndex: 1 }); + await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); await testSubjects.missingOrFail('logsExplorerFlyoutLogLevel'); }); it('should display a message code block when available', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutLogMessage'); await dataGrid.closeFlyout(); - await dataGrid.clickRowToggle({ rowIndex: 1 }); + await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); await testSubjects.missingOrFail('logsExplorerFlyoutLogMessage'); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout_highlights.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout_highlights.ts index 9fa836815a554..b143d59e96cad 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout_highlights.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/flyout_highlights.ts @@ -89,7 +89,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the service container with all fields', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); await testSubjects.existOrFail('logsExplorerFlyoutService'); await testSubjects.existOrFail('logsExplorerFlyoutTrace'); @@ -100,7 +100,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the service container even when 1 field is missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1 }); + await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); await testSubjects.missingOrFail('logsExplorerFlyoutService'); await testSubjects.existOrFail('logsExplorerFlyoutTrace'); @@ -111,7 +111,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should not load the service container if all fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 2 }); + await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 }); await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionServiceInfra'); await dataGrid.closeFlyout(); }); @@ -159,7 +159,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the cloud container with all fields', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud'); await testSubjects.existOrFail('logsExplorerFlyoutCloudProvider'); await testSubjects.existOrFail('logsExplorerFlyoutCloudRegion'); @@ -170,7 +170,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the cloud container even when some fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1 }); + await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionCloud'); await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider'); @@ -183,7 +183,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should not load the cloud container if all fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 2 }); + await dataGrid.clickRowToggle({ rowIndex: 2, columnIndex: 4 }); await testSubjects.missingOrFail('logsExplorerFlyoutHighlightSectionCloud'); await testSubjects.missingOrFail('logsExplorerFlyoutCloudProvider'); await testSubjects.missingOrFail('logsExplorerFlyoutCloudRegion'); @@ -231,7 +231,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the other container with all fields', async () => { - await dataGrid.clickRowToggle(); + await dataGrid.clickRowToggle({ columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther'); await testSubjects.existOrFail('logsExplorerFlyoutLogPathFile'); await testSubjects.existOrFail('logsExplorerFlyoutNamespace'); @@ -241,7 +241,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should load the other container even when some fields are missing', async () => { - await dataGrid.clickRowToggle({ rowIndex: 1 }); + await dataGrid.clickRowToggle({ rowIndex: 1, columnIndex: 4 }); await testSubjects.existOrFail('logsExplorerFlyoutHighlightSectionOther'); await testSubjects.missingOrFail('logsExplorerFlyoutLogPathFile'); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/header_menu.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/header_menu.ts index a4768c4415e4b..e6fe60cf248fc 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/header_menu.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/header_menu.ts @@ -71,7 +71,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await discoverLink.isDisplayed()).to.be(true); }); - it('should navigate to discover keeping the current columns/filters/query/time/data view', async () => { + it('should navigate to discover keeping the current filters/query/time/data view and use fallback columns for virtual columns', async () => { await retry.try(async () => { await testSubjects.existOrFail('superDatePickerstartDatePopoverButton'); await testSubjects.existOrFail('superDatePickerendDatePopoverButton'); @@ -91,8 +91,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { expect(await PageObjects.discover.getColumnHeaders()).to.eql([ '@timestamp', - 'resource', - 'content', + 'host.name', + 'service.name', + 'message', ]); }); await retry.try(async () => { diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/index.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/index.ts index c248e31df92e3..98cde3a1d8267 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/index.ts @@ -17,5 +17,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./flyout')); loadTestFile(require.resolve('./header_menu')); loadTestFile(require.resolve('./flyout_highlights.ts')); + loadTestFile(require.resolve('./custom_control_columns.ts')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts b/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts index d664d36e99f1e..2ebb4395bf317 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/rules/rules_list.ts @@ -22,6 +22,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const svlCommonPage = getPageObject('svlCommonPage'); const svlCommonNavigation = getPageObject('svlCommonNavigation'); const svlTriggersActionsUI = getPageObject('svlTriggersActionsUI'); + const header = getPageObject('header'); const svlObltNavigation = getService('svlObltNavigation'); const testSubjects = getService('testSubjects'); const supertest = getService('supertest'); @@ -300,6 +301,10 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); await testSubjects.click('disableButton'); + await testSubjects.click('confirmModalConfirmButton'); + + await header.waitUntilLoadingHasFinished(); + await refreshRulesList(); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); @@ -387,6 +392,9 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('bulkAction'); await testSubjects.click('bulkDisable'); + await testSubjects.click('confirmModalConfirmButton'); + await header.waitUntilLoadingHasFinished(); + await retry.try(async () => { const resultToast = await toasts.getToastElement(1); const toastText = await resultToast.getVisibleText(); diff --git a/x-pack/test_serverless/functional/test_suites/security/common_configs/config.group6.ts b/x-pack/test_serverless/functional/test_suites/security/common_configs/config.group6.ts index f3276ecd3c67c..8ebd1f6958ec7 100644 --- a/x-pack/test_serverless/functional/test_suites/security/common_configs/config.group6.ts +++ b/x-pack/test_serverless/functional/test_suites/security/common_configs/config.group6.ts @@ -15,7 +15,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ require.resolve('../../common/discover/embeddable'), require.resolve('../../common/discover/x_pack'), - require.resolve('../../common/discover_ml_uptime/discover'), + // flaky for Security project, should be checked with Admin role permissions. + // https://github.com/elastic/kibana/issues/172365 + // require.resolve('../../common/discover_ml_uptime/discover'), require.resolve('../../common/context'), ], junit: { diff --git a/x-pack/test_serverless/shared/services/svl_user_manager.ts b/x-pack/test_serverless/shared/services/svl_user_manager.ts index 9ed3395340494..2299b4d78e974 100644 --- a/x-pack/test_serverless/shared/services/svl_user_manager.ts +++ b/x-pack/test_serverless/shared/services/svl_user_manager.ts @@ -9,6 +9,8 @@ import { ServerlessProjectType, SERVERLESS_ROLES_ROOT_PATH } from '@kbn/es'; import { SamlSessionManager } from '@kbn/test'; import { readRolesFromResource } from '@kbn/es'; import { resolve } from 'path'; +import { Role } from '@kbn/test/src/auth/types'; +import { isServerlessProjectType } from '@kbn/es/src/utils'; import { FtrProviderContext } from '../../functional/ftr_provider_context'; export function SvlUserManagerProvider({ getService }: FtrProviderContext) { @@ -17,11 +19,32 @@ export function SvlUserManagerProvider({ getService }: FtrProviderContext) { const isCloud = !!process.env.TEST_CLOUD; const kbnServerArgs = config.get('kbnTestServer.serverArgs') as string[]; const projectType = kbnServerArgs - .find((arg) => arg.startsWith('--serverless'))! - .split('=')[1] as ServerlessProjectType; + .filter((arg) => arg.startsWith('--serverless')) + .reduce((acc, arg) => { + const match = arg.match(/--serverless[=\s](\w+)/); + return acc + (match ? match[1] : ''); + }, '') as ServerlessProjectType; - const resourcePath = resolve(SERVERLESS_ROLES_ROOT_PATH, projectType, 'roles.yml'); - const supportedRoles = readRolesFromResource(resourcePath); + if (!isServerlessProjectType(projectType)) { + throw new Error(`Unsupported serverless projectType: ${projectType}`); + } + + const supportedRoles = readRolesFromResource( + resolve(SERVERLESS_ROLES_ROOT_PATH, projectType, 'roles.yml') + ); + const defaultRolesToMap = new Map([ + ['es', 'developer'], + ['security', 'editor'], + ['oblt', 'editor'], + ]); + + const getDefaultRole = () => { + if (defaultRolesToMap.has(projectType)) { + return defaultRolesToMap.get(projectType)!; + } else { + throw new Error(`Default role is not defined for ${projectType} project`); + } + }; // Sharing the instance within FTR config run means cookies are persistent for each role between tests. const sessionManager = new SamlSessionManager({ @@ -37,5 +60,18 @@ export function SvlUserManagerProvider({ getService }: FtrProviderContext) { supportedRoles, }); - return sessionManager; + const DEFAULT_ROLE = getDefaultRole(); + + return { + async getSessionCookieForRole(role: string) { + return sessionManager.getSessionCookieForRole(role); + }, + async getApiCredentialsForRole(role: string) { + return sessionManager.getApiCredentialsForRole(role); + }, + async getUserData(role: string) { + return sessionManager.getUserData(role); + }, + DEFAULT_ROLE, + }; } diff --git a/yarn.lock b/yarn.lock index d75766cfa5b0e..eb678a777ab9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1658,10 +1658,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@61.2.0": - version "61.2.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-61.2.0.tgz#fa065b85324d5660e4b6355390cca8699ef0d6ff" - integrity sha512-LBmZ+6wSR9/BCR+go5eIBy51Jpxr0cbK/a7vslZNYIGCdpHsWxGZgcqG3KgshFxOmUvcP7kj9LEEmeCTEGhbUQ== +"@elastic/charts@63.0.0": + version "63.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-63.0.0.tgz#c7f54cd60a1a59a28b5654886392e05a10fd67b8" + integrity sha512-nvLg/qFJXYuKOdTDYj3iuwJ/X4zhkHdIB91yezd7fo+YvpBRiAUzJfc6Dpy6M5JkmGwx7Dq8zjGt6mO8ngOhog== dependencies: "@popperjs/core" "^2.11.8" bezier-easing "^2.1.0" @@ -1739,10 +1739,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@92.2.1": - version "92.2.1" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-92.2.1.tgz#172b5122e1025307bbb8e2c6a115c3feb3a16f42" - integrity sha512-FujsbJtuh8mxG5mbqclQBdPoW1kn9kXd/hpaMXUJa7bb0bmqlJRmagULPTZ+5e60Q6PEQt+e7MFDsaq4elSthQ== +"@elastic/eui@93.0.0": + version "93.0.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-93.0.0.tgz#43ba256d94f3d3acddee23427c9fa5285a43d7e4" + integrity sha512-Q5gY9roWZsB1At0rxG5d9M5P7+J4hfv8fzvuqNHoVjIxbeibHRCSq2TtOQcSUjhKE5Tu/BIZijzuYX8vITGaSQ== dependencies: "@hello-pangea/dnd" "^16.3.0" "@types/lodash" "^4.14.198" @@ -5748,6 +5748,10 @@ version "0.0.0" uid "" +"@kbn/security-hardening@link:packages/kbn-security-hardening": + version "0.0.0" + uid "" + "@kbn/security-plugin-types-common@link:x-pack/packages/security/plugin_types_common": version "0.0.0" uid "" @@ -7750,14 +7754,32 @@ "@types/node" ">=18.0.0" axios "^1.6.0" -"@smithy/eventstream-codec@^2.0.12": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-2.0.12.tgz#99fab750d0ac3941f341d912d3c3a1ab985e1a7a" - integrity sha512-ZZQLzHBJkbiAAdj2C5K+lBlYp/XJ+eH2uy+jgJgYIFW/o5AM59Hlj7zyI44/ZTDIQWmBxb3EFv/c5t44V8/g8A== +"@smithy/eventstream-codec@^2.0.12", "@smithy/eventstream-codec@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-2.1.1.tgz#4405ab0f9c77d439c575560c4886e59ee17d6d38" + integrity sha512-E8KYBxBIuU4c+zrpR22VsVrOPoEDzk35bQR3E+xm4k6Pa6JqzkDOdMyf9Atac5GPNKHJBdVaQ4JtjdWX2rl/nw== dependencies: "@aws-crypto/crc32" "3.0.0" - "@smithy/types" "^2.4.0" - "@smithy/util-hex-encoding" "^2.0.0" + "@smithy/types" "^2.9.1" + "@smithy/util-hex-encoding" "^2.1.1" + tslib "^2.5.0" + +"@smithy/eventstream-serde-node@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-2.1.1.tgz#2e1afa27f9c7eb524c1c53621049c5e4e3cea6a5" + integrity sha512-LF882q/aFidFNDX7uROAGxq3H0B7rjyPkV6QDn6/KDQ+CG7AFkRccjxRf1xqajq/Pe4bMGGr+VKAaoF6lELIQw== + dependencies: + "@smithy/eventstream-serde-universal" "^2.1.1" + "@smithy/types" "^2.9.1" + tslib "^2.5.0" + +"@smithy/eventstream-serde-universal@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-2.1.1.tgz#0f5eec9ad033017973a67bafb5549782499488d2" + integrity sha512-LR0mMT+XIYTxk4k2fIxEA1BPtW3685QlqufUEUAX1AJcfFfxNDKEvuCRZbO8ntJb10DrIFVJR9vb0MhDCi0sAQ== + dependencies: + "@smithy/eventstream-codec" "^2.1.1" + "@smithy/types" "^2.9.1" tslib "^2.5.0" "@smithy/is-array-buffer@^2.0.0": @@ -7767,10 +7789,10 @@ dependencies: tslib "^2.5.0" -"@smithy/types@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@smithy/types/-/types-2.4.0.tgz#ed35e429e3ea3d089c68ed1bf951d0ccbdf2692e" - integrity sha512-iH1Xz68FWlmBJ9vvYeHifVMWJf82ONx+OybPW8ZGf5wnEv2S0UXcU4zwlwJkRXuLKpcSLHrraHbn2ucdVXLb4g== +"@smithy/types@^2.4.0", "@smithy/types@^2.9.1": + version "2.9.1" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-2.9.1.tgz#ed04d4144eed3b8bd26d20fc85aae8d6e357ebb9" + integrity sha512-vjXlKNXyprDYDuJ7UW5iobdmyDm6g8dDG+BFUncAg/3XJaN45Gy5RWWWUVgrzIK7S4R1KWgIX5LeJcfvSI24bw== dependencies: tslib "^2.5.0" @@ -7782,10 +7804,10 @@ "@smithy/is-array-buffer" "^2.0.0" tslib "^2.5.0" -"@smithy/util-hex-encoding@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz#0aa3515acd2b005c6d55675e377080a7c513b59e" - integrity sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA== +"@smithy/util-hex-encoding@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-2.1.1.tgz#978252b9fb242e0a59bae4ead491210688e0d15f" + integrity sha512-3UNdP2pkYUUBGEXzQI9ODTDK+Tcu1BlCyDBaRHwyxhA+8xLP8agEKQq4MGmpjqb4VQAjq9TwlCQX0kP6XDKYLg== dependencies: tslib "^2.5.0" @@ -9346,6 +9368,13 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== +"@types/event-stream@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/event-stream/-/event-stream-4.0.5.tgz#29f1be5f4c0de2e0312cf3b5f7146c975c08d918" + integrity sha512-pQ/RR/iuBW8K8WmwYaaC1nkZH0cHonNAIw6ktG8BCNrNuqNeERfBzNIAOq6Z7tvLzpjcMV02SZ5pxAekAYQpWA== + dependencies: + "@types/node" "*" + "@types/expect@^1.20.4": version "1.20.4" resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" @@ -9390,6 +9419,11 @@ resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.0.tgz#cbb49815a5e1129d5f23836a98d65d93822409af" integrity sha512-dxdRrUov2HVTbSRFX+7xwUPlbGYVEZK6PrSqClg2QPos3PNe0bCajkDDkDeeC1znjSH03KOEqVbXpnJuWa2wgQ== +"@types/flat@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@types/flat/-/flat-5.0.5.tgz#2304df0b2b1e6dde50d81f029593e0a1bc2474d3" + integrity sha512-nPLljZQKSnac53KDUDzuzdRfGI0TDb5qPrb+SrQyN3MtdQrOnGsKniHN1iYZsJEBIVQve94Y6gNz22sgISZq+Q== + "@types/flot@^0.0.31": version "0.0.31" resolved "https://registry.yarnpkg.com/@types/flot/-/flot-0.0.31.tgz#0daca37c6c855b69a0a7e2e37dd0f84b3db8c8c1" @@ -16620,6 +16654,11 @@ events@^3.0.0, events@^3.2.0, events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +eventsource-parser@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-1.1.1.tgz#576f8bcf391c5e5ccdea817abd9ead36d1754247" + integrity sha512-3Ej2iLj6ZnX+5CMxqyUb8syl9yVZwcwm8IIMrOJlF7I51zxOOrRlU3zxSb/6hFbl03ts1ZxHAGJdWLZOLyKG7w== + evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -17272,7 +17311,7 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" -flat@^5.0.2: +flat@5, flat@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==