diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 15a828ac76a94c..aa0442041059ba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -460,6 +460,7 @@ src/plugins/kibana_usage_collection @elastic/kibana-core src/plugins/kibana_utils @elastic/kibana-app-services x-pack/plugins/kubernetes_security @elastic/sec-cloudnative-integrations packages/kbn-language-documentation-popover @elastic/kibana-visualizations +packages/kbn-lens-embeddable-utils @elastic/infra-monitoring-ui x-pack/plugins/lens @elastic/kibana-visualizations x-pack/plugins/license_api_guard @elastic/platform-deployment-management x-pack/plugins/license_management @elastic/platform-deployment-management @@ -588,6 +589,7 @@ examples/screenshot_mode_example @elastic/kibana-app-services src/plugins/screenshot_mode @elastic/appex-sharedux x-pack/examples/screenshotting_example @elastic/appex-sharedux x-pack/plugins/screenshotting @elastic/kibana-reporting-services +packages/kbn-search-api-panels @elastic/enterprise-search-frontend examples/search_examples @elastic/kibana-data-discovery packages/kbn-search-response-warnings @elastic/kibana-data-discovery x-pack/plugins/searchprofiler @elastic/platform-deployment-management @@ -752,6 +754,7 @@ src/plugins/url_forwarding @elastic/kibana-visualizations packages/kbn-url-state @elastic/security-threat-hunting-investigations src/plugins/usage_collection @elastic/kibana-core test/plugin_functional/plugins/usage_collection @elastic/kibana-core +packages/kbn-use-tracked-promise @elastic/infra-monitoring-ui packages/kbn-user-profile-components @elastic/kibana-security examples/user_profile_examples @elastic/kibana-security x-pack/test/security_api_integration/plugins/user_profiles_consumer @elastic/kibana-security diff --git a/.i18nrc.json b/.i18nrc.json index 02337d5b8f0d4e..2463d023971eda 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -92,6 +92,7 @@ "server": "src/legacy/server", "share": "src/plugins/share", "sharedUXPackages": "packages/shared-ux", + "searchApiPanels": "packages/kbn-search-api-panels/", "searchResponseWarnings": "packages/kbn-search-response-warnings", "securitySolutionPackages": "x-pack/packages/security-solution", "serverlessPackages": "packages/serverless", diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index 174637a15a63f0..f7f59ab3740198 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github description: API docs for the actions plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] --- import actionsObj from './actions.devdocs.json'; diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index efad0ca51a157c..f37618f63a824e 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github description: API docs for the advancedSettings plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] --- import advancedSettingsObj from './advanced_settings.devdocs.json'; diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index e5cbe0c4e9ba96..b24567d1f291b1 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github description: API docs for the aiops plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] --- import aiopsObj from './aiops.devdocs.json'; diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index a85af92331ff37..5e8fa5c86b1580 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/alerting title: "alerting" image: https://source.unsplash.com/400x175/?github description: API docs for the alerting plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] --- import alertingObj from './alerting.devdocs.json'; diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index 09fa69675cc983..7d421c5090d3ae 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github description: API docs for the apm plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] --- import apmObj from './apm.devdocs.json'; diff --git a/api_docs/asset_manager.mdx b/api_docs/asset_manager.mdx index c689fcf41d2919..15342810011d81 100644 --- a/api_docs/asset_manager.mdx +++ b/api_docs/asset_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/assetManager title: "assetManager" image: https://source.unsplash.com/400x175/?github description: API docs for the assetManager plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'assetManager'] --- import assetManagerObj from './asset_manager.devdocs.json'; diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index a2ea6a1b04cb37..34d929d1d93e6b 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github description: API docs for the banners plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] --- import bannersObj from './banners.devdocs.json'; diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index 595e8ee3b09f54..4b6e8c5823eaa2 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github description: API docs for the bfetch plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] --- import bfetchObj from './bfetch.devdocs.json'; diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index 537c941b0fd1cc..53ac40db1a5112 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github description: API docs for the canvas plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] --- import canvasObj from './canvas.devdocs.json'; diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index b5766ae183e283..c664beb3cdc6d5 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github description: API docs for the cases plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] --- import casesObj from './cases.devdocs.json'; diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index d73c7ddb361472..08a44567f3d13e 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github description: API docs for the charts plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] --- import chartsObj from './charts.devdocs.json'; diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index ab0811e78f98d6..15457fc193df6f 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github description: API docs for the cloud plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; diff --git a/api_docs/cloud_chat.mdx b/api_docs/cloud_chat.mdx index 8c036104f049ce..33cfa9e79b0055 100644 --- a/api_docs/cloud_chat.mdx +++ b/api_docs/cloud_chat.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudChat title: "cloudChat" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudChat plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudChat'] --- import cloudChatObj from './cloud_chat.devdocs.json'; diff --git a/api_docs/cloud_chat_provider.mdx b/api_docs/cloud_chat_provider.mdx index 602867e70c6f0d..80039c741b6884 100644 --- a/api_docs/cloud_chat_provider.mdx +++ b/api_docs/cloud_chat_provider.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudChatProvider title: "cloudChatProvider" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudChatProvider plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudChatProvider'] --- import cloudChatProviderObj from './cloud_chat_provider.devdocs.json'; diff --git a/api_docs/cloud_data_migration.mdx b/api_docs/cloud_data_migration.mdx index 55b3eaed4cbdbf..76d41d9ff55fe9 100644 --- a/api_docs/cloud_data_migration.mdx +++ b/api_docs/cloud_data_migration.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDataMigration title: "cloudDataMigration" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDataMigration plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDataMigration'] --- import cloudDataMigrationObj from './cloud_data_migration.devdocs.json'; diff --git a/api_docs/cloud_defend.mdx b/api_docs/cloud_defend.mdx index 512acba29356dd..79f2f1208888bc 100644 --- a/api_docs/cloud_defend.mdx +++ b/api_docs/cloud_defend.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDefend title: "cloudDefend" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDefend plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDefend'] --- import cloudDefendObj from './cloud_defend.devdocs.json'; diff --git a/api_docs/cloud_experiments.mdx b/api_docs/cloud_experiments.mdx index 01cb7954ec4071..6451c0a55f7610 100644 --- a/api_docs/cloud_experiments.mdx +++ b/api_docs/cloud_experiments.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudExperiments title: "cloudExperiments" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudExperiments plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudExperiments'] --- import cloudExperimentsObj from './cloud_experiments.devdocs.json'; diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index 6c1216fdfa6404..8cac1402c07113 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudSecurityPosture plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] --- import cloudSecurityPostureObj from './cloud_security_posture.devdocs.json'; diff --git a/api_docs/console.mdx b/api_docs/console.mdx index ac2194327cc0e3..0568f097f37ebb 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/content_management.mdx b/api_docs/content_management.mdx index 24b6f5b9d11eca..b4494ac4cbde6c 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: 2023-08-11 +date: 2023-08-14 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 9d227c1b74d7d2..46397a43a99ac5 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: 2023-08-11 +date: 2023-08-14 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 7ff323025db853..73fee3ecce8672 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: 2023-08-11 +date: 2023-08-14 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 7d198687d3c1d6..662c7d20da6532 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: 2023-08-11 +date: 2023-08-14 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 eccc4eb77cda94..d0d831a152b443 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.mdx b/api_docs/data.mdx index 167764358fe788..e7dd7e88429a7c 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: 2023-08-11 +date: 2023-08-14 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 4d813a7c64e466..ca8eef794fd7cc 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 13f8b1421e57d3..846f0293c55a1a 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: 2023-08-11 +date: 2023-08-14 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 36947c06134761..59080818199bd2 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: 2023-08-11 +date: 2023-08-14 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 23b344e0db1578..bc158603b333ea 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: 2023-08-11 +date: 2023-08-14 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 71fff17678face..9125d5728c278d 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: 2023-08-11 +date: 2023-08-14 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 08ff8f217cd2ca..1c632aa1adace7 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: 2023-08-11 +date: 2023-08-14 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 efcfa6e156e808..8e1743319b5c54 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] --- import dataVisualizerObj from './data_visualizer.devdocs.json'; diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index 2ac6dd1ac9336c..752aebd20e8ffc 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -21,9 +21,9 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | @kbn/es-query, @kbn/visualization-ui-components, observability, securitySolution, timelines, lists, threatIntelligence, savedSearch, dataViews, savedObjectsManagement, unifiedSearch, controls, @kbn/unified-field-list, @kbn/event-annotation-components, lens, triggersActionsUi, ml, logsShared, visTypeTimeseries, apm, exploratoryView, fleet, dataVisualizer, stackAlerts, infra, canvas, enterpriseSearch, graph, transform, upgradeAssistant, uptime, ux, maps, dataViewManagement, inputControlVis, visDefaultEditor, presentationUtil, visTypeTimelion, visTypeVega, data | - | | | @kbn/es-query, @kbn/visualization-ui-components, observability, securitySolution, timelines, lists, threatIntelligence, savedSearch, dataViews, savedObjectsManagement, unifiedSearch, controls, @kbn/unified-field-list, @kbn/event-annotation-components, lens, triggersActionsUi, ml, logsShared, visTypeTimeseries, apm, exploratoryView, fleet, dataVisualizer, stackAlerts, infra, canvas, enterpriseSearch, graph, transform, upgradeAssistant, uptime, ux, maps, dataViewManagement, inputControlVis, visDefaultEditor, presentationUtil, visTypeTimelion, visTypeVega, data | - | | | @kbn/es-query, @kbn/visualization-ui-components, observability, securitySolution, timelines, lists, threatIntelligence, savedSearch, data, savedObjectsManagement, unifiedSearch, controls, @kbn/unified-field-list, @kbn/event-annotation-components, lens, triggersActionsUi, ml, logsShared, visTypeTimeseries, apm, exploratoryView, fleet, dataVisualizer, stackAlerts, infra, canvas, enterpriseSearch, graph, transform, upgradeAssistant, uptime, ux, maps, dataViewManagement, inputControlVis, visDefaultEditor, presentationUtil, visTypeTimelion, visTypeVega | - | -| | inspector, data, advancedSettings, savedObjects, embeddable, dataViewEditor, unifiedSearch, visualizations, controls, dashboard, licensing, savedObjectsTagging, eventAnnotation, dataViewFieldEditor, lens, security, triggersActionsUi, cases, @kbn/ml-date-picker, aiops, observabilityShared, discover, exploratoryView, fleet, maps, telemetry, dataVisualizer, ml, observability, banners, reporting, timelines, cloudSecurityPosture, runtimeFields, indexManagement, dashboardEnhanced, imageEmbeddable, graph, monitoring, securitySolution, synthetics, transform, uptime, cloudLinks, console, dataViewManagement, filesManagement, uiActions, visTypeVislib | - | +| | inspector, data, advancedSettings, savedObjects, embeddable, dataViewEditor, unifiedSearch, visualizations, controls, dashboard, licensing, savedObjectsTagging, eventAnnotation, dataViewFieldEditor, lens, security, triggersActionsUi, cases, @kbn/ml-date-picker, aiops, observabilityShared, exploratoryView, fleet, maps, telemetry, dataVisualizer, ml, observability, banners, reporting, timelines, cloudSecurityPosture, runtimeFields, indexManagement, dashboardEnhanced, imageEmbeddable, graph, monitoring, securitySolution, synthetics, transform, uptime, cloudLinks, console, dataViewManagement, filesManagement, uiActions, visTypeVislib | - | | | home, data, esUiShared, savedObjectsManagement, exploratoryView, fleet, ml, observability, apm, indexLifecycleManagement, observabilityOnboarding, synthetics, upgradeAssistant, uptime, ux, kibanaOverview | - | -| | share, uiActions, guidedOnboarding, home, management, data, advancedSettings, spaces, savedObjects, visualizations, serverless, controls, dashboard, savedObjectsTagging, expressionXY, lens, expressionMetricVis, expressionGauge, security, alerting, triggersActionsUi, cases, aiops, discover, exploratoryView, observabilityAIAssistant, fleet, maps, licenseManagement, dataVisualizer, ml, observability, infra, profiling, apm, expressionImage, expressionMetric, expressionError, expressionRevealImage, expressionRepeatImage, expressionShape, indexManagement, crossClusterReplication, enterpriseSearch, globalSearchBar, graph, grokdebugger, indexLifecycleManagement, ingestPipelines, logstash, monitoring, observabilityOnboarding, osquery, devTools, painlessLab, remoteClusters, rollup, searchprofiler, newsfeed, securitySolution, serverlessSearch, snapshotRestore, synthetics, transform, upgradeAssistant, uptime, ux, watcher, cloudDataMigration, console, dataViewManagement, filesManagement, kibanaOverview, visDefaultEditor, expressionHeatmap, expressionLegacyMetricVis, expressionPartitionVis, expressionTagcloud, visTypeTable, visTypeTimelion, visTypeTimeseries, visTypeVega, visTypeVislib | - | +| | share, uiActions, guidedOnboarding, home, management, advancedSettings, spaces, savedObjects, serverless, visualizations, controls, dashboard, savedObjectsTagging, expressionXY, lens, expressionMetricVis, expressionGauge, security, alerting, triggersActionsUi, cases, aiops, observabilityAIAssistant, exploratoryView, fleet, maps, licenseManagement, dataVisualizer, ml, observability, infra, profiling, apm, expressionImage, expressionMetric, expressionError, expressionRevealImage, expressionRepeatImage, expressionShape, indexManagement, crossClusterReplication, enterpriseSearch, globalSearchBar, graph, grokdebugger, indexLifecycleManagement, ingestPipelines, logstash, monitoring, observabilityOnboarding, osquery, devTools, painlessLab, remoteClusters, rollup, searchprofiler, newsfeed, securitySolution, serverlessSearch, snapshotRestore, synthetics, transform, upgradeAssistant, uptime, ux, watcher, cloudDataMigration, console, filesManagement, kibanaOverview, visDefaultEditor, expressionHeatmap, expressionLegacyMetricVis, expressionPartitionVis, expressionTagcloud, visTypeTable, visTypeTimelion, visTypeTimeseries, visTypeVega, visTypeVislib | - | | | encryptedSavedObjects, actions, data, ml, logstash, securitySolution, cloudChat | - | | | actions, ml, savedObjectsTagging, enterpriseSearch | - | | | @kbn/core-saved-objects-browser-internal, @kbn/core, savedObjects, presentationUtil, visualizations, aiops, ml, dataVisualizer, dashboardEnhanced, graph, lens, securitySolution, eventAnnotation, @kbn/core-saved-objects-browser-mocks | - | diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 821e19dfb5a776..959268b1e24713 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -592,7 +592,6 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [inspector_stats.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/search/search_source/inspect/inspector_stats.ts#:~:text=title), [response_writer.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/search/tabify/response_writer.ts#:~:text=title), [field.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/search/aggs/param_types/field.ts#:~:text=title), [get_display_value.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts#:~:text=title), [painless_error.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/errors/painless_error.tsx#:~:text=title), [agg_config.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/search/aggs/agg_config.test.ts#:~:text=title), [_terms_other_bucket_helper.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts#:~:text=title), [multi_terms.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts#:~:text=title), [multi_terms.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/search/aggs/buckets/multi_terms.test.ts#:~:text=title), [rare_terms.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/search/aggs/buckets/rare_terms.test.ts#:~:text=title)+ 3 more | - | | | [search_interceptor.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/search_interceptor/search_interceptor.ts#:~:text=toMountPoint), [search_interceptor.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/search_interceptor/search_interceptor.ts#:~:text=toMountPoint), [search_interceptor.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/search_interceptor/search_interceptor.ts#:~:text=toMountPoint), [search_interceptor.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/search_interceptor/search_interceptor.ts#:~:text=toMountPoint), [shard_failure_open_modal_button.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/shard_failure_modal/shard_failure_open_modal_button.tsx#:~:text=toMountPoint), [shard_failure_open_modal_button.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/shard_failure_modal/shard_failure_open_modal_button.tsx#:~:text=toMountPoint), [handle_warnings.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/fetch/handle_warnings.tsx#:~:text=toMountPoint), [handle_warnings.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/fetch/handle_warnings.tsx#:~:text=toMountPoint), [delete_button.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/session/sessions_mgmt/components/actions/delete_button.tsx#:~:text=toMountPoint), [delete_button.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/session/sessions_mgmt/components/actions/delete_button.tsx#:~:text=toMountPoint)+ 8 more | - | | | [get_columns.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/session/sessions_mgmt/lib/get_columns.tsx#:~:text=RedirectAppLinks), [get_columns.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/session/sessions_mgmt/lib/get_columns.tsx#:~:text=RedirectAppLinks), [get_columns.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/session/sessions_mgmt/lib/get_columns.tsx#:~:text=RedirectAppLinks), [connected_search_session_indicator.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/session/session_indicator/connected_search_session_indicator/connected_search_session_indicator.tsx#:~:text=RedirectAppLinks), [connected_search_session_indicator.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/session/session_indicator/connected_search_session_indicator/connected_search_session_indicator.tsx#:~:text=RedirectAppLinks), [connected_search_session_indicator.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/session/session_indicator/connected_search_session_indicator/connected_search_session_indicator.tsx#:~:text=RedirectAppLinks) | - | -| | [main.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/session/sessions_mgmt/components/main.tsx#:~:text=KibanaThemeProvider), [main.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/session/sessions_mgmt/components/main.tsx#:~:text=KibanaThemeProvider), [main.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/search/session/sessions_mgmt/components/main.tsx#:~:text=KibanaThemeProvider) | - | | | [session_service.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/server/search/session/session_service.ts#:~:text=authc) | - | | | [data_table.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx#:~:text=executeTriggerActions), [data_table.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx#:~:text=executeTriggerActions) | - | | | [persistable_state.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/query/filters/persistable_state.ts#:~:text=SavedObjectReference), [persistable_state.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/query/filters/persistable_state.ts#:~:text=SavedObjectReference), [persistable_state.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/query/filters/persistable_state.ts#:~:text=SavedObjectReference), [persistable_state.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/query/persistable_state.ts#:~:text=SavedObjectReference), [persistable_state.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/query/persistable_state.ts#:~:text=SavedObjectReference), [persistable_state.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data/common/query/persistable_state.ts#:~:text=SavedObjectReference) | - | @@ -604,7 +603,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [shared_imports.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_editor/public/shared_imports.ts#:~:text=toMountPoint), [open_editor.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_editor/public/open_editor.tsx#:~:text=toMountPoint), [open_editor.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_editor/public/open_editor.tsx#:~:text=toMountPoint) | - | +| | [shared_imports.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_editor/public/shared_imports.ts#:~:text=toMountPoint) | - | @@ -612,7 +611,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [shared_imports.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_field_editor/public/shared_imports.ts#:~:text=toMountPoint), [open_delete_modal.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_field_editor/public/open_delete_modal.tsx#:~:text=toMountPoint), [open_delete_modal.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_field_editor/public/open_delete_modal.tsx#:~:text=toMountPoint), [open_editor.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_field_editor/public/open_editor.tsx#:~:text=toMountPoint), [open_editor.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_field_editor/public/open_editor.tsx#:~:text=toMountPoint) | - | +| | [shared_imports.ts](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_field_editor/public/shared_imports.ts#:~:text=toMountPoint), [open_delete_modal.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_field_editor/public/open_delete_modal.tsx#:~:text=toMountPoint), [open_delete_modal.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_field_editor/public/open_delete_modal.tsx#:~:text=toMountPoint) | - | @@ -633,7 +632,6 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [table.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx#:~:text=getNonScriptedFields), [edit_index_pattern.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx#:~:text=getNonScriptedFields), [edit_index_pattern.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx#:~:text=getNonScriptedFields), [edit_index_pattern.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx#:~:text=getNonScriptedFields), [edit_index_pattern.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx#:~:text=getNonScriptedFields) | - | | | [scripted_fields_table.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx#:~:text=getScriptedFields) | - | | | [table.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx#:~:text=toMountPoint), [table.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx#:~:text=toMountPoint), [remove_data_view.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/components/edit_index_pattern/remove_data_view.tsx#:~:text=toMountPoint), [remove_data_view.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/components/edit_index_pattern/remove_data_view.tsx#:~:text=toMountPoint) | - | -| | [mount_management_section.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/management_app/mount_management_section.tsx#:~:text=KibanaThemeProvider), [mount_management_section.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/management_app/mount_management_section.tsx#:~:text=KibanaThemeProvider), [mount_management_section.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/data_view_management/public/management_app/mount_management_section.tsx#:~:text=KibanaThemeProvider) | - | @@ -686,8 +684,6 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=create), [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=create) | - | | | [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=create), [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=create) | - | | | [fetch_hits_in_interval.ts](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts#:~:text=EsQuerySearchAfter), [fetch_hits_in_interval.ts](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts#:~:text=EsQuerySearchAfter), [get_es_query_search_after.ts](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/context/utils/get_es_query_search_after.ts#:~:text=EsQuerySearchAfter), [get_es_query_search_after.ts](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/context/utils/get_es_query_search_after.ts#:~:text=EsQuerySearchAfter), [get_es_query_search_after.ts](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/context/utils/get_es_query_search_after.ts#:~:text=EsQuerySearchAfter) | - | -| | [use_alert_results_toast.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/main/hooks/use_alert_results_toast.tsx#:~:text=toMountPoint), [use_alert_results_toast.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/main/hooks/use_alert_results_toast.tsx#:~:text=toMountPoint), [use_context_app_fetch.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx#:~:text=toMountPoint), [use_context_app_fetch.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx#:~:text=toMountPoint), [use_context_app_fetch.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx#:~:text=toMountPoint), [use_context_app_fetch.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx#:~:text=toMountPoint), [not_found_route.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/not_found/not_found_route.tsx#:~:text=toMountPoint), [not_found_route.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/not_found/not_found_route.tsx#:~:text=toMountPoint), [view_alert_utils.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx#:~:text=toMountPoint), [view_alert_utils.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx#:~:text=toMountPoint)+ 4 more | - | -| | [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=KibanaThemeProvider), [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=KibanaThemeProvider), [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=KibanaThemeProvider), [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=KibanaThemeProvider), [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=KibanaThemeProvider), [show_open_search_panel.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx#:~:text=KibanaThemeProvider), [show_open_search_panel.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx#:~:text=KibanaThemeProvider), [show_open_search_panel.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx#:~:text=KibanaThemeProvider), [open_alerts_popover.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx#:~:text=KibanaThemeProvider), [open_alerts_popover.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx#:~:text=KibanaThemeProvider)+ 1 more | - | | | [on_save_search.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx#:~:text=SavedObjectSaveModal), [on_save_search.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx#:~:text=SavedObjectSaveModal) | 8.8.0 | | | [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=executeTriggerActions), [saved_search_embeddable.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx#:~:text=executeTriggerActions), [search_embeddable_factory.ts](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/embeddable/search_embeddable_factory.ts#:~:text=executeTriggerActions), [plugin.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/plugin.tsx#:~:text=executeTriggerActions) | - | | | [discover_state.test.ts](https://github.com/elastic/kibana/tree/main/src/plugins/discover/public/application/main/services/discover_state.test.ts#:~:text=savedObjects) | - | @@ -1525,7 +1521,7 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | | [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes), [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_types.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_types.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes), [legacy_migrations.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_migrations.ts#:~:text=SavedObjectAttributes) | - | | | [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject), [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject), [user_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/user_risk_score_dashboards.ts#:~:text=SavedObject), [user_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/user_risk_score_dashboards.ts#:~:text=SavedObject) | - | | | [timelines.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts#:~:text=convertToMultiNamespaceTypeVersion), [notes.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts#:~:text=convertToMultiNamespaceTypeVersion), [pinned_events.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts#:~:text=convertToMultiNamespaceTypeVersion), [legacy_saved_object_mappings.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/rule_actions/legacy_saved_object_mappings.ts#:~:text=convertToMultiNamespaceTypeVersion) | - | -| | [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [receiver.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [receiver.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID)+ 32 more | - | +| | [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [policy_hooks.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [api_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [lists.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID), [manifest_manager.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_ID)+ 32 more | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_NAME) | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION), [index.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts#:~:text=ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION) | - | | | [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [constants.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [form.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/utils.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/view/utils.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [service_actions.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/service/service_actions.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [service_actions.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/service/service_actions.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID), [service_actions.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/event_filters/service/service_actions.ts#:~:text=ENDPOINT_EVENT_FILTERS_LIST_ID)+ 30 more | - | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index baf4c031c779a4..6e77d89a3d7ba9 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 8768fdc466d4f8..54058ca1978d08 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] --- import devToolsObj from './dev_tools.devdocs.json'; diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index e11eee9229f3ac..b8906da5ca6094 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] --- import discoverObj from './discover.devdocs.json'; diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index 41d22a88e2b75b..7245ee4c0accc2 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: 2023-08-11 +date: 2023-08-14 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 3707e235a22a53..a9a8c9a5ba05d6 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ecsDataQualityDashboard'] --- import ecsDataQualityDashboardObj from './ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index a0cc256050116c..5e2f142c32d092 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: 2023-08-11 +date: 2023-08-14 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 dc872247c08b32..977de89ba11d93 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: 2023-08-11 +date: 2023-08-14 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 b8ae3f70429f6f..b32c88854c53d0 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: 2023-08-11 +date: 2023-08-14 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 4d918c25e31995..a53415fe5730f4 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: 2023-08-11 +date: 2023-08-14 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 ae1fde9c546da4..45364a1f98b557 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: 2023-08-11 +date: 2023-08-14 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 9f8dc9f5b9b915..7f997b0931d3eb 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] --- import eventAnnotationObj from './event_annotation.devdocs.json'; diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index 00143bfced399f..d8cd0a7c952f65 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; diff --git a/api_docs/exploratory_view.devdocs.json b/api_docs/exploratory_view.devdocs.json index 7a3538c920f496..c1a1a39096b3e0 100644 --- a/api_docs/exploratory_view.devdocs.json +++ b/api_docs/exploratory_view.devdocs.json @@ -1432,6 +1432,26 @@ "path": "x-pack/plugins/exploratory_view/public/plugin.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "exploratoryView", + "id": "def-public.ExploratoryViewPublicPluginsStart.observabilityAIAssistant", + "type": "Object", + "tags": [], + "label": "observabilityAIAssistant", + "description": [], + "signature": [ + { + "pluginId": "observabilityAIAssistant", + "scope": "public", + "docId": "kibObservabilityAIAssistantPluginApi", + "section": "def-public.ObservabilityAIAssistantPluginStart", + "text": "ObservabilityAIAssistantPluginStart" + } + ], + "path": "x-pack/plugins/exploratory_view/public/plugin.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/exploratory_view.mdx b/api_docs/exploratory_view.mdx index 57f0777fa174de..0e3917951134df 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'exploratoryView'] --- import exploratoryViewObj from './exploratory_view.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/uptime](https://github.com/orgs/elastic/teams/uptime) for ques | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 131 | 1 | 131 | 14 | +| 132 | 1 | 132 | 14 | ## Client diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index 2fb84dc9ccdd88..bf260f104ff66a 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: 2023-08-11 +date: 2023-08-14 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 f8ef4062ffbc45..5d871db277c38a 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: 2023-08-11 +date: 2023-08-14 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 ce5126b93e90f2..300bc7e6c10d0a 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: 2023-08-11 +date: 2023-08-14 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 a020a2e66e00ae..f94c1306a60360 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: 2023-08-11 +date: 2023-08-14 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 ef48277ab9cd4d..503733e8ff7a7b 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: 2023-08-11 +date: 2023-08-14 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 1b977292523ad5..50e9c87eab2cb3 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: 2023-08-11 +date: 2023-08-14 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 642b74242adc9e..49f37bbf78e086 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: 2023-08-11 +date: 2023-08-14 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 c638effc160945..ac77d91367ed00 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: 2023-08-11 +date: 2023-08-14 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 137582ef439e9c..2960d2af53241e 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: 2023-08-11 +date: 2023-08-14 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 09c931b8925de8..31245ca89351c0 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: 2023-08-11 +date: 2023-08-14 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 f8e2d01bb322b2..9266693d21d8f3 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: 2023-08-11 +date: 2023-08-14 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 41489f6ac5068e..a38d6aa1a791b4 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: 2023-08-11 +date: 2023-08-14 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 1bd60e3e92a564..dae84bef815dc9 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: 2023-08-11 +date: 2023-08-14 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 759c21715f053d..b6a62e9b1891db 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: 2023-08-11 +date: 2023-08-14 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 593b4dddf7d5f5..ecb06f5db3a32a 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: 2023-08-11 +date: 2023-08-14 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 9c3b51afd75eb8..bcdb95bcefb4a4 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: 2023-08-11 +date: 2023-08-14 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 cc71ba9086d31e..88d2f1eb855d6d 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: 2023-08-11 +date: 2023-08-14 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 5ad3f34a0734b1..af4c517bd4e483 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: 2023-08-11 +date: 2023-08-14 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 a059a665e74523..743c23d48be736 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: 2023-08-11 +date: 2023-08-14 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 dbbb11c5a8b8ba..919d501d6f26a2 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: 2023-08-11 +date: 2023-08-14 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 7af13b9297f0c6..12baece52b539f 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: 2023-08-11 +date: 2023-08-14 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 ae8f7589e3594b..f1bf80d72db4b1 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: 2023-08-11 +date: 2023-08-14 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 1e62c195536842..ec90ae82342a63 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: 2023-08-11 +date: 2023-08-14 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 c2bb2169499d54..f05f5416b0b1a6 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: 2023-08-11 +date: 2023-08-14 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 2a66e673fbbaec..7a579254b1793f 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] --- import indexLifecycleManagementObj from './index_lifecycle_management.devdocs.json'; diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index 2ee61cb7882ba7..bc978ecee51dc4 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] --- import indexManagementObj from './index_management.devdocs.json'; diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index b09189552053e1..78e5119f8542dc 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] --- import infraObj from './infra.devdocs.json'; diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index d2abcf38dba77c..43eec59fb7ae47 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: 2023-08-11 +date: 2023-08-14 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 4d349dce5e07be..2598bd53774e89 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: 2023-08-11 +date: 2023-08-14 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 cb29923a9f0111..990e8a707c3ea3 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] --- import kbnAceObj from './kbn_ace.devdocs.json'; diff --git a/api_docs/kbn_aiops_components.mdx b/api_docs/kbn_aiops_components.mdx index 39b40403ead736..5384ac8c40ede5 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: 2023-08-11 +date: 2023-08-14 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 c688a3102492d0..d03b69fc0c4dbd 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] --- import kbnAiopsUtilsObj from './kbn_aiops_utils.devdocs.json'; diff --git a/api_docs/kbn_alerting_state_types.mdx b/api_docs/kbn_alerting_state_types.mdx index 019e2d221f4626..8a2295e6628154 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-state-types'] --- import kbnAlertingStateTypesObj from './kbn_alerting_state_types.devdocs.json'; diff --git a/api_docs/kbn_alerts_as_data_utils.mdx b/api_docs/kbn_alerts_as_data_utils.mdx index 24a3b6a8fe2bf9..f5288a5a3df066 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: 2023-08-11 +date: 2023-08-14 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 602f8fff06f4c6..2227d5ef5eee19 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: 2023-08-11 +date: 2023-08-14 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 3799e21c31770b..51729a36d8fb19 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: 2023-08-11 +date: 2023-08-14 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 08b78279c60217..b66e5c1b0d9739 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] --- import kbnAnalyticsClientObj from './kbn_analytics_client.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 8d2a0dc59c7a0b..c82996d08386a8 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: 2023-08-11 +date: 2023-08-14 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 4fa7b3a9b54a30..7b866fca0071f1 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: 2023-08-11 +date: 2023-08-14 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 acea057c01244d..93ec32570d4e87 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: 2023-08-11 +date: 2023-08-14 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 a1aadb46780920..47a36161989d62 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_gainsight.mdx b/api_docs/kbn_analytics_shippers_gainsight.mdx index da337f1c108016..d0448fb33658ba 100644 --- a/api_docs/kbn_analytics_shippers_gainsight.mdx +++ b/api_docs/kbn_analytics_shippers_gainsight.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-gainsight title: "@kbn/analytics-shippers-gainsight" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-gainsight plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-gainsight'] --- import kbnAnalyticsShippersGainsightObj from './kbn_analytics_shippers_gainsight.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index ebfcb57436aecf..44417181d7e8f7 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: 2023-08-11 +date: 2023-08-14 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 54080540eb5d88..1118c083b3d360 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: 2023-08-11 +date: 2023-08-14 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.mdx b/api_docs/kbn_apm_synthtrace_client.mdx index f835e70d8c3da5..504096ae79dce0 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: 2023-08-11 +date: 2023-08-14 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 6aa982892d28e8..56d329613133ea 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: 2023-08-11 +date: 2023-08-14 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 fc2b56b4265286..d62814d153f1b6 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_cases_components.mdx b/api_docs/kbn_cases_components.mdx index ae5a205148b3f7..4079fc68550868 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: 2023-08-11 +date: 2023-08-14 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 23a2263cb230b6..1b814a33229498 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: 2023-08-11 +date: 2023-08-14 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 a9fe4235fbc5d8..4ecd4c65cb4985 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: 2023-08-11 +date: 2023-08-14 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 482c0a10c6bf02..802bd2be627462 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: 2023-08-11 +date: 2023-08-14 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 bf5b599ddd7d1f..6d86af733f86ed 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: 2023-08-11 +date: 2023-08-14 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 471b752fa61805..a870d211713248 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: 2023-08-11 +date: 2023-08-14 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 82544bf688cb31..a0a1c256c72f2d 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: 2023-08-11 +date: 2023-08-14 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 52fb86f92aef6d..20cde6ec2bd997 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: 2023-08-11 +date: 2023-08-14 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 26c692e06089b3..761e21d4b8b4af 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor'] --- import kbnCodeEditorObj from './kbn_code_editor.devdocs.json'; diff --git a/api_docs/kbn_code_editor_mocks.mdx b/api_docs/kbn_code_editor_mocks.mdx index 655b137b4bdbc5..246e8e5130b468 100644 --- a/api_docs/kbn_code_editor_mocks.mdx +++ b/api_docs/kbn_code_editor_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor-mocks title: "@kbn/code-editor-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor-mocks plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor-mocks'] --- import kbnCodeEditorMocksObj from './kbn_code_editor_mocks.devdocs.json'; diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index 5803262a815e36..01ef8ebf47619e 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] --- import kbnColoringObj from './kbn_coloring.devdocs.json'; diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index 2c0120f3425b3a..7616d423f5bd93 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: 2023-08-11 +date: 2023-08-14 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 8e6ea0f0965688..e076c29113ab4b 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: 2023-08-11 +date: 2023-08-14 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 c87360e2a14a13..bad8bc335ba3c5 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: 2023-08-11 +date: 2023-08-14 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.mdx b/api_docs/kbn_content_management_content_editor.mdx index b056fe5b8ccc10..b5898f65d38d45 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: 2023-08-11 +date: 2023-08-14 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 90d6d47e440ba0..dfa962ec2d50c5 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: 2023-08-11 +date: 2023-08-14 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.mdx b/api_docs/kbn_content_management_table_list_view.mdx index 16692165c6f42e..0c2051fcbcbe2d 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: 2023-08-11 +date: 2023-08-14 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_table.mdx b/api_docs/kbn_content_management_table_list_view_table.mdx index 66f20c8d9cf458..f44350fda15f38 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list-view-table'] --- import kbnContentManagementTableListViewTableObj from './kbn_content_management_table_list_view_table.devdocs.json'; diff --git a/api_docs/kbn_content_management_utils.mdx b/api_docs/kbn_content_management_utils.mdx index 52c847e782b0c7..47ee4a3d21c567 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: 2023-08-11 +date: 2023-08-14 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 d87f1f6d360f48..c4b84520af9a53 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: 2023-08-11 +date: 2023-08-14 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 a6a7db7de10a1f..88321f2b51d410 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: 2023-08-11 +date: 2023-08-14 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 729086a0a52fe4..02b2e7fffab824 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: 2023-08-11 +date: 2023-08-14 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 cdb500e4e7ecb9..b11fd1d1cec1b8 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: 2023-08-11 +date: 2023-08-14 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 f46ec5700900d9..df9c8fbe56d31c 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: 2023-08-11 +date: 2023-08-14 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 ecfc2d0a72e863..0c4ef485810942 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: 2023-08-11 +date: 2023-08-14 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 b98dfdef613c68..c37bba3efa1e27 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: 2023-08-11 +date: 2023-08-14 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 ac2e6b6237eaad..d5f719ee9427a1 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: 2023-08-11 +date: 2023-08-14 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 d0ab22bd7f1fba..aa30608970a7bd 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: 2023-08-11 +date: 2023-08-14 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 8082aea52c0ac9..491e7f2bfe8af2 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: 2023-08-11 +date: 2023-08-14 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 dab44f8f155efc..93acd337012e28 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: 2023-08-11 +date: 2023-08-14 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 322ac37d23d20a..1b25087729d1b9 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: 2023-08-11 +date: 2023-08-14 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 5f3c3a7e576e32..ef1a33f748ffbd 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: 2023-08-11 +date: 2023-08-14 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 c55082f7f8f6d2..1d9d0977d35696 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: 2023-08-11 +date: 2023-08-14 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 cd3c27e807218f..164c9cac10b615 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: 2023-08-11 +date: 2023-08-14 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 473e7c24a74edd..f8eaaf66109b85 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: 2023-08-11 +date: 2023-08-14 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 0afddf48eb2a50..a0b4ec03f7ca92 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: 2023-08-11 +date: 2023-08-14 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 0e366837d272dc..43f5294c3f09bd 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: 2023-08-11 +date: 2023-08-14 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 464ac26c55c68f..311cde6c47eea2 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: 2023-08-11 +date: 2023-08-14 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 db400b1c4c963a..f1610eb4b8ad93 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: 2023-08-11 +date: 2023-08-14 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 8e8335eff297ce..7bea44873c65f6 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: 2023-08-11 +date: 2023-08-14 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 7b284b301a1ba8..056422f072b5e7 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: 2023-08-11 +date: 2023-08-14 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 92725e4fe49dc6..7021e3c316cd03 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: 2023-08-11 +date: 2023-08-14 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 4d1939243cda7e..d668af8c99669d 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: 2023-08-11 +date: 2023-08-14 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 e261d217df6d95..8e3b3f7f60d4cf 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: 2023-08-11 +date: 2023-08-14 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 f2559ba6ff5f51..226a283596d0f1 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: 2023-08-11 +date: 2023-08-14 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 ed0ff4ca4177f6..59c3f93a05c7b8 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: 2023-08-11 +date: 2023-08-14 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 89f296b65cd905..c54680157ab8d7 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: 2023-08-11 +date: 2023-08-14 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 5ad25223390e24..043e5a90555c25 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: 2023-08-11 +date: 2023-08-14 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 c7d85f74c0c702..7fe69a9394fd96 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: 2023-08-11 +date: 2023-08-14 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 62243ce4fd2f7e..82a6f381934a8d 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: 2023-08-11 +date: 2023-08-14 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 9c91240f4bda20..46a65274c578e4 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: 2023-08-11 +date: 2023-08-14 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 0dc30a8e558911..da3b242e3a7a43 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: 2023-08-11 +date: 2023-08-14 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 4893876c4b5ae7..0bd6e4d719990e 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: 2023-08-11 +date: 2023-08-14 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 df31882bf401e6..867e60b08852c9 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: 2023-08-11 +date: 2023-08-14 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 788f8dc9471f0e..6507dffcb150c4 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: 2023-08-11 +date: 2023-08-14 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 b2f1647986eec0..27bcf1ecb8871d 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: 2023-08-11 +date: 2023-08-14 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 f310dec5221087..579db53a0f84d4 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: 2023-08-11 +date: 2023-08-14 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 d862cab3c2244e..25a67a053ff7ce 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: 2023-08-11 +date: 2023-08-14 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 e0ded39d87f168..7c6ded18d638d0 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: 2023-08-11 +date: 2023-08-14 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 dc09fd934b69f2..e9e953b4e894ba 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: 2023-08-11 +date: 2023-08-14 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 77b3caae052074..c9aa40682d21b0 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: 2023-08-11 +date: 2023-08-14 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 c9998c0b9c1003..d1c9ad0ac8c783 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: 2023-08-11 +date: 2023-08-14 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 14d3b05eaeac3c..2acf871dddedb1 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: 2023-08-11 +date: 2023-08-14 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 1bb966bac6c588..0f217c3d46e9a1 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: 2023-08-11 +date: 2023-08-14 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 7523a9aa74b214..fa5027c1164f7f 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: 2023-08-11 +date: 2023-08-14 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 965c1a0d82f28e..837900fbecbbe9 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: 2023-08-11 +date: 2023-08-14 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 20342df0349954..40e925d6cb4991 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: 2023-08-11 +date: 2023-08-14 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 b06cb5388daa2b..49a2f194354311 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: 2023-08-11 +date: 2023-08-14 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 eb0b650760f099..9c2d18f20a1cf6 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: 2023-08-11 +date: 2023-08-14 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 5dae3a60683d02..033f0a74371bee 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: 2023-08-11 +date: 2023-08-14 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 dc01dcfd287c62..b8c93e3fb653f2 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: 2023-08-11 +date: 2023-08-14 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 d2eb7d91215b64..51069800e502a5 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: 2023-08-11 +date: 2023-08-14 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 847d306e4ecc63..67420e1d7d42ab 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: 2023-08-11 +date: 2023-08-14 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 accbbcc822f39e..b5a97a9c0e855f 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: 2023-08-11 +date: 2023-08-14 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 a9cfa4a9853916..8f27aeff0848ff 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: 2023-08-11 +date: 2023-08-14 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 53654397c6b736..dd84ef10759de0 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: 2023-08-11 +date: 2023-08-14 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 cddeb36053c03d..d98986cf8ce5ad 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: 2023-08-11 +date: 2023-08-14 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 fb2f343bd34b0e..124357ce4a71d9 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: 2023-08-11 +date: 2023-08-14 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 38c9c4b1666c5b..d29e2fc28f8977 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: 2023-08-11 +date: 2023-08-14 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 5d04b545035354..6dd38761fe75cd 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: 2023-08-11 +date: 2023-08-14 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 90471ebf30566f..eb1a4b6afff5f1 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: 2023-08-11 +date: 2023-08-14 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 20df2146cc2751..1e07416e85f270 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: 2023-08-11 +date: 2023-08-14 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 d14acba0944508..c8e20fec38644b 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: 2023-08-11 +date: 2023-08-14 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 703ccfd34498b4..51f1626f0dd50d 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: 2023-08-11 +date: 2023-08-14 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 17d61c7be3fe7a..5dd7ae73c2ca0d 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: 2023-08-11 +date: 2023-08-14 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 6302b14522ad31..74c97035e7ca39 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: 2023-08-11 +date: 2023-08-14 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 9b9a489fda6389..2fd2c5571665a9 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: 2023-08-11 +date: 2023-08-14 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 2c49cb76a8551c..2a0fc8266fd0ab 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: 2023-08-11 +date: 2023-08-14 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 82728a75430e64..1956213ebb84e0 100644 --- a/api_docs/kbn_core_http_server.devdocs.json +++ b/api_docs/kbn_core_http_server.devdocs.json @@ -3327,6 +3327,10 @@ "plugin": "taskManager", "path": "x-pack/plugins/task_manager/server/routes/background_task_utilization.ts" }, + { + "plugin": "taskManager", + "path": "x-pack/plugins/task_manager/server/routes/metrics.ts" + }, { "plugin": "licensing", "path": "x-pack/plugins/licensing/server/routes/info.ts" @@ -3643,14 +3647,6 @@ "plugin": "triggersActionsUi", "path": "x-pack/plugins/triggers_actions_ui/server/routes/config.ts" }, - { - "plugin": "telemetry", - "path": "src/plugins/telemetry/server/routes/telemetry_config.ts" - }, - { - "plugin": "telemetry", - "path": "src/plugins/telemetry/server/routes/telemetry_last_reported.ts" - }, { "plugin": "savedObjectsTagging", "path": "x-pack/plugins/saved_objects_tagging/server/routes/tags/get_all_tags.ts" @@ -4385,27 +4381,27 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/find_rules/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/read_rule/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/find_rules/route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/read_rule/route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/route.ts" }, { "plugin": "securitySolution", @@ -5139,6 +5135,18 @@ "plugin": "taskManager", "path": "x-pack/plugins/task_manager/server/routes/health.test.ts" }, + { + "plugin": "taskManager", + "path": "x-pack/plugins/task_manager/server/routes/metrics.test.ts" + }, + { + "plugin": "taskManager", + "path": "x-pack/plugins/task_manager/server/routes/metrics.test.ts" + }, + { + "plugin": "taskManager", + "path": "x-pack/plugins/task_manager/server/routes/metrics.test.ts" + }, { "plugin": "triggersActionsUi", "path": "x-pack/plugins/triggers_actions_ui/server/routes/config.test.ts" @@ -6469,18 +6477,6 @@ "plugin": "ml", "path": "x-pack/plugins/ml/server/routes/job_service.ts" }, - { - "plugin": "telemetry", - "path": "src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts" - }, - { - "plugin": "telemetry", - "path": "src/plugins/telemetry/server/routes/telemetry_opt_in.ts" - }, - { - "plugin": "telemetry", - "path": "src/plugins/telemetry/server/routes/telemetry_usage_stats.ts" - }, { "plugin": "savedObjectsTagging", "path": "x-pack/plugins/saved_objects_tagging/server/routes/tags/create_tag.ts" @@ -7119,63 +7115,63 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_create_rules/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_delete_rules/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/create_rule/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/coverage_overview/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_create_rules/route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_delete_rules/route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/create_rule/route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/coverage_overview/route.ts" }, { "plugin": "securitySolution", @@ -7433,22 +7429,6 @@ "plugin": "savedObjectsManagement", "path": "src/plugins/saved_objects_management/server/routes/scroll_count.ts" }, - { - "plugin": "telemetry", - "path": "src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts" - }, - { - "plugin": "telemetry", - "path": "src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts" - }, - { - "plugin": "telemetry", - "path": "src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts" - }, - { - "plugin": "telemetry", - "path": "src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts" - }, { "plugin": "visTypeTimelion", "path": "src/plugins/vis_types/timelion/server/routes/run.ts" @@ -8607,14 +8587,6 @@ "plugin": "guidedOnboarding", "path": "src/plugins/guided_onboarding/server/routes/plugin_state_routes.ts" }, - { - "plugin": "telemetry", - "path": "src/plugins/telemetry/server/routes/telemetry_user_has_seen_notice.ts" - }, - { - "plugin": "telemetry", - "path": "src/plugins/telemetry/server/routes/telemetry_last_reported.ts" - }, { "plugin": "fleet", "path": "x-pack/plugins/fleet/server/services/security/fleet_router.ts" @@ -8877,15 +8849,15 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_update_rules/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/update_rule/route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_update_rules/route.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts" + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/update_rule/route.ts" }, { "plugin": "securitySolution", @@ -14046,6 +14018,18 @@ "plugin": "ml", "path": "x-pack/plugins/ml/server/routes/management.ts" }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_config.ts" + }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_config.ts" + }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_last_reported.ts" + }, { "plugin": "logsShared", "path": "x-pack/plugins/logs_shared/server/lib/adapters/framework/kibana_framework_adapter.ts" @@ -14574,6 +14558,14 @@ "plugin": "ml", "path": "x-pack/plugins/ml/server/routes/trained_models.ts" }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_user_has_seen_notice.ts" + }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_last_reported.ts" + }, { "plugin": "logsShared", "path": "x-pack/plugins/logs_shared/server/lib/adapters/framework/kibana_framework_adapter.ts" @@ -15082,6 +15074,18 @@ "plugin": "ml", "path": "x-pack/plugins/ml/server/routes/alerting.ts" }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts" + }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_opt_in.ts" + }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_usage_stats.ts" + }, { "plugin": "logsShared", "path": "x-pack/plugins/logs_shared/server/lib/adapters/framework/kibana_framework_adapter.ts" @@ -15234,6 +15238,22 @@ "plugin": "dataViewFieldEditor", "path": "src/plugins/data_view_field_editor/server/routes/field_preview.ts" }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts" + }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts" + }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts" + }, + { + "plugin": "telemetry", + "path": "src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts" + }, { "plugin": "@kbn/core-http-router-server-internal", "path": "packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.ts" diff --git a/api_docs/kbn_core_http_server.mdx b/api_docs/kbn_core_http_server.mdx index 82667ac4ef3c73..242467416fc910 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: 2023-08-11 +date: 2023-08-14 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 ac140d73e2b381..44a2cb41beb316 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: 2023-08-11 +date: 2023-08-14 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 a48c4e42074b7f..639692a55c973b 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: 2023-08-11 +date: 2023-08-14 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 41ec6e7832ade0..dcc8d2d6f855c7 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: 2023-08-11 +date: 2023-08-14 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 1e5ae6297f990f..fc35e2916cdc54 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: 2023-08-11 +date: 2023-08-14 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 21f486618582ad..31c25b3ab55503 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: 2023-08-11 +date: 2023-08-14 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 3ab45c144385e7..f82d6c9669de81 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: 2023-08-11 +date: 2023-08-14 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 a235f73337acf6..53f6f9d4e6c9b5 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: 2023-08-11 +date: 2023-08-14 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 fbf3e937342a94..9713f2421c0de3 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: 2023-08-11 +date: 2023-08-14 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 2acbf405a37672..381cdd6437dbb6 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: 2023-08-11 +date: 2023-08-14 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 271158872085c5..1f4d0affd0bebc 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: 2023-08-11 +date: 2023-08-14 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 f181b8adde3e17..fad0248c5d0a96 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: 2023-08-11 +date: 2023-08-14 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 e37b57748c89de..740f1c16ba0e0e 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: 2023-08-11 +date: 2023-08-14 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 618616c93b3bf0..f2233632dfee86 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: 2023-08-11 +date: 2023-08-14 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 3bdee1a4b465b4..6a3712da9faf09 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: 2023-08-11 +date: 2023-08-14 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 7529be3283e76a..be51ee6d645e28 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: 2023-08-11 +date: 2023-08-14 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 73faa2a2c058c0..84e4ba73afb2ac 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: 2023-08-11 +date: 2023-08-14 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 e62baf21a6a26a..9b4486c3718534 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: 2023-08-11 +date: 2023-08-14 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 2787674950f97c..db633a773bb085 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: 2023-08-11 +date: 2023-08-14 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 8d17c99d5aefff..e0c415d680a23f 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: 2023-08-11 +date: 2023-08-14 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 d01bf0eef4e0b5..00354370e26002 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: 2023-08-11 +date: 2023-08-14 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 db641c23b5c167..2b35e4c63f9f4b 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: 2023-08-11 +date: 2023-08-14 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 76ab578ca9ac5c..52f822eaff515d 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: 2023-08-11 +date: 2023-08-14 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 f4857e75484b81..0f887a083a4fc0 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: 2023-08-11 +date: 2023-08-14 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 48bd68f7423fd1..055d7cb649d46a 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: 2023-08-11 +date: 2023-08-14 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 4d870950003b45..6b029fa9bbf477 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: 2023-08-11 +date: 2023-08-14 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 130657c82057ee..a96d6180d42705 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: 2023-08-11 +date: 2023-08-14 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 b640733927526b..3b99b3571ee3b8 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: 2023-08-11 +date: 2023-08-14 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 6ab1207dba9d80..eba9fb646e5380 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: 2023-08-11 +date: 2023-08-14 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 b61883ef0faf37..bae99044d202f9 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: 2023-08-11 +date: 2023-08-14 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 de5e9a65e03e1e..089d4a88cfd644 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: 2023-08-11 +date: 2023-08-14 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 3674b168de8e59..6885e63f3e3ae1 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: 2023-08-11 +date: 2023-08-14 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 1167151882fc8e..1d88404c88b827 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: 2023-08-11 +date: 2023-08-14 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 338dc9621271fa..1d7744efe8beab 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: 2023-08-11 +date: 2023-08-14 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 4953be8f184dd0..1d117a4c1d09f1 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: 2023-08-11 +date: 2023-08-14 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 5695cf5c0cbda5..5738a5220b4924 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: 2023-08-11 +date: 2023-08-14 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 9fd366713450b1..a9c7d61cf3c6bc 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: 2023-08-11 +date: 2023-08-14 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_server.mdx b/api_docs/kbn_core_plugins_server.mdx index 98fc42f121edbd..cde7e04eaec5a2 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: 2023-08-11 +date: 2023-08-14 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 1916d0fc6b4192..bc601ca5e2e2c1 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: 2023-08-11 +date: 2023-08-14 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 d316fbfd4e5063..74b49cbe0bc0e0 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: 2023-08-11 +date: 2023-08-14 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 a5b86aa2e0595d..bffbfd135a0791 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: 2023-08-11 +date: 2023-08-14 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 d89ba8654171af..859beec1afa160 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: 2023-08-11 +date: 2023-08-14 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 1ea63418e6ef5f..a6eeac381726b4 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: 2023-08-11 +date: 2023-08-14 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 f93fff0c1fcf8f..1c2bf2b3494cb3 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: 2023-08-11 +date: 2023-08-14 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 3c98b8e7cd8c59..c519f7303168fc 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: 2023-08-11 +date: 2023-08-14 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 bce1c4dfbbe6a9..f3167c5ee80936 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: 2023-08-11 +date: 2023-08-14 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 43a76d7b9cf368..34b9e9e8896b16 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: 2023-08-11 +date: 2023-08-14 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 6eb08f9729cd2d..222d66943e6e72 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: 2023-08-11 +date: 2023-08-14 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 3e83a424f42e0e..3d794274ce45bd 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: 2023-08-11 +date: 2023-08-14 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 146223dc48caaa..9d55291a90ded3 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: 2023-08-11 +date: 2023-08-14 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 0a47fb79af85fb..d4e0201b2b1cc6 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: 2023-08-11 +date: 2023-08-14 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 92b4800d929222..b30e0f9a606ba9 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: 2023-08-11 +date: 2023-08-14 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 bbb8a825d4d4fc..e7db768f37c416 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: 2023-08-11 +date: 2023-08-14 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 ceefaf5cbeafb9..4eeae8f4ea2698 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: 2023-08-11 +date: 2023-08-14 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 1faf6ebe0e53d2..7510dd11ad002c 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: 2023-08-11 +date: 2023-08-14 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 f6bed826ea1c37..85f50f732d09b1 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: 2023-08-11 +date: 2023-08-14 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 d0a3fa14c77cda..6bba7ad7b4d4d7 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: 2023-08-11 +date: 2023-08-14 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 25dad7079d5103..2acfe284aaa0b2 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: 2023-08-11 +date: 2023-08-14 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 106a0457952e27..c8545d6c436fa7 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: 2023-08-11 +date: 2023-08-14 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 7d0144334d52ef..f8bef1c7c0d360 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: 2023-08-11 +date: 2023-08-14 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 a399c0f1329533..b0f30590cbe4f1 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: 2023-08-11 +date: 2023-08-14 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 29377aff2208dc..e2460624f0ce36 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: 2023-08-11 +date: 2023-08-14 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 1e4a2823e38b59..5d4d3930d3b18a 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: 2023-08-11 +date: 2023-08-14 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.mdx b/api_docs/kbn_core_status_common_internal.mdx index 8e67896ef64c6c..e2a73177084cf3 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common-internal'] --- import kbnCoreStatusCommonInternalObj from './kbn_core_status_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx index 86ca8260635d6b..2e831a9b97b201 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: 2023-08-11 +date: 2023-08-14 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 bca4e58d6f2832..d9e345f44c476b 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: 2023-08-11 +date: 2023-08-14 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 775f6ed0325cc6..94b7ec147df9e7 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: 2023-08-11 +date: 2023-08-14 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 8600a414da1c73..616908041be555 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: 2023-08-11 +date: 2023-08-14 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 9a63c6f8684a42..f086d7912a8996 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: 2023-08-11 +date: 2023-08-14 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 27d8c8c3615d97..58a223618b859e 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: 2023-08-11 +date: 2023-08-14 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_so_type_serializer.mdx b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx index 03a818dbc9f8ae..feb1fd0fb50e63 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: 2023-08-11 +date: 2023-08-14 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 df096bb32afe73..e2bc75e8fee26b 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: 2023-08-11 +date: 2023-08-14 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 1a690ce8ca3179..c8aa81cd81b586 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: 2023-08-11 +date: 2023-08-14 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 078b5b61c87afd..b407782e900c3f 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: 2023-08-11 +date: 2023-08-14 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 de8f5e37a71175..c947a948f76497 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: 2023-08-11 +date: 2023-08-14 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 42888989c1d710..afcced69ef7969 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: 2023-08-11 +date: 2023-08-14 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 dc0eacf1817631..18a9b178798c79 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: 2023-08-11 +date: 2023-08-14 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 72338cc121aff4..93dda9bcf1bfd3 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: 2023-08-11 +date: 2023-08-14 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 6e403fc98be17e..4cee2aabd2455a 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: 2023-08-11 +date: 2023-08-14 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 37cd3bb5e6fc8e..05ee66d45bf0dd 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: 2023-08-11 +date: 2023-08-14 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 9b2d559647af40..e798d9b1f1fa87 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: 2023-08-11 +date: 2023-08-14 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 97c8a42e92e5f8..2b6a6270c84fa4 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: 2023-08-11 +date: 2023-08-14 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 b7ba1c6982cda9..7dd76278ddbab9 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: 2023-08-11 +date: 2023-08-14 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 b44078da120ced..8f73e165162d69 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: 2023-08-11 +date: 2023-08-14 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 db3bd1c4a212f0..dee4289bc05663 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: 2023-08-11 +date: 2023-08-14 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 3c1fd63d98ffcc..f2a20d74a06c68 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: 2023-08-11 +date: 2023-08-14 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 581b2d19a67dcc..e0d37e9ad00fd5 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: 2023-08-11 +date: 2023-08-14 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 2bdd551a726e87..75aa743eaf9c21 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: 2023-08-11 +date: 2023-08-14 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 e3e5fc256186d8..7f8b335a8cda72 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_cypress_config.mdx b/api_docs/kbn_cypress_config.mdx index 0e99bc11beab05..27c4f292d99674 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cypress-config'] --- import kbnCypressConfigObj from './kbn_cypress_config.devdocs.json'; diff --git a/api_docs/kbn_data_service.mdx b/api_docs/kbn_data_service.mdx index 50681528b9a02e..7126b3ea9e67b0 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/data-service'] --- import kbnDataServiceObj from './kbn_data_service.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 14a40b92fc24fd..b1daffbd5dfa2a 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: 2023-08-11 +date: 2023-08-14 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 34a8335a386a9d..ab07ed38bbb193 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: 2023-08-11 +date: 2023-08-14 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 81a5082f51fa58..398e65e9c0a0bd 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: 2023-08-11 +date: 2023-08-14 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 136927962625b7..ab794e410b6bb0 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: 2023-08-11 +date: 2023-08-14 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 3ff66d5b3b6e08..6f3debecd29e04 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-ml'] --- import kbnDeeplinksMlObj from './kbn_deeplinks_ml.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_observability.mdx b/api_docs/kbn_deeplinks_observability.mdx index cea675bd91dd1b..70b569be8b75e7 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/deeplinks-observability'] --- import kbnDeeplinksObservabilityObj from './kbn_deeplinks_observability.devdocs.json'; diff --git a/api_docs/kbn_deeplinks_search.mdx b/api_docs/kbn_deeplinks_search.mdx index 1ef5a03b94b42b..8b4cdcc2870e42 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: 2023-08-11 +date: 2023-08-14 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 3c7a0628ae9b55..db328c46247b83 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: 2023-08-11 +date: 2023-08-14 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 a1d8f1ed2eced2..eb3c3e4f3b7455 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: 2023-08-11 +date: 2023-08-14 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 dc8f97efe3d840..1cd1a27d737eee 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: 2023-08-11 +date: 2023-08-14 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 1ea4a59543aacd..9d8ea36051196c 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: 2023-08-11 +date: 2023-08-14 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 1cf6474003e1f7..70ab434fcd6335 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: 2023-08-11 +date: 2023-08-14 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 fdb7f633debefa..20917eb15c9de9 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: 2023-08-11 +date: 2023-08-14 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 0ff3c5adfe9900..e50043b29e2b9d 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: 2023-08-11 +date: 2023-08-14 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 10a222b06b4602..54f5525834feb4 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: 2023-08-11 +date: 2023-08-14 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 33e3c7a1c69ba7..cd8b7f9fdc1cd6 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: 2023-08-11 +date: 2023-08-14 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 4f12dca89d97b9..935cfe90d5e5ff 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: 2023-08-11 +date: 2023-08-14 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 d26122b1bb5d95..1a4814fbedc26f 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: 2023-08-11 +date: 2023-08-14 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 e669402a386211..2320841db89278 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: 2023-08-11 +date: 2023-08-14 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 e471065d680c30..c8f598e9227ad9 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: 2023-08-11 +date: 2023-08-14 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 59b24bd57d6486..d9854f2b2c712e 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: 2023-08-11 +date: 2023-08-14 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 1118c9ab845110..122cec13b65709 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: 2023-08-11 +date: 2023-08-14 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_assistant.mdx b/api_docs/kbn_elastic_assistant.mdx index 9160fea2f0a293..b5a5c12fb3f38c 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/elastic-assistant'] --- import kbnElasticAssistantObj from './kbn_elastic_assistant.devdocs.json'; diff --git a/api_docs/kbn_es.mdx b/api_docs/kbn_es.mdx index 8f88080d35e902..259f3032ed423c 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: 2023-08-11 +date: 2023-08-14 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 2ad08088203e4f..0fafef0ea18666 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: 2023-08-11 +date: 2023-08-14 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 1387cc6ec52bca..bb370d9cb46541 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: 2023-08-11 +date: 2023-08-14 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 3a7c2bcdd36851..c8d02878757b0c 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: 2023-08-11 +date: 2023-08-14 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 464d5e31f05a72..e476d8e8cea909 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: 2023-08-11 +date: 2023-08-14 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 90c2304279417f..b57f88e48ded9b 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_event_annotation_common.mdx b/api_docs/kbn_event_annotation_common.mdx index 199fb1758b0a2e..911e9420465b84 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: 2023-08-11 +date: 2023-08-14 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 bc2407357d7472..097e920eaad267 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: 2023-08-11 +date: 2023-08-14 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 1d4f974c56b10c..262444a91681dd 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: 2023-08-11 +date: 2023-08-14 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 1a8859de0092fe..be805539e3ff48 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] --- import kbnFieldTypesObj from './kbn_field_types.devdocs.json'; diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index c892feccae45c6..0c60172f6fb0fe 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: 2023-08-11 +date: 2023-08-14 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 829256156269fc..cd37d77da57f39 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: 2023-08-11 +date: 2023-08-14 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_generate.mdx b/api_docs/kbn_generate.mdx index ab0b3ba5adf5da..5ef7a6ca082cf0 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: 2023-08-11 +date: 2023-08-14 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 4c3415b9419799..bf433af79e55d1 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: 2023-08-11 +date: 2023-08-14 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 fb778684f8a184..ea1b2176dc091c 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv'] --- import kbnGenerateCsvObj from './kbn_generate_csv.devdocs.json'; diff --git a/api_docs/kbn_generate_csv_types.mdx b/api_docs/kbn_generate_csv_types.mdx index 1c0ccec4b7b313..2f143222e5cf54 100644 --- a/api_docs/kbn_generate_csv_types.mdx +++ b/api_docs/kbn_generate_csv_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-csv-types title: "@kbn/generate-csv-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-csv-types plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv-types'] --- import kbnGenerateCsvTypesObj from './kbn_generate_csv_types.devdocs.json'; diff --git a/api_docs/kbn_guided_onboarding.mdx b/api_docs/kbn_guided_onboarding.mdx index 357c27fe461aa0..6a887d9ecdc9a4 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: 2023-08-11 +date: 2023-08-14 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 8f3eda505dd312..e5650091e91783 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: 2023-08-11 +date: 2023-08-14 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 39fe818ab9c672..7cbefdaa8550d9 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: 2023-08-11 +date: 2023-08-14 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 3d27bbc363d878..4f01041faf2f78 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: 2023-08-11 +date: 2023-08-14 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 f716e67a770820..a37d23d355e178 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: 2023-08-11 +date: 2023-08-14 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 d823dfaed566c9..14cdefab8d1cf5 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: 2023-08-11 +date: 2023-08-14 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 d4afb11fff5ccd..cf1eacd5b7c80b 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: 2023-08-11 +date: 2023-08-14 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 d2e51134eece6c..852d462e67fa33 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: 2023-08-11 +date: 2023-08-14 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 8b1b64c7863417..6f8e8eaf5820f5 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: 2023-08-11 +date: 2023-08-14 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 9f07802dc31409..44f6b022a401b8 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: 2023-08-11 +date: 2023-08-14 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 a68e7147bcf669..25d4a94a9a1565 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: 2023-08-11 +date: 2023-08-14 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 d8526125fde893..8814419c3e07d9 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: 2023-08-11 +date: 2023-08-14 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 7558844d53dd38..661a389c0fc509 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: 2023-08-11 +date: 2023-08-14 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 2288a800a03413..898bbfa5c27130 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: 2023-08-11 +date: 2023-08-14 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 2bfa83b074b93f..c693d824ce947f 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: 2023-08-11 +date: 2023-08-14 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 d9dae665142cd3..564f063dfcb035 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: 2023-08-11 +date: 2023-08-14 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.mdx b/api_docs/kbn_language_documentation_popover.mdx index 5b18bd1a33bf02..767b7c911d0cdc 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/language-documentation-popover'] --- import kbnLanguageDocumentationPopoverObj from './kbn_language_documentation_popover.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 297bcc0e8b9416..7351a56b311bd2 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: 2023-08-11 +date: 2023-08-14 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 2710699ca62eb0..e8a7d8599359a9 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] --- import kbnLoggingMocksObj from './kbn_logging_mocks.devdocs.json'; diff --git a/api_docs/kbn_managed_vscode_config.mdx b/api_docs/kbn_managed_vscode_config.mdx index 5c179cb68f54aa..6568bb2a4759d4 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: 2023-08-11 +date: 2023-08-14 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 eda58daa0f4b7f..4248aae3c1b50f 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: 2023-08-11 +date: 2023-08-14 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_storybook_config.mdx b/api_docs/kbn_management_storybook_config.mdx index 1f4627550c1354..bbb4768e4c6452 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: 2023-08-11 +date: 2023-08-14 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 296d30cd04cd73..2e010aec2a94e7 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: 2023-08-11 +date: 2023-08-14 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 c8483ca9edc37c..f611fb005cf406 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: 2023-08-11 +date: 2023-08-14 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 d2ef8d1d4f117d..284079ad9f917c 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: 2023-08-11 +date: 2023-08-14 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.mdx b/api_docs/kbn_ml_anomaly_utils.mdx index fe52cb3eb08ca3..d9760f50970793 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-anomaly-utils'] --- import kbnMlAnomalyUtilsObj from './kbn_ml_anomaly_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_category_validator.mdx b/api_docs/kbn_ml_category_validator.mdx index 0ba49a496063c5..38ca4f9e6dfc7c 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: 2023-08-11 +date: 2023-08-14 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_data_frame_analytics_utils.mdx b/api_docs/kbn_ml_data_frame_analytics_utils.mdx index dd7b234563cfab..badaa40cf7f760 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: 2023-08-11 +date: 2023-08-14 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 976408c3c872c0..33452f37700637 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: 2023-08-11 +date: 2023-08-14 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 5172db2cc8b54d..01d27a308f26e4 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: 2023-08-11 +date: 2023-08-14 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 2768209f55d254..8a1d19207b3969 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: 2023-08-11 +date: 2023-08-14 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 ed21c797bbf89e..c49d0244838b77 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: 2023-08-11 +date: 2023-08-14 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 a9e831490f3ea7..9b447e4c80551d 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: 2023-08-11 +date: 2023-08-14 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 3c948ad94cdd79..3f89161163597b 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: 2023-08-11 +date: 2023-08-14 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 5aea91d505819a..ccc74b59e61758 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: 2023-08-11 +date: 2023-08-14 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 c7cb02299f4344..580e5bac0f0446 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: 2023-08-11 +date: 2023-08-14 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 ee9b12f62801b6..35a8e66ddf69c2 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: 2023-08-11 +date: 2023-08-14 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 4cedbeff87dde3..505c9f7efcc8b1 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: 2023-08-11 +date: 2023-08-14 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 68e793d44c20c0..7a646516150961 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: 2023-08-11 +date: 2023-08-14 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 97a971f001786f..f3532e843c5c71 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: 2023-08-11 +date: 2023-08-14 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 c9ebd984cb6328..3be60df080cad5 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: 2023-08-11 +date: 2023-08-14 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 a6054abca2f5b6..6027c7047de7d0 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: 2023-08-11 +date: 2023-08-14 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 812d7b5fc02920..a7a6b49e78787e 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: 2023-08-11 +date: 2023-08-14 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 a71021687533bd..a3fd06a5529d2d 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: 2023-08-11 +date: 2023-08-14 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 ad053696f1b71d..1ef0a9b74cdb8d 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: 2023-08-11 +date: 2023-08-14 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_url_state.mdx b/api_docs/kbn_ml_url_state.mdx index a5544638cc9ce5..a687d7d273c4c5 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-url-state'] --- import kbnMlUrlStateObj from './kbn_ml_url_state.devdocs.json'; diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index 3c005974008166..baf45a41c74622 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: 2023-08-11 +date: 2023-08-14 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 d71eebcd036bf1..e07efe125be2c3 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: 2023-08-11 +date: 2023-08-14 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 92f7ecfe042173..fa9d920c82c82d 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-alert-details'] --- import kbnObservabilityAlertDetailsObj from './kbn_observability_alert_details.devdocs.json'; diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index db9ac01e9e23e9..f9f27545b2ed5f 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: 2023-08-11 +date: 2023-08-14 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 b7f8fc8418bc3d..25f5167fd3465c 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: 2023-08-11 +date: 2023-08-14 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 4cb6e3324db9c4..cc8647ebd44b7d 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: 2023-08-11 +date: 2023-08-14 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_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index c65b95b481a52c..84eff3ebc68d02 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: 2023-08-11 +date: 2023-08-14 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_generator.mdx b/api_docs/kbn_plugin_generator.mdx index ebed5ed1a344f5..605ba415df0602 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: 2023-08-11 +date: 2023-08-14 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 13e2e68a03566e..2bdf3ca252c651 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] --- import kbnPluginHelpersObj from './kbn_plugin_helpers.devdocs.json'; diff --git a/api_docs/kbn_random_sampling.mdx b/api_docs/kbn_random_sampling.mdx index a5d463766f0b94..53180a2d0aa43c 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: 2023-08-11 +date: 2023-08-14 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 0f3e624aaf6882..925e8002cbfb0c 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: 2023-08-11 +date: 2023-08-14 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 3fd442396eeb50..10828baa2a836e 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: 2023-08-11 +date: 2023-08-14 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 d072b7ef1502f6..7a65c4b3b0ef98 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: 2023-08-11 +date: 2023-08-14 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 d4101bfac8ea2d..e9ff0f7c621b0b 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: 2023-08-11 +date: 2023-08-14 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 b297a1726adfd8..98a5ce76ec1df6 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: 2023-08-11 +date: 2023-08-14 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 e9bd0f742c8619..fb7e6df35e2a1f 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: 2023-08-11 +date: 2023-08-14 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 74b358802933be..4a6a590cad0aad 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: 2023-08-11 +date: 2023-08-14 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 10545441cf6513..91f984a7165029 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: 2023-08-11 +date: 2023-08-14 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 b89bd8d34f20b6..c8f5d49bf4f721 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: 2023-08-11 +date: 2023-08-14 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 8568a60356cac2..e80f3d0f6008ff 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: 2023-08-11 +date: 2023-08-14 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 cacddca765224c..47ae26a0e007e0 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: 2023-08-11 +date: 2023-08-14 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 80ad5a6c089033..41537f918d61c4 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-common'] --- import kbnReportingCommonObj from './kbn_reporting_common.devdocs.json'; diff --git a/api_docs/kbn_rison.mdx b/api_docs/kbn_rison.mdx index d72498527b77ef..038db1ec61a561 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rison'] --- import kbnRisonObj from './kbn_rison.devdocs.json'; diff --git a/api_docs/kbn_rrule.mdx b/api_docs/kbn_rrule.mdx index ecc3904044fafb..14805b80aebf9b 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: 2023-08-11 +date: 2023-08-14 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 332bb5be49d28f..ab8a607a575225 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: 2023-08-11 +date: 2023-08-14 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 9b60d4a33bda96..811c5bc4f69fd5 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: 2023-08-11 +date: 2023-08-14 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 new file mode 100644 index 00000000000000..5f1b06dd23303a --- /dev/null +++ b/api_docs/kbn_search_api_panels.devdocs.json @@ -0,0 +1,1051 @@ +{ + "id": "@kbn/search-api-panels", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.CodeBox", + "type": "Function", + "tags": [], + "label": "CodeBox", + "description": [], + "signature": [ + "({ application, codeSnippet, http, languageType, languages, pluginId, selectedLanguage, setSelectedLanguage, sharePlugin, showTryInConsole, }: React.PropsWithChildren) => JSX.Element" + ], + "path": "packages/kbn-search-api-panels/components/code_box.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.CodeBox.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n application,\n codeSnippet,\n http,\n languageType,\n languages,\n pluginId,\n selectedLanguage,\n setSelectedLanguage,\n sharePlugin,\n showTryInConsole,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren" + ], + "path": "packages/kbn-search-api-panels/components/code_box.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.GithubLink", + "type": "Function", + "tags": [], + "label": "GithubLink", + "description": [], + "signature": [ + "({ label, href, http, pluginId }: React.PropsWithChildren<{ label: string; href: string; http: ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + }, + "; pluginId: string; }>) => JSX.Element" + ], + "path": "packages/kbn-search-api-panels/components/github_link.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.GithubLink.$1", + "type": "CompoundType", + "tags": [], + "label": "{ label, href, http, pluginId }", + "description": [], + "signature": [ + "React.PropsWithChildren<{ label: string; href: string; http: ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + }, + "; pluginId: string; }>" + ], + "path": "packages/kbn-search-api-panels/components/github_link.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.IngestData", + "type": "Function", + "tags": [], + "label": "IngestData", + "description": [], + "signature": [ + "({ codeSnippet, selectedLanguage, setSelectedLanguage, docLinks, http, pluginId, application, sharePlugin, languages, showTryInConsole, }: React.PropsWithChildren) => JSX.Element" + ], + "path": "packages/kbn-search-api-panels/components/ingest_data.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.IngestData.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n codeSnippet,\n selectedLanguage,\n setSelectedLanguage,\n docLinks,\n http,\n pluginId,\n application,\n sharePlugin,\n languages,\n showTryInConsole,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren" + ], + "path": "packages/kbn-search-api-panels/components/ingest_data.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.InstallClientPanel", + "type": "Function", + "tags": [], + "label": "InstallClientPanel", + "description": [], + "signature": [ + "({ codeSnippet, showTryInConsole, language, languages, setSelectedLanguage, http, pluginId, application, sharePlugin, isPanelLeft, overviewPanelProps, }: React.PropsWithChildren) => JSX.Element" + ], + "path": "packages/kbn-search-api-panels/components/install_client.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.InstallClientPanel.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n codeSnippet,\n showTryInConsole,\n language,\n languages,\n setSelectedLanguage,\n http,\n pluginId,\n application,\n sharePlugin,\n isPanelLeft = true,\n overviewPanelProps,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren" + ], + "path": "packages/kbn-search-api-panels/components/install_client.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.IntegrationsPanel", + "type": "Function", + "tags": [], + "label": "IntegrationsPanel", + "description": [], + "signature": [ + "({ docLinks, http, pluginId, }: React.PropsWithChildren<", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.IntegrationsPanelProps", + "text": "IntegrationsPanelProps" + }, + ">) => JSX.Element" + ], + "path": "packages/kbn-search-api-panels/components/integrations_panel.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.IntegrationsPanel.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n docLinks,\n http,\n pluginId,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren<", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.IntegrationsPanelProps", + "text": "IntegrationsPanelProps" + }, + ">" + ], + "path": "packages/kbn-search-api-panels/components/integrations_panel.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageClientPanel", + "type": "Function", + "tags": [], + "label": "LanguageClientPanel", + "description": [], + "signature": [ + "({ language, setSelectedLanguage, isSelectedLanguage, http, pluginId, src, }: React.PropsWithChildren) => JSX.Element" + ], + "path": "packages/kbn-search-api-panels/components/language_client_panel.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageClientPanel.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n language,\n setSelectedLanguage,\n isSelectedLanguage,\n http,\n pluginId,\n src,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren" + ], + "path": "packages/kbn-search-api-panels/components/language_client_panel.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.OverviewPanel", + "type": "Function", + "tags": [], + "label": "OverviewPanel", + "description": [], + "signature": [ + "({ children, description, leftPanelContent, links, rightPanelContent, title, overviewPanelProps, }: React.PropsWithChildren) => JSX.Element" + ], + "path": "packages/kbn-search-api-panels/components/overview_panel.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.OverviewPanel.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n children,\n description,\n leftPanelContent,\n links,\n rightPanelContent,\n title,\n overviewPanelProps,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren" + ], + "path": "packages/kbn-search-api-panels/components/overview_panel.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.SelectClientPanel", + "type": "Function", + "tags": [], + "label": "SelectClientPanel", + "description": [], + "signature": [ + "({ docLinks, children, http, isPanelLeft, overviewPanelProps, }: React.PropsWithChildren<", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.SelectClientPanelProps", + "text": "SelectClientPanelProps" + }, + ">) => JSX.Element" + ], + "path": "packages/kbn-search-api-panels/components/select_client.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.SelectClientPanel.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n docLinks,\n children,\n http,\n isPanelLeft = true,\n overviewPanelProps,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren<", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.SelectClientPanelProps", + "text": "SelectClientPanelProps" + }, + ">" + ], + "path": "packages/kbn-search-api-panels/components/select_client.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.TryInConsoleButton", + "type": "Function", + "tags": [], + "label": "TryInConsoleButton", + "description": [], + "signature": [ + "({ request, application, sharePlugin, }: ", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.TryInConsoleButtonProps", + "text": "TryInConsoleButtonProps" + }, + ") => JSX.Element | null" + ], + "path": "packages/kbn-search-api-panels/components/try_in_console_button.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.TryInConsoleButton.$1", + "type": "Object", + "tags": [], + "label": "{\n request,\n application,\n sharePlugin,\n}", + "description": [], + "signature": [ + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.TryInConsoleButtonProps", + "text": "TryInConsoleButtonProps" + } + ], + "path": "packages/kbn-search-api-panels/components/try_in_console_button.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.WelcomeBanner", + "type": "Function", + "tags": [], + "label": "WelcomeBanner", + "description": [], + "signature": [ + "({ userProfile, assetBasePath, image, showDescription, }: React.PropsWithChildren<", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.WelcomeBannerProps", + "text": "WelcomeBannerProps" + }, + ">) => JSX.Element" + ], + "path": "packages/kbn-search-api-panels/index.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.WelcomeBanner.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n userProfile,\n assetBasePath,\n image,\n showDescription = true,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren<", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.WelcomeBannerProps", + "text": "WelcomeBannerProps" + }, + ">" + ], + "path": "packages/kbn-search-api-panels/index.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.IntegrationsPanelProps", + "type": "Interface", + "tags": [], + "label": "IntegrationsPanelProps", + "description": [], + "path": "packages/kbn-search-api-panels/components/integrations_panel.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.IntegrationsPanelProps.docLinks", + "type": "Any", + "tags": [], + "label": "docLinks", + "description": [], + "signature": [ + "any" + ], + "path": "packages/kbn-search-api-panels/components/integrations_panel.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.IntegrationsPanelProps.http", + "type": "Object", + "tags": [], + "label": "http", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } + ], + "path": "packages/kbn-search-api-panels/components/integrations_panel.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.IntegrationsPanelProps.pluginId", + "type": "string", + "tags": [], + "label": "pluginId", + "description": [], + "path": "packages/kbn-search-api-panels/components/integrations_panel.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition", + "type": "Interface", + "tags": [], + "label": "LanguageDefinition", + "description": [], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.advancedConfig", + "type": "string", + "tags": [], + "label": "advancedConfig", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.apiReference", + "type": "string", + "tags": [], + "label": "apiReference", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.basicConfig", + "type": "string", + "tags": [], + "label": "basicConfig", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.configureClient", + "type": "CompoundType", + "tags": [], + "label": "configureClient", + "description": [], + "signature": [ + "string | ((args: ", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.LanguageDefinitionSnippetArguments", + "text": "LanguageDefinitionSnippetArguments" + }, + ") => string)" + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.docLink", + "type": "string", + "tags": [], + "label": "docLink", + "description": [], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.iconType", + "type": "string", + "tags": [], + "label": "iconType", + "description": [], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.id", + "type": "Enum", + "tags": [], + "label": "id", + "description": [], + "signature": [ + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.Languages", + "text": "Languages" + } + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.ingestData", + "type": "CompoundType", + "tags": [], + "label": "ingestData", + "description": [], + "signature": [ + "string | ((args: ", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.LanguageDefinitionSnippetArguments", + "text": "LanguageDefinitionSnippetArguments" + }, + ") => string)" + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.ingestDataIndex", + "type": "CompoundType", + "tags": [], + "label": "ingestDataIndex", + "description": [], + "signature": [ + "string | ((args: ", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.LanguageDefinitionSnippetArguments", + "text": "LanguageDefinitionSnippetArguments" + }, + ") => string)" + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.installClient", + "type": "string", + "tags": [], + "label": "installClient", + "description": [], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.languageStyling", + "type": "string", + "tags": [], + "label": "languageStyling", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.name", + "type": "string", + "tags": [], + "label": "name", + "description": [], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.buildSearchQuery", + "type": "CompoundType", + "tags": [], + "label": "buildSearchQuery", + "description": [], + "signature": [ + "string | ((args: ", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.LanguageDefinitionSnippetArguments", + "text": "LanguageDefinitionSnippetArguments" + }, + ") => string)" + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinition.testConnection", + "type": "CompoundType", + "tags": [], + "label": "testConnection", + "description": [], + "signature": [ + "string | ((args: ", + { + "pluginId": "@kbn/search-api-panels", + "scope": "common", + "docId": "kibKbnSearchApiPanelsPluginApi", + "section": "def-common.LanguageDefinitionSnippetArguments", + "text": "LanguageDefinitionSnippetArguments" + }, + ") => string)" + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinitionSnippetArguments", + "type": "Interface", + "tags": [], + "label": "LanguageDefinitionSnippetArguments", + "description": [], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinitionSnippetArguments.url", + "type": "string", + "tags": [], + "label": "url", + "description": [], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinitionSnippetArguments.apiKey", + "type": "string", + "tags": [], + "label": "apiKey", + "description": [], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.LanguageDefinitionSnippetArguments.indexName", + "type": "string", + "tags": [], + "label": "indexName", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.SelectClientPanelProps", + "type": "Interface", + "tags": [], + "label": "SelectClientPanelProps", + "description": [], + "path": "packages/kbn-search-api-panels/components/select_client.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.SelectClientPanelProps.docLinks", + "type": "Object", + "tags": [], + "label": "docLinks", + "description": [], + "signature": [ + "{ elasticsearchClients: string; kibanaRunApiInConsole: string; }" + ], + "path": "packages/kbn-search-api-panels/components/select_client.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.SelectClientPanelProps.http", + "type": "Object", + "tags": [], + "label": "http", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } + ], + "path": "packages/kbn-search-api-panels/components/select_client.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.SelectClientPanelProps.isPanelLeft", + "type": "CompoundType", + "tags": [], + "label": "isPanelLeft", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-search-api-panels/components/select_client.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.SelectClientPanelProps.overviewPanelProps", + "type": "CompoundType", + "tags": [], + "label": "overviewPanelProps", + "description": [], + "signature": [ + "Partial<(", + "DisambiguateSet", + "<", + "_EuiPanelButtonlike", + ", ", + "_EuiPanelDivlike", + "> & ", + "_EuiPanelDivlike", + ") | (", + "DisambiguateSet", + "<", + "_EuiPanelDivlike", + ", ", + "_EuiPanelButtonlike", + "> & ", + "_EuiPanelButtonlike", + ")> | undefined" + ], + "path": "packages/kbn-search-api-panels/components/select_client.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.TryInConsoleButtonProps", + "type": "Interface", + "tags": [], + "label": "TryInConsoleButtonProps", + "description": [], + "path": "packages/kbn-search-api-panels/components/try_in_console_button.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.TryInConsoleButtonProps.request", + "type": "string", + "tags": [], + "label": "request", + "description": [], + "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.application", + "type": "Object", + "tags": [], + "label": "application", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-application-browser", + "scope": "common", + "docId": "kibKbnCoreApplicationBrowserPluginApi", + "section": "def-common.ApplicationStart", + "text": "ApplicationStart" + }, + " | 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", + "type": "CompoundType", + "tags": [], + "label": "sharePlugin", + "description": [], + "signature": [ + "{ toggleShareContextMenu: (options: ", + { + "pluginId": "share", + "scope": "public", + "docId": "kibSharePluginApi", + "section": "def-public.ShowShareMenuOptions", + "text": "ShowShareMenuOptions" + }, + ") => void; } & { url: ", + { + "pluginId": "share", + "scope": "public", + "docId": "kibSharePluginApi", + "section": "def-public.BrowserUrlService", + "text": "BrowserUrlService" + }, + "; navigate(options: ", + "RedirectOptions", + "<", + { + "pluginId": "@kbn/utility-types", + "scope": "common", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-common.SerializableRecord", + "text": "SerializableRecord" + }, + ">): void; }" + ], + "path": "packages/kbn-search-api-panels/components/try_in_console_button.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.WelcomeBannerProps", + "type": "Interface", + "tags": [], + "label": "WelcomeBannerProps", + "description": [], + "path": "packages/kbn-search-api-panels/index.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.WelcomeBannerProps.userProfile", + "type": "Object", + "tags": [], + "label": "userProfile", + "description": [], + "signature": [ + "{ user: { full_name?: string | undefined; username?: string | undefined; }; }" + ], + "path": "packages/kbn-search-api-panels/index.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.WelcomeBannerProps.assetBasePath", + "type": "string", + "tags": [], + "label": "assetBasePath", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-search-api-panels/index.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.WelcomeBannerProps.image", + "type": "string", + "tags": [], + "label": "image", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-search-api-panels/index.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.WelcomeBannerProps.showDescription", + "type": "CompoundType", + "tags": [], + "label": "showDescription", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-search-api-panels/index.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [ + { + "parentPluginId": "@kbn/search-api-panels", + "id": "def-common.Languages", + "type": "Enum", + "tags": [], + "label": "Languages", + "description": [], + "path": "packages/kbn-search-api-panels/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_search_api_panels.mdx b/api_docs/kbn_search_api_panels.mdx new file mode 100644 index 00000000000000..09d0dfc4f23c3e --- /dev/null +++ b/api_docs/kbn_search_api_panels.mdx @@ -0,0 +1,36 @@ +--- +#### +#### 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: kibKbnSearchApiPanelsPluginApi +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: 2023-08-14 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/search-api-panels'] +--- +import kbnSearchApiPanelsObj from './kbn_search_api_panels.devdocs.json'; + + + +Contact [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/teams/enterprise-search-frontend) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 58 | 1 | 58 | 0 | + +## Common + +### Functions + + +### Interfaces + + +### Enums + + diff --git a/api_docs/kbn_search_response_warnings.mdx b/api_docs/kbn_search_response_warnings.mdx index 0b32efe452eec1..19a6ec4f84bf4a 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: 2023-08-11 +date: 2023-08-14 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_solution_navigation.mdx b/api_docs/kbn_security_solution_navigation.mdx index 623bc4266bf5cd..d082563ad49e33 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: 2023-08-11 +date: 2023-08-14 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 9583e3ef0fb439..2a621a788cb432 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: 2023-08-11 +date: 2023-08-14 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 2440a3944ddbbd..38e1b8759b41fa 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: 2023-08-11 +date: 2023-08-14 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 9595b7a37f1218..3f410a7792f6f0 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: 2023-08-11 +date: 2023-08-14 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 7213ff97726887..65df67ea4569e0 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: 2023-08-11 +date: 2023-08-14 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 069e8ff637f39f..b626862000207d 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: 2023-08-11 +date: 2023-08-14 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 41437d89166339..c22ee98afe4d3c 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: 2023-08-11 +date: 2023-08-14 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 07499f581ae308..6c9655ff781258 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: 2023-08-11 +date: 2023-08-14 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 5f071496fdf971..ea05beadd813e4 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: 2023-08-11 +date: 2023-08-14 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 2c4d0d162fcd0f..f97e9ffa6d083c 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: 2023-08-11 +date: 2023-08-14 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 b88dd85e3e168d..1d3b73e11ea42d 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: 2023-08-11 +date: 2023-08-14 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 1ec08956dc1c31..fcaa0d08a68a08 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: 2023-08-11 +date: 2023-08-14 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 32a1abbecab302..6824cf9618985d 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: 2023-08-11 +date: 2023-08-14 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 afacfe67257782..76b86cdcba0d81 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: 2023-08-11 +date: 2023-08-14 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 f3cc8580e00ad9..770965238ef5bc 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: 2023-08-11 +date: 2023-08-14 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.devdocs.json b/api_docs/kbn_securitysolution_list_constants.devdocs.json index 90a4d358f937ed..cdd32ded1156ea 100644 --- a/api_docs/kbn_securitysolution_list_constants.devdocs.json +++ b/api_docs/kbn_securitysolution_list_constants.devdocs.json @@ -307,14 +307,6 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts" }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts" - }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts" - }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts" @@ -343,6 +335,14 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/server/lib/telemetry/tasks/security_lists.ts" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts" @@ -842,27 +842,27 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts" + "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts" + "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts" + "path": "x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts" + "path": "x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts" + "path": "x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts" + "path": "x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts" }, { "plugin": "lists", diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index ccb8df22753bac..36247d20f01c39 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: 2023-08-11 +date: 2023-08-14 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 96be46073e76da..54fc60a6bcb9fa 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: 2023-08-11 +date: 2023-08-14 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 98f8f90c95b934..a6af57a80708f7 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: 2023-08-11 +date: 2023-08-14 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 79fa8453456a63..369906f658f7cc 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: 2023-08-11 +date: 2023-08-14 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 e84b14427c777b..1fffcd71751d56 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: 2023-08-11 +date: 2023-08-14 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 b4e268f625a164..625c2466375114 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: 2023-08-11 +date: 2023-08-14 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 d6f88427170d1e..a07ea484b9b634 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: 2023-08-11 +date: 2023-08-14 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 4bb4d2d9e30ab9..91c45261c8e13d 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: 2023-08-11 +date: 2023-08-14 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_project_switcher.mdx b/api_docs/kbn_serverless_project_switcher.mdx index 92ca70b7f42725..eb09144d4b8a1a 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: 2023-08-11 +date: 2023-08-14 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_storybook_config.mdx b/api_docs/kbn_serverless_storybook_config.mdx index cc67dcdd1e7a64..56ed6e669da14d 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: 2023-08-11 +date: 2023-08-14 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 25e7d254ef3f70..3bc9b1a9ca8dca 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: 2023-08-11 +date: 2023-08-14 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 f6a2868fdd6a64..1f907a9e7892f4 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: 2023-08-11 +date: 2023-08-14 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_avatar_user_profile_components.mdx b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx index 03eb2ad4638da6..6b8b766eb5e632 100644 --- a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx +++ b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-user-profile-components title: "@kbn/shared-ux-avatar-user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-user-profile-components plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-user-profile-components'] --- import kbnSharedUxAvatarUserProfileComponentsObj from './kbn_shared_ux_avatar_user_profile_components.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 d53a67f3419cae..143518d9d7d2b6 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: 2023-08-11 +date: 2023-08-14 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_exit_full_screen_mocks.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx index 93dc5f19a25cb3..d497f8cf4b6417 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen-mocks title: "@kbn/shared-ux-button-exit-full-screen-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen-mocks plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen-mocks'] --- import kbnSharedUxButtonExitFullScreenMocksObj from './kbn_shared_ux_button_exit_full_screen_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index 97bd6354e74c0a..29a6362c4807a7 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: 2023-08-11 +date: 2023-08-14 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 0466a03317080c..3b56ddeea9539f 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: 2023-08-11 +date: 2023-08-14 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 06ad7bbfb6322c..e1497fb556dd2a 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: 2023-08-11 +date: 2023-08-14 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 7abcb733de0536..b86a2c502328b7 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: 2023-08-11 +date: 2023-08-14 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_file_context.mdx b/api_docs/kbn_shared_ux_file_context.mdx index ac4a26fc135e09..1b2beefa573ad8 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: 2023-08-11 +date: 2023-08-14 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 2be295b573f501..78c45710649b77 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: 2023-08-11 +date: 2023-08-14 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 6ca017f8f2dab0..2b6a3714be9ea9 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: 2023-08-11 +date: 2023-08-14 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 2a10fa88f3bf0c..c2d23c349e3fae 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: 2023-08-11 +date: 2023-08-14 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 5857b29e6040a6..9a1dd12c878fa8 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: 2023-08-11 +date: 2023-08-14 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 d3743f864ff863..719edba9937a50 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: 2023-08-11 +date: 2023-08-14 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 c191c2e722da17..9c7286b96203ca 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: 2023-08-11 +date: 2023-08-14 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 f25b052638dac3..a0838a22fad9a2 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: 2023-08-11 +date: 2023-08-14 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 0b0d19fa03515d..0c40be0c07ce71 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: 2023-08-11 +date: 2023-08-14 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 191816be2d0143..c85211bfbcda08 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: 2023-08-11 +date: 2023-08-14 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 65807653635c40..5f4f9285a28e28 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: 2023-08-11 +date: 2023-08-14 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 aa189dde428b60..f56bfebf5c04c0 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: 2023-08-11 +date: 2023-08-14 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 987504901d1b44..76527e20aca965 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: 2023-08-11 +date: 2023-08-14 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 c8e96f9f7e58c6..4a951a3aee1259 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: 2023-08-11 +date: 2023-08-14 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 6757520fbbd7b7..ff66b6ba25c99f 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: 2023-08-11 +date: 2023-08-14 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 8343838209b03c..c6c7b869c3ee31 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: 2023-08-11 +date: 2023-08-14 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 ee3fa6c8173970..1d6684edda58b4 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: 2023-08-11 +date: 2023-08-14 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 bf2316b91cc938..ab75411da71185 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: 2023-08-11 +date: 2023-08-14 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 706fbe9af537d9..73bf90e4400f3b 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: 2023-08-11 +date: 2023-08-14 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 6b5eae287adac6..56216b7168b774 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: 2023-08-11 +date: 2023-08-14 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 a44407da272f29..7bb394ee84e085 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: 2023-08-11 +date: 2023-08-14 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 54ed5c1f65be87..b1c6a73e9c47d6 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: 2023-08-11 +date: 2023-08-14 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 76c5fb436ed1f9..8f6cdfb9260368 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: 2023-08-11 +date: 2023-08-14 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 8a8370f9f20ad6..e13f95eae7e7fc 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: 2023-08-11 +date: 2023-08-14 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 42ddd65e40a7b8..c06535a8e6b2ea 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: 2023-08-11 +date: 2023-08-14 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 1a0cc472a697cd..7a97dae6b36aaa 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: 2023-08-11 +date: 2023-08-14 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 de731faf58a05b..06dec451910ddf 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: 2023-08-11 +date: 2023-08-14 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 52cc79ff350fe5..bec0ce5cf0793d 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: 2023-08-11 +date: 2023-08-14 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 efe1c2182dd8c3..96b0d69a0b1549 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: 2023-08-11 +date: 2023-08-14 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 99e3e80252965e..08a5c0223b2c67 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: 2023-08-11 +date: 2023-08-14 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 7ceb54db5f4a85..98b4770aab5e99 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: 2023-08-11 +date: 2023-08-14 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 4a4f12358397b7..134d8ed0bf2c71 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: 2023-08-11 +date: 2023-08-14 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 e82fc131c74d1b..7f158fcd978701 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index b0c10f7b951969..35483e9d264a99 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: 2023-08-11 +date: 2023-08-14 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 f3c99bea3842a1..b2a7582300d395 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: 2023-08-11 +date: 2023-08-14 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 e26c93f97b71ee..438110c00e1c3b 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: 2023-08-11 +date: 2023-08-14 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 d16e67f8a08823..928d30178e2cec 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: 2023-08-11 +date: 2023-08-14 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 fd58c14b823af6..74aa221091be6a 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index 87f429af0eddb7..5a4b60c9549e81 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: 2023-08-11 +date: 2023-08-14 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 75f48424c636b7..b0579f1ba8179c 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: 2023-08-11 +date: 2023-08-14 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.mdx b/api_docs/kbn_text_based_editor.mdx index d28bc1f971dfe3..a443716952c87d 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/text-based-editor'] --- import kbnTextBasedEditorObj from './kbn_text_based_editor.devdocs.json'; diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index a1e8636cfb21e7..1b6652d1909f79 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_ts_projects.mdx b/api_docs/kbn_ts_projects.mdx index 03efc531c0b36f..5192212f7346e9 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: 2023-08-11 +date: 2023-08-14 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 492a7fc64f0292..7569cef02b6f42 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: 2023-08-11 +date: 2023-08-14 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 463c4adc60a0ca..37d1b9f3151dc7 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: 2023-08-11 +date: 2023-08-14 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 cb0b11bfb50a78..ff89d87d4edc61 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: 2023-08-11 +date: 2023-08-14 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 4c866341a57726..0e7925f6b98d76 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_unified_field_list.mdx b/api_docs/kbn_unified_field_list.mdx index 1fb51e64915e30..c062f65c96685e 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-field-list'] --- import kbnUnifiedFieldListObj from './kbn_unified_field_list.devdocs.json'; diff --git a/api_docs/kbn_url_state.mdx b/api_docs/kbn_url_state.mdx index e70185a2d5b645..1141cd4b7c13de 100644 --- a/api_docs/kbn_url_state.mdx +++ b/api_docs/kbn_url_state.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-url-state title: "@kbn/url-state" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/url-state plugin -date: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/url-state'] --- import kbnUrlStateObj from './kbn_url_state.devdocs.json'; diff --git a/api_docs/kbn_use_tracked_promise.devdocs.json b/api_docs/kbn_use_tracked_promise.devdocs.json new file mode 100644 index 00000000000000..2e695a7761e9a1 --- /dev/null +++ b/api_docs/kbn_use_tracked_promise.devdocs.json @@ -0,0 +1,80 @@ +{ + "id": "@kbn/use-tracked-promise", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/use-tracked-promise", + "id": "def-common.useTrackedPromise", + "type": "Function", + "tags": [], + "label": "useTrackedPromise", + "description": [ + "\nThis hook manages a Promise factory and can create new Promises from it. The\nstate of these Promises is tracked and they can be canceled when superseded\nto avoid race conditions.\n\n```\nconst [requestState, performRequest] = useTrackedPromise(\n {\n cancelPreviousOn: 'resolution',\n createPromise: async (url: string) => {\n return await fetchSomething(url)\n },\n onResolve: response => {\n setSomeState(response.data);\n },\n onReject: response => {\n setSomeError(response);\n },\n },\n [fetchSomething]\n);\n```\n\nThe `onResolve` and `onReject` handlers are registered separately, because\nthe hook will inject a rejection when in case of a canellation. The\n`cancelPreviousOn` attribute can be used to indicate when the preceding\npending promises should be canceled:\n\n'never': No preceding promises will be canceled.\n\n'creation': Any preceding promises will be canceled as soon as a new one is\ncreated.\n\n'settlement': Any preceding promise will be canceled when a newer promise is\nresolved or rejected.\n\n'resolution': Any preceding promise will be canceled when a newer promise is\nresolved.\n\n'rejection': Any preceding promise will be canceled when a newer promise is\nrejected.\n\nAny pending promises will be canceled when the component using the hook is\nunmounted, but their status will not be tracked to avoid React warnings\nabout memory leaks.\n\nThe last argument is a normal React hook dependency list that indicates\nunder which conditions a new reference to the configuration object should be\nused.\n\nThe `onResolve`, `onReject` and possible uncatched errors are only triggered\nif the underlying component is mounted. To ensure they always trigger (i.e.\nif the promise is called in a `useLayoutEffect`) use the `triggerOrThrow`\nattribute:\n\n'whenMounted': (default) they are called only if the component is mounted.\n\n'always': they always call. The consumer is then responsible of ensuring no\nside effects happen if the underlying component is not mounted." + ], + "signature": [ + "({ createPromise, onResolve, onReject, cancelPreviousOn, triggerOrThrow, }: UseTrackedPromiseArgs, dependencies: React.DependencyList) => [", + "PromiseState", + ", (...args: Arguments) => Promise, () => void]" + ], + "path": "packages/kbn-use-tracked-promise/use_tracked_promise.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/use-tracked-promise", + "id": "def-common.useTrackedPromise.$1", + "type": "Object", + "tags": [], + "label": "{\n createPromise,\n onResolve = noOp,\n onReject = noOp,\n cancelPreviousOn = 'never',\n triggerOrThrow = 'whenMounted',\n }", + "description": [], + "signature": [ + "UseTrackedPromiseArgs" + ], + "path": "packages/kbn-use-tracked-promise/use_tracked_promise.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/use-tracked-promise", + "id": "def-common.useTrackedPromise.$2", + "type": "Object", + "tags": [], + "label": "dependencies", + "description": [], + "signature": [ + "React.DependencyList" + ], + "path": "packages/kbn-use-tracked-promise/use_tracked_promise.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_use_tracked_promise.mdx b/api_docs/kbn_use_tracked_promise.mdx new file mode 100644 index 00000000000000..fa39802014019d --- /dev/null +++ b/api_docs/kbn_use_tracked_promise.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: kibKbnUseTrackedPromisePluginApi +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: 2023-08-14 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/use-tracked-promise'] +--- +import kbnUseTrackedPromiseObj from './kbn_use_tracked_promise.devdocs.json'; + + + +Contact [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3 | 0 | 2 | 1 | + +## Common + +### Functions + + diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index d0736c9957c653..4aad4f65b594ce 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: 2023-08-11 +date: 2023-08-14 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 89ca0031d665bc..8f1a05d49235d4 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: 2023-08-11 +date: 2023-08-14 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 e306a57de9c3ed..466da9673ceaa7 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: 2023-08-11 +date: 2023-08-14 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.mdx b/api_docs/kbn_utils.mdx index f51f9e1722d7db..53bee6b556c597 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] --- import kbnUtilsObj from './kbn_utils.devdocs.json'; diff --git a/api_docs/kbn_visualization_ui_components.mdx b/api_docs/kbn_visualization_ui_components.mdx index 401ef7887c3f86..66f254fdf07ece 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/visualization-ui-components'] --- import kbnVisualizationUiComponentsObj from './kbn_visualization_ui_components.devdocs.json'; diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx index 2c4d82820d316b..c1ff33fcf24d78 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator'] --- import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json'; diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index ad9cc9b230005d..e3cbffd21a448b 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: 2023-08-11 +date: 2023-08-14 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 db6b50d2ea25c0..8fbe44a9b89d4a 100644 --- a/api_docs/kibana_react.devdocs.json +++ b/api_docs/kibana_react.devdocs.json @@ -760,18 +760,6 @@ "plugin": "management", "path": "src/plugins/management/public/components/management_app/management_app.tsx" }, - { - "plugin": "data", - "path": "src/plugins/data/public/search/session/sessions_mgmt/components/main.tsx" - }, - { - "plugin": "data", - "path": "src/plugins/data/public/search/session/sessions_mgmt/components/main.tsx" - }, - { - "plugin": "data", - "path": "src/plugins/data/public/search/session/sessions_mgmt/components/main.tsx" - }, { "plugin": "advancedSettings", "path": "src/plugins/advanced_settings/public/management_app/components/form/form.tsx" @@ -844,6 +832,18 @@ "plugin": "savedObjects", "path": "src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx" }, + { + "plugin": "serverless", + "path": "x-pack/plugins/serverless/public/plugin.tsx" + }, + { + "plugin": "serverless", + "path": "x-pack/plugins/serverless/public/plugin.tsx" + }, + { + "plugin": "serverless", + "path": "x-pack/plugins/serverless/public/plugin.tsx" + }, { "plugin": "visualizations", "path": "src/plugins/visualizations/public/visualize_app/utils/use/use_visualize_app_state.tsx" @@ -904,18 +904,6 @@ "plugin": "visualizations", "path": "src/plugins/visualizations/public/visualize_app/index.tsx" }, - { - "plugin": "serverless", - "path": "x-pack/plugins/serverless/public/plugin.tsx" - }, - { - "plugin": "serverless", - "path": "x-pack/plugins/serverless/public/plugin.tsx" - }, - { - "plugin": "serverless", - "path": "x-pack/plugins/serverless/public/plugin.tsx" - }, { "plugin": "controls", "path": "src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx" @@ -1317,48 +1305,16 @@ "path": "x-pack/plugins/aiops/public/embeddable/embeddable_change_point_chart.tsx" }, { - "plugin": "discover", - "path": "src/plugins/discover/public/embeddable/saved_search_embeddable.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/embeddable/saved_search_embeddable.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/embeddable/saved_search_embeddable.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/embeddable/saved_search_embeddable.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/embeddable/saved_search_embeddable.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx" + "plugin": "observabilityAIAssistant", + "path": "x-pack/plugins/observability_ai_assistant/public/application.tsx" }, { - "plugin": "discover", - "path": "src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx" + "plugin": "observabilityAIAssistant", + "path": "x-pack/plugins/observability_ai_assistant/public/application.tsx" }, { - "plugin": "discover", - "path": "src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx" + "plugin": "observabilityAIAssistant", + "path": "x-pack/plugins/observability_ai_assistant/public/application.tsx" }, { "plugin": "exploratoryView", @@ -1372,18 +1328,6 @@ "plugin": "exploratoryView", "path": "x-pack/plugins/exploratory_view/public/application/index.tsx" }, - { - "plugin": "observabilityAIAssistant", - "path": "x-pack/plugins/observability_ai_assistant/public/application.tsx" - }, - { - "plugin": "observabilityAIAssistant", - "path": "x-pack/plugins/observability_ai_assistant/public/application.tsx" - }, - { - "plugin": "observabilityAIAssistant", - "path": "x-pack/plugins/observability_ai_assistant/public/application.tsx" - }, { "plugin": "fleet", "path": "x-pack/plugins/fleet/public/applications/integrations/app.tsx" @@ -2128,18 +2072,6 @@ "plugin": "console", "path": "src/plugins/console/public/application/index.tsx" }, - { - "plugin": "dataViewManagement", - "path": "src/plugins/data_view_management/public/management_app/mount_management_section.tsx" - }, - { - "plugin": "dataViewManagement", - "path": "src/plugins/data_view_management/public/management_app/mount_management_section.tsx" - }, - { - "plugin": "dataViewManagement", - "path": "src/plugins/data_view_management/public/management_app/mount_management_section.tsx" - }, { "plugin": "filesManagement", "path": "src/plugins/files_management/public/mount_management_section.tsx" @@ -3318,14 +3250,6 @@ "plugin": "dataViewEditor", "path": "src/plugins/data_view_editor/public/shared_imports.ts" }, - { - "plugin": "dataViewEditor", - "path": "src/plugins/data_view_editor/public/open_editor.tsx" - }, - { - "plugin": "dataViewEditor", - "path": "src/plugins/data_view_editor/public/open_editor.tsx" - }, { "plugin": "unifiedSearch", "path": "src/plugins/unified_search/public/query_string_input/query_string_input.tsx" @@ -3502,14 +3426,6 @@ "plugin": "dataViewFieldEditor", "path": "src/plugins/data_view_field_editor/public/open_delete_modal.tsx" }, - { - "plugin": "dataViewFieldEditor", - "path": "src/plugins/data_view_field_editor/public/open_editor.tsx" - }, - { - "plugin": "dataViewFieldEditor", - "path": "src/plugins/data_view_field_editor/public/open_editor.tsx" - }, { "plugin": "lens", "path": "x-pack/plugins/lens/public/persistence/saved_objects_utils/confirm_modal_promise.tsx" @@ -3690,62 +3606,6 @@ "plugin": "observabilityShared", "path": "x-pack/plugins/observability_shared/public/components/header_menu/header_menu_portal.tsx" }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/main/hooks/use_alert_results_toast.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/main/hooks/use_alert_results_toast.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/not_found/not_found_route.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/not_found/not_found_route.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/view_alert/view_alert_utils.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/view_alert/view_alert_utils.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/view_alert/view_alert_utils.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/view_alert/view_alert_utils.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/index.tsx" - }, - { - "plugin": "discover", - "path": "src/plugins/discover/public/application/index.tsx" - }, { "plugin": "exploratoryView", "path": "x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.tsx" diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index 3ca005f473d61c..e25a9d43f22c74 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: 2023-08-11 +date: 2023-08-14 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 2e06bfc93237d7..9b6fedf7ff3095 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: 2023-08-11 +date: 2023-08-14 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 fa6abbd8d8f438..4abc7b4276230a 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: 2023-08-11 +date: 2023-08-14 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 486ee7fec86568..097f08300c7d66 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: 2023-08-11 +date: 2023-08-14 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 c66783d83f4920..bebd88a625a744 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: 2023-08-11 +date: 2023-08-14 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 cc789ac6e50191..6adc5b985efe8f 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] --- import licenseManagementObj from './license_management.devdocs.json'; diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index 00639d8119c07e..0af1ee2fb396a5 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 90852e424a2ad4..4219a82f52bbb2 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] --- import listsObj from './lists.devdocs.json'; diff --git a/api_docs/logs_shared.mdx b/api_docs/logs_shared.mdx index 9b213c3873a937..7aac1d98f9f2a8 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: 2023-08-11 +date: 2023-08-14 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 4e9247adb679c9..b49f0cd90b55e0 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: 2023-08-11 +date: 2023-08-14 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 f4b8727ae554f4..5683c0a213fccd 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: 2023-08-11 +date: 2023-08-14 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 519313b17a929f..35fcfdcb8f8c4a 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index 45c60a643eaedc..ccca65b6fb7b17 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index c733a6d311c248..92e219a6d9f66a 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: 2023-08-11 +date: 2023-08-14 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 4dd1dec3ef6d13..0d101157f8305d 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: 2023-08-11 +date: 2023-08-14 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 7fe10615fad48e..fda89cf97a82fa 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: 2023-08-11 +date: 2023-08-14 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 31f84fbbb482a7..b78d2900dc3b83 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/notifications.mdx b/api_docs/notifications.mdx index 0868fcb787d8cf..858cca841a8f3d 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'notifications'] --- import notificationsObj from './notifications.devdocs.json'; diff --git a/api_docs/observability.devdocs.json b/api_docs/observability.devdocs.json index 8cc7e821fa1a3f..0617660cdfa851 100644 --- a/api_docs/observability.devdocs.json +++ b/api_docs/observability.devdocs.json @@ -2205,6 +2205,26 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "observability", + "id": "def-public.ObservabilityPublicPluginsSetup.observabilityAIAssistant", + "type": "Object", + "tags": [], + "label": "observabilityAIAssistant", + "description": [], + "signature": [ + { + "pluginId": "observabilityAIAssistant", + "scope": "public", + "docId": "kibObservabilityAIAssistantPluginApi", + "section": "def-public.ObservabilityAIAssistantPluginSetup", + "text": "ObservabilityAIAssistantPluginSetup" + } + ], + "path": "x-pack/plugins/observability/public/plugin.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "observability", "id": "def-public.ObservabilityPublicPluginsSetup.share", @@ -2655,6 +2675,26 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "observability", + "id": "def-public.ObservabilityPublicPluginsStart.observabilityAIAssistant", + "type": "Object", + "tags": [], + "label": "observabilityAIAssistant", + "description": [], + "signature": [ + { + "pluginId": "observabilityAIAssistant", + "scope": "public", + "docId": "kibObservabilityAIAssistantPluginApi", + "section": "def-public.ObservabilityAIAssistantPluginStart", + "text": "ObservabilityAIAssistantPluginStart" + } + ], + "path": "x-pack/plugins/observability/public/plugin.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "observability", "id": "def-public.ObservabilityPublicPluginsStart.ruleTypeRegistry", @@ -14839,6 +14879,22 @@ "initialIsOpen": false } ], - "objects": [] + "objects": [ + { + "parentPluginId": "observability", + "id": "def-common.observabilityPaths", + "type": "Object", + "tags": [], + "label": "observabilityPaths", + "description": [], + "signature": [ + "{ alerts: string; alertDetails: (alertId: string) => string; rules: string; ruleDetails: (ruleId: string) => string; slos: string; slosWelcome: string; sloCreate: string; sloCreateWithEncodedForm: (encodedParams: string) => string; sloEdit: (sloId: string) => string; sloDetails: (sloId: string, instanceId?: string | undefined) => string; }" + ], + "path": "x-pack/plugins/observability/common/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ] } } \ No newline at end of file diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index bc3cd0aa831f4a..fedab58d58ffa7 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/actionable-observability](https://github.com/orgs/elastic/team | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 508 | 45 | 501 | 16 | +| 511 | 45 | 504 | 16 | ## Client @@ -62,6 +62,9 @@ Contact [@elastic/actionable-observability](https://github.com/orgs/elastic/team ## Common +### Objects + + ### Functions diff --git a/api_docs/observability_a_i_assistant.devdocs.json b/api_docs/observability_a_i_assistant.devdocs.json index f2fd46f618d937..355f2bb93edc92 100644 --- a/api_docs/observability_a_i_assistant.devdocs.json +++ b/api_docs/observability_a_i_assistant.devdocs.json @@ -43,6 +43,38 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "observabilityAIAssistant", + "id": "def-public.ObservabilityAIAssistantActionMenuItem", + "type": "Function", + "tags": [], + "label": "ObservabilityAIAssistantActionMenuItem", + "description": [], + "signature": [ + "React.ForwardRefExoticComponent>" + ], + "path": "x-pack/plugins/observability_ai_assistant/public/index.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "observabilityAIAssistant", + "id": "def-public.ObservabilityAIAssistantActionMenuItem.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "observabilityAIAssistant", "id": "def-public.ObservabilityAIAssistantProvider", @@ -94,6 +126,25 @@ "children": [], "returnComment": [], "initialIsOpen": false + }, + { + "parentPluginId": "observabilityAIAssistant", + "id": "def-public.useObservabilityAIAssistantOptional", + "type": "Function", + "tags": [], + "label": "useObservabilityAIAssistantOptional", + "description": [], + "signature": [ + "() => ", + "ObservabilityAIAssistantService", + " | undefined" + ], + "path": "x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [], + "initialIsOpen": false } ], "interfaces": [ diff --git a/api_docs/observability_a_i_assistant.mdx b/api_docs/observability_a_i_assistant.mdx index 41c9829cb7a690..4b2a196d265537 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityAIAssistant'] --- import observabilityAIAssistantObj from './observability_a_i_assistant.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/obs-ai-assistant](https://github.com/orgs/elastic/teams/obs-ai | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 45 | 0 | 43 | 7 | +| 48 | 0 | 45 | 7 | ## Client diff --git a/api_docs/observability_onboarding.mdx b/api_docs/observability_onboarding.mdx index 4b6ef8413d5ee4..56c5c7dccdff33 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityOnboarding'] --- import observabilityOnboardingObj from './observability_onboarding.devdocs.json'; diff --git a/api_docs/observability_shared.mdx b/api_docs/observability_shared.mdx index 1a51ae011f14f4..b76db7f7ca09e8 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: 2023-08-11 +date: 2023-08-14 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 2a9c8b001d4acd..704c7e05ac18f6 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index c565083087ea6d..9904789db2832f 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: 2023-08-11 +date: 2023-08-14 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 | |--------------|----------|------------------------| -| 660 | 550 | 39 | +| 662 | 552 | 39 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 71887 | 556 | 61394 | 1474 | +| 71956 | 558 | 61461 | 1476 | ## Plugin Directory @@ -74,7 +74,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 115 | 3 | 111 | 3 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | The Event Annotation service contains expressions for event annotations | 191 | 30 | 191 | 2 | | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 111 | 0 | 111 | 11 | -| | [@elastic/uptime](https://github.com/orgs/elastic/teams/uptime) | - | 131 | 1 | 131 | 14 | +| | [@elastic/uptime](https://github.com/orgs/elastic/teams/uptime) | - | 132 | 1 | 132 | 14 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'error' renderer to expressions | 17 | 0 | 15 | 2 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Expression Gauge plugin adds a `gauge` renderer and function to the expression plugin. The renderer will display the `gauge` chart. | 59 | 0 | 59 | 2 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Expression Heatmap plugin adds a `heatmap` renderer and function to the expression plugin. The renderer will display the `heatmap` chart. | 112 | 14 | 108 | 2 | @@ -132,8 +132,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 34 | 0 | 34 | 2 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 17 | 0 | 17 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 2 | 0 | 2 | 1 | -| | [@elastic/actionable-observability](https://github.com/orgs/elastic/teams/actionable-observability) | - | 508 | 45 | 501 | 16 | -| | [@elastic/obs-ai-assistant](https://github.com/orgs/elastic/teams/obs-ai-assistant) | - | 45 | 0 | 43 | 7 | +| | [@elastic/actionable-observability](https://github.com/orgs/elastic/teams/actionable-observability) | - | 511 | 45 | 504 | 16 | +| | [@elastic/obs-ai-assistant](https://github.com/orgs/elastic/teams/obs-ai-assistant) | - | 48 | 0 | 45 | 7 | | | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 14 | 0 | 14 | 0 | | | [@elastic/observability-ui](https://github.com/orgs/elastic/teams/observability-ui) | - | 277 | 1 | 276 | 11 | | | [@elastic/security-defend-workflows](https://github.com/orgs/elastic/teams/security-defend-workflows) | - | 24 | 0 | 24 | 7 | @@ -155,7 +155,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-reporting-services](https://github.com/orgs/elastic/teams/kibana-reporting-services) | Kibana Screenshotting Plugin | 27 | 0 | 8 | 5 | | searchprofiler | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 0 | 0 | 0 | 0 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides authentication and authorization features, and exposes functionality to understand the capabilities of the currently authenticated user. | 270 | 0 | 87 | 3 | -| | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | - | 194 | 2 | 128 | 32 | +| | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | - | 195 | 3 | 129 | 33 | | | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | ESS customizations for Security Solution. | 6 | 0 | 6 | 0 | | | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | Serverless customizations for security. | 6 | 0 | 6 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | The core Serverless plugin, providing APIs to Serverless Project plugins. | 17 | 0 | 16 | 0 | @@ -517,6 +517,7 @@ 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) | - | 107 | 0 | 104 | 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) | - | 58 | 1 | 58 | 0 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 16 | 0 | 8 | 0 | | | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 44 | 0 | 41 | 0 | | | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 27 | 0 | 21 | 0 | @@ -601,6 +602,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@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 field list and field stats which can be integrated into apps | 306 | 0 | 277 | 9 | | | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 4 | 0 | 0 | 0 | +| | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | - | 3 | 0 | 2 | 1 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 80 | 0 | 21 | 2 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 36 | 0 | 15 | 1 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 2 | 0 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index bc1e2cafe21afb..2c9e774a4c05de 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: 2023-08-11 +date: 2023-08-14 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 34101cd5cb0692..c3e197360dbc89 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling'] --- import profilingObj from './profiling.devdocs.json'; diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index d7d1030ac7b001..1e149f9813102b 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: 2023-08-11 +date: 2023-08-14 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 69b50036a8c523..a290848e2103cc 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: 2023-08-11 +date: 2023-08-14 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 640d8b84d47889..b3cbcf458dfa26 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: 2023-08-11 +date: 2023-08-14 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 928d8e54049025..25281eabeebef5 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: 2023-08-11 +date: 2023-08-14 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 3ff6d1e7ec70ec..7db817287652a4 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: 2023-08-11 +date: 2023-08-14 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 88f0af9217efd0..4b10692940d3dd 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: 2023-08-11 +date: 2023-08-14 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 c454fe88acce3a..3ce507611fb8f6 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index f07fcc48f5adfe..057ed3d7ab7c47 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] --- import savedObjectsManagementObj from './saved_objects_management.devdocs.json'; diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index afd30deea39c65..632a34440da91f 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: 2023-08-11 +date: 2023-08-14 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 2c4d5905900746..26b92547623b3c 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: 2023-08-11 +date: 2023-08-14 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 f678346de945c2..da0b4b55479356 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: 2023-08-11 +date: 2023-08-14 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 c3d511a299449b..15b670e86770b6 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: 2023-08-11 +date: 2023-08-14 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 ff681c7a4b7a48..4024e7f779c8bf 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: 2023-08-11 +date: 2023-08-14 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 a6f73629787864..cc1abbd0f8ca5b 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] --- import securityObj from './security.devdocs.json'; diff --git a/api_docs/security_solution.devdocs.json b/api_docs/security_solution.devdocs.json index 62ef5e036ebbd9..f85bb8c6c3920c 100644 --- a/api_docs/security_solution.devdocs.json +++ b/api_docs/security_solution.devdocs.json @@ -101,7 +101,7 @@ "label": "experimentalFeatures", "description": [], "signature": [ - "{ readonly tGridEnabled: boolean; readonly tGridEventRenderedViewEnabled: boolean; readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly chartEmbeddablesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly alertsPreviewChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly insightsRelatedAlertsByProcessAncestry: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionsEnabled: boolean; readonly endpointResponseActionsEnabled: boolean; readonly alertDetailsPageEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly alertsPageChartsEnabled: boolean; readonly alertTypeEnabled: boolean; readonly securityFlyoutEnabled: boolean; readonly alertsPageFiltersEnabled: boolean; readonly newUserDetailsFlyout: boolean; readonly detectionsCoverageOverview: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly discoverInTimeline: boolean; }" + "{ readonly tGridEnabled: boolean; readonly tGridEventRenderedViewEnabled: boolean; readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly chartEmbeddablesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly alertsPreviewChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly insightsRelatedAlertsByProcessAncestry: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionsEnabled: boolean; readonly endpointResponseActionsEnabled: boolean; readonly alertDetailsPageEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly alertsPageChartsEnabled: boolean; readonly alertTypeEnabled: boolean; readonly alertsPageFiltersEnabled: boolean; readonly newUserDetailsFlyout: boolean; readonly detectionsCoverageOverview: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly discoverInTimeline: boolean; }" ], "path": "x-pack/plugins/security_solution/public/plugin.tsx", "deprecated": false, @@ -831,7 +831,7 @@ "\nExperimental flag needed to enable the link" ], "signature": [ - "\"tGridEnabled\" | \"tGridEventRenderedViewEnabled\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"chartEmbeddablesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"alertsPreviewChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"insightsRelatedAlertsByProcessAncestry\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionsEnabled\" | \"endpointResponseActionsEnabled\" | \"alertDetailsPageEnabled\" | \"responseActionUploadEnabled\" | \"alertsPageChartsEnabled\" | \"alertTypeEnabled\" | \"securityFlyoutEnabled\" | \"alertsPageFiltersEnabled\" | \"newUserDetailsFlyout\" | \"detectionsCoverageOverview\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"discoverInTimeline\" | undefined" + "\"tGridEnabled\" | \"tGridEventRenderedViewEnabled\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"chartEmbeddablesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"alertsPreviewChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"insightsRelatedAlertsByProcessAncestry\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionsEnabled\" | \"endpointResponseActionsEnabled\" | \"alertDetailsPageEnabled\" | \"responseActionUploadEnabled\" | \"alertsPageChartsEnabled\" | \"alertTypeEnabled\" | \"alertsPageFiltersEnabled\" | \"newUserDetailsFlyout\" | \"detectionsCoverageOverview\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"discoverInTimeline\" | undefined" ], "path": "x-pack/plugins/security_solution/public/common/links/types.ts", "deprecated": false, @@ -911,7 +911,7 @@ "\nExperimental flag needed to disable the link. Opposite of experimentalKey" ], "signature": [ - "\"tGridEnabled\" | \"tGridEventRenderedViewEnabled\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"chartEmbeddablesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"alertsPreviewChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"insightsRelatedAlertsByProcessAncestry\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionsEnabled\" | \"endpointResponseActionsEnabled\" | \"alertDetailsPageEnabled\" | \"responseActionUploadEnabled\" | \"alertsPageChartsEnabled\" | \"alertTypeEnabled\" | \"securityFlyoutEnabled\" | \"alertsPageFiltersEnabled\" | \"newUserDetailsFlyout\" | \"detectionsCoverageOverview\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"discoverInTimeline\" | undefined" + "\"tGridEnabled\" | \"tGridEventRenderedViewEnabled\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"chartEmbeddablesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"alertsPreviewChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"insightsRelatedAlertsByProcessAncestry\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionsEnabled\" | \"endpointResponseActionsEnabled\" | \"alertDetailsPageEnabled\" | \"responseActionUploadEnabled\" | \"alertsPageChartsEnabled\" | \"alertTypeEnabled\" | \"alertsPageFiltersEnabled\" | \"newUserDetailsFlyout\" | \"detectionsCoverageOverview\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"discoverInTimeline\" | undefined" ], "path": "x-pack/plugins/security_solution/public/common/links/types.ts", "deprecated": false, @@ -3330,7 +3330,9 @@ "signature": [ "AppFeatureSecurityKey", " | ", - "AppFeatureCasesKey" + "AppFeatureCasesKey", + " | ", + "AppFeatureAssistantKey" ], "path": "x-pack/plugins/security_solution/common/types/app_features.ts", "deprecated": false, @@ -3399,7 +3401,7 @@ "label": "ExperimentalFeatures", "description": [], "signature": [ - "{ readonly tGridEnabled: boolean; readonly tGridEventRenderedViewEnabled: boolean; readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly chartEmbeddablesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly alertsPreviewChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly insightsRelatedAlertsByProcessAncestry: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionsEnabled: boolean; readonly endpointResponseActionsEnabled: boolean; readonly alertDetailsPageEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly alertsPageChartsEnabled: boolean; readonly alertTypeEnabled: boolean; readonly securityFlyoutEnabled: boolean; readonly alertsPageFiltersEnabled: boolean; readonly newUserDetailsFlyout: boolean; readonly detectionsCoverageOverview: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly discoverInTimeline: boolean; }" + "{ readonly tGridEnabled: boolean; readonly tGridEventRenderedViewEnabled: boolean; readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly chartEmbeddablesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly alertsPreviewChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly insightsRelatedAlertsByProcessAncestry: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionsEnabled: boolean; readonly endpointResponseActionsEnabled: boolean; readonly alertDetailsPageEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly alertsPageChartsEnabled: boolean; readonly alertTypeEnabled: boolean; readonly alertsPageFiltersEnabled: boolean; readonly newUserDetailsFlyout: boolean; readonly detectionsCoverageOverview: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly discoverInTimeline: boolean; }" ], "path": "x-pack/plugins/security_solution/common/experimental_features.ts", "deprecated": false, @@ -3450,7 +3452,9 @@ "AppFeatureSecurityKey", " | ", "AppFeatureCasesKey", - ".casesConnectors)[]" + ".casesConnectors | ", + "AppFeatureAssistantKey", + ".assistant)[]" ], "path": "x-pack/plugins/security_solution/common/types/app_features.ts", "deprecated": false, @@ -3467,7 +3471,7 @@ "\nA list of allowed values that can be used in `xpack.securitySolution.enableExperimental`.\nThis object is then used to validate and parse the value entered." ], "signature": [ - "{ readonly tGridEnabled: boolean; readonly tGridEventRenderedViewEnabled: boolean; readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly chartEmbeddablesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly alertsPreviewChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly insightsRelatedAlertsByProcessAncestry: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionsEnabled: boolean; readonly endpointResponseActionsEnabled: boolean; readonly alertDetailsPageEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly alertsPageChartsEnabled: boolean; readonly alertTypeEnabled: boolean; readonly securityFlyoutEnabled: boolean; readonly alertsPageFiltersEnabled: boolean; readonly newUserDetailsFlyout: boolean; readonly detectionsCoverageOverview: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly discoverInTimeline: boolean; }" + "{ readonly tGridEnabled: boolean; readonly tGridEventRenderedViewEnabled: boolean; readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly chartEmbeddablesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly alertsPreviewChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly insightsRelatedAlertsByProcessAncestry: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionsEnabled: boolean; readonly endpointResponseActionsEnabled: boolean; readonly alertDetailsPageEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly alertsPageChartsEnabled: boolean; readonly alertTypeEnabled: boolean; readonly alertsPageFiltersEnabled: boolean; readonly newUserDetailsFlyout: boolean; readonly detectionsCoverageOverview: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly discoverInTimeline: boolean; }" ], "path": "x-pack/plugins/security_solution/common/experimental_features.ts", "deprecated": false, @@ -3499,6 +3503,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "securitySolution", + "id": "def-common.AppFeatureKey.Unnamed", + "type": "Any", + "tags": [], + "label": "Unnamed", + "description": [], + "signature": [ + "any" + ], + "path": "x-pack/plugins/security_solution/common/types/app_features.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "securitySolution", "id": "def-common.AppFeatureKey.Unnamed", diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index a52378d6e2547e..89dbc17f84244c 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] --- import securitySolutionObj from './security_solution.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-solution](https://github.com/orgs/elastic/teams/secur | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 194 | 2 | 128 | 32 | +| 195 | 3 | 129 | 33 | ## Client diff --git a/api_docs/security_solution_ess.mdx b/api_docs/security_solution_ess.mdx index fb8248f42f31b1..7fe58250bb650f 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: 2023-08-11 +date: 2023-08-14 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 27d5f6f3680dbf..0573ec951f98e6 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: 2023-08-11 +date: 2023-08-14 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 46824e0e10ab0b..3990fb5bee0bd4 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: 2023-08-11 +date: 2023-08-14 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 a2fdf5819cb29f..c550ec317ce6d4 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: 2023-08-11 +date: 2023-08-14 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 73da4ad9f9111d..2fd2f3f00da60e 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: 2023-08-11 +date: 2023-08-14 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 550e51cb0f0418..eef4483c965684 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: 2023-08-11 +date: 2023-08-14 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 8f81d457f7415a..0904554c292c99 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: 2023-08-11 +date: 2023-08-14 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 c9227da961ca29..4a120c21ab68a7 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: 2023-08-11 +date: 2023-08-14 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 66df012cb9f097..8ed77560643806 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: 2023-08-11 +date: 2023-08-14 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 3bb5ccdb0ff6c4..186a18ab193ca8 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: 2023-08-11 +date: 2023-08-14 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 1c0604e5efa4a4..4c7c25304f0938 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: 2023-08-11 +date: 2023-08-14 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 d04568c1292ddb..e14f833d5bd24a 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: 2023-08-11 +date: 2023-08-14 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 bb9985d7df43e0..c182fd0981e9a1 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: 2023-08-11 +date: 2023-08-14 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 f749de82fa1149..1a6fbf191914e4 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: 2023-08-11 +date: 2023-08-14 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 feb2deaf6e497b..9e1dc60792e39c 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: 2023-08-11 +date: 2023-08-14 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 8e25ae8a81f262..0c3aab7b95f3ae 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] --- import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json'; diff --git a/api_docs/text_based_languages.mdx b/api_docs/text_based_languages.mdx index fd4f3c1281454a..5c624aa7cfdf24 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'textBasedLanguages'] --- import textBasedLanguagesObj from './text_based_languages.devdocs.json'; diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx index 55eefe39343e3b..5e260b4abe28f9 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index 8db4258b83835c..1a2dca4aea889d 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: 2023-08-11 +date: 2023-08-14 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 6999f6b6202b6a..382eeead916846 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 351eaba5fd360a..fcc734c3f09daa 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: 2023-08-11 +date: 2023-08-14 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 7c1731cd03d4a1..43486dd88e1722 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: 2023-08-11 +date: 2023-08-14 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 185ea96738c729..f1d7577bfad5b4 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] --- import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json'; diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx index 877e7c31e4a13c..d5bd027711d8d6 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: 2023-08-11 +date: 2023-08-14 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 723f9e5f5df8c6..18f84c402819e5 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: 2023-08-11 +date: 2023-08-14 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 283f5306586498..cee7a119f1c600 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: 2023-08-11 +date: 2023-08-14 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 240a3ee4217fc4..a193b5337552dd 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: 2023-08-11 +date: 2023-08-14 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 6989741b6a623b..6231c8d2a44c2d 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: 2023-08-11 +date: 2023-08-14 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 62f2e922b51d95..f078dbfb5c1db4 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: 2023-08-11 +date: 2023-08-14 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 cc8aa551fe4b65..14dec8f744b332 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: 2023-08-11 +date: 2023-08-14 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 74637a4502f966..4653c4b7718b19 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: 2023-08-11 +date: 2023-08-14 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 a4f496ee98007b..2a380b0bb70a40 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: 2023-08-11 +date: 2023-08-14 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 1a4e4c888f38ad..7670a16ed68636 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: 2023-08-11 +date: 2023-08-14 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 aaa3db43215b17..baa878684cee96 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: 2023-08-11 +date: 2023-08-14 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 e39a6a93bec88a..d1dadcdd02cfe5 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: 2023-08-11 +date: 2023-08-14 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 c605dec3de204f..3b57c35b97e426 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: 2023-08-11 +date: 2023-08-14 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 4a3f2055b779d3..d493cfd41c38a8 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: 2023-08-11 +date: 2023-08-14 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 b9d4679bb363f7..193c56102658ae 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: 2023-08-11 +date: 2023-08-14 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 44a0526c32a7c9..93c86a70626e45 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: 2023-08-11 +date: 2023-08-14 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 e92d3c910284bf..024931acad4e6f 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: 2023-08-11 +date: 2023-08-14 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 b271ba18b64c50..b2fda95c7a9cb9 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: 2023-08-11 +date: 2023-08-14 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; diff --git a/config/serverless.es.yml b/config/serverless.es.yml index 5d971c9d2d03ae..bb16628f3f5eba 100644 --- a/config/serverless.es.yml +++ b/config/serverless.es.yml @@ -11,6 +11,7 @@ xpack.serverless.observability.enabled: false xpack.uptime.enabled: false enterpriseSearch.enabled: false monitoring.ui.enabled: false +xpack.fleet.enabled: false ## Enable the Serverless Search plugin xpack.serverless.search.enabled: true @@ -26,6 +27,3 @@ telemetry.labels.serverless: search # Alerts config xpack.actions.enabledActionTypes: ['.email', '.index', '.slack', '.jira', '.webhook', '.teams'] - -# Fleet specific configuration -xpack.fleet.internal.capabilities: ['serverless_search'] diff --git a/config/serverless.yml b/config/serverless.yml index 7a79f262cea73c..b0fc28f058b2e8 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -32,8 +32,11 @@ xpack.remote_clusters.enabled: false xpack.snapshot_restore.enabled: false xpack.license_management.enabled: false -# Disable index management actions from the UI +# Management team UI configurations +# Disable index actions from the Index Management UI xpack.index_management.enableIndexActions: false +# Disable legacy index templates from Index Management UI +xpack.index_management.enableLegacyTemplates: false # Keep deeplinks visible so that they are shown in the sidenav dev_tools.deeplinks.navLinkStatus: visible @@ -90,6 +93,7 @@ vis_type_timeseries.readOnly: true vis_type_vislib.readOnly: true vis_type_xy.readOnly: true input_control_vis.readOnly: true +xpack.graph.enabled: false # Disable cases in stack management xpack.cases.stack.enabled: false diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 1e58e8a9e6135b..5da373c8d9a514 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -10,6 +10,7 @@ Review important information about the {kib} 8.x releases. +* <> * <> * <> * <> @@ -45,6 +46,53 @@ Review important information about the {kib} 8.x releases. * <> -- +[[release-notes-8.9.1]] +== {kib} 8.9.1 + +coming::[8.9.1] + +Review the following information about the {kib} 8.9.1 release. + +[float] +[[breaking-changes-8.9.1]] +=== Breaking changes + +Breaking changes can prevent your application from optimal operation and performance. +Before you upgrade to 8.9.0, review the breaking changes, then mitigate the impact to your application. + +There are no breaking changes in the {kib} 8.9.1 release. + +To review the breaking changes in the previous release, check {kibana-ref-all}/8.9/release-notes-8.9.0.html#breaking-changes-8.9.0[8.9.0]. + +[float] +[[fixes-v8.9.1]] +=== Bug Fixes +APM:: +* Fixes flame graph rendering on the transaction detail page ({kibana-pull}162968[#162968]). +* Check if documents are missing `span.name` ({kibana-pull}162899[#162899]). +* Fixes transaction action menu for Trace Explorer and dependency operations ({kibana-pull}162213[#162213]). +Canvas:: +* Fixes embeddables not rendering in Canvas ({kibana-pull}163013[#163013]). +Discover:: +* Fixes grid styles to enable better content wrapping ({kibana-pull}162325[#162325]). +* Fixes search sessions using temporary data views ({kibana-pull}161029[#161029]). +* Make share links and search session information shorter for temporary data views ({kibana-pull}161180[#161180]). +Elastic Security:: +For the Elastic Security 8.9.1 release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_]. +Fleet:: +* Fixes for query error on Agents list in the UI ({kibana-pull}162816[#162816]). +* Remove duplicate path being pushed to package archive ({kibana-pull}162724[#162724]). +Management:: +* Resolves potential errors present in v8.9.0 with data views that contain field filters that have been edited ({kibana-pull}162860[#162860]). +Uptime:: +* Fixes Monitor not found 404 message display ({kibana-pull}163501[#163501]). + +[float] +[[enhancement-v8.9.1]] +=== Enhancements +Discover:: +* Set legend width to extra large and enable text wrapping in legend labels ({kibana-pull}163009[#163009]). + [[release-notes-8.9.0]] == {kib} 8.9.0 diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 7cad590c7f52c8..57e5ed40571ea5 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -215,7 +215,7 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel. |{kib-repo}blob/{branch}/src/plugins/interactive_setup/README.md[interactiveSetup] -|The plugin provides UI and APIs for the interactive setup mode. +|This plugin provides UI and APIs for interactive setup mode a.k.a "enrollment flow". |{kib-repo}blob/{branch}/src/plugins/kibana_overview/README.md[kibanaOverview] @@ -654,7 +654,7 @@ Elastic. |{kib-repo}blob/{branch}/x-pack/plugins/observability_ai_assistant/README.md[observabilityAIAssistant] -|This plugin provides the Observability AI Assistant service and UI components. +|This document gives an overview of the features of the Observability AI Assistant at the time of writing, and how to use them. At a high level, the Observability AI Assistant offers contextual insights, and a chat functionality that we enrich with function calling, allowing the LLM to hook into the user's data. We also allow the LLM to store things it considers new information as embeddings into Elasticsearch, and query this knowledge base when it decides it needs more information, using ELSER. |{kib-repo}blob/{branch}/x-pack/plugins/observability_onboarding/README.md[observabilityOnboarding] diff --git a/docs/user/troubleshooting/using-server-logs.asciidoc b/docs/user/troubleshooting/using-server-logs.asciidoc index ee6f1858478cd4..894b229d3f34dc 100644 --- a/docs/user/troubleshooting/using-server-logs.asciidoc +++ b/docs/user/troubleshooting/using-server-logs.asciidoc @@ -54,12 +54,12 @@ Once you set up the APM infrastructure, you can enable the APM agent and put {ki *Prerequisites* {kib} logs are configured to be in {ecs-ref}/ecs-reference.html[ECS JSON] format to include tracing identifiers. Open {kib} Logs and search for an operation you are interested in. -For example, suppose you want to investigate the response times for queries to the `/api/telemetry/v2/clusters/_stats` {kib} endpoint. +For example, suppose you want to investigate the response times for queries to the `/internal/telemetry/clusters/_stats` {kib} endpoint. Open Kibana Logs and search for the HTTP server response for the endpoint. It looks similar to the following (some fields are omitted for brevity). [source,json] ---- { - "message":"POST /api/telemetry/v2/clusters/_stats 200 1014ms - 43.2KB", + "message":"POST /internal/telemetry/clusters/_stats 200 1014ms - 43.2KB", "log":{"level":"DEBUG","logger":"http.server.response"}, "trace":{"id":"9b99131a6f66587971ef085ef97dfd07"}, "transaction":{"id":"d0c5bbf14f5febca"} diff --git a/examples/state_containers_examples/public/common/example_page.tsx b/examples/state_containers_examples/public/common/example_page.tsx index ab1141fd96e6cc..b60fb87248d9f9 100644 --- a/examples/state_containers_examples/public/common/example_page.tsx +++ b/examples/state_containers_examples/public/common/example_page.tsx @@ -7,7 +7,7 @@ */ import React, { PropsWithChildren } from 'react'; -import { EuiPage, EuiPageSideBar_Deprecated as EuiPageSideBar, EuiSideNav } from '@elastic/eui'; +import { EuiPage, EuiPageTemplate, EuiSideNav } from '@elastic/eui'; import { CoreStart } from '@kbn/core/public'; export interface ExampleLink { @@ -53,9 +53,9 @@ export const StateContainersExamplesPage: React.FC = ({ }: PropsWithChildren) => { return ( - + - + {children} ); diff --git a/examples/state_containers_examples/public/todo/todo.tsx b/examples/state_containers_examples/public/todo/todo.tsx index f7577331aee65f..3abd0941da8468 100644 --- a/examples/state_containers_examples/public/todo/todo.tsx +++ b/examples/state_containers_examples/public/todo/todo.tsx @@ -15,8 +15,8 @@ import { EuiCheckbox, EuiFieldText, EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, - EuiPageContentBody_Deprecated as EuiPageContentBody, + EuiPageTemplate, + EuiPageSection, EuiPageHeader, EuiPageHeaderSection, EuiSpacer, @@ -202,8 +202,8 @@ export const TodoAppPage: React.FC<{ - - + + @@ -233,8 +233,8 @@ export const TodoAppPage: React.FC<{ setUseHashedUrl(!useHashedUrl)}> {useHashedUrl ? 'Use Expanded State' : 'Use Hashed State'} - - + + ); diff --git a/examples/state_containers_examples/public/with_data_services/app.tsx b/examples/state_containers_examples/public/with_data_services/app.tsx index a70c956e17bb9b..2dda3faf0db88e 100644 --- a/examples/state_containers_examples/public/with_data_services/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/app.tsx @@ -13,7 +13,7 @@ import { Router } from '@kbn/shared-ux-router'; import { EuiFieldText, EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, + EuiPageTemplate, EuiPageHeader, EuiText, EuiTitle, @@ -99,7 +99,7 @@ export const App = ({ useDefaultBehaviors={true} showSaveQuery={true} /> - +

In addition to state from query bar also sync your arbitrary application state:

@@ -109,7 +109,7 @@ export const App = ({ onChange={(e) => appStateContainer.set({ ...appState, name: e.target.value })} aria-label="My name" /> -
+ diff --git a/examples/ui_actions_explorer/public/app.tsx b/examples/ui_actions_explorer/public/app.tsx index b467c4c47d5851..e267279296324f 100644 --- a/examples/ui_actions_explorer/public/app.tsx +++ b/examples/ui_actions_explorer/public/app.tsx @@ -9,18 +9,19 @@ import React, { useState } from 'react'; import ReactDOM from 'react-dom'; -import { EuiPage } from '@elastic/eui'; - -import { EuiButton } from '@elastic/eui'; -import { EuiPageBody } from '@elastic/eui'; -import { EuiPageContent_Deprecated as EuiPageContent } from '@elastic/eui'; -import { EuiPageContentBody_Deprecated as EuiPageContentBody } from '@elastic/eui'; -import { EuiSpacer } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { EuiFieldText } from '@elastic/eui'; -import { EuiCallOut } from '@elastic/eui'; -import { EuiPageHeader } from '@elastic/eui'; -import { EuiModalBody } from '@elastic/eui'; +import { + EuiPage, + EuiButton, + EuiPageBody, + EuiPageTemplate, + EuiPageSection, + EuiSpacer, + EuiText, + EuiFieldText, + EuiCallOut, + EuiPageHeader, + EuiModalBody, +} from '@elastic/eui'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { UiActionsStart, createAction } from '@kbn/ui-actions-plugin/public'; import { AppMountParameters, OverlayStart } from '@kbn/core/public'; @@ -39,9 +40,11 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { return ( - Ui Actions Explorer - - + + + + +

By default there is a single action attached to the `HELLO_WORLD_TRIGGER`. Clicking @@ -105,8 +108,8 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { - - + + ); diff --git a/examples/ui_actions_explorer/public/page.tsx b/examples/ui_actions_explorer/public/page.tsx index 05d64781c2ea35..a713cd89eeea11 100644 --- a/examples/ui_actions_explorer/public/page.tsx +++ b/examples/ui_actions_explorer/public/page.tsx @@ -8,14 +8,7 @@ import React from 'react'; -import { - EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiPageHeader, - EuiPageHeaderSection, - EuiTitle, -} from '@elastic/eui'; +import { EuiPageBody, EuiPageTemplate, EuiPageSection, EuiPageHeader } from '@elastic/eui'; interface PageProps { title: string; @@ -25,16 +18,12 @@ interface PageProps { export function Page({ title, children }: PageProps) { return ( - - - -

{title}

- - - - - {children} - + + + + + {children} +
); } diff --git a/package.json b/package.json index 27016cdefdb0b4..16f0ec705f349f 100644 --- a/package.json +++ b/package.json @@ -481,6 +481,7 @@ "@kbn/kibana-utils-plugin": "link:src/plugins/kibana_utils", "@kbn/kubernetes-security-plugin": "link:x-pack/plugins/kubernetes_security", "@kbn/language-documentation-popover": "link:packages/kbn-language-documentation-popover", + "@kbn/lens-embeddable-utils": "link:packages/kbn-lens-embeddable-utils", "@kbn/lens-plugin": "link:x-pack/plugins/lens", "@kbn/license-api-guard-plugin": "link:x-pack/plugins/license_api_guard", "@kbn/license-management-plugin": "link:x-pack/plugins/license_management", @@ -591,6 +592,7 @@ "@kbn/screenshot-mode-plugin": "link:src/plugins/screenshot_mode", "@kbn/screenshotting-example-plugin": "link:x-pack/examples/screenshotting_example", "@kbn/screenshotting-plugin": "link:x-pack/plugins/screenshotting", + "@kbn/search-api-panels": "link:packages/kbn-search-api-panels", "@kbn/search-examples-plugin": "link:examples/search_examples", "@kbn/search-response-warnings": "link:packages/kbn-search-response-warnings", "@kbn/searchprofiler-plugin": "link:x-pack/plugins/searchprofiler", @@ -741,6 +743,7 @@ "@kbn/url-state": "link:packages/kbn-url-state", "@kbn/usage-collection-plugin": "link:src/plugins/usage_collection", "@kbn/usage-collection-test-plugin": "link:test/plugin_functional/plugins/usage_collection", + "@kbn/use-tracked-promise": "link:packages/kbn-use-tracked-promise", "@kbn/user-profile-components": "link:packages/kbn-user-profile-components", "@kbn/user-profile-examples-plugin": "link:examples/user_profile_examples", "@kbn/user-profiles-consumer-plugin": "link:x-pack/test/security_api_integration/plugins/user_profiles_consumer", diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.test.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.test.tsx index 63db10979d0269..b2a1cecede239b 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.test.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.test.tsx @@ -61,7 +61,7 @@ describe('Header', () => { const toggleNav = async () => { fireEvent.click(await screen.findByTestId('toggleNavButton')); // click - expect(screen.queryAllByText('Hello, goodbye!')).toHaveLength(0); // title is not shown + expect(await screen.findByText('Hello, goodbye!')).not.toBeVisible(); fireEvent.click(await screen.findByTestId('toggleNavButton')); // click again diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx index 551fa7895795d9..f9e076db4410ec 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx @@ -141,14 +141,14 @@ const Logo = ( ); return ( - + {loadingCount === 0 ? ( ) : ( @@ -157,7 +157,7 @@ const Logo = ( size="l" aria-hidden={false} onClick={navigateHome} - data-test-subj="nav-header-loading-spinner" + data-test-subj="globalLoadingIndicator" /> )} diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx index c05e8c3c4b94d0..1d48a6eccfbb50 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { css } from '@emotion/react'; -import { EuiCollapsibleNav, EuiCollapsibleNavProps, useIsWithinMinBreakpoint } from '@elastic/eui'; +import { EuiCollapsibleNav, EuiCollapsibleNavProps } from '@elastic/eui'; const SIZE_EXPANDED = 248; -const SIZE_COLLAPSED = 48; +const SIZE_COLLAPSED = 0; export interface ProjectNavigationProps { isOpen: boolean; @@ -31,28 +31,30 @@ export const ProjectNavigation: React.FC = ({ flex-direction: row, `; - // on small screen isOpen hides the nav, - // on larger screen isOpen makes it smaller const DOCKED_BREAKPOINT = 's' as const; - const isCollapsible = useIsWithinMinBreakpoint(DOCKED_BREAKPOINT); - const isVisible = isCollapsible ? true : isOpen; - const isCollapsed = isCollapsible ? !isOpen : false; + const isVisible = isOpen; return ( - - {!isCollapsed && children} - + <> + { + /* must render the tree to initialize the navigation, even if it shouldn't be visible */ + !isOpen && + } + + {isOpen && children} + + ); }; diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/stack_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/stack_schema.ts new file mode 100644 index 00000000000000..362d64de05d98c --- /dev/null +++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/stack_schema.ts @@ -0,0 +1,86 @@ +/* + * 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. + */ +// ---------------------------------- WARNING ---------------------------------- +// this file was generated, and should not be edited by hand +// ---------------------------------- WARNING ---------------------------------- +import * as rt from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { AlertSchema } from './alert_schema'; +const ISO_DATE_PATTERN = /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z$/; +export const IsoDateString = new rt.Type( + 'IsoDateString', + rt.string.is, + (input, context): Either => { + if (typeof input === 'string' && ISO_DATE_PATTERN.test(input)) { + return rt.success(input); + } else { + return rt.failure(input, context); + } + }, + rt.identity +); +export type IsoDateStringC = typeof IsoDateString; +export const schemaDate = IsoDateString; +export const schemaDateArray = rt.array(IsoDateString); +export const schemaDateRange = rt.partial({ + gte: schemaDate, + lte: schemaDate, +}); +export const schemaDateRangeArray = rt.array(schemaDateRange); +export const schemaUnknown = rt.unknown; +export const schemaUnknownArray = rt.array(rt.unknown); +export const schemaString = rt.string; +export const schemaStringArray = rt.array(schemaString); +export const schemaNumber = rt.number; +export const schemaNumberArray = rt.array(schemaNumber); +export const schemaStringOrNumber = rt.union([schemaString, schemaNumber]); +export const schemaStringOrNumberArray = rt.array(schemaStringOrNumber); +export const schemaBoolean = rt.boolean; +export const schemaBooleanArray = rt.array(schemaBoolean); +const schemaGeoPointCoords = rt.type({ + type: schemaString, + coordinates: schemaNumberArray, +}); +const schemaGeoPointString = schemaString; +const schemaGeoPointLatLon = rt.type({ + lat: schemaNumber, + lon: schemaNumber, +}); +const schemaGeoPointLocation = rt.type({ + location: schemaNumberArray, +}); +const schemaGeoPointLocationString = rt.type({ + location: schemaString, +}); +export const schemaGeoPoint = rt.union([ + schemaGeoPointCoords, + schemaGeoPointString, + schemaGeoPointLatLon, + schemaGeoPointLocation, + schemaGeoPointLocationString, +]); +export const schemaGeoPointArray = rt.array(schemaGeoPoint); +// prettier-ignore +const StackAlertRequired = rt.type({ +}); +const StackAlertOptional = rt.partial({ + kibana: rt.partial({ + alert: rt.partial({ + evaluation: rt.partial({ + conditions: schemaString, + value: schemaString, + }), + title: schemaString, + }), + }), +}); + +// prettier-ignore +export const StackAlertSchema = rt.intersection([StackAlertRequired, StackAlertOptional, AlertSchema]); +// prettier-ignore +export type StackAlert = rt.TypeOf; diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/index.ts b/packages/kbn-alerts-as-data-utils/src/schemas/index.ts index a8aa3194aa8ef3..77d9476d2034b1 100644 --- a/packages/kbn-alerts-as-data-utils/src/schemas/index.ts +++ b/packages/kbn-alerts-as-data-utils/src/schemas/index.ts @@ -23,6 +23,7 @@ export type { ObservabilityMetricsAlert } from './generated/observability_metric export type { ObservabilitySloAlert } from './generated/observability_slo_schema'; export type { ObservabilityUptimeAlert } from './generated/observability_uptime_schema'; export type { SecurityAlert } from './generated/security_schema'; +export type { StackAlert } from './generated/stack_schema'; export type AADAlert = | Alert diff --git a/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts b/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts index 67a319b8dd498b..64abc36c05602e 100644 --- a/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts +++ b/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts @@ -80,8 +80,8 @@ async function getKibanaUrl({ target, logger }: { target: string; logger: Logger export async function getServiceUrls({ logger, target, kibana }: RunOptions & { logger: Logger }) { if (!target) { // assume things are running locally - kibana = kibana || 'http://localhost:5601'; - target = 'http://localhost:9200'; + kibana = kibana || 'http://127.0.0.1:5601'; + target = 'http://127.0.0.1:9200'; } if (!target) { diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index c2c9022b42b2a9..468018de2d680e 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -1505,6 +1505,9 @@ }, "prerelease_integrations_enabled": { "type": "boolean" + }, + "secret_storage_requirements_met": { + "type": "boolean" } } }, diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 49105db3e5db41..0950be22289331 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -391,6 +391,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { remoteClusters: `${ELASTICSEARCH_DOCS}remote-clusters.html`, remoteClustersProxy: `${ELASTICSEARCH_DOCS}remote-clusters.html#proxy-mode`, remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}remote-clusters-settings.html#remote-cluster-proxy-settings`, + remoteClustersOnPremSetupTrustWithCert: `${ELASTICSEARCH_DOCS}remote-clusters-cert.html`, + remoteClustersOnPremSetupTrustWithApiKey: `${ELASTICSEARCH_DOCS}remote-clusters-api-key.html`, + remoteClustersCloudSetupTrust: `${ELASTIC_WEBSITE_URL}guide/en/cloud/current/ec-enable-ccs.html`, rrf: `${ELASTICSEARCH_DOCS}rrf.html`, scriptParameters: `${ELASTICSEARCH_DOCS}modules-scripting-using.html#prefer-params`, secureCluster: `${ELASTICSEARCH_DOCS}secure-cluster.html`, diff --git a/packages/kbn-es-types/src/search.ts b/packages/kbn-es-types/src/search.ts index b403c5abef2212..13ebc02b65aa6d 100644 --- a/packages/kbn-es-types/src/search.ts +++ b/packages/kbn-es-types/src/search.ts @@ -162,6 +162,20 @@ export type AggregateOf< cardinality: { value: number; }; + change_point: { + bucket?: { + key: string; + }; + type: Record< + string, + { + change_point?: number; + r_value?: number; + trend?: string; + p_value: number; + } + >; + }; children: { doc_count: number; } & SubAggregateOf; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/README.md b/packages/kbn-lens-embeddable-utils/README.md similarity index 63% rename from x-pack/plugins/infra/public/common/visualizations/lens/README.md rename to packages/kbn-lens-embeddable-utils/README.md index c0fa8340f180e1..662ba0c92e7cf0 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/README.md +++ b/packages/kbn-lens-embeddable-utils/README.md @@ -1,11 +1,20 @@ -# Lens Attributes Builder +# @kbn/lens-embeddable-utils -The Lens Attributes Builder is a utility for creating JSON objects used to render charts with Lens. It simplifies the process of configuring and building the necessary attributes for different chart types. +## Lens Attributes Builder -## Usage + The Lens Attributes Builder is a utility for creating JSON objects used to render charts with Lens. It simplifies the process of configuring and building the necessary attributes for different chart types. -### Creating a Metric Chart +**Notes**: + +This utililty is primarily used by Infra Observability UI and not meant to be used as an official solution provided by the Lens team. + +- The tool has partial support of Lens charts, currently limited to XY and Metric charts. +- XY Bucket and Breakdown dimensions are limited respectively to Date Histogram and Top values. + +### Usage + +#### Creating a Metric Chart To create a metric chart, use the `MetricChart` class and provide the required configuration. Here's an example: @@ -22,13 +31,13 @@ const metricChart = new MetricChart({ }, }, }, - formulaAPI, }), dataView, + formulaAPI }); ``` -### Creating an XY Chart +#### Creating an XY Chart To create an XY chart, use the `XYChart` class and provide the required configuration. Here's an example: @@ -45,13 +54,72 @@ const xyChart = new XYChart({ }, }, }], - formulaAPI, + options: { + buckets: {type: 'date_histogram'}, + }, + })], + dataView, + formulaAPI +}); +``` + +#### Variations of the XY Chart + +XYChart has different series type variations. Here is an example of how to build a line (default) and area charts + +#### Line chart + +```ts +const xyChart = new XYChart({ + layers: [new XYDataLayer({ + data: [{ + label: 'Inbound (RX)', + value: "average(system.load.1) / max(system.load.cores)", + format: { + id: 'percent', + params: { + decimals: 1, + }, + }, + + }], + options: { + buckets: {type: 'date_histogram'}, + seriesType: 'line' // default. it doesn't need to be informed. + } + })], + dataView, + formulaAPI +}); +``` + +#### Area chart + +```ts +const xyChart = new XYChart({ + layers: [new XYDataLayer({ + data: [{ + label: 'Inbound (RX)', + value: "average(system.load.1) / max(system.load.cores)", + format: { + id: 'percent', + params: { + decimals: 1, + }, + }, + + }], + options: { + buckets: {type: 'date_histogram'}, + seriesType: 'area' + } })], dataView, + formulaAPI }); ``` -### Adding Multiple Layers to an XY Chart +#### Adding Multiple Layers to an XY Chart An XY chart can have multiple layers. Here's an example of containing a Reference Line Layer: @@ -69,10 +137,13 @@ const xyChart = new XYChart({ }, }, }], - formulaAPI, + options: { + buckets: {type: 'date_histogram'}, + }, }), new XYReferenceLineLayer({ data: [{ + value: "1", format: { id: 'percent', @@ -84,10 +155,11 @@ const xyChart = new XYChart({ }), ], dataView, + formulaAPI }); ``` -### Adding Multiple Data Sources in the Same Layer +#### Adding Multiple Data Sources in the Same Layer In an XY chart, it's possible to define multiple data sources within the same layer. @@ -115,13 +187,16 @@ const xyChart = new XYChart({ }, }, }], - formulaAPI, + options: { + buckets: {type: 'date_histogram'}, + }, }), dataView, + formulaAPI }); ``` -### Building Lens Chart Attributes +#### Building Lens Chart Attributes The `LensAttributesBuilder` is responsible for creating the full JSON object that combines the attributes returned by the chart classes. Here's an example: @@ -150,10 +225,10 @@ const builder = new LensAttributesBuilder({ }, }, }, - formulaAPI, }), dataView, - }) + formulaAPI + }), }); const lensAttributes = builder.build(); @@ -163,4 +238,4 @@ const lensAttributes = builder.build(); viewMode={ViewMode.VIEW} ... /> -``` +``` \ No newline at end of file diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/data_view_cache.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/data_view_cache.ts similarity index 83% rename from x-pack/plugins/infra/public/common/visualizations/lens/data_view_cache.ts rename to packages/kbn-lens-embeddable-utils/attribute_builder/data_view_cache.ts index f079ddd417710e..6eb76c9f5fec7e 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/data_view_cache.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/data_view_cache.ts @@ -1,13 +1,14 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { DataViewSpec, DataView } from '@kbn/data-plugin/common'; -export const DEFAULT_AD_HOC_DATA_VIEW_ID = 'infra_lens_ad_hoc_default'; +export const DEFAULT_AD_HOC_DATA_VIEW_ID = 'lens_ad_hoc_default'; export class DataViewCache { private static instance: DataViewCache; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.test.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/lens_attributes_builder.test.ts similarity index 92% rename from x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.test.ts rename to packages/kbn-lens-embeddable-utils/attribute_builder/lens_attributes_builder.test.ts index 2b88909e462b31..199379e74eb9db 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.test.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/lens_attributes_builder.test.ts @@ -1,8 +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; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 'jest-canvas-mock'; @@ -19,7 +20,7 @@ import { } from './visualization_types'; import type { FormulaPublicApi, GenericIndexPatternColumn } from '@kbn/lens-plugin/public'; import { ReferenceBasedIndexPatternColumn } from '@kbn/lens-plugin/public/datasources/form_based/operations/definitions/column_types'; -import type { FormulaConfig } from '../types'; +import type { FormulaValueConfig } from './types'; const mockDataView = { id: 'mock-id', @@ -85,7 +86,7 @@ const REFERENCE_LINE_LAYER: ReferenceBasedIndexPatternColumn = { scale: 'ratio', }; -const getFormula = (value: string): FormulaConfig => ({ +const getFormula = (value: string): FormulaValueConfig => ({ value, format: { id: 'percent', @@ -106,10 +107,10 @@ describe('lens_attributes_builder', () => { const metriChart = new MetricChart({ layers: new MetricLayer({ data: getFormula(AVERAGE_CPU_USER_FORMULA), - formulaAPI, }), dataView: mockDataView, + formulaAPI, }); const builder = new LensAttributesBuilder({ visualization: metriChart }); const { @@ -148,10 +149,10 @@ describe('lens_attributes_builder', () => { options: { showTrendLine: true, }, - formulaAPI, }), dataView: mockDataView, + formulaAPI, }); const builder = new LensAttributesBuilder({ visualization: metriChart }); const { @@ -204,10 +205,13 @@ describe('lens_attributes_builder', () => { layers: [ new XYDataLayer({ data: [getFormula(AVERAGE_CPU_USER_FORMULA)], - formulaAPI, + options: { + buckets: { type: 'date_histogram' }, + }, }), ], dataView: mockDataView, + formulaAPI, }); const builder = new LensAttributesBuilder({ visualization: xyChart }); const { @@ -248,13 +252,23 @@ describe('lens_attributes_builder', () => { layers: [ new XYDataLayer({ data: [getFormula(AVERAGE_CPU_USER_FORMULA)], - formulaAPI, + options: { + buckets: { type: 'date_histogram' }, + }, }), new XYReferenceLinesLayer({ - data: [getFormula('1')], + data: [ + { + value: '1', + format: { + id: 'percent', + }, + }, + ], }), ], dataView: mockDataView, + formulaAPI, }); const builder = new LensAttributesBuilder({ visualization: xyChart }); const { @@ -316,10 +330,13 @@ describe('lens_attributes_builder', () => { layers: [ new XYDataLayer({ data: [getFormula(AVERAGE_CPU_USER_FORMULA), getFormula(AVERAGE_CPU_SYSTEM_FORMULA)], - formulaAPI, + options: { + buckets: { type: 'date_histogram' }, + }, }), ], dataView: mockDataView, + formulaAPI, }); const builder = new LensAttributesBuilder({ visualization: xyChart }); const { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/lens_attributes_builder.ts similarity index 61% rename from x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.ts rename to packages/kbn-lens-embeddable-utils/attribute_builder/lens_attributes_builder.ts index d873f074ee3465..e09d9cad8b0bd5 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/lens_attributes_builder.ts @@ -1,15 +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. + * 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 { LensAttributes, LensVisualizationState, Chart, VisualizationAttributesBuilder, -} from '../types'; +} from './types'; import { DataViewCache } from './data_view_cache'; import { getAdhocDataView } from './utils'; @@ -17,12 +19,12 @@ export class LensAttributesBuilder> implements VisualizationAttributesBuilder { private dataViewCache: DataViewCache; - constructor(private state: { visualization: T }) { + constructor(private lens: { visualization: T }) { this.dataViewCache = DataViewCache.getInstance(); } build(): LensAttributes { - const { visualization } = this.state; + const { visualization } = this.lens; return { title: visualization.getTitle(), visualizationType: visualization.getVisualizationType(), @@ -34,10 +36,17 @@ export class LensAttributesBuilder> }, }, internalReferences: visualization.getReferences(), + // EmbeddableComponent receive filters. filters: [], + // EmbeddableComponent receive query. query: { language: 'kuery', query: '' }, visualization: visualization.getVisualizationState(), - adHocDataViews: getAdhocDataView(this.dataViewCache.getSpec(visualization.getDataView())), + // Getting the spec from a data view is a heavy operation, that's why the result is cached. + adHocDataViews: getAdhocDataView( + visualization + .getDataViews() + .reduce((acc, curr) => ({ ...acc, ...this.dataViewCache.getSpec(curr) }), {}) + ), }, }; } diff --git a/packages/kbn-lens-embeddable-utils/attribute_builder/types.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/types.ts new file mode 100644 index 00000000000000..6c9011b9782dc3 --- /dev/null +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/types.ts @@ -0,0 +1,91 @@ +/* + * 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 { SavedObjectReference } from '@kbn/core/server'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { + FormBasedPersistedState, + MetricVisualizationState, + PersistedIndexPatternLayer, + TypedLensByValueInput, + XYState, + FormulaPublicApi, + XYLayerConfig, +} from '@kbn/lens-plugin/public'; +export type LensAttributes = TypedLensByValueInput['attributes']; + +// Attributes +export type LensVisualizationState = XYState | MetricVisualizationState; + +export interface VisualizationAttributesBuilder { + build(): LensAttributes; +} + +// Column +export interface BaseChartColumn { + getValueConfig(): TValueConfig; +} + +export interface ChartColumn extends BaseChartColumn { + getData( + id: string, + baseLayer: PersistedIndexPatternLayer, + dataView: DataView, + formulaAPI: FormulaPublicApi + ): PersistedIndexPatternLayer; +} + +export interface StaticChartColumn extends BaseChartColumn { + getData(id: string, baseLayer: PersistedIndexPatternLayer): PersistedIndexPatternLayer; +} + +// Layer +export type LensLayerConfig = XYLayerConfig | MetricVisualizationState; + +export interface ChartLayer { + getName(): string | undefined; + getLayer( + layerId: string, + accessorId: string, + dataView: DataView, + formulaAPI: FormulaPublicApi + ): FormBasedPersistedState['layers']; + getReference(layerId: string, dataView: DataView): SavedObjectReference[]; + getLayerConfig(layerId: string, acessorId: string): TLayerConfig; + getDataView(): DataView | undefined; +} + +// Chart +export interface Chart { + getTitle(): string; + getVisualizationType(): string; + getLayers(): FormBasedPersistedState['layers']; + getVisualizationState(): TVisualizationState; + getReferences(): SavedObjectReference[]; + getDataViews(): DataView[]; +} +export interface ChartConfig< + TLayer extends ChartLayer | Array> +> { + formulaAPI: FormulaPublicApi; + dataView: DataView; + layers: TLayer; + title?: string; +} + +// Formula +type LensFormula = Parameters[1]; +export type FormulaValueConfig = Omit & { + color?: string; + value: string; +}; + +export type StaticValueConfig = Omit & { + color?: string; + value: string; +}; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/utils.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/utils.ts similarity index 68% rename from x-pack/plugins/infra/public/common/visualizations/lens/utils.ts rename to packages/kbn-lens-embeddable-utils/attribute_builder/utils.ts index 2a28e1ac659a9d..ed75956cadbe19 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/utils.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/utils.ts @@ -1,10 +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. + * 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 { + +import type { DateHistogramIndexPatternColumn, PersistedIndexPatternLayer, TermsIndexPatternColumn, @@ -17,12 +19,27 @@ export const DEFAULT_AD_HOC_DATA_VIEW_ID = 'infra_lens_ad_hoc_default'; const DEFAULT_BREAKDOWN_SIZE = 10; +export function nonNullable(v: T): v is NonNullable { + return v != null; +} + +export type DateHistogramColumnParams = DateHistogramIndexPatternColumn['params']; + +export type TopValuesColumnParams = Pick< + TermsIndexPatternColumn['params'], + 'size' | 'orderDirection' | 'orderBy' +>; + export const getHistogramColumn = ({ columnName, - overrides, + options, }: { columnName: string; - overrides?: Partial>; + options?: Partial< + Pick & { + params: DateHistogramColumnParams; + } + >; }) => { return { [columnName]: { @@ -32,32 +49,32 @@ export const getHistogramColumn = ({ operationType: 'date_histogram', scale: 'interval', sourceField: '@timestamp', - ...overrides, - params: { interval: 'auto', ...overrides?.params }, + ...options, + params: { interval: 'auto', ...options?.params }, } as DateHistogramIndexPatternColumn, }; }; export const getTopValuesColumn = ({ columnName, - overrides, + field, + options, }: { columnName: string; - overrides?: Partial> & { - breakdownSize?: number; - }; + field: string; + options?: Partial; }): PersistedIndexPatternLayer['columns'] => { - const { breakdownSize = DEFAULT_BREAKDOWN_SIZE, sourceField } = overrides ?? {}; + const { size = DEFAULT_BREAKDOWN_SIZE, ...params } = options ?? {}; return { [columnName]: { - label: `Top ${breakdownSize} values of ${sourceField}`, + label: `Top ${size} values of ${field}`, dataType: 'string', operationType: 'terms', scale: 'ordinal', - sourceField, + sourceField: field, isBucketed: true, params: { - size: breakdownSize, + size, orderBy: { type: 'alphabetical', fallback: false, @@ -72,6 +89,7 @@ export const getTopValuesColumn = ({ exclude: [], includeIsRegex: false, excludeIsRegex: false, + ...params, }, } as TermsIndexPatternColumn, }; diff --git a/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/index.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/index.ts new file mode 100644 index 00000000000000..7927ff37b2f16c --- /dev/null +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/index.ts @@ -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 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 { XYChart, type XYVisualOptions } from './xy_chart'; +export { MetricChart } from './metric_chart'; + +export * from './layers'; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/formula.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/columns/formula.ts similarity index 55% rename from x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/formula.ts rename to packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/columns/formula.ts index b1e30ce0bd225a..9ca0f215bc2b28 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/formula.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/columns/formula.ts @@ -1,28 +1,30 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { FormulaPublicApi, PersistedIndexPatternLayer } from '@kbn/lens-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; -import type { FormulaConfig, ChartColumn } from '../../../../types'; +import type { FormulaValueConfig, ChartColumn } from '../../../types'; export class FormulaColumn implements ChartColumn { - constructor(private formulaConfig: FormulaConfig, private formulaAPI: FormulaPublicApi) {} + constructor(private valueConfig: FormulaValueConfig) {} - getFormulaConfig(): FormulaConfig { - return this.formulaConfig; + getValueConfig(): FormulaValueConfig { + return this.valueConfig; } getData( id: string, baseLayer: PersistedIndexPatternLayer, - dataView: DataView + dataView: DataView, + formulaAPI: FormulaPublicApi ): PersistedIndexPatternLayer { - const { value, ...rest } = this.getFormulaConfig(); - const formulaLayer = this.formulaAPI.insertOrReplaceFormulaColumn( + const { value, ...rest } = this.getValueConfig(); + const formulaLayer = formulaAPI.insertOrReplaceFormulaColumn( id, { formula: value, ...rest }, baseLayer, diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/reference_line.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/columns/reference_line.ts similarity index 65% rename from x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/reference_line.ts rename to packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/columns/reference_line.ts index d9f9c5f2709979..f376ce40293f06 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/reference_line.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/columns/reference_line.ts @@ -1,23 +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; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { PersistedIndexPatternLayer } from '@kbn/lens-plugin/public'; import type { ReferenceBasedIndexPatternColumn } from '@kbn/lens-plugin/public/datasources/form_based/operations/definitions/column_types'; -import type { FormulaConfig, ChartColumn } from '../../../../types'; +import type { StaticChartColumn, StaticValueConfig } from '../../../types'; -export class ReferenceLineColumn implements ChartColumn { - constructor(private formulaConfig: FormulaConfig) {} +export class ReferenceLineColumn implements StaticChartColumn { + constructor(private valueConfig: StaticValueConfig) {} - getFormulaConfig(): FormulaConfig { - return this.formulaConfig; + getValueConfig(): StaticValueConfig { + return this.valueConfig; } getData(id: string, baseLayer: PersistedIndexPatternLayer): PersistedIndexPatternLayer { - const { label, ...params } = this.getFormulaConfig(); + const { label, ...params } = this.getValueConfig(); return { linkToLayers: [], columnOrder: [...baseLayer.columnOrder, id], diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/index.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/index.ts similarity index 53% rename from x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/index.ts rename to packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/index.ts index fb8300573b79ae..e7bf72a214a5ca 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/index.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/index.ts @@ -1,13 +1,14 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { MetricLayer, type MetricLayerOptions } from './metric_layer'; export { XYDataLayer, type XYLayerOptions } from './xy_data_layer'; export { XYReferenceLinesLayer } from './xy_reference_lines_layer'; -export { FormulaColumn as FormulaDataColumn } from './column/formula'; -export { ReferenceLineColumn } from './column/reference_line'; +export { FormulaColumn as FormulaDataColumn } from './columns/formula'; +export { ReferenceLineColumn } from './columns/reference_line'; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/metric_layer.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/metric_layer.ts similarity index 63% rename from x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/metric_layer.ts rename to packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/metric_layer.ts index c5f65c27640add..dbfc4481cb3d54 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/metric_layer.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/metric_layer.ts @@ -1,8 +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; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { SavedObjectReference } from '@kbn/core/server'; @@ -13,9 +14,9 @@ import type { MetricVisualizationState, PersistedIndexPatternLayer, } from '@kbn/lens-plugin/public'; -import type { ChartColumn, ChartLayer, FormulaConfig } from '../../../types'; +import type { ChartColumn, ChartLayer, FormulaValueConfig } from '../../types'; import { getDefaultReferences, getHistogramColumn } from '../../utils'; -import { FormulaColumn } from './column/formula'; +import { FormulaColumn } from './columns/formula'; const HISTOGRAM_COLUMN_NAME = 'x_date_histogram'; @@ -27,28 +28,32 @@ export interface MetricLayerOptions { } interface MetricLayerConfig { - data: FormulaConfig; + data: FormulaValueConfig; options?: MetricLayerOptions; - formulaAPI: FormulaPublicApi; + /** + * It is possible to define a specific dataView for the layer. It will override the global chart one + **/ + dataView?: DataView; } export class MetricLayer implements ChartLayer { private column: ChartColumn; constructor(private layerConfig: MetricLayerConfig) { - this.column = new FormulaColumn(layerConfig.data, layerConfig.formulaAPI); + this.column = new FormulaColumn(layerConfig.data); } getLayer( layerId: string, accessorId: string, - dataView: DataView + chartDataView: DataView, + formulaAPI: FormulaPublicApi ): FormBasedPersistedState['layers'] { const baseLayer: PersistedIndexPatternLayer = { columnOrder: [HISTOGRAM_COLUMN_NAME], columns: getHistogramColumn({ columnName: HISTOGRAM_COLUMN_NAME, - overrides: { - sourceField: dataView.timeFieldName, + options: { + sourceField: (this.layerConfig.dataView ?? chartDataView).timeFieldName, params: { interval: 'auto', includeEmptyRows: true, @@ -66,23 +71,29 @@ export class MetricLayer implements ChartLayer { columnOrder: [], columns: {}, }, - dataView + this.layerConfig.dataView ?? chartDataView, + formulaAPI ), }, ...(this.layerConfig.options?.showTrendLine ? { [`${layerId}_trendline`]: { linkToLayers: [layerId], - ...this.column.getData(`${accessorId}_trendline`, baseLayer, dataView), + ...this.column.getData( + `${accessorId}_trendline`, + baseLayer, + this.layerConfig.dataView ?? chartDataView, + formulaAPI + ), }, } : {}), }; } - getReference(layerId: string, dataView: DataView): SavedObjectReference[] { + getReference(layerId: string, chartDataView: DataView): SavedObjectReference[] { return [ - ...getDefaultReferences(dataView, layerId), - ...getDefaultReferences(dataView, `${layerId}_trendline`), + ...getDefaultReferences(this.layerConfig.dataView ?? chartDataView, layerId), + ...getDefaultReferences(this.layerConfig.dataView ?? chartDataView, `${layerId}_trendline`), ]; } @@ -107,6 +118,10 @@ export class MetricLayer implements ChartLayer { }; } getName(): string | undefined { - return this.column.getFormulaConfig().label; + return this.column.getValueConfig().label; + } + + getDataView(): DataView | undefined { + return this.layerConfig.dataView; } } diff --git a/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/xy_data_layer.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/xy_data_layer.ts new file mode 100644 index 00000000000000..956fd5370a8129 --- /dev/null +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/xy_data_layer.ts @@ -0,0 +1,177 @@ +/* + * 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 { SavedObjectReference } from '@kbn/core/server'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { + FormulaPublicApi, + FormBasedPersistedState, + PersistedIndexPatternLayer, + XYDataLayerConfig, + SeriesType, + TermsIndexPatternColumn, + DateHistogramIndexPatternColumn, +} from '@kbn/lens-plugin/public'; +import type { ChartColumn, ChartLayer, FormulaValueConfig } from '../../types'; +import { + getDefaultReferences, + getHistogramColumn, + getTopValuesColumn, + nonNullable, + type TopValuesColumnParams, + type DateHistogramColumnParams, +} from '../../utils'; +import { FormulaColumn } from './columns/formula'; + +const BREAKDOWN_COLUMN_NAME = 'aggs_breakdown'; +const HISTOGRAM_COLUMN_NAME = 'x_date_histogram'; + +interface TopValuesBucketedColumn { + type: 'top_values'; + field: TermsIndexPatternColumn['sourceField']; + params?: Partial; +} +interface DateHistogramBucketedColumn { + type: 'date_histogram'; + field?: DateHistogramIndexPatternColumn['sourceField']; + params?: Partial; +} + +export interface XYLayerOptions { + // Add more types as support for them is implemented + breakdown?: TopValuesBucketedColumn; + // Add more types as support for them is implemented + buckets?: DateHistogramBucketedColumn; + seriesType?: SeriesType; +} + +interface XYLayerConfig { + data: FormulaValueConfig[]; + options?: XYLayerOptions; + /** + * It is possible to define a specific dataView for the layer. It will override the global chart one + **/ + dataView?: DataView; +} + +export class XYDataLayer implements ChartLayer { + private column: ChartColumn[]; + private layerConfig: XYLayerConfig; + constructor(layerConfig: XYLayerConfig) { + this.column = layerConfig.data.map((dataItem) => new FormulaColumn(dataItem)); + this.layerConfig = { + ...layerConfig, + options: { + ...layerConfig.options, + buckets: { + type: 'date_histogram', + ...layerConfig.options?.buckets, + }, + }, + }; + } + + getName(): string | undefined { + return this.column[0].getValueConfig().label; + } + + getBaseLayer(dataView: DataView, options: XYLayerOptions) { + return { + ...(options.buckets?.type === 'date_histogram' + ? getHistogramColumn({ + columnName: HISTOGRAM_COLUMN_NAME, + options: { + ...options.buckets.params, + sourceField: options.buckets.field ?? dataView.timeFieldName, + }, + }) + : {}), + ...(options.breakdown?.type === 'top_values' + ? { + ...getTopValuesColumn({ + columnName: BREAKDOWN_COLUMN_NAME, + field: options?.breakdown.field, + options: { + ...options.breakdown.params, + }, + }), + } + : {}), + }; + } + + getLayer( + layerId: string, + accessorId: string, + chartDataView: DataView, + formulaAPI: FormulaPublicApi + ): FormBasedPersistedState['layers'] { + const columnOrder: string[] = []; + if (this.layerConfig.options?.breakdown?.type === 'top_values') { + columnOrder.push(BREAKDOWN_COLUMN_NAME); + } + if (this.layerConfig.options?.buckets?.type === 'date_histogram') { + columnOrder.push(HISTOGRAM_COLUMN_NAME); + } + + const baseLayer: PersistedIndexPatternLayer = { + columnOrder, + columns: { + ...this.getBaseLayer( + this.layerConfig.dataView ?? chartDataView, + this.layerConfig.options ?? {} + ), + }, + }; + + return { + [layerId]: this.column.reduce( + (acc, curr, index) => ({ + ...acc, + ...curr.getData( + `${accessorId}_${index}`, + acc, + this.layerConfig.dataView ?? chartDataView, + formulaAPI + ), + }), + baseLayer + ), + }; + } + + getReference(layerId: string, chartDataView: DataView): SavedObjectReference[] { + return getDefaultReferences(this.layerConfig.dataView ?? chartDataView, layerId); + } + + getLayerConfig(layerId: string, accessorId: string): XYDataLayerConfig { + return { + layerId, + seriesType: this.layerConfig.options?.seriesType ?? 'line', + accessors: this.column.map((_, index) => `${accessorId}_${index}`), + yConfig: this.layerConfig.data + .map(({ color }, index) => + color ? { forAccessor: `${accessorId}_${index}`, color } : undefined + ) + .filter(nonNullable), + layerType: 'data', + xAccessor: + this.layerConfig.options?.buckets?.type === 'date_histogram' + ? HISTOGRAM_COLUMN_NAME + : undefined, + splitAccessor: + this.layerConfig.options?.breakdown?.type === 'top_values' + ? BREAKDOWN_COLUMN_NAME + : undefined, + }; + } + + getDataView(): DataView | undefined { + return this.layerConfig.dataView; + } +} diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_reference_lines_layer.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/xy_reference_lines_layer.ts similarity index 58% rename from x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_reference_lines_layer.ts rename to packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/xy_reference_lines_layer.ts index c876473ee5f187..a91d1438f0daab 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_reference_lines_layer.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/layers/xy_reference_lines_layer.ts @@ -1,8 +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; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { SavedObjectReference } from '@kbn/core/server'; @@ -12,42 +13,42 @@ import type { PersistedIndexPatternLayer, XYReferenceLineLayerConfig, } from '@kbn/lens-plugin/public'; -import type { ChartColumn, ChartLayer, FormulaConfig } from '../../../types'; +import type { ChartLayer, StaticValueConfig, StaticChartColumn } from '../../types'; import { getDefaultReferences } from '../../utils'; -import { ReferenceLineColumn } from './column/reference_line'; +import { ReferenceLineColumn } from './columns/reference_line'; interface XYReferenceLinesLayerConfig { - data: FormulaConfig[]; + data: StaticValueConfig[]; + /** + * It is possible to define a specific dataView for the layer. It will override the global chart one + **/ + dataView?: DataView; } export class XYReferenceLinesLayer implements ChartLayer { - private column: ChartColumn[]; - constructor(layerConfig: XYReferenceLinesLayerConfig) { + private column: StaticChartColumn[]; + constructor(private layerConfig: XYReferenceLinesLayerConfig) { this.column = layerConfig.data.map((p) => new ReferenceLineColumn(p)); } getName(): string | undefined { - return this.column[0].getFormulaConfig().label; + return this.column[0].getValueConfig().label; } - getLayer( - layerId: string, - accessorId: string, - dataView: DataView - ): FormBasedPersistedState['layers'] { + getLayer(layerId: string, accessorId: string): FormBasedPersistedState['layers'] { const baseLayer = { columnOrder: [], columns: {} } as PersistedIndexPatternLayer; return { [`${layerId}_reference`]: this.column.reduce((acc, curr, index) => { return { ...acc, - ...curr.getData(`${accessorId}_${index}_reference_column`, acc, dataView), + ...curr.getData(`${accessorId}_${index}_reference_column`, acc), }; }, baseLayer), }; } - getReference(layerId: string, dataView: DataView): SavedObjectReference[] { - return getDefaultReferences(dataView, `${layerId}_reference`); + getReference(layerId: string, chartDataView: DataView): SavedObjectReference[] { + return getDefaultReferences(this.layerConfig.dataView ?? chartDataView, `${layerId}_reference`); } getLayerConfig(layerId: string, accessorId: string): XYReferenceLineLayerConfig { @@ -56,10 +57,14 @@ export class XYReferenceLinesLayer implements ChartLayer `${accessorId}_${index}_reference_column`), yConfig: this.column.map((layer, index) => ({ - color: layer.getFormulaConfig().color, + color: layer.getValueConfig().color, forAccessor: `${accessorId}_${index}_reference_column`, axisMode: 'left', })), }; } + + getDataView(): DataView | undefined { + return this.layerConfig.dataView; + } } diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/metric_chart.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/metric_chart.ts similarity index 66% rename from x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/metric_chart.ts rename to packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/metric_chart.ts index 136545e4a61bd3..9c3f5bf3afe983 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/metric_chart.ts +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/metric_chart.ts @@ -1,17 +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. + * 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 { FormBasedPersistedState, MetricVisualizationState } from '@kbn/lens-plugin/public'; import type { SavedObjectReference } from '@kbn/core/server'; import type { DataView } from '@kbn/data-views-plugin/public'; +import type { Chart, ChartConfig, ChartLayer } from '../types'; import { DEFAULT_LAYER_ID } from '../utils'; -import type { Chart, ChartConfig, ChartLayer } from '../../types'; - const ACCESSOR = 'metric_formula_accessor'; export class MetricChart implements Chart { @@ -22,7 +22,12 @@ export class MetricChart implements Chart { } getLayers(): FormBasedPersistedState['layers'] { - return this.chartConfig.layers.getLayer(DEFAULT_LAYER_ID, ACCESSOR, this.chartConfig.dataView); + return this.chartConfig.layers.getLayer( + DEFAULT_LAYER_ID, + ACCESSOR, + this.chartConfig.dataView, + this.chartConfig.formulaAPI + ); } getVisualizationState(): MetricVisualizationState { @@ -33,8 +38,10 @@ export class MetricChart implements Chart { return this.chartConfig.layers.getReference(DEFAULT_LAYER_ID, this.chartConfig.dataView); } - getDataView(): DataView { - return this.chartConfig.dataView; + getDataViews(): DataView[] { + return [this.chartConfig.dataView, this.chartConfig.layers.getDataView()].filter( + (x): x is DataView => !!x + ); } getTitle(): string { diff --git a/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/xy_chart.ts b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/xy_chart.ts new file mode 100644 index 00000000000000..cef612adc6b129 --- /dev/null +++ b/packages/kbn-lens-embeddable-utils/attribute_builder/visualization_types/xy_chart.ts @@ -0,0 +1,139 @@ +/* + * 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 { + FormBasedPersistedState, + XYArgs, + XYLayerConfig, + XYState, +} from '@kbn/lens-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { SavedObjectReference } from '@kbn/core/server'; +import type { Chart, ChartConfig, ChartLayer } from '../types'; +import { DEFAULT_LAYER_ID } from '../utils'; + +const ACCESSOR = 'formula_accessor'; + +// This needs be more specialized by `preferredSeriesType` +export interface XYVisualOptions { + lineInterpolation?: XYArgs['curveType']; + missingValues?: XYArgs['fittingFunction']; + endValues?: XYArgs['endValue']; + showDottedLine?: boolean; +} + +export class XYChart implements Chart { + private _layers: Array> | null = null; + constructor( + private chartConfig: ChartConfig>> & { + visualOptions?: XYVisualOptions; + } + ) {} + + getVisualizationType(): string { + return 'lnsXY'; + } + + private get layers() { + if (!this._layers) { + this._layers = Array.isArray(this.chartConfig.layers) + ? this.chartConfig.layers + : [this.chartConfig.layers]; + } + + return this._layers; + } + + getLayers(): FormBasedPersistedState['layers'] { + return this.layers.reduce((acc, curr, index) => { + const layerId = `${DEFAULT_LAYER_ID}_${index}`; + const accessorId = `${ACCESSOR}_${index}`; + return { + ...acc, + ...curr.getLayer( + layerId, + accessorId, + this.chartConfig.dataView, + this.chartConfig.formulaAPI + ), + }; + }, {}); + } + + getVisualizationState(): XYState { + return { + ...getXYVisualizationState({ + layers: [ + ...this.chartConfig.layers.map((layerItem, index) => { + const layerId = `${DEFAULT_LAYER_ID}_${index}`; + const accessorId = `${ACCESSOR}_${index}`; + return layerItem.getLayerConfig(layerId, accessorId); + }), + ], + }), + fittingFunction: this.chartConfig.visualOptions?.missingValues ?? 'None', + endValue: this.chartConfig.visualOptions?.endValues, + curveType: this.chartConfig.visualOptions?.lineInterpolation, + emphasizeFitting: !this.chartConfig.visualOptions?.showDottedLine, + }; + } + + getReferences(): SavedObjectReference[] { + return this.layers.flatMap((p, index) => { + const layerId = `${DEFAULT_LAYER_ID}_${index}`; + return p.getReference(layerId, this.chartConfig.dataView); + }); + } + + getDataViews(): DataView[] { + return [ + this.chartConfig.dataView, + ...this.chartConfig.layers.map((p) => p.getDataView()).filter((x): x is DataView => !!x), + ]; + } + + getTitle(): string { + return this.chartConfig.title ?? this.layers[0].getName() ?? ''; + } +} + +export const getXYVisualizationState = ( + custom: Omit, 'layers'> & { layers: XYState['layers'] } +): XYState => ({ + legend: { + isVisible: false, + position: 'right', + showSingleSeries: false, + }, + valueLabels: 'show', + yLeftScale: 'linear', + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + labelsOrientation: { + x: 0, + yLeft: 0, + yRight: 0, + }, + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + preferredSeriesType: 'line', + valuesInLegend: false, + hideEndzones: true, + ...custom, +}); diff --git a/packages/kbn-lens-embeddable-utils/index.ts b/packages/kbn-lens-embeddable-utils/index.ts new file mode 100644 index 00000000000000..77259a91ae7f6c --- /dev/null +++ b/packages/kbn-lens-embeddable-utils/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './attribute_builder/types'; + +export type { + MetricLayerOptions, + XYLayerOptions, + XYVisualOptions, +} from './attribute_builder/visualization_types'; + +export { + FormulaDataColumn, + MetricChart, + MetricLayer, + ReferenceLineColumn, + XYChart, + XYDataLayer, + XYReferenceLinesLayer, +} from './attribute_builder/visualization_types'; + +export { LensAttributesBuilder } from './attribute_builder/lens_attributes_builder'; diff --git a/packages/kbn-lens-embeddable-utils/jest.config.js b/packages/kbn-lens-embeddable-utils/jest.config.js new file mode 100644 index 00000000000000..cc0647cb2c626e --- /dev/null +++ b/packages/kbn-lens-embeddable-utils/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-lens-embeddable-utils'], + setupFiles: ['jest-canvas-mock'], +}; diff --git a/packages/kbn-lens-embeddable-utils/kibana.jsonc b/packages/kbn-lens-embeddable-utils/kibana.jsonc new file mode 100644 index 00000000000000..d637ea2f24ccbd --- /dev/null +++ b/packages/kbn-lens-embeddable-utils/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-browser", + "id": "@kbn/lens-embeddable-utils", + "owner": "@elastic/infra-monitoring-ui" +} diff --git a/packages/kbn-lens-embeddable-utils/package.json b/packages/kbn-lens-embeddable-utils/package.json new file mode 100644 index 00000000000000..c70d63093593fa --- /dev/null +++ b/packages/kbn-lens-embeddable-utils/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/lens-embeddable-utils", + "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-lens-embeddable-utils/tsconfig.json b/packages/kbn-lens-embeddable-utils/tsconfig.json new file mode 100644 index 00000000000000..eca0b277bff1df --- /dev/null +++ b/packages/kbn-lens-embeddable-utils/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": ["jest", "node"] + }, + "include": ["**/*.ts"], + "exclude": ["target/**/*"], + "kbn_references": ["@kbn/core", "@kbn/data-plugin", "@kbn/data-views-plugin", "@kbn/lens-plugin"] +} diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index d63b19f67f7837..c58dab33506954 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -28,7 +28,7 @@ pageLoadAssetSize: dashboard: 82025 dashboardEnhanced: 65646 data: 454087 - dataViewEditor: 12000 + dataViewEditor: 13000 dataViewFieldEditor: 27000 dataViewManagement: 5000 dataViews: 47000 @@ -97,7 +97,7 @@ pageLoadAssetSize: navigation: 37269 newsfeed: 42228 observability: 115443 - observabilityAIAssistant: 16759 + observabilityAIAssistant: 25000 observabilityOnboarding: 19573 observabilityShared: 52256 osquery: 107090 diff --git a/packages/kbn-search-api-panels/README.md b/packages/kbn-search-api-panels/README.md new file mode 100644 index 00000000000000..dba35349a67874 --- /dev/null +++ b/packages/kbn-search-api-panels/README.md @@ -0,0 +1,3 @@ +# @kbn/search-api-panels + +Empty package generated by @kbn/generate \ No newline at end of file diff --git a/x-pack/plugins/serverless_search/public/application/components/code_box.scss b/packages/kbn-search-api-panels/components/code_box.scss similarity index 100% rename from x-pack/plugins/serverless_search/public/application/components/code_box.scss rename to packages/kbn-search-api-panels/components/code_box.scss diff --git a/x-pack/plugins/serverless_search/public/application/components/code_box.tsx b/packages/kbn-search-api-panels/components/code_box.tsx similarity index 70% rename from x-pack/plugins/serverless_search/public/application/components/code_box.tsx rename to packages/kbn-search-api-panels/components/code_box.tsx index 59d1dbe0bc1431..55b89153282856 100644 --- a/x-pack/plugins/serverless_search/public/application/components/code_box.tsx +++ b/packages/kbn-search-api-panels/components/code_box.tsx @@ -1,10 +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. + * 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, { useState } from 'react'; + import { EuiButtonEmpty, EuiCodeBlock, @@ -19,49 +22,46 @@ import { EuiThemeProvider, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { PLUGIN_ID } from '../../../common'; -import { useKibanaServices } from '../hooks/use_kibana'; -import { consoleDefinition } from './languages/console'; -import { LanguageDefinition, LanguageDefinitionSnippetArguments } from './languages/types'; +import type { HttpStart } from '@kbn/core-http-browser'; +import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; + +import { LanguageDefinition } from '../types'; import { TryInConsoleButton } from './try_in_console_button'; import './code_box.scss'; interface CodeBoxProps { languages: LanguageDefinition[]; - code: keyof LanguageDefinition; - codeArgs: LanguageDefinitionSnippetArguments; + codeSnippet: string; // overrides the language type for syntax highlighting languageType?: string; selectedLanguage: LanguageDefinition; setSelectedLanguage: (language: LanguageDefinition) => void; + http: HttpStart; + pluginId: string; + application?: ApplicationStart; + sharePlugin: SharePluginStart; + showTryInConsole: boolean; } -const getCodeSnippet = ( - language: Partial, - key: keyof LanguageDefinition, - args: LanguageDefinitionSnippetArguments -): string => { - const snippetVal = language[key]; - if (snippetVal === undefined) return ''; - if (typeof snippetVal === 'string') return snippetVal; - return snippetVal(args); -}; - export const CodeBox: React.FC = ({ - code, - codeArgs, - languages, + application, + codeSnippet, + http, languageType, + languages, + pluginId, selectedLanguage, setSelectedLanguage, + sharePlugin, + showTryInConsole, }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const { http } = useKibanaServices(); + const items = languages.map((language) => ( { setSelectedLanguage(language); setIsPopoverOpen(false); @@ -74,7 +74,7 @@ export const CodeBox: React.FC = ({ const button = ( = ({ ); - const codeSnippet = getCodeSnippet(selectedLanguage, code, codeArgs); - const showTryInConsole = code in consoleDefinition; return ( @@ -110,7 +108,7 @@ export const CodeBox: React.FC = ({ {(copy) => ( - {i18n.translate('xpack.serverlessSearch.codeBox.copyButtonLabel', { + {i18n.translate('searchApiPanels.welcomeBanner.codeBox.copyButtonLabel', { defaultMessage: 'Copy', })} @@ -119,7 +117,11 @@ export const CodeBox: React.FC = ({ {showTryInConsole && ( - + )} diff --git a/x-pack/plugins/serverless_search/public/application/components/shared/github_link.tsx b/packages/kbn-search-api-panels/components/github_link.tsx similarity index 62% rename from x-pack/plugins/serverless_search/public/application/components/shared/github_link.tsx rename to packages/kbn-search-api-panels/components/github_link.tsx index f5214b1fe94745..19c7a83ed2de35 100644 --- a/x-pack/plugins/serverless_search/public/application/components/shared/github_link.tsx +++ b/packages/kbn-search-api-panels/components/github_link.tsx @@ -1,21 +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. + * 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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiLink } from '@elastic/eui'; import React from 'react'; -import { PLUGIN_ID } from '../../../../common'; -import { useKibanaServices } from '../../hooks/use_kibana'; -export const GithubLink: React.FC<{ label: string; href: string }> = ({ label, href }) => { - const { http } = useKibanaServices(); +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiLink } from '@elastic/eui'; +import { HttpStart } from '@kbn/core-http-browser'; + +export const GithubLink: React.FC<{ + label: string; + href: string; + http: HttpStart; + pluginId: string; +}> = ({ label, href, http, pluginId }) => { return ( - + diff --git a/x-pack/plugins/serverless_search/public/application/components/overview_panels/ingest_data.tsx b/packages/kbn-search-api-panels/components/ingest_data.tsx similarity index 59% rename from x-pack/plugins/serverless_search/public/application/components/overview_panels/ingest_data.tsx rename to packages/kbn-search-api-panels/components/ingest_data.tsx index 6dd7459b917839..9f82b91e76159d 100644 --- a/x-pack/plugins/serverless_search/public/application/components/overview_panels/ingest_data.tsx +++ b/packages/kbn-search-api-panels/components/ingest_data.tsx @@ -1,51 +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. + * 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, { useState } from 'react'; + import { EuiCheckableCard, EuiFormFieldset, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { docLinks } from '../../../../common/doc_links'; -import { CodeBox } from '../code_box'; -import { languageDefinitions } from '../languages/languages'; -import { LanguageDefinition, LanguageDefinitionSnippetArguments } from '../languages/types'; +import type { HttpStart } from '@kbn/core-http-browser'; +import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import { CodeBox } from './code_box'; +import { LanguageDefinition } from '../types'; import { OverviewPanel } from './overview_panel'; import { IntegrationsPanel } from './integrations_panel'; interface IngestDataProps { - codeArguments: LanguageDefinitionSnippetArguments; + codeSnippet: string; selectedLanguage: LanguageDefinition; setSelectedLanguage: (language: LanguageDefinition) => void; + docLinks: any; + http: HttpStart; + pluginId: string; + application?: ApplicationStart; + sharePlugin: SharePluginStart; + languages: LanguageDefinition[]; + showTryInConsole: boolean; } export const IngestData: React.FC = ({ - codeArguments, + codeSnippet, selectedLanguage, setSelectedLanguage, + docLinks, + http, + pluginId, + application, + sharePlugin, + languages, + showTryInConsole, }) => { const [selectedIngestMethod, setSelectedIngestMethod] = useState< 'ingestViaApi' | 'ingestViaIntegration' >('ingestViaApi'); return ( ) : ( - + ) } links={[ @@ -53,7 +74,7 @@ export const IngestData: React.FC = ({ ? [ { href: selectedLanguage.apiReference, - label: i18n.translate('xpack.serverlessSearch.ingestData.clientDocLink', { + label: i18n.translate('searchApiPanels.welcomeBanner.ingestData.clientDocLink', { defaultMessage: '{languageName} API reference', values: { languageName: selectedLanguage.name }, }), @@ -62,18 +83,18 @@ export const IngestData: React.FC = ({ : []), { href: docLinks.integrations, - label: i18n.translate('xpack.serverlessSearch.ingestData.integrationsLink', { + label: i18n.translate('searchApiPanels.welcomeBanner.ingestData.integrationsLink', { defaultMessage: 'About Integrations', }), }, ]} - title={i18n.translate('xpack.serverlessSearch.ingestData.title', { + title={i18n.translate('searchApiPanels.welcomeBanner.ingestData.title', { defaultMessage: 'Ingest data', })} > = ({ label={

- {i18n.translate('xpack.serverlessSearch.ingestData.ingestApiLabel', { + {i18n.translate('searchApiPanels.welcomeBanner.ingestData.ingestApiLabel', { defaultMessage: 'Ingest via API', })}

@@ -96,7 +117,7 @@ export const IngestData: React.FC = ({ onChange={() => setSelectedIngestMethod('ingestViaApi')} > - {i18n.translate('xpack.serverlessSearch.ingestData.ingestApiDescription', { + {i18n.translate('searchApiPanels.welcomeBanner.ingestData.ingestApiDescription', { defaultMessage: 'The most flexible way to index data, enabling full control over your customization and optimization options.', })} @@ -109,7 +130,7 @@ export const IngestData: React.FC = ({ label={

- {i18n.translate('xpack.serverlessSearch.ingestData.ingestIntegrationLabel', { + {i18n.translate('searchApiPanels.welcomeBanner.ingestData.ingestIntegrationLabel', { defaultMessage: 'Ingest via integration', })}

@@ -120,10 +141,13 @@ export const IngestData: React.FC = ({ onChange={() => setSelectedIngestMethod('ingestViaIntegration')} > - {i18n.translate('xpack.serverlessSearch.ingestData.ingestIntegrationDescription', { - defaultMessage: - 'Specialized ingestion tools optimized for transforming data and shipping it to Elasticsearch.', - })} + {i18n.translate( + 'searchApiPanels.welcomeBanner.ingestData.ingestIntegrationDescription', + { + defaultMessage: + 'Specialized ingestion tools optimized for transforming data and shipping it to Elasticsearch.', + } + )}
diff --git a/packages/kbn-search-api-panels/components/install_client.tsx b/packages/kbn-search-api-panels/components/install_client.tsx new file mode 100644 index 00000000000000..9083cf902f885e --- /dev/null +++ b/packages/kbn-search-api-panels/components/install_client.tsx @@ -0,0 +1,147 @@ +/* + * 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 { EuiSpacer, EuiCallOut, EuiText, EuiPanelProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { HttpStart } from '@kbn/core-http-browser'; +import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import { CodeBox } from './code_box'; +import { OverviewPanel } from './overview_panel'; +import { LanguageDefinition, Languages } from '../types'; +import { GithubLink } from './github_link'; + +interface InstallClientProps { + codeSnippet: string; + showTryInConsole: boolean; + language: LanguageDefinition; + setSelectedLanguage: (language: LanguageDefinition) => void; + http: HttpStart; + pluginId: string; + application?: ApplicationStart; + sharePlugin: SharePluginStart; + isPanelLeft?: boolean; + languages: LanguageDefinition[]; + overviewPanelProps?: Partial; +} + +const Link: React.FC<{ language: Languages; http: HttpStart; pluginId: string }> = ({ + language, + http, + pluginId, +}) => { + switch (language) { + case Languages.CURL: + return ( + + ); + case Languages.JAVASCRIPT: + return ( + + ); + case Languages.RUBY: + return ( + + ); + } + return null; +}; + +export const InstallClientPanel: React.FC = ({ + codeSnippet, + showTryInConsole, + language, + languages, + setSelectedLanguage, + http, + pluginId, + application, + sharePlugin, + isPanelLeft = true, + overviewPanelProps, +}) => { + const panelContent = ( + <> + + + + + + + {i18n.translate('searchApiPanels.welcomeBanner.apiCallout.content', { + defaultMessage: + 'Console enables you to call Elasticsearch and Kibana REST APIs directly, without needing to install a language client.', + })} + + + + ); + return ( + + ); +}; diff --git a/x-pack/plugins/serverless_search/public/application/components/overview_panels/integrations_panel.tsx b/packages/kbn-search-api-panels/components/integrations_panel.tsx similarity index 72% rename from x-pack/plugins/serverless_search/public/application/components/overview_panels/integrations_panel.tsx rename to packages/kbn-search-api-panels/components/integrations_panel.tsx index f49c12d63d2a38..15a5f70375cec7 100644 --- a/x-pack/plugins/serverless_search/public/application/components/overview_panels/integrations_panel.tsx +++ b/packages/kbn-search-api-panels/components/integrations_panel.tsx @@ -1,10 +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. + * 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 { EuiThemeProvider, EuiPanel, @@ -16,13 +19,22 @@ import { EuiText, EuiLink, } from '@elastic/eui'; +import { HttpStart } from '@kbn/core-http-browser'; import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { docLinks } from '../../../../common/doc_links'; -import { LEARN_MORE_LABEL } from '../../../../common/i18n_string'; -import { GithubLink } from '../shared/github_link'; +import { LEARN_MORE_LABEL } from '../constants'; +import { GithubLink } from './github_link'; -export const IntegrationsPanel: React.FC = () => { +export interface IntegrationsPanelProps { + docLinks: any; + http: HttpStart; + pluginId: string; +} + +export const IntegrationsPanel: React.FC = ({ + docLinks, + http, + pluginId, +}) => { return ( @@ -33,7 +45,7 @@ export const IntegrationsPanel: React.FC = () => {

- {i18n.translate('xpack.serverlessSearch.ingestData.logstashTitle', { + {i18n.translate('searchApiPanels.welcomeBanner.ingestData.logstashTitle', { defaultMessage: 'Logstash', })}

@@ -42,7 +54,7 @@ export const IntegrationsPanel: React.FC = () => {

- {i18n.translate('xpack.serverlessSearch.ingestData.logstashDescription', { + {i18n.translate('searchApiPanels.welcomeBanner.ingestData.logstashDescription', { defaultMessage: 'Add data to your data stream or index to make it searchable. Choose an ingestion method that fits your application and workflow.', })} @@ -60,9 +72,11 @@ export const IntegrationsPanel: React.FC = () => { @@ -76,14 +90,14 @@ export const IntegrationsPanel: React.FC = () => {

- {i18n.translate('xpack.serverlessSearch.ingestData.beatsTitle', { + {i18n.translate('searchApiPanels.welcomeBanner.ingestData.beatsTitle', { defaultMessage: 'Beats', })}

- {i18n.translate('xpack.serverlessSearch.ingestData.beatsDescription', { + {i18n.translate('searchApiPanels.welcomeBanner.ingestData.beatsDescription', { defaultMessage: 'Lightweight, single-purpose data shippers for Elasticsearch. Use Beats to send operational data from your servers.', })} @@ -100,9 +114,11 @@ export const IntegrationsPanel: React.FC = () => {
@@ -116,14 +132,14 @@ export const IntegrationsPanel: React.FC = () => {

- {i18n.translate('xpack.serverlessSearch.ingestData.connectorsTitle', { + {i18n.translate('searchApiPanels.welcomeBanner.ingestData.connectorsTitle', { defaultMessage: 'Connector Client', })}

- {i18n.translate('xpack.serverlessSearch.ingestData.connectorsDescription', { + {i18n.translate('searchApiPanels.welcomeBanner.ingestData.connectorsDescription', { defaultMessage: 'Specialized integrations for syncing data from third-party sources to Elasticsearch. Use Elastic Connectors to sync content from a range of databases and object stores.', })} @@ -140,9 +156,14 @@ export const IntegrationsPanel: React.FC = () => { diff --git a/x-pack/plugins/serverless_search/public/application/components/overview_panels/language_client_panel.tsx b/packages/kbn-search-api-panels/components/language_client_panel.tsx similarity index 76% rename from x-pack/plugins/serverless_search/public/application/components/overview_panels/language_client_panel.tsx rename to packages/kbn-search-api-panels/components/language_client_panel.tsx index ebadfdbb61a840..1a2cdab27a35ce 100644 --- a/x-pack/plugins/serverless_search/public/application/components/overview_panels/language_client_panel.tsx +++ b/packages/kbn-search-api-panels/components/language_client_panel.tsx @@ -1,9 +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. + * 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 { EuiFlexGroup, EuiFlexItem, @@ -14,24 +18,29 @@ import { useEuiTheme, } from '@elastic/eui'; -import React from 'react'; -import { PLUGIN_ID } from '../../../../common'; -import { useKibanaServices } from '../../hooks/use_kibana'; -import { LanguageDefinition } from '../languages/types'; +import type { HttpStart } from '@kbn/core-http-browser'; + +import { LanguageDefinition } from '../types'; import './select_client.scss'; + interface SelectClientProps { language: LanguageDefinition; setSelectedLanguage: (language: LanguageDefinition) => void; isSelectedLanguage: boolean; + http: HttpStart; + pluginId?: string; + src?: string; } export const LanguageClientPanel: React.FC = ({ language, setSelectedLanguage, isSelectedLanguage, + http, + pluginId, + src, }) => { const { euiTheme } = useEuiTheme(); - const { http } = useKibanaServices(); return ( @@ -51,7 +60,9 @@ export const LanguageClientPanel: React.FC = ({ diff --git a/x-pack/plugins/serverless_search/public/application/components/overview_panels/overview_panel.tsx b/packages/kbn-search-api-panels/components/overview_panel.tsx similarity index 71% rename from x-pack/plugins/serverless_search/public/application/components/overview_panels/overview_panel.tsx rename to packages/kbn-search-api-panels/components/overview_panel.tsx index def3f1e4c93595..56501fab19e372 100644 --- a/x-pack/plugins/serverless_search/public/application/components/overview_panels/overview_panel.tsx +++ b/packages/kbn-search-api-panels/components/overview_panel.tsx @@ -1,10 +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. + * 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 { EuiFlexGroup, EuiFlexItem, @@ -13,15 +16,17 @@ import { EuiPanel, EuiTitle, EuiLink, + EuiPanelProps, } from '@elastic/eui'; -import React from 'react'; -import { LEARN_MORE_LABEL } from '../../../../common/i18n_string'; +import { LEARN_MORE_LABEL } from '../constants'; interface OverviewPanelProps { description?: React.ReactNode | string; - leftPanelContent: React.ReactNode; + leftPanelContent?: React.ReactNode; links?: Array<{ label: string; href: string }>; + rightPanelContent?: React.ReactNode; title: string; + overviewPanelProps?: Partial; } export const OverviewPanel: React.FC = ({ @@ -29,15 +34,17 @@ export const OverviewPanel: React.FC = ({ description, leftPanelContent, links, + rightPanelContent, title, + overviewPanelProps, }) => { return ( <> - {leftPanelContent} + {leftPanelContent && {leftPanelContent}} - +

{title}

@@ -62,6 +69,7 @@ export const OverviewPanel: React.FC = ({ ) : null}
+ {rightPanelContent && {rightPanelContent}}
diff --git a/x-pack/plugins/serverless_search/public/application/components/overview_panels/select_client.scss b/packages/kbn-search-api-panels/components/select_client.scss similarity index 100% rename from x-pack/plugins/serverless_search/public/application/components/overview_panels/select_client.scss rename to packages/kbn-search-api-panels/components/select_client.scss diff --git a/packages/kbn-search-api-panels/components/select_client.tsx b/packages/kbn-search-api-panels/components/select_client.tsx new file mode 100644 index 00000000000000..1e9a3d294f7603 --- /dev/null +++ b/packages/kbn-search-api-panels/components/select_client.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 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 { + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPanelProps, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { HttpStart } from '@kbn/core-http-browser'; +import { OverviewPanel } from './overview_panel'; +import './select_client.scss'; + +export interface SelectClientPanelProps { + docLinks: { elasticsearchClients: string; kibanaRunApiInConsole: string }; + http: HttpStart; + isPanelLeft?: boolean; + overviewPanelProps?: Partial; +} + +export const SelectClientPanel: React.FC = ({ + docLinks, + children, + http, + isPanelLeft = true, + overviewPanelProps, +}) => { + const panelContent = ( + <> + + + + + {i18n.translate('searchApiPanels.welcomeBanner.selectClient.heading', { + defaultMessage: 'Choose one', + })} + + + + + + + {children} + + + +

+ {i18n.translate('searchApiPanels.welcomeBanner.selectClient.callout.description', { + defaultMessage: + 'With Console, you can get started right away with our REST API’s. No installation required. ', + })} + + + + {i18n.translate('searchApiPanels.welcomeBanner.selectClient.callout.link', { + defaultMessage: 'Try Console now', + })} + + +

+
+ + ); + return ( + + {i18n.translate( + 'searchApiPanels.welcomeBanner.selectClient.description.console.link', + { + defaultMessage: 'Console', + } + )} + + ), + }} + /> + } + leftPanelContent={isPanelLeft ? panelContent : undefined} + rightPanelContent={!isPanelLeft ? panelContent : undefined} + links={[ + { + href: docLinks.elasticsearchClients, + label: i18n.translate( + 'searchApiPanels.welcomeBanner.selectClient.elasticsearchClientDocLink', + { + defaultMessage: 'Elasticsearch clients ', + } + ), + }, + { + href: docLinks.kibanaRunApiInConsole, + label: i18n.translate( + 'searchApiPanels.welcomeBanner.selectClient.apiRequestConsoleDocLink', + { + defaultMessage: 'Run API requests in Console ', + } + ), + }, + ]} + title={i18n.translate('searchApiPanels.welcomeBanner.selectClient.title', { + defaultMessage: 'Select your client', + })} + overviewPanelProps={overviewPanelProps} + /> + ); +}; diff --git a/x-pack/plugins/serverless_search/public/application/components/try_in_console_button.tsx b/packages/kbn-search-api-panels/components/try_in_console_button.tsx similarity index 63% rename from x-pack/plugins/serverless_search/public/application/components/try_in_console_button.tsx rename to packages/kbn-search-api-panels/components/try_in_console_button.tsx index 9426e8f31ea788..35f6ef5d00184d 100644 --- a/x-pack/plugins/serverless_search/public/application/components/try_in_console_button.tsx +++ b/packages/kbn-search-api-panels/components/try_in_console_button.tsx @@ -1,26 +1,31 @@ /* * 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. + * 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 { EuiButtonEmpty } from '@elastic/eui'; +import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { compressToEncodedURIComponent } from 'lz-string'; -import { useKibanaServices } from '../hooks/use_kibana'; - export interface TryInConsoleButtonProps { request: string; + application?: ApplicationStart; + sharePlugin: SharePluginStart; } -export const TryInConsoleButton = ({ request }: TryInConsoleButtonProps) => { - const { - application, - share: { url }, - } = useKibanaServices(); +export const TryInConsoleButton = ({ + request, + application, + sharePlugin, +}: TryInConsoleButtonProps) => { + const { url } = sharePlugin; const canShowDevtools = !!application?.capabilities?.dev_tools?.show; if (!canShowDevtools || !url) return null; @@ -37,7 +42,7 @@ export const TryInConsoleButton = ({ request }: TryInConsoleButtonProps) => { return ( diff --git a/packages/kbn-search-api-panels/constants.ts b/packages/kbn-search-api-panels/constants.ts new file mode 100644 index 00000000000000..dda3028ac96afd --- /dev/null +++ b/packages/kbn-search-api-panels/constants.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const LEARN_MORE_LABEL: string = i18n.translate( + 'searchApiPanels.welcomeBanner.panels.learnMore', + { + defaultMessage: 'Learn more', + } +); +export const API_KEY_PLACEHOLDER = 'your_api_key'; +export const ELASTICSEARCH_URL_PLACEHOLDER = 'https://your_deployment_url'; +export const INDEX_NAME_PLACEHOLDER = 'index_name'; diff --git a/packages/kbn-search-api-panels/index.tsx b/packages/kbn-search-api-panels/index.tsx new file mode 100644 index 00000000000000..d3781084eacae8 --- /dev/null +++ b/packages/kbn-search-api-panels/index.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer, EuiImage, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export * from './components/code_box'; +export * from './components/github_link'; +export * from './components/ingest_data'; +export * from './components/integrations_panel'; +export * from './components/language_client_panel'; +export * from './components/overview_panel'; +export * from './components/select_client'; +export * from './components/try_in_console_button'; +export * from './components/install_client'; + +export * from './types'; + +export interface WelcomeBannerProps { + userProfile: { + user: { + full_name?: string; + username?: string; + }; + }; + assetBasePath?: string; + image?: string; + showDescription?: boolean; +} + +export const WelcomeBanner: React.FC = ({ + userProfile, + assetBasePath, + image, + showDescription = true, +}) => ( + + + {/* Reversing column direction here so screenreaders keep h1 as the first element */} + + + +

+ {i18n.translate('searchApiPanels.welcomeBanner.header.title', { + defaultMessage: 'Get started with Elasticsearch', + })} +

+
+
+ + +

+ {i18n.translate('searchApiPanels.welcomeBanner.header.greeting.title', { + defaultMessage: 'Hi {name}!', + values: { name: userProfile?.user?.full_name || userProfile?.user?.username }, + })} +

+
+
+
+ + {showDescription && ( + + {i18n.translate('searchApiPanels.welcomeBanner.header.description', { + defaultMessage: + "Set up your programming language client, ingest some data, and you'll be ready to start searching within minutes.", + })} + + )} + +
+ + + + +
+); diff --git a/packages/kbn-search-api-panels/jest.config.js b/packages/kbn-search-api-panels/jest.config.js new file mode 100644 index 00000000000000..07a6db594c9447 --- /dev/null +++ b/packages/kbn-search-api-panels/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-search-api-panels'], +}; diff --git a/packages/kbn-search-api-panels/kibana.jsonc b/packages/kbn-search-api-panels/kibana.jsonc new file mode 100644 index 00000000000000..96c4e5beacf237 --- /dev/null +++ b/packages/kbn-search-api-panels/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/search-api-panels", + "owner": "@elastic/enterprise-search-frontend" +} diff --git a/packages/kbn-search-api-panels/package.json b/packages/kbn-search-api-panels/package.json new file mode 100644 index 00000000000000..8bc3c22474800f --- /dev/null +++ b/packages/kbn-search-api-panels/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/search-api-panels", + "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-search-api-panels/tsconfig.json b/packages/kbn-search-api-panels/tsconfig.json new file mode 100644 index 00000000000000..82fd44f2cbb32f --- /dev/null +++ b/packages/kbn-search-api-panels/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/i18n", + "@kbn/core-http-browser", + "@kbn/core-application-browser", + "@kbn/share-plugin", + "@kbn/i18n-react" + ] +} diff --git a/x-pack/plugins/serverless_search/public/application/components/languages/types.ts b/packages/kbn-search-api-panels/types.ts similarity index 83% rename from x-pack/plugins/serverless_search/public/application/components/languages/types.ts rename to packages/kbn-search-api-panels/types.ts index 7849b800fc1a04..1d35f440673def 100644 --- a/x-pack/plugins/serverless_search/public/application/components/languages/types.ts +++ b/packages/kbn-search-api-panels/types.ts @@ -1,8 +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; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 enum Languages { diff --git a/packages/kbn-use-tracked-promise/README.md b/packages/kbn-use-tracked-promise/README.md new file mode 100644 index 00000000000000..276f0ba0e9b5a3 --- /dev/null +++ b/packages/kbn-use-tracked-promise/README.md @@ -0,0 +1,62 @@ +# @kbn/use-tracked-promise + +/** + * This hook manages a Promise factory and can create new Promises from it. The + * state of these Promises is tracked and they can be canceled when superseded + * to avoid race conditions. + * + * ``` + * const [requestState, performRequest] = useTrackedPromise( + * { + * cancelPreviousOn: 'resolution', + * createPromise: async (url: string) => { + * return await fetchSomething(url) + * }, + * onResolve: response => { + * setSomeState(response.data); + * }, + * onReject: response => { + * setSomeError(response); + * }, + * }, + * [fetchSomething] + * ); + * ``` + * + * The `onResolve` and `onReject` handlers are registered separately, because + * the hook will inject a rejection when in case of a canellation. The + * `cancelPreviousOn` attribute can be used to indicate when the preceding + * pending promises should be canceled: + * + * 'never': No preceding promises will be canceled. + * + * 'creation': Any preceding promises will be canceled as soon as a new one is + * created. + * + * 'settlement': Any preceding promise will be canceled when a newer promise is + * resolved or rejected. + * + * 'resolution': Any preceding promise will be canceled when a newer promise is + * resolved. + * + * 'rejection': Any preceding promise will be canceled when a newer promise is + * rejected. + * + * Any pending promises will be canceled when the component using the hook is + * unmounted, but their status will not be tracked to avoid React warnings + * about memory leaks. + * + * The last argument is a normal React hook dependency list that indicates + * under which conditions a new reference to the configuration object should be + * used. + * + * The `onResolve`, `onReject` and possible uncatched errors are only triggered + * if the underlying component is mounted. To ensure they always trigger (i.e. + * if the promise is called in a `useLayoutEffect`) use the `triggerOrThrow` + * attribute: + * + * 'whenMounted': (default) they are called only if the component is mounted. + * + * 'always': they always call. The consumer is then responsible of ensuring no + * side effects happen if the underlying component is not mounted. + */ \ No newline at end of file diff --git a/packages/kbn-use-tracked-promise/index.ts b/packages/kbn-use-tracked-promise/index.ts new file mode 100644 index 00000000000000..fb7dece2955790 --- /dev/null +++ b/packages/kbn-use-tracked-promise/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 { useTrackedPromise } from './use_tracked_promise'; diff --git a/packages/kbn-use-tracked-promise/jest.config.js b/packages/kbn-use-tracked-promise/jest.config.js new file mode 100644 index 00000000000000..3cc6b8f82572fa --- /dev/null +++ b/packages/kbn-use-tracked-promise/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-use-tracked-promise'], +}; diff --git a/packages/kbn-use-tracked-promise/kibana.jsonc b/packages/kbn-use-tracked-promise/kibana.jsonc new file mode 100644 index 00000000000000..a7b90045c462aa --- /dev/null +++ b/packages/kbn-use-tracked-promise/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/use-tracked-promise", + "owner": "@elastic/infra-monitoring-ui" +} diff --git a/packages/kbn-use-tracked-promise/package.json b/packages/kbn-use-tracked-promise/package.json new file mode 100644 index 00000000000000..a099336423e87b --- /dev/null +++ b/packages/kbn-use-tracked-promise/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/use-tracked-promise", + "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-use-tracked-promise/tsconfig.json b/packages/kbn-use-tracked-promise/tsconfig.json new file mode 100644 index 00000000000000..2f9ddddbeea23c --- /dev/null +++ b/packages/kbn-use-tracked-promise/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [] +} diff --git a/packages/kbn-use-tracked-promise/use_tracked_promise.ts b/packages/kbn-use-tracked-promise/use_tracked_promise.ts new file mode 100644 index 00000000000000..149c9feab06cdc --- /dev/null +++ b/packages/kbn-use-tracked-promise/use_tracked_promise.ts @@ -0,0 +1,300 @@ +/* + * 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. + */ + +/* eslint-disable max-classes-per-file */ + +import { DependencyList, useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; + +interface UseTrackedPromiseArgs { + createPromise: (...args: Arguments) => Promise; + onResolve?: (result: Result) => void; + onReject?: (value: unknown) => void; + cancelPreviousOn?: 'creation' | 'settlement' | 'resolution' | 'rejection' | 'never'; + triggerOrThrow?: 'always' | 'whenMounted'; +} + +/** + * This hook manages a Promise factory and can create new Promises from it. The + * state of these Promises is tracked and they can be canceled when superseded + * to avoid race conditions. + * + * ``` + * const [requestState, performRequest] = useTrackedPromise( + * { + * cancelPreviousOn: 'resolution', + * createPromise: async (url: string) => { + * return await fetchSomething(url) + * }, + * onResolve: response => { + * setSomeState(response.data); + * }, + * onReject: response => { + * setSomeError(response); + * }, + * }, + * [fetchSomething] + * ); + * ``` + * + * The `onResolve` and `onReject` handlers are registered separately, because + * the hook will inject a rejection when in case of a canellation. The + * `cancelPreviousOn` attribute can be used to indicate when the preceding + * pending promises should be canceled: + * + * 'never': No preceding promises will be canceled. + * + * 'creation': Any preceding promises will be canceled as soon as a new one is + * created. + * + * 'settlement': Any preceding promise will be canceled when a newer promise is + * resolved or rejected. + * + * 'resolution': Any preceding promise will be canceled when a newer promise is + * resolved. + * + * 'rejection': Any preceding promise will be canceled when a newer promise is + * rejected. + * + * Any pending promises will be canceled when the component using the hook is + * unmounted, but their status will not be tracked to avoid React warnings + * about memory leaks. + * + * The last argument is a normal React hook dependency list that indicates + * under which conditions a new reference to the configuration object should be + * used. + * + * The `onResolve`, `onReject` and possible uncatched errors are only triggered + * if the underlying component is mounted. To ensure they always trigger (i.e. + * if the promise is called in a `useLayoutEffect`) use the `triggerOrThrow` + * attribute: + * + * 'whenMounted': (default) they are called only if the component is mounted. + * + * 'always': they always call. The consumer is then responsible of ensuring no + * side effects happen if the underlying component is not mounted. + */ +export const useTrackedPromise = ( + { + createPromise, + onResolve = noOp, + onReject = noOp, + cancelPreviousOn = 'never', + triggerOrThrow = 'whenMounted', + }: UseTrackedPromiseArgs, + dependencies: DependencyList +) => { + const isComponentMounted = useMountedState(); + const shouldTriggerOrThrow = useCallback(() => { + switch (triggerOrThrow) { + case 'always': + return true; + case 'whenMounted': + return isComponentMounted(); + } + }, [isComponentMounted, triggerOrThrow]); + + /** + * If a promise is currently pending, this holds a reference to it and its + * cancellation function. + */ + const pendingPromises = useRef>>([]); + + /** + * The state of the promise most recently created by the `createPromise` + * factory. It could be uninitialized, pending, resolved or rejected. + */ + const [promiseState, setPromiseState] = useState>({ + state: 'uninitialized', + }); + + const reset = useCallback(() => { + setPromiseState({ + state: 'uninitialized', + }); + }, []); + + const execute = useMemo( + () => + (...args: Arguments) => { + let rejectCancellationPromise!: (value: any) => void; + const cancellationPromise = new Promise((_, reject) => { + rejectCancellationPromise = reject; + }); + + // remember the list of prior pending promises for cancellation + const previousPendingPromises = pendingPromises.current; + + const cancelPreviousPendingPromises = () => { + previousPendingPromises.forEach((promise) => promise.cancel()); + }; + + const newPromise = createPromise(...args); + const newCancelablePromise = Promise.race([newPromise, cancellationPromise]); + + // track this new state + setPromiseState({ + state: 'pending', + promise: newCancelablePromise, + }); + + if (cancelPreviousOn === 'creation') { + cancelPreviousPendingPromises(); + } + + const newPendingPromise: CancelablePromise = { + cancel: () => { + rejectCancellationPromise(new CanceledPromiseError()); + }, + cancelSilently: () => { + rejectCancellationPromise(new SilentCanceledPromiseError()); + }, + promise: newCancelablePromise.then( + (value) => { + if (['settlement', 'resolution'].includes(cancelPreviousOn)) { + cancelPreviousPendingPromises(); + } + + // remove itself from the list of pending promises + pendingPromises.current = pendingPromises.current.filter( + (pendingPromise) => pendingPromise.promise !== newPendingPromise.promise + ); + + if (onResolve && shouldTriggerOrThrow()) { + onResolve(value); + } + + setPromiseState((previousPromiseState) => + previousPromiseState.state === 'pending' && + previousPromiseState.promise === newCancelablePromise + ? { + state: 'resolved', + promise: newPendingPromise.promise, + value, + } + : previousPromiseState + ); + + return value; + }, + (value) => { + if (!(value instanceof SilentCanceledPromiseError)) { + if (['settlement', 'rejection'].includes(cancelPreviousOn)) { + cancelPreviousPendingPromises(); + } + + // remove itself from the list of pending promises + pendingPromises.current = pendingPromises.current.filter( + (pendingPromise) => pendingPromise.promise !== newPendingPromise.promise + ); + + if (shouldTriggerOrThrow()) { + if (onReject) { + onReject(value); + } else { + throw value; + } + } + + setPromiseState((previousPromiseState) => + previousPromiseState.state === 'pending' && + previousPromiseState.promise === newCancelablePromise + ? { + state: 'rejected', + promise: newCancelablePromise, + value, + } + : previousPromiseState + ); + } + } + ), + }; + + // add the new promise to the list of pending promises + pendingPromises.current = [...pendingPromises.current, newPendingPromise]; + + // silence "unhandled rejection" warnings + newPendingPromise.promise.catch(noOp); + + return newPendingPromise.promise; + }, + // the dependencies are managed by the caller + // eslint-disable-next-line react-hooks/exhaustive-deps + dependencies + ); + + /** + * Cancel any pending promises silently to avoid memory leaks and race + * conditions. + */ + useEffect( + () => () => { + pendingPromises.current.forEach((promise) => promise.cancelSilently()); + }, + [] + ); + + return [promiseState, execute, reset] as [typeof promiseState, typeof execute, typeof reset]; +}; + +export interface UninitializedPromiseState { + state: 'uninitialized'; +} + +export interface PendingPromiseState { + state: 'pending'; + promise: Promise; +} + +export interface ResolvedPromiseState { + state: 'resolved'; + promise: Promise; + value: ResolvedValue; +} + +export interface RejectedPromiseState { + state: 'rejected'; + promise: Promise; + value: RejectedValue; +} + +export type SettledPromiseState = + | ResolvedPromiseState + | RejectedPromiseState; + +export type PromiseState = + | UninitializedPromiseState + | PendingPromiseState + | SettledPromiseState; + +export const isRejectedPromiseState = ( + promiseState: PromiseState +): promiseState is RejectedPromiseState => promiseState.state === 'rejected'; + +interface CancelablePromise { + // reject the promise prematurely with a CanceledPromiseError + cancel: () => void; + // reject the promise prematurely with a SilentCanceledPromiseError + cancelSilently: () => void; + // the tracked promise + promise: Promise; +} + +export class CanceledPromiseError extends Error { + public isCanceled = true; + + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class SilentCanceledPromiseError extends CanceledPromiseError {} + +const noOp = () => undefined; diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx index 5926d72fa65c57..3e4a3c3327162d 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/navigation_section_ui.tsx @@ -164,6 +164,7 @@ export const NavigationSectionUI: FC = ({ navNode, items = [] }) => { > navigationNodeToEuiItem(item, { navigateToUrl, basePath }) )} diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/recently_accessed.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/recently_accessed.tsx index da6e92707e022b..051beb931a3717 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/recently_accessed.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/recently_accessed.tsx @@ -71,7 +71,11 @@ export const RecentlyAccessed: FC = ({ initialIsOpen={!defaultIsCollapsed} data-test-subj={`nav-bucket-recentlyAccessed`} > - + ); }; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts index d0b9d01272b241..7a32d157325350 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts @@ -107,7 +107,7 @@ describe('checking migration metadata changes on all registered SO types', () => "ingest-download-sources": "d7edc5e588d9afa61c4b831604582891c54ef1c7", "ingest-outputs": "b4e636b13a5d0f89f0400fb67811d4cca4736eb0", "ingest-package-policies": "55816507db0134b8efbe0509e311a91ce7e1c6cc", - "ingest_manager_settings": "418311b03c8eda53f5d2ea6f54c1356afaa65511", + "ingest_manager_settings": "64955ef1b7a9ffa894d4bb9cf863b5602bfa6885", "inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83", "kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad", "legacy-url-alias": "9b8cca3fbb2da46fd12823d3cd38fdf1c9f24bc8", diff --git a/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.test.tsx b/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.test.tsx new file mode 100644 index 00000000000000..d3c5bb4736e4f8 --- /dev/null +++ b/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.test.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ControlGroupInput } from '../../../common'; +import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public'; +import { storybookFlightsDataView } from '@kbn/presentation-util-plugin/public/mocks'; +import { OPTIONS_LIST_CONTROL } from '../../../common'; +import { ControlGroupContainer } from '../../control_group/embeddable/control_group_container'; +import { pluginServices } from '../../services'; +import { injectStorybookDataView } from '../../services/data_views/data_views.story'; +import { OptionsListEmbeddableFactory } from './options_list_embeddable_factory'; +import { OptionsListEmbeddable } from './options_list_embeddable'; + +pluginServices.getServices().controls.getControlFactory = jest + .fn() + .mockImplementation((type: string) => { + if (type === OPTIONS_LIST_CONTROL) return new OptionsListEmbeddableFactory(); + }); + +describe('initialize', () => { + describe('without selected options', () => { + test('should notify control group when initialization is finished', async () => { + const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage(); + const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; + const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput); + + // data view not required for test case + // setInitializationFinished is called before fetching options when value is not provided + injectStorybookDataView(undefined); + + const control = await container.addOptionsListControl({ + dataViewId: 'demoDataFlights', + fieldName: 'OriginCityName', + }); + + expect(container.getInput().panels[control.getInput().id].type).toBe(OPTIONS_LIST_CONTROL); + expect(container.getOutput().embeddableLoaded[control.getInput().id]).toBe(true); + }); + }); + + describe('with selected options', () => { + test('should set error message when data view can not be found', async () => { + const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage(); + const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; + const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput); + + injectStorybookDataView(undefined); + + const control = (await container.addOptionsListControl({ + dataViewId: 'demoDataFlights', + fieldName: 'OriginCityName', + selectedOptions: ['Seoul', 'Tokyo'], + })) as OptionsListEmbeddable; + + // await redux dispatch + await new Promise((resolve) => process.nextTick(resolve)); + + const reduxState = control.getState(); + expect(reduxState.output.loading).toBe(false); + expect(reduxState.componentState.error).toBe( + 'mock DataViews service currentDataView is undefined, call injectStorybookDataView to set' + ); + }); + + test('should set error message when field can not be found', async () => { + const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage(); + const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; + const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput); + + injectStorybookDataView(storybookFlightsDataView); + + const control = (await container.addOptionsListControl({ + dataViewId: 'demoDataFlights', + fieldName: 'myField', + selectedOptions: ['Seoul', 'Tokyo'], + })) as OptionsListEmbeddable; + + // await redux dispatch + await new Promise((resolve) => process.nextTick(resolve)); + + const reduxState = control.getState(); + expect(reduxState.output.loading).toBe(false); + expect(reduxState.componentState.error).toBe('Could not locate field: myField'); + }); + + test('should notify control group when initialization is finished', async () => { + const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage(); + const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; + const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput); + + injectStorybookDataView(storybookFlightsDataView); + + const control = await container.addOptionsListControl({ + dataViewId: 'demoDataFlights', + fieldName: 'OriginCityName', + selectedOptions: ['Seoul', 'Tokyo'], + }); + + expect(container.getInput().panels[control.getInput().id].type).toBe(OPTIONS_LIST_CONTROL); + expect(container.getOutput().embeddableLoaded[control.getInput().id]).toBe(true); + }); + }); +}); diff --git a/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx b/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx index 3b7555ed97f829..5431cdffc560a1 100644 --- a/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx +++ b/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx @@ -245,13 +245,6 @@ export class OptionsListEmbeddable if (!this.dataView || this.dataView.id !== dataViewId) { try { this.dataView = await this.dataViewsService.get(dataViewId); - if (!this.dataView) - throw new Error( - i18n.translate('controls.optionsList.errors.dataViewNotFound', { - defaultMessage: 'Could not locate data view: {dataViewId}', - values: { dataViewId }, - }) - ); } catch (e) { this.dispatch.setErrorMessage(e.message); } @@ -260,25 +253,21 @@ export class OptionsListEmbeddable } if (this.dataView && (!this.field || this.field.name !== fieldName)) { - try { - const originalField = this.dataView.getFieldByName(fieldName); - if (!originalField) { - throw new Error( - i18n.translate('controls.optionsList.errors.fieldNotFound', { - defaultMessage: 'Could not locate field: {fieldName}', - values: { fieldName }, - }) - ); - } - - this.field = originalField.toSpec(); - } catch (e) { - this.dispatch.setErrorMessage(e.message); + const field = this.dataView.getFieldByName(fieldName); + if (field) { + this.field = field.toSpec(); + this.dispatch.setField(this.field); + } else { + this.dispatch.setErrorMessage( + i18n.translate('controls.optionsList.errors.fieldNotFound', { + defaultMessage: 'Could not locate field: {fieldName}', + values: { fieldName }, + }) + ); } - this.dispatch.setField(this.field); } - return { dataView: this.dataView, field: this.field! }; + return { dataView: this.dataView, field: this.field }; }; private runOptionsListQuery = async (size: number = MIN_OPTIONS_LIST_REQUEST_SIZE) => { diff --git a/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.test.tsx b/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.test.tsx new file mode 100644 index 00000000000000..9629e78dd52855 --- /dev/null +++ b/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.test.tsx @@ -0,0 +1,211 @@ +/* + * 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 { of } from 'rxjs'; +import { ControlGroupInput } from '../../../common'; +import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public'; +import { storybookFlightsDataView } from '@kbn/presentation-util-plugin/public/mocks'; +import { RANGE_SLIDER_CONTROL } from '../../../common'; +import { ControlGroupContainer } from '../../control_group/embeddable/control_group_container'; +import { pluginServices } from '../../services'; +import { injectStorybookDataView } from '../../services/data_views/data_views.story'; +import { RangeSliderEmbeddableFactory } from './range_slider_embeddable_factory'; +import { RangeSliderEmbeddable } from './range_slider_embeddable'; + +let totalResults = 20; +beforeEach(() => { + totalResults = 20; + + pluginServices.getServices().controls.getControlFactory = jest + .fn() + .mockImplementation((type: string) => { + if (type === RANGE_SLIDER_CONTROL) return new RangeSliderEmbeddableFactory(); + }); + + pluginServices.getServices().data.searchSource.create = jest.fn().mockImplementation(() => { + let isAggsRequest = false; + return { + setField: (key: string) => { + if (key === 'aggs') { + isAggsRequest = true; + } + }, + fetch$: () => { + return isAggsRequest + ? of({ + rawResponse: { aggregations: { minAgg: { value: 0 }, maxAgg: { value: 1000 } } }, + }) + : of({ + rawResponse: { hits: { total: { value: totalResults } } }, + }); + }, + }; + }); +}); + +describe('initialize', () => { + describe('without selected range', () => { + test('should notify control group when initialization is finished', async () => { + const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage(); + const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; + const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput); + + // data view not required for test case + // setInitializationFinished is called before fetching slider range when value is not provided + injectStorybookDataView(undefined); + + const control = await container.addRangeSliderControl({ + dataViewId: 'demoDataFlights', + fieldName: 'AvgTicketPrice', + }); + + expect(container.getInput().panels[control.getInput().id].type).toBe(RANGE_SLIDER_CONTROL); + expect(container.getOutput().embeddableLoaded[control.getInput().id]).toBe(true); + }); + }); + + describe('with selected range', () => { + test('should set error message when data view can not be found', async () => { + const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage(); + const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; + const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput); + + injectStorybookDataView(undefined); + + const control = (await container.addRangeSliderControl({ + dataViewId: 'demoDataFlights', + fieldName: 'AvgTicketPrice', + value: ['150', '300'], + })) as RangeSliderEmbeddable; + + // await redux dispatch + await new Promise((resolve) => process.nextTick(resolve)); + + const reduxState = control.getState(); + expect(reduxState.output.loading).toBe(false); + expect(reduxState.componentState.error).toBe( + 'mock DataViews service currentDataView is undefined, call injectStorybookDataView to set' + ); + }); + + test('should set error message when field can not be found', async () => { + const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage(); + const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; + const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput); + + injectStorybookDataView(storybookFlightsDataView); + + const control = (await container.addRangeSliderControl({ + dataViewId: 'demoDataFlights', + fieldName: 'myField', + value: ['150', '300'], + })) as RangeSliderEmbeddable; + + // await redux dispatch + await new Promise((resolve) => process.nextTick(resolve)); + + const reduxState = control.getState(); + expect(reduxState.output.loading).toBe(false); + expect(reduxState.componentState.error).toBe('Could not locate field: myField'); + }); + + test('should set invalid state when filter returns zero results', async () => { + const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage(); + const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; + const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput); + + injectStorybookDataView(storybookFlightsDataView); + totalResults = 0; + + const control = (await container.addRangeSliderControl({ + dataViewId: 'demoDataFlights', + fieldName: 'AvgTicketPrice', + value: ['150', '300'], + })) as RangeSliderEmbeddable; + + // await redux dispatch + await new Promise((resolve) => process.nextTick(resolve)); + + const reduxState = control.getState(); + expect(reduxState.output.filters?.length).toBe(0); + expect(reduxState.componentState.isInvalid).toBe(true); + }); + + test('should set range and filter', async () => { + const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage(); + const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; + const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput); + + injectStorybookDataView(storybookFlightsDataView); + + const control = (await container.addRangeSliderControl({ + dataViewId: 'demoDataFlights', + fieldName: 'AvgTicketPrice', + value: ['150', '300'], + })) as RangeSliderEmbeddable; + + // await redux dispatch + await new Promise((resolve) => process.nextTick(resolve)); + + const reduxState = control.getState(); + expect(reduxState.output.filters?.length).toBe(1); + expect(reduxState.output.filters?.[0].query).toEqual({ + range: { + AvgTicketPrice: { + gte: 150, + lte: 300, + }, + }, + }); + expect(reduxState.componentState.isInvalid).toBe(false); + expect(reduxState.componentState.min).toBe(0); + expect(reduxState.componentState.max).toBe(1000); + }); + + test('should notify control group when initialization is finished', async () => { + const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage(); + const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; + const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput); + + injectStorybookDataView(storybookFlightsDataView); + + const control = await container.addRangeSliderControl({ + dataViewId: 'demoDataFlights', + fieldName: 'AvgTicketPrice', + value: ['150', '300'], + }); + + expect(container.getInput().panels[control.getInput().id].type).toBe(RANGE_SLIDER_CONTROL); + expect(container.getOutput().embeddableLoaded[control.getInput().id]).toBe(true); + }); + + test('should notify control group when initialization throws', async () => { + const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage(); + const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput; + const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput); + + injectStorybookDataView(storybookFlightsDataView); + + pluginServices.getServices().data.searchSource.create = jest.fn().mockImplementation(() => ({ + setField: () => {}, + fetch$: () => { + throw new Error('Simulated _search request error'); + }, + })); + + const control = await container.addRangeSliderControl({ + dataViewId: 'demoDataFlights', + fieldName: 'AvgTicketPrice', + value: ['150', '300'], + }); + + expect(container.getInput().panels[control.getInput().id].type).toBe(RANGE_SLIDER_CONTROL); + expect(container.getOutput().embeddableLoaded[control.getInput().id]).toBe(true); + }); + }); +}); diff --git a/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx b/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx index 5ee60361460417..e45927862abfcf 100644 --- a/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx +++ b/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx @@ -62,14 +62,6 @@ interface RangeSliderDataFetchProps { validate?: boolean; } -const fieldMissingError = (fieldName: string) => - new Error( - i18n.translate('controls.rangeSlider.errors.fieldNotFound', { - defaultMessage: 'Could not locate field: {fieldName}', - values: { fieldName }, - }) - ); - export const RangeSliderControlContext = createContext(null); export const useRangeSlider = (): RangeSliderEmbeddable => { const rangeSlider = useContext(RangeSliderControlContext); @@ -147,15 +139,14 @@ export class RangeSliderEmbeddable try { await this.runRangeSliderQuery(); await this.buildFilter(); - if (initialValue) { - this.setInitializationFinished(); - } } catch (e) { - batch(() => { - this.dispatch.setLoading(false); - this.dispatch.setErrorMessage(e.message); - }); + this.onLoadingError(e.message); + } + + if (initialValue) { + this.setInitializationFinished(); } + this.setupSubscriptions(); }; @@ -182,7 +173,7 @@ export class RangeSliderEmbeddable await this.runRangeSliderQuery(); await this.buildFilter(); } catch (e) { - this.dispatch.setErrorMessage(e.message); + this.onLoadingError(e.message); } }) ); @@ -209,34 +200,27 @@ export class RangeSliderEmbeddable if (!this.dataView || this.dataView.id !== dataViewId) { try { this.dataView = await this.dataViewsService.get(dataViewId); - if (!this.dataView) { - throw new Error( - i18n.translate('controls.rangeSlider.errors.dataViewNotFound', { - defaultMessage: 'Could not locate data view: {dataViewId}', - values: { dataViewId }, - }) - ); - } this.dispatch.setDataViewId(this.dataView.id); } catch (e) { - this.dispatch.setErrorMessage(e.message); + this.onLoadingError(e.message); } } if (this.dataView && (!this.field || this.field.name !== fieldName)) { - try { - this.field = this.dataView.getFieldByName(fieldName); - if (this.field === undefined) { - throw fieldMissingError(fieldName); - } - + this.field = this.dataView.getFieldByName(fieldName); + if (this.field) { this.dispatch.setField(this.field?.toSpec()); - } catch (e) { - this.dispatch.setErrorMessage(e.message); + } else { + this.onLoadingError( + i18n.translate('controls.rangeSlider.errors.fieldNotFound', { + defaultMessage: 'Could not locate field: {fieldName}', + values: { fieldName }, + }) + ); } } - return { dataView: this.dataView, field: this.field! }; + return { dataView: this.dataView, field: this.field }; }; private runRangeSliderQuery = async () => { @@ -245,16 +229,6 @@ export class RangeSliderEmbeddable const { dataView, field } = await this.getCurrentDataViewAndField(); if (!dataView || !field) return; - const { fieldName } = this.getInput(); - - if (!field) { - batch(() => { - this.dispatch.setLoading(false); - this.dispatch.publishFilters([]); - }); - throw fieldMissingError(fieldName); - } - const embeddableInput = this.getInput(); const { ignoreParentSettings, timeRange: globalTimeRange, timeslice } = embeddableInput; let { filters = [] } = embeddableInput; @@ -278,8 +252,6 @@ export class RangeSliderEmbeddable const { min, max } = await this.fetchMinMax({ dataView, field, - }).catch((e) => { - throw e; }); this.dispatch.setMinMax({ @@ -332,9 +304,7 @@ export class RangeSliderEmbeddable }; searchSource.setField('aggs', aggs); - const resp = await lastValueFrom(searchSource.fetch$()).catch((e) => { - throw e; - }); + const resp = await lastValueFrom(searchSource.fetch$()); const min = get(resp, 'rawResponse.aggregations.minAgg.value'); const max = get(resp, 'rawResponse.aggregations.maxAgg.value'); @@ -397,11 +367,8 @@ export class RangeSliderEmbeddable searchSource.setField('query', query); } - const { - rawResponse: { - hits: { total }, - }, - } = await lastValueFrom(searchSource.fetch$()); + const resp = await lastValueFrom(searchSource.fetch$()); + const total = resp?.rawResponse?.hits?.total; const docCount = typeof total === 'number' ? total : total?.value; if (!docCount) { @@ -425,6 +392,14 @@ export class RangeSliderEmbeddable }); }; + private onLoadingError(errorMessage: string) { + batch(() => { + this.dispatch.setLoading(false); + this.dispatch.publishFilters([]); + this.dispatch.setErrorMessage(errorMessage); + }); + } + public clearSelections() { this.dispatch.setSelectedRange(['', '']); } @@ -434,7 +409,7 @@ export class RangeSliderEmbeddable await this.runRangeSliderQuery(); await this.buildFilter(); } catch (e) { - this.dispatch.setErrorMessage(e.message); + this.onLoadingError(e.message); } }; diff --git a/src/plugins/controls/public/services/data/data.story.ts b/src/plugins/controls/public/services/data/data.story.ts index d94bd40e093f0d..a3b19e4ae5ead0 100644 --- a/src/plugins/controls/public/services/data/data.story.ts +++ b/src/plugins/controls/public/services/data/data.story.ts @@ -19,9 +19,7 @@ export const dataServiceFactory: DataServiceFactory = () => ({ setField: () => {}, fetch$: () => of({ - resp: { - rawResponse: { aggregations: { minAgg: { value: 0 }, maxAgg: { value: 1000 } } }, - }, + rawResponse: { aggregations: { minAgg: { value: 0 }, maxAgg: { value: 1000 } } }, }), }), } as unknown as DataPublicPluginStart['search']['searchSource'], diff --git a/src/plugins/controls/public/services/data_views/data_views.story.ts b/src/plugins/controls/public/services/data_views/data_views.story.ts index 5d118fd975e2c2..9042a834ad3578 100644 --- a/src/plugins/controls/public/services/data_views/data_views.story.ts +++ b/src/plugins/controls/public/services/data_views/data_views.story.ts @@ -13,17 +13,39 @@ import { ControlsDataViewsService } from './types'; export type DataViewsServiceFactory = PluginServiceFactory; -let currentDataView: DataView; -export const injectStorybookDataView = (dataView: DataView) => (currentDataView = dataView); +let currentDataView: DataView | undefined; +export const injectStorybookDataView = (dataView?: DataView) => (currentDataView = dataView); export const dataViewsServiceFactory: DataViewsServiceFactory = () => ({ - get: (() => - new Promise((r) => - setTimeout(() => r(currentDataView), 100) + get: ((dataViewId) => + new Promise((resolve, reject) => + setTimeout(() => { + if (!currentDataView) { + reject( + new Error( + 'mock DataViews service currentDataView is undefined, call injectStorybookDataView to set' + ) + ); + } else if (currentDataView.id === dataViewId) { + resolve(currentDataView); + } else { + reject( + new Error( + `mock DataViews service currentDataView.id: ${currentDataView.id} does not match requested dataViewId: ${dataViewId}` + ) + ); + } + }, 100) ) as unknown) as DataViewsPublicPluginStart['get'], getIdsWithTitle: (() => - new Promise((r) => - setTimeout(() => r([{ id: currentDataView.id, title: currentDataView.title }]), 100) + new Promise((resolve) => + setTimeout(() => { + const idsWithTitle: Array<{ id: string | undefined; title: string }> = []; + if (currentDataView) { + idsWithTitle.push({ id: currentDataView.id, title: currentDataView.title }); + } + resolve(idsWithTitle); + }, 100) ) as unknown) as DataViewsPublicPluginStart['getIdsWithTitle'], getDefaultId: () => Promise.resolve(currentDataView?.id ?? null), }); diff --git a/src/plugins/controls/public/services/options_list/options_list_service.test.ts b/src/plugins/controls/public/services/options_list/options_list_service.test.ts new file mode 100644 index 00000000000000..52af6fcdd8c010 --- /dev/null +++ b/src/plugins/controls/public/services/options_list/options_list_service.test.ts @@ -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 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 { DataView, FieldSpec } from '@kbn/data-views-plugin/common'; +import { KibanaPluginServiceParams } from '@kbn/presentation-util-plugin/public'; +import type { OptionsListRequest } from '../../../common/options_list/types'; +import type { ControlsPluginStartDeps } from '../../types'; +import type { ControlsHTTPService } from '../http/types'; +import type { ControlsDataService } from '../data/types'; +import { optionsListServiceFactory } from './options_list_service'; + +describe('runOptionsListRequest', () => { + test('should return OptionsListFailureResponse when fetch throws', async () => { + const mockCore = { + coreStart: { + uiSettings: { + get: () => { + return undefined; + }, + }, + }, + } as unknown as KibanaPluginServiceParams; + const mockData = { + query: { + timefilter: { + timefilter: {}, + }, + }, + } as unknown as ControlsDataService; + const mockHttp = { + fetch: () => { + throw new Error('Simulated network error'); + }, + } as unknown as ControlsHTTPService; + const optionsListService = optionsListServiceFactory(mockCore, { + data: mockData, + http: mockHttp, + }); + + const response = (await optionsListService.runOptionsListRequest( + { + dataView: { + toSpec: () => { + return {}; + }, + title: 'myDataView', + } as unknown as DataView, + field: { + name: 'myField', + } as unknown as FieldSpec, + } as unknown as OptionsListRequest, + {} as unknown as AbortSignal + )) as any; + + expect(response.error.message).toBe('Simulated network error'); + }); +}); diff --git a/src/plugins/controls/public/services/options_list/options_list_service.ts b/src/plugins/controls/public/services/options_list/options_list_service.ts index 5620a0db4e6d7f..d64a3a777e8ce9 100644 --- a/src/plugins/controls/public/services/options_list/options_list_service.ts +++ b/src/plugins/controls/public/services/options_list/options_list_service.ts @@ -151,6 +151,9 @@ export type OptionsListServiceFactory = KibanaPluginServiceFactory< OptionsListServiceRequiredServices >; -export const optionsListServiceFactory: OptionsListServiceFactory = (core, requiredServices) => { - return new OptionsListService(core.coreStart, requiredServices); +export const optionsListServiceFactory: OptionsListServiceFactory = ( + startParams, + requiredServices +) => { + return new OptionsListService(startParams.coreStart, requiredServices); }; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts index 5c1165dc68c5de..43e8f5ea5933e3 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts @@ -51,7 +51,7 @@ test('throws error when provided validation function returns invalid', async () }).rejects.toThrow('Dashboard failed saved object result validation'); }); -test('returns undefined when provided validation function returns redireted', async () => { +test('returns undefined when provided validation function returns redirected', async () => { const creationOptions: DashboardCreationOptions = { validateLoadedSavedObject: jest.fn().mockImplementation(() => 'redirected'), }; @@ -59,6 +59,24 @@ test('returns undefined when provided validation function returns redireted', as expect(dashboard).toBeUndefined(); }); +/** + * Because the getInitialInput function may have side effects, we only want to call it once we are certain that the + * the loaded saved object passes validation. + * + * This is especially relevant in the Dashboard App case where calling the getInitialInput function removes the _a + * param from the URL. In alais match situations this caused a bug where the state from the URL wasn't properly applied + * after the redirect. + */ +test('does not get initial input when provided validation function returns redirected', async () => { + const creationOptions: DashboardCreationOptions = { + validateLoadedSavedObject: jest.fn().mockImplementation(() => 'redirected'), + getInitialInput: jest.fn(), + }; + const dashboard = await createDashboard(creationOptions, 0, 'test-id'); + expect(dashboard).toBeUndefined(); + expect(creationOptions.getInitialInput).not.toHaveBeenCalled(); +}); + test('pulls state from dashboard saved object when given a saved object id', async () => { pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest .fn() diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts index a595bca8c1841c..753b881e5a94ef 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts @@ -137,7 +137,6 @@ export const initializeDashboard = async ({ useUnifiedSearchIntegration, useSessionStorageIntegration, } = creationOptions ?? {}; - const overrideInput = getInitialInput?.(); // -------------------------------------------------------------------------------------- // Run validation. @@ -161,6 +160,7 @@ export const initializeDashboard = async ({ // -------------------------------------------------------------------------------------- // Combine input from saved object, session storage, & passed input to create initial input. // -------------------------------------------------------------------------------------- + const overrideInput = getInitialInput?.(); const initialInput: DashboardContainerInput = cloneDeep({ ...DEFAULT_DASHBOARD_INPUT, ...(loadDashboardReturn?.dashboardInput ?? {}), diff --git a/src/plugins/data/public/search/session/sessions_mgmt/application/render.tsx b/src/plugins/data/public/search/session/sessions_mgmt/application/render.tsx index 87ceb6ee8a9423..b8c2ca10ee7b05 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/application/render.tsx +++ b/src/plugins/data/public/search/session/sessions_mgmt/application/render.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { AppDependencies } from '..'; import { SearchSessionsMgmtMain } from '../components/main'; @@ -20,18 +21,17 @@ export const renderApp = ( return () => undefined; } - const { Context: I18nContext } = i18n; // uiSettings is required by the listing table to format dates in the timezone from Settings const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ uiSettings, }); render( - + - , + , elem ); diff --git a/src/plugins/data/public/search/session/sessions_mgmt/components/main.tsx b/src/plugins/data/public/search/session/sessions_mgmt/components/main.tsx index 78da4972b49d46..69706b21dde800 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/components/main.tsx +++ b/src/plugins/data/public/search/session/sessions_mgmt/components/main.tsx @@ -6,11 +6,10 @@ * Side Public License, v 1. */ +import React from 'react'; import { EuiButtonEmpty, EuiPageHeader, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { CoreStart, HttpStart } from '@kbn/core/public'; -import React from 'react'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import type { SearchSessionsMgmtAPI } from '../lib/api'; import type { AsyncSearchIntroDocumentation } from '../lib/documentation'; import { SearchSessionsMgmtTable } from './table'; @@ -30,7 +29,7 @@ interface Props { export function SearchSessionsMgmtMain({ documentation, ...tableProps }: Props) { return ( - + <> - + ); } diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json index 73eb71508a8955..9d46f36ffd29ce 100644 --- a/src/plugins/data/tsconfig.json +++ b/src/plugins/data/tsconfig.json @@ -48,7 +48,8 @@ "@kbn/core-application-browser", "@kbn/core-saved-objects-server", "@kbn/core-saved-objects-utils-server", - "@kbn/data-service" + "@kbn/data-service", + "@kbn/react-kibana-context-render" ], "exclude": [ "target/**/*", diff --git a/src/plugins/data_view_editor/public/open_editor.tsx b/src/plugins/data_view_editor/public/open_editor.tsx index 3f998601f38f18..04a60124ad1bfa 100644 --- a/src/plugins/data_view_editor/public/open_editor.tsx +++ b/src/plugins/data_view_editor/public/open_editor.tsx @@ -8,11 +8,11 @@ import React from 'react'; import { CoreStart, OverlayRef } from '@kbn/core/public'; -import { I18nProvider } from '@kbn/i18n-react'; import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; -import { createKibanaReactContext, toMountPoint, DataPublicPluginStart } from './shared_imports'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { createKibanaReactContext, DataPublicPluginStart } from './shared_imports'; import { CloseEditor, DataViewEditorContext, DataViewEditorProps } from './types'; import { DataViewEditorLazy } from './components/data_view_editor_lazy'; @@ -67,22 +67,20 @@ export const getEditorOpener = overlayRef = overlays.openFlyout( toMountPoint( - - { - closeEditor(); - onCancel(); - }} - editData={editData} - defaultTypeIsRollup={defaultTypeIsRollup} - requireTimestampField={requireTimestampField} - allowAdHocDataView={allowAdHocDataView} - showManagementLink={Boolean(editData && editData.isPersisted())} - /> - + { + closeEditor(); + onCancel(); + }} + editData={editData} + defaultTypeIsRollup={defaultTypeIsRollup} + requireTimestampField={requireTimestampField} + allowAdHocDataView={allowAdHocDataView} + showManagementLink={Boolean(editData && editData.isPersisted())} + /> , - { theme$: core.theme.theme$ } + { theme: core.theme, i18n: core.i18n } ), { hideCloseButton: true, diff --git a/src/plugins/data_view_editor/public/plugin.test.tsx b/src/plugins/data_view_editor/public/plugin.test.tsx index a95da2edc44f81..bc728a99a9ab70 100644 --- a/src/plugins/data_view_editor/public/plugin.test.tsx +++ b/src/plugins/data_view_editor/public/plugin.test.tsx @@ -64,16 +64,15 @@ describe('DataViewEditorPlugin', () => { expect(openFlyout).toHaveBeenCalled(); - const [[arg]] = openFlyout.mock.calls; - const i18nProvider = arg.props.children; - expect(i18nProvider.props.children.type).toBe(DataViewEditorLazy); + const [[{ __reactMount__ }]] = openFlyout.mock.calls; + expect(__reactMount__.props.children.type).toBe(DataViewEditorLazy); // We force call the "onSave" prop from the component // and make sure that the the spy is being called. // Note: we are testing implementation details, if we change or rename the "onSave" prop on // the component, we will need to update this test accordingly. - expect(i18nProvider.props.children.props.onSave).toBeDefined(); - i18nProvider.props.children.props.onSave(); + expect(__reactMount__.props.children.props.onSave).toBeDefined(); + __reactMount__.props.children.props.onSave(); expect(onSaveSpy).toHaveBeenCalled(); }); diff --git a/src/plugins/data_view_editor/tsconfig.json b/src/plugins/data_view_editor/tsconfig.json index 99e066ee3fe667..2e7815ea009a8f 100644 --- a/src/plugins/data_view_editor/tsconfig.json +++ b/src/plugins/data_view_editor/tsconfig.json @@ -18,6 +18,7 @@ "@kbn/test-jest-helpers", "@kbn/ui-theme", "@kbn/kibana-utils-plugin", + "@kbn/react-kibana-mount", ], "exclude": [ "target/**/*", diff --git a/src/plugins/data_view_field_editor/public/open_editor.tsx b/src/plugins/data_view_field_editor/public/open_editor.tsx index ccc130ba043612..d898733c46dd28 100644 --- a/src/plugins/data_view_field_editor/public/open_editor.tsx +++ b/src/plugins/data_view_field_editor/public/open_editor.tsx @@ -6,9 +6,10 @@ * Side Public License, v 1. */ +import React from 'react'; import { CoreStart, OverlayRef } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { FieldEditorLoader } from './components/field_editor_loader'; import { euiFlyoutClassname } from './constants'; import type { ApiService } from './lib/api'; @@ -21,7 +22,7 @@ import type { FieldFormatsStart, DataViewField, } from './shared_imports'; -import { createKibanaReactContext, toMountPoint } from './shared_imports'; +import { createKibanaReactContext } from './shared_imports'; import type { CloseEditor, Field, InternalFieldType, PluginStart } from './types'; /** @@ -196,7 +197,7 @@ export const getFieldEditorOpener = uiSettings={uiSettings} /> , - { theme$: core.theme.theme$ } + { theme: core.theme, i18n: core.i18n } ), { className: euiFlyoutClassname, diff --git a/src/plugins/data_view_field_editor/public/plugin.test.tsx b/src/plugins/data_view_field_editor/public/plugin.test.tsx index 3a95f35569f075..a6609f0d7a6072 100644 --- a/src/plugins/data_view_field_editor/public/plugin.test.tsx +++ b/src/plugins/data_view_field_editor/public/plugin.test.tsx @@ -67,15 +67,15 @@ describe('DataViewFieldEditorPlugin', () => { expect(openFlyout).toHaveBeenCalled(); - const [[arg]] = openFlyout.mock.calls; - expect(arg.props.children.type).toBe(FieldEditorLoader); + const [[{ __reactMount__ }]] = openFlyout.mock.calls; + expect(__reactMount__.props.children.type).toBe(FieldEditorLoader); // We force call the "onSave" prop from the component // and make sure that the the spy is being called. // Note: we are testing implementation details, if we change or rename the "onSave" prop on // the component, we will need to update this test accordingly. - expect(arg.props.children.props.onSave).toBeDefined(); - arg.props.children.props.onSave(); + expect(__reactMount__.props.children.props.onSave).toBeDefined(); + __reactMount__.props.children.props.onSave(); expect(onSaveSpy).toHaveBeenCalled(); }); diff --git a/src/plugins/data_view_field_editor/tsconfig.json b/src/plugins/data_view_field_editor/tsconfig.json index 264c9fdb590358..43512e56fe17f1 100644 --- a/src/plugins/data_view_field_editor/tsconfig.json +++ b/src/plugins/data_view_field_editor/tsconfig.json @@ -26,6 +26,7 @@ "@kbn/field-types", "@kbn/utility-types", "@kbn/config-schema", + "@kbn/react-kibana-mount", ], "exclude": [ "target/**/*", diff --git a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx index c1125177b03db4..d7b21b395fddec 100644 --- a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx @@ -12,10 +12,9 @@ import { Redirect } from 'react-router-dom'; import { Router, Routes, Route } from '@kbn/shared-ux-router'; import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n-react'; import { StartServicesAccessor } from '@kbn/core/public'; - -import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { ManagementAppMountParams } from '@kbn/management-plugin/public'; import { IndexPatternTableWithRouter, @@ -40,7 +39,18 @@ export async function mountManagementSection( params: ManagementAppMountParams ) { const [ - { application, chrome, uiSettings, settings, notifications, overlays, http, docLinks, theme }, + { + application, + chrome, + uiSettings, + settings, + notifications, + overlays, + http, + docLinks, + theme, + i18n: coreI18n, + }, { data, dataViewFieldEditor, @@ -83,29 +93,27 @@ export async function mountManagementSection( }; ReactDOM.render( - - - - - - - - - - - - - - - - - - - - - - - , + + + + + + + + + + + + + + + + + + + + + , params.element ); diff --git a/src/plugins/data_view_management/tsconfig.json b/src/plugins/data_view_management/tsconfig.json index 1f1ec7282f0460..88b3e84cc23f31 100644 --- a/src/plugins/data_view_management/tsconfig.json +++ b/src/plugins/data_view_management/tsconfig.json @@ -34,6 +34,7 @@ "@kbn/config-schema", "@kbn/shared-ux-router", "@kbn/core-ui-settings-browser", + "@kbn/react-kibana-context-render", ], "exclude": [ "target/**/*", diff --git a/src/plugins/data_views/server/rest_api_routes/public/index.ts b/src/plugins/data_views/server/rest_api_routes/public/index.ts index f4f64841e1de9c..ebd7a2a6febf0b 100644 --- a/src/plugins/data_views/server/rest_api_routes/public/index.ts +++ b/src/plugins/data_views/server/rest_api_routes/public/index.ts @@ -46,7 +46,8 @@ const routes = [ updateRoutes.registerUpdateDataViewRoute, updateRoutes.registerUpdateDataViewRouteLegacy, ...Object.values(scriptedRoutes), - swapReferencesRoute, + swapReferencesRoute({ previewRoute: false }), + swapReferencesRoute({ previewRoute: true }), ]; export { routes }; diff --git a/src/plugins/data_views/server/rest_api_routes/public/swap_references.ts b/src/plugins/data_views/server/rest_api_routes/public/swap_references.ts index e8296394857d81..abeb5a5e4cb4e1 100644 --- a/src/plugins/data_views/server/rest_api_routes/public/swap_references.ts +++ b/src/plugins/data_views/server/rest_api_routes/public/swap_references.ts @@ -27,8 +27,10 @@ interface GetDataViewArgs { interface SwapRefResponse { result: Array<{ id: string; type: string }>; - preview: boolean; - deleteSuccess?: boolean; + deleteStatus?: { + remainingRefs: number; + deletePerformed: boolean; + }; } export const swapReference = async ({ @@ -43,135 +45,147 @@ export const swapReference = async ({ const idSchema = schema.string(); -export const swapReferencesRoute = ( - router: IRouter, - getStartServices: StartServicesAccessor< - DataViewsServerPluginStartDependencies, - DataViewsServerPluginStart - >, - usageCollection?: UsageCounter -) => { - router.versioned.post({ path: DATA_VIEW_SWAP_REFERENCES_PATH, access: 'public' }).addVersion( - { - version: INITIAL_REST_VERSION, - validate: { - request: { - body: schema.object({ - from_id: idSchema, - from_type: schema.maybe(schema.string()), - to_id: idSchema, - for_id: schema.maybe(schema.oneOf([idSchema, schema.arrayOf(idSchema)])), - for_type: schema.maybe(schema.string()), - preview: schema.maybe(schema.boolean()), - delete: schema.maybe(schema.boolean()), - }), - }, - response: { - 200: { +export const swapReferencesRoute = + ({ previewRoute }: { previewRoute: boolean }) => + ( + router: IRouter, + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + >, + usageCollection?: UsageCounter + ) => { + const path = previewRoute + ? `${DATA_VIEW_SWAP_REFERENCES_PATH}/_preview` + : DATA_VIEW_SWAP_REFERENCES_PATH; + router.versioned.post({ path, access: 'public' }).addVersion( + { + version: INITIAL_REST_VERSION, + validate: { + request: { body: schema.object({ - result: schema.arrayOf(schema.object({ id: idSchema, type: schema.string() })), - preview: schema.boolean(), - deleteSuccess: schema.maybe(schema.boolean()), + fromId: idSchema, + fromType: schema.maybe(schema.string()), + toId: idSchema, + forId: schema.maybe(schema.oneOf([idSchema, schema.arrayOf(idSchema)])), + forType: schema.maybe(schema.string()), + delete: schema.maybe(schema.boolean()), }), }, + response: { + 200: { + body: schema.object({ + result: schema.arrayOf(schema.object({ id: idSchema, type: schema.string() })), + deleteStatus: schema.maybe( + schema.object({ + remainingRefs: schema.number(), + deletePerformed: schema.boolean(), + }) + ), + }), + }, + }, }, }, - }, - router.handleLegacyErrors( - handleErrors(async (ctx, req, res) => { - const savedObjectsClient = (await ctx.core).savedObjects.client; - const [core] = await getStartServices(); - const types = core.savedObjects.getTypeRegistry().getAllTypes(); - const type = req.body.from_type || DATA_VIEW_SAVED_OBJECT_TYPE; - const preview = req.body.preview !== undefined ? req.body.preview : true; - const searchId = - !Array.isArray(req.body.for_id) && req.body.for_id !== undefined - ? [req.body.for_id] - : req.body.for_id; - - usageCollection?.incrementCounter({ counterName: 'swap_references' }); - - // verify 'to' object actually exists - try { - await savedObjectsClient.get(type, req.body.to_id); - } catch (e) { - throw new Error(`Could not find object with type ${type} and id ${req.body.to_id}`); - } - - // assemble search params - const findParams: SavedObjectsFindOptions = { - type: types.map((t) => t.name), - hasReference: { type, id: req.body.from_id }, - }; - - if (req.body.for_type) { - findParams.type = [req.body.for_type]; - } - - const { saved_objects: savedObjects } = await savedObjectsClient.find(findParams); - - const filteredSavedObjects = searchId - ? savedObjects.filter((so) => searchId?.includes(so.id)) - : savedObjects; - - // create summary of affected objects - const resultSummary = filteredSavedObjects.map((savedObject) => ({ - id: savedObject.id, - type: savedObject.type, - })); - - const body: SwapRefResponse = { - result: resultSummary, - preview, - }; - - // bail if preview - if (preview) { + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = (await ctx.core).savedObjects.client; + const [core] = await getStartServices(); + const types = core.savedObjects.getTypeRegistry().getAllTypes(); + const type = req.body.fromType || DATA_VIEW_SAVED_OBJECT_TYPE; + const searchId = + !Array.isArray(req.body.forId) && req.body.forId !== undefined + ? [req.body.forId] + : req.body.forId; + + usageCollection?.incrementCounter({ counterName: 'swap_references' }); + + // verify 'to' object actually exists + try { + await savedObjectsClient.get(type, req.body.toId); + } catch (e) { + throw new Error(`Could not find object with type ${type} and id ${req.body.toId}`); + } + + // assemble search params + const findParams: SavedObjectsFindOptions = { + type: types.map((t) => t.name), + hasReference: { type, id: req.body.fromId }, + }; + + if (req.body.forType) { + findParams.type = [req.body.forType]; + } + + const { saved_objects: savedObjects } = await savedObjectsClient.find(findParams); + + const filteredSavedObjects = searchId + ? savedObjects.filter((so) => searchId?.includes(so.id)) + : savedObjects; + + // create summary of affected objects + const resultSummary = filteredSavedObjects.map((savedObject) => ({ + id: savedObject.id, + type: savedObject.type, + })); + + const body: SwapRefResponse = { + result: resultSummary, + }; + + // bail if preview + if (previewRoute) { + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body, + }); + } + + // iterate over list and update references + for (const savedObject of filteredSavedObjects) { + const updatedRefs = savedObject.references.map((ref) => { + if (ref.type === type && ref.id === req.body.fromId) { + return { ...ref, id: req.body.toId }; + } else { + return ref; + } + }); + + await savedObjectsClient.update( + savedObject.type, + savedObject.id, + {}, + { + references: updatedRefs, + } + ); + } + + if (req.body.delete) { + const verifyNoMoreRefs = await savedObjectsClient.find(findParams); + if (verifyNoMoreRefs.total > 0) { + body.deleteStatus = { + remainingRefs: verifyNoMoreRefs.total, + deletePerformed: false, + }; + } else { + await savedObjectsClient.delete(type, req.body.fromId, { refresh: 'wait_for' }); + body.deleteStatus = { + remainingRefs: verifyNoMoreRefs.total, + deletePerformed: true, + }; + } + } + return res.ok({ headers: { 'content-type': 'application/json', }, body, }); - } - - // iterate over list and update references - for (const savedObject of filteredSavedObjects) { - const updatedRefs = savedObject.references.map((ref) => { - if (ref.type === type && ref.id === req.body.from_id) { - return { ...ref, id: req.body.to_id }; - } else { - return ref; - } - }); - - await savedObjectsClient.update( - savedObject.type, - savedObject.id, - {}, - { - references: updatedRefs, - } - ); - } - - if (req.body.delete) { - const verifyNoMoreRefs = await savedObjectsClient.find(findParams); - if (verifyNoMoreRefs.total > 0) { - body.deleteSuccess = false; - } else { - await savedObjectsClient.delete(type, req.body.from_id); - body.deleteSuccess = true; - } - } - - return res.ok({ - headers: { - 'content-type': 'application/json', - }, - body, - }); - }) - ) - ); -}; + }) + ) + ); + }; diff --git a/src/plugins/discover/kibana.jsonc b/src/plugins/discover/kibana.jsonc index 96c4aef67fe18a..1c6ffaae833cb0 100644 --- a/src/plugins/discover/kibana.jsonc +++ b/src/plugins/discover/kibana.jsonc @@ -26,7 +26,7 @@ "expressions", "unifiedSearch", "unifiedHistogram", - "contentManagement" + "contentManagement", ], "optionalPlugins": [ "home", @@ -35,7 +35,8 @@ "spaces", "triggersActionsUi", "savedObjectsTaggingOss", - "lens" + "lens", + "serverless" ], "requiredBundles": ["kibanaUtils", "kibanaReact", "unifiedSearch"], "extraPublicDirs": ["common"] diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index 303b06005e1ac5..6355d6de47e80c 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -30,7 +30,7 @@ import { ContextAppContent } from './context_app_content'; import { SurrDocType } from './services/context'; import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; import { useDiscoverServices } from '../../hooks/use_discover_services'; -import { getRootBreadcrumbs } from '../../utils/breadcrumbs'; +import { setBreadcrumbs } from '../../utils/breadcrumbs'; const ContextAppContentMemoized = memo(ContextAppContent); @@ -78,14 +78,13 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) => }); useEffect(() => { - services.chrome.setBreadcrumbs([ - ...getRootBreadcrumbs({ breadcrumb: referrer, services }), - { - text: i18n.translate('discover.context.breadcrumb', { - defaultMessage: 'Surrounding documents', - }), - }, - ]); + setBreadcrumbs({ + services, + rootBreadcrumbPath: referrer, + titleBreadcrumbText: i18n.translate('discover.context.breadcrumb', { + defaultMessage: 'Surrounding documents', + }), + }); }, [locator, referrer, services]); useExecutionContext(core.executionContext, { diff --git a/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx b/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx index a2069f22c97c02..743e503227e781 100644 --- a/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx @@ -7,7 +7,8 @@ */ import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { MarkdownSimple, toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { MarkdownSimple } from '@kbn/kibana-react-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import { SortDirection } from '@kbn/data-plugin/public'; import type { DataTableRecord } from '@kbn/discover-utils/types'; @@ -42,8 +43,7 @@ export function useContextAppFetch({ useNewFieldsApi, }: ContextAppFetchProps) { const services = useDiscoverServices(); - const { uiSettings: config, data, toastNotifications, filterManager, core } = services; - const { theme$ } = core.theme; + const { uiSettings: config, data, toastNotifications, filterManager } = services; const searchSource = useMemo(() => { return data.search.searchSource.createEmpty(); @@ -70,16 +70,9 @@ export function useContextAppFetch({ setState(createError('anchorStatus', FailureReason.INVALID_TIEBREAKER)); toastNotifications.addDanger({ title: errorTitle, - text: toMountPoint( - wrapWithTheme( - - {i18n.translate('discover.context.invalidTieBreakerFiledSetting', { - defaultMessage: 'Invalid tie breaker field setting', - })} - , - theme$ - ) - ), + text: i18n.translate('discover.context.invalidTieBreakerFiledSetting', { + defaultMessage: 'Invalid tie breaker field setting', + }), }); return; } @@ -108,7 +101,10 @@ export function useContextAppFetch({ setState(createError('anchorStatus', FailureReason.UNKNOWN, error)); toastNotifications.addDanger({ title: errorTitle, - text: toMountPoint(wrapWithTheme({error.message}, theme$)), + text: toMountPoint({error.message}, { + theme: services.core.theme, + i18n: services.core.i18n, + }), }); } }, [ @@ -120,7 +116,6 @@ export function useContextAppFetch({ anchorId, searchSource, useNewFieldsApi, - theme$, ]); const fetchSurroundingRows = useCallback( @@ -161,9 +156,10 @@ export function useContextAppFetch({ setState(createError(statusKey, FailureReason.UNKNOWN, error)); toastNotifications.addDanger({ title: errorTitle, - text: toMountPoint( - wrapWithTheme({error.message}, theme$) - ), + text: toMountPoint({error.message}, { + theme: services.core.theme, + i18n: services.core.i18n, + }), }); } }, @@ -177,7 +173,6 @@ export function useContextAppFetch({ dataView, toastNotifications, useNewFieldsApi, - theme$, data, ] ); diff --git a/src/plugins/discover/public/application/doc/components/doc.tsx b/src/plugins/discover/public/application/doc/components/doc.tsx index 527ba4377f0788..384d0d7a4ffac6 100644 --- a/src/plugins/discover/public/application/doc/components/doc.tsx +++ b/src/plugins/discover/public/application/doc/components/doc.tsx @@ -12,7 +12,7 @@ import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPage, EuiPageBody } from '@e import type { DataView } from '@kbn/data-views-plugin/public'; import { i18n } from '@kbn/i18n'; import type { DataTableRecord } from '@kbn/discover-utils/types'; -import { getRootBreadcrumbs } from '../../../utils/breadcrumbs'; +import { setBreadcrumbs } from '../../../utils/breadcrumbs'; import { DocViewer } from '../../../services/doc_views/components/doc_viewer'; import { ElasticRequestState } from '../types'; import { useEsDocSearch } from '../../../hooks/use_es_doc_search'; @@ -53,10 +53,11 @@ export function Doc(props: DocProps) { const indexExistsLink = docLinks.links.apis.indexExists; useEffect(() => { - chrome.setBreadcrumbs([ - ...getRootBreadcrumbs({ breadcrumb: props.referrer, services }), - { text: `${props.index}#${props.id}` }, - ]); + setBreadcrumbs({ + services, + titleBreadcrumbText: `${props.index}#${props.id}`, + rootBreadcrumbPath: props.referrer, + }); }, [chrome, props.referrer, props.index, props.id, dataView, locator, services]); return ( diff --git a/src/plugins/discover/public/application/index.tsx b/src/plugins/discover/public/application/index.tsx index b8a24be2b2fcc1..a9571e08e21ad0 100644 --- a/src/plugins/discover/public/application/index.tsx +++ b/src/plugins/discover/public/application/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { DiscoverRouter } from './discover_router'; import { DiscoverServices } from '../build_services'; import type { DiscoverProfileRegistry } from '../customizations/profile_registry'; @@ -36,15 +36,16 @@ export const renderApp = ({ element, services, profileRegistry, isDev }: RenderA }); } const unmount = toMountPoint( - wrapWithTheme( - , - core.theme.theme$ - ) + , + { + theme: core.theme, + i18n: core.i18n, + } )(element); return () => { diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx index 48c9a3306dd126..d7066306ee8d11 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.test.tsx @@ -20,12 +20,11 @@ import { } from '../../services/discover_data_state_container'; import { discoverServiceMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { buildDataTableRecord } from '@kbn/discover-utils'; import { DiscoverHistogramLayout, DiscoverHistogramLayoutProps } from './discover_histogram_layout'; import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public'; -import { CoreTheme } from '@kbn/core/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { createSearchSessionMock } from '../../../../__mocks__/search_session'; import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; @@ -127,16 +126,14 @@ const mountComponent = async ({ }; stateContainer.searchSessionManager = createSearchSessionMock(session).searchSessionManager; - const coreTheme$ = new BehaviorSubject({ darkMode: false }); - const component = mountWithIntl( - - + + - - + + ); // wait for lazy modules diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx index 81b774a0aff0d3..2901b49de15865 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx @@ -20,12 +20,11 @@ import { } from '../../services/discover_data_state_container'; import { createDiscoverServicesMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { buildDataTableRecord } from '@kbn/discover-utils'; import { DiscoverMainContent, DiscoverMainContentProps } from './discover_main_content'; import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public'; -import { CoreTheme } from '@kbn/core/public'; import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { DiscoverDocuments } from './discover_documents'; @@ -105,16 +104,14 @@ const mountComponent = async ({ onAddFilter: jest.fn(), }; - const coreTheme$ = new BehaviorSubject({ darkMode: false }); - const component = mountWithIntl( - - + + - - + + ); await act(async () => { diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index ba2f3148075ced..2b406e6a456824 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -49,8 +49,6 @@ export const getTopNavLinks = ({ }), run: async (anchorElement: HTMLElement) => { openAlertsPopover({ - I18nContext: services.core.i18n.Context, - theme$: services.core.theme.theme$, anchorElement, services, stateContainer: state, @@ -107,8 +105,6 @@ export const getTopNavLinks = ({ run: () => showOpenSearchPanel({ onOpenSavedSearch: state.actions.onOpenSavedSearch, - I18nContext: services.core.i18n.Context, - theme$: services.core.theme.theme$, services, }), }; diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.test.tsx index 60290869f3a056..03e5712df5e2eb 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.test.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.test.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { ReactNode } from 'react'; +import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; @@ -16,8 +16,6 @@ import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_ import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'; -const Context = ({ children }: { children: ReactNode }) => <>{children}; - const mount = (dataView = dataViewMock) => { const stateContainer = getDiscoverStateMock({ isTimeBased: true }); stateContainer.actions.setDataView(dataView); @@ -29,7 +27,6 @@ const mount = (dataView = dataViewMock) => { adHocDataViews={[]} services={discoverServiceMock} onClose={jest.fn()} - I18nContext={Context} /> ); 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 f722bc5558bee4..75202710945ddd 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 @@ -8,12 +8,11 @@ import React, { useCallback, useState, useMemo } from 'react'; import ReactDOM from 'react-dom'; -import type { Observable } from 'rxjs'; -import type { CoreTheme, I18nStart } from '@kbn/core/public'; import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DataView } from '@kbn/data-plugin/common'; -import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { DiscoverStateContainer } from '../../services/discover_state'; import { DiscoverServices } from '../../../../build_services'; @@ -28,7 +27,6 @@ interface AlertsPopoverProps { stateContainer: DiscoverStateContainer; savedQueryId?: string; adHocDataViews: DataView[]; - I18nContext: I18nStart['Context']; services: DiscoverServices; } @@ -163,15 +161,11 @@ function closeAlertsPopover() { } export function openAlertsPopover({ - I18nContext, - theme$, anchorElement, stateContainer, services, adHocDataViews, }: { - I18nContext: I18nStart['Context']; - theme$: Observable; anchorElement: HTMLElement; stateContainer: DiscoverStateContainer; services: DiscoverServices; @@ -186,20 +180,17 @@ export function openAlertsPopover({ document.body.appendChild(container); const element = ( - + - - - + - + ); ReactDOM.render(element, container); } diff --git a/src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx b/src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx index b437a753af37d6..c9d5dd12b544e7 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/show_open_search_panel.tsx @@ -8,23 +8,18 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { CoreTheme, I18nStart } from '@kbn/core/public'; -import { Observable } from 'rxjs'; -import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { DiscoverServices } from '../../../../build_services'; import { OpenSearchPanel } from './open_search_panel'; let isOpen = false; export function showOpenSearchPanel({ - I18nContext, onOpenSavedSearch, - theme$, services, }: { - I18nContext: I18nStart['Context']; onOpenSavedSearch: (id: string) => void; - theme$: Observable; services: DiscoverServices; }) { if (isOpen) { @@ -41,13 +36,11 @@ export function showOpenSearchPanel({ document.body.appendChild(container); const element = ( - + - - - + - + ); ReactDOM.render(element, container); } diff --git a/src/plugins/discover/public/application/main/discover_main_app.tsx b/src/plugins/discover/public/application/main/discover_main_app.tsx index 5e236611be24b8..dbc1db449b0300 100644 --- a/src/plugins/discover/public/application/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/main/discover_main_app.tsx @@ -11,7 +11,7 @@ import { RootDragDropProvider } from '@kbn/dom-drag-drop'; import { useUrlTracking } from './hooks/use_url_tracking'; import { DiscoverStateContainer } from './services/discover_state'; import { DiscoverLayout } from './components/layout'; -import { setBreadcrumbsTitle } from '../../utils/breadcrumbs'; +import { setBreadcrumbs } from '../../utils/breadcrumbs'; import { addHelpMenuToAppChrome } from '../../components/help_menu/help_menu_util'; import { useDiscoverServices } from '../../hooks/use_discover_services'; import { useSavedSearchAliasMatchRedirect } from '../../hooks/saved_search_alias_match_redirect'; @@ -68,7 +68,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) { if (mode === 'standalone') { const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : ''; chrome.docTitle.change(`Discover${pageTitleSuffix}`); - setBreadcrumbsTitle({ title: savedSearch.title, services }); + setBreadcrumbs({ titleBreadcrumbText: savedSearch.title, services }); } }, [mode, chrome.docTitle, savedSearch.id, savedSearch.title, services]); diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 39726044a25022..cf62f96ca7d1ad 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -22,7 +22,7 @@ import { useSingleton } from './hooks/use_singleton'; import { MainHistoryLocationState } from '../../../common/locator'; import { DiscoverStateContainer, getDiscoverStateContainer } from './services/discover_state'; import { DiscoverMainApp } from './discover_main_app'; -import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../../utils/breadcrumbs'; +import { setBreadcrumbs } from '../../utils/breadcrumbs'; import { LoadingIndicator } from '../../components/common/loading_indicator'; import { DiscoverError } from '../../components/common/error_alert'; import { useDiscoverServices } from '../../hooks/use_discover_services'; @@ -161,11 +161,7 @@ export function DiscoverMainRoute({ ); } - chrome.setBreadcrumbs( - currentSavedSearch && currentSavedSearch.title - ? getSavedSearchBreadcrumbs({ id: currentSavedSearch.title, services }) - : getRootBreadcrumbs({ services }) - ); + setBreadcrumbs({ services, titleBreadcrumbText: currentSavedSearch?.title ?? undefined }); } setLoading(false); if (services.analytics) { diff --git a/src/plugins/discover/public/application/main/hooks/use_alert_results_toast.tsx b/src/plugins/discover/public/application/main/hooks/use_alert_results_toast.tsx index 424cd554ecd15f..f0140a275da187 100644 --- a/src/plugins/discover/public/application/main/hooks/use_alert_results_toast.tsx +++ b/src/plugins/discover/public/application/main/hooks/use_alert_results_toast.tsx @@ -6,10 +6,9 @@ * Side Public License, v 1. */ +import { useEffect } from 'react'; import { ToastsStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import { MarkdownSimple, toMountPoint } from '@kbn/kibana-react-plugin/public'; -import React, { useEffect } from 'react'; export const displayPossibleDocsDiffInfoAlert = (toastNotifications: ToastsStart) => { const infoTitle = i18n.translate('discover.viewAlert.documentsMayVaryInfoTitle', { @@ -22,7 +21,7 @@ export const displayPossibleDocsDiffInfoAlert = (toastNotifications: ToastsStart toastNotifications.addInfo({ title: infoTitle, - text: toMountPoint({infoDescription}), + text: infoDescription, }); }; diff --git a/src/plugins/discover/public/application/not_found/not_found_route.tsx b/src/plugins/discover/public/application/not_found/not_found_route.tsx index e3a6e665e95ae3..4b2aa2b990221e 100644 --- a/src/plugins/discover/public/application/not_found/not_found_route.tsx +++ b/src/plugins/discover/public/application/not_found/not_found_route.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { Redirect } from 'react-router-dom'; -import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { getUrlTracker } from '../../kibana_services'; import { useDiscoverServices } from '../../hooks/use_discover_services'; @@ -33,20 +33,21 @@ export function NotFoundRoute() { bannerId = core.overlays.banners.replace( bannerId, toMountPoint( - wrapWithTheme( - -

- -

-
, - core.theme.theme$ - ) + +

+ +

+
, + { + theme: core.theme, + i18n: core.i18n, + } ) ); @@ -56,7 +57,7 @@ export function NotFoundRoute() { core.overlays.banners.remove(bannerId); } }, 15000); - }, [core.overlays.banners, history, urlForwarding, core.theme.theme$]); + }, [core, history, urlForwarding]); return ; } diff --git a/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx b/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx index f29fd7d40b51bc..d8a7bfeae9b341 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx @@ -14,7 +14,8 @@ import type { Rule } from '@kbn/alerting-plugin/common'; import type { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { ISearchSource, SerializedSearchSourceFields, getTime } from '@kbn/data-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { MarkdownSimple, toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { MarkdownSimple } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { Filter } from '@kbn/es-query'; import { DiscoverAppLocatorParams } from '../../../common/locator'; @@ -55,13 +56,15 @@ export const getAlertUtils = ( const errorTitle = i18n.translate('discover.viewAlert.dataViewErrorTitle', { defaultMessage: 'Error fetching data view', }); + const errorText = i18n.translate('discover.viewAlert.dataViewErrorText', { + defaultMessage: 'Data view failure of the alert rule with id {alertId}.', + values: { + alertId, + }, + }); toastNotifications.addDanger({ title: errorTitle, - text: toMountPoint( - - {new Error(`Data view failure of the alert rule with id ${alertId}.`).message} - - ), + text: errorText, }); }; @@ -76,7 +79,10 @@ export const getAlertUtils = ( }); toastNotifications.addDanger({ title: errorTitle, - text: toMountPoint({error.message}), + text: toMountPoint({error.message}, { + theme: core.theme, + i18n: core.i18n, + }), }); throw new Error(errorTitle); } @@ -96,7 +102,10 @@ export const getAlertUtils = ( }); toastNotifications.addDanger({ title: errorTitle, - text: toMountPoint({error.message}), + text: toMountPoint({error.message}, { + theme: core.theme, + i18n: core.i18n, + }), }); throw new Error(errorTitle); } diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 8d57429440e494..254292e6d07e64 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -52,6 +52,7 @@ import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import type { SettingsStart } from '@kbn/core-ui-settings-browser'; import type { ContentClient } from '@kbn/content-management-plugin/public'; +import type { ServerlessPluginStart } from '@kbn/serverless/public'; import { getHistory } from './kibana_services'; import { DiscoverStartPlugins } from './plugin'; import { DiscoverContextAppLocator } from './application/context/services/locator'; @@ -109,6 +110,7 @@ export interface DiscoverServices { lens: LensPublicStart; uiActions: UiActionsStart; contentClient: ContentClient; + serverless?: ServerlessPluginStart; } export const buildServices = memoize(function ( @@ -168,5 +170,6 @@ export const buildServices = memoize(function ( lens: plugins.lens, uiActions: plugins.uiActions, contentClient: plugins.contentManagement.client, + serverless: plugins.serverless, }; }); diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index f65582ee38290e..23e22de975ec38 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -18,7 +18,6 @@ import React from 'react'; import ReactDOM, { unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; import { isEqual } from 'lodash'; -import { I18nProvider } from '@kbn/i18n-react'; import type { KibanaExecutionContext } from '@kbn/core/public'; import { Container, @@ -41,7 +40,8 @@ import { import type { ISearchSource } from '@kbn/data-plugin/public'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { CellActionsProvider } from '@kbn/cell-actions'; @@ -639,21 +639,22 @@ export class SavedSearchEmbeddable Array.isArray(searchProps.columns) ) { ReactDOM.render( - - - - - - - , + + + + + , domNode ); @@ -678,15 +679,16 @@ export class SavedSearchEmbeddable const { getTriggerCompatibleActions } = searchProps.services.uiActions; ReactDOM.render( - - - - - - - - - , + + + + + + + , domNode ); diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index afeb8ac1e75ffe..6db46f53c35fb1 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -44,6 +44,7 @@ import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/publ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; +import type { ServerlessPluginStart } from '@kbn/serverless/public'; import { DOC_TABLE_LEGACY, TRUNCATE_MAX_HEIGHT } from '@kbn/discover-utils'; import { PLUGIN_ID } from '../common'; import { DocViewInput, DocViewInputFn } from './services/doc_views/doc_views_types'; @@ -211,6 +212,7 @@ export interface DiscoverStartPlugins { unifiedSearch: UnifiedSearchPublicPluginStart; lens: LensPublicStart; contentManagement: ContentManagementPublicStart; + serverless?: ServerlessPluginStart; } /** diff --git a/src/plugins/discover/public/utils/breadcrumbs.test.ts b/src/plugins/discover/public/utils/breadcrumbs.test.ts new file mode 100644 index 00000000000000..5970be8322c332 --- /dev/null +++ b/src/plugins/discover/public/utils/breadcrumbs.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { serverlessMock } from '@kbn/serverless/public/mocks'; +import { createDiscoverServicesMock } from '../__mocks__/services'; +import { setBreadcrumbs } from './breadcrumbs'; +import { createMemoryHistory } from 'history'; +import type { HistoryLocationState } from '../build_services'; + +describe('Breadcrumbs', () => { + const discoverServiceMock = createDiscoverServicesMock(); + beforeEach(() => { + (discoverServiceMock.chrome.setBreadcrumbs as jest.Mock).mockClear(); + }); + + test('should set breadcrumbs with default root', () => { + setBreadcrumbs({ services: discoverServiceMock }); + expect(discoverServiceMock.chrome.setBreadcrumbs).toHaveBeenCalledWith([{ text: 'Discover' }]); + }); + + test('should set breadcrumbs with title', () => { + setBreadcrumbs({ services: discoverServiceMock, titleBreadcrumbText: 'Saved Search' }); + expect(discoverServiceMock.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'Discover', href: '#/' }, + { text: 'Saved Search' }, + ]); + }); + + test('should set breadcrumbs with custom root path', () => { + setBreadcrumbs({ + services: discoverServiceMock, + titleBreadcrumbText: 'Saved Search', + rootBreadcrumbPath: '#/custom-path', + }); + expect(discoverServiceMock.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'Discover', href: '#/custom-path' }, + { text: 'Saved Search' }, + ]); + }); + + test('should set breadcrumbs with profile root path', () => { + setBreadcrumbs({ + services: { + ...discoverServiceMock, + history: () => { + const history = createMemoryHistory({}); + history.push('/p/my-profile'); + return history; + }, + }, + titleBreadcrumbText: 'Saved Search', + }); + expect(discoverServiceMock.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'Discover', href: '#/p/my-profile/' }, + { text: 'Saved Search' }, + ]); + }); +}); + +describe('Serverless Breadcrumbs', () => { + const discoverServiceMock = { + ...createDiscoverServicesMock(), + serverless: serverlessMock.createStart(), + }; + beforeEach(() => { + (discoverServiceMock.serverless.setBreadcrumbs as jest.Mock).mockClear(); + }); + + test('should not set any root', () => { + setBreadcrumbs({ services: discoverServiceMock }); + expect(discoverServiceMock.serverless.setBreadcrumbs).toHaveBeenCalledWith([]); + }); + + test('should set title breadcrumb', () => { + setBreadcrumbs({ services: discoverServiceMock, titleBreadcrumbText: 'Saved Search' }); + expect(discoverServiceMock.serverless.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'Saved Search' }, + ]); + }); + + test("shouldn't set root breadcrumbs, even when there is a custom root path", () => { + setBreadcrumbs({ + services: discoverServiceMock, + titleBreadcrumbText: 'Saved Search', + rootBreadcrumbPath: '#/custom-path', + }); + expect(discoverServiceMock.serverless.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'Saved Search' }, + ]); + }); + + test("shouldn't set root breadcrumbs, even when there is a custom profile", () => { + setBreadcrumbs({ + services: { + ...discoverServiceMock, + history: () => { + const history = createMemoryHistory({}); + history.push('/p/my-profile'); + return history; + }, + }, + titleBreadcrumbText: 'Saved Search', + }); + expect(discoverServiceMock.serverless.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'Saved Search' }, + ]); + }); +}); diff --git a/src/plugins/discover/public/utils/breadcrumbs.ts b/src/plugins/discover/public/utils/breadcrumbs.ts index 2381a162689009..304251ee896bb3 100644 --- a/src/plugins/discover/public/utils/breadcrumbs.ts +++ b/src/plugins/discover/public/utils/breadcrumbs.ts @@ -17,7 +17,7 @@ const getRootPath = ({ history }: DiscoverServices) => { return profile ? addProfile(rootPath, profile) : rootPath; }; -export function getRootBreadcrumbs({ +function getRootBreadcrumbs({ breadcrumb, services, }: { @@ -34,49 +34,44 @@ export function getRootBreadcrumbs({ ]; } -export function getSavedSearchBreadcrumbs({ - id, - services, -}: { - id: string; - services: DiscoverServices; -}) { - return [ - ...getRootBreadcrumbs({ services }), - { - text: id, - }, - ]; -} - /** * Helper function to set the Discover's breadcrumb * if there's an active savedSearch, its title is appended */ -export function setBreadcrumbsTitle({ - title, +export function setBreadcrumbs({ + rootBreadcrumbPath, + titleBreadcrumbText, services, }: { - title: string | undefined; + rootBreadcrumbPath?: string; + titleBreadcrumbText?: string; services: DiscoverServices; }) { + const rootBreadcrumbs = getRootBreadcrumbs({ + breadcrumb: rootBreadcrumbPath, + services, + }); const discoverBreadcrumbsTitle = i18n.translate('discover.discoverBreadcrumbTitle', { defaultMessage: 'Discover', }); - if (title) { - services.chrome.setBreadcrumbs([ - { - text: discoverBreadcrumbsTitle, - href: getRootPath(services), - }, - { text: title }, - ]); + if (services.serverless) { + // in serverless only set breadcrumbs for saved search title + // the root breadcrumbs are set automatically by the serverless navigation + if (titleBreadcrumbText) { + services.serverless.setBreadcrumbs([{ text: titleBreadcrumbText }]); + } else { + services.serverless.setBreadcrumbs([]); + } } else { - services.chrome.setBreadcrumbs([ - { - text: discoverBreadcrumbsTitle, - }, - ]); + if (titleBreadcrumbText) { + services.chrome.setBreadcrumbs([...rootBreadcrumbs, { text: titleBreadcrumbText }]); + } else { + services.chrome.setBreadcrumbs([ + { + text: discoverBreadcrumbsTitle, + }, + ]); + } } } diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 9fcaaeccd7b918..4c42e6967027c8 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -67,6 +67,9 @@ "@kbn/discover-utils", "@kbn/search-response-warnings", "@kbn/content-management-plugin", + "@kbn/serverless", + "@kbn/react-kibana-mount", + "@kbn/react-kibana-context-render" ], "exclude": [ "target/**/*" diff --git a/src/plugins/files/server/usage/integration_tests/usage.test.ts b/src/plugins/files/server/usage/integration_tests/usage.test.ts index 02cc7dfee55fa4..94554530237326 100644 --- a/src/plugins/files/server/usage/integration_tests/usage.test.ts +++ b/src/plugins/files/server/usage/integration_tests/usage.test.ts @@ -6,6 +6,10 @@ * Side Public License, v 1. */ +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { setupIntegrationEnvironment, TestEnvironmentUtils } from '../../test_utils'; describe('Files usage telemetry', () => { @@ -45,7 +49,9 @@ describe('Files usage telemetry', () => { ]); const { body } = await request - .post(root, '/api/telemetry/v2/clusters/_stats') + .post(root, '/internal/telemetry/clusters/_stats') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true }); expect(body[0].stats.stack_stats.kibana.plugins.files).toMatchInlineSnapshot(` diff --git a/src/plugins/files/tsconfig.json b/src/plugins/files/tsconfig.json index 08d910f23c5e97..a45132f21d5921 100644 --- a/src/plugins/files/tsconfig.json +++ b/src/plugins/files/tsconfig.json @@ -32,6 +32,7 @@ "@kbn/core-elasticsearch-server-mocks", "@kbn/core-saved-objects-server-mocks", "@kbn/logging", + "@kbn/core-http-common", ], "exclude": [ "target/**/*", diff --git a/src/plugins/interactive_setup/README.md b/src/plugins/interactive_setup/README.md index 9fd43eb0445b67..1c4d9d56771c78 100644 --- a/src/plugins/interactive_setup/README.md +++ b/src/plugins/interactive_setup/README.md @@ -1,3 +1,43 @@ -# `interactiveSetup` plugin +# Interactive Setup Plugin -The plugin provides UI and APIs for the interactive setup mode. +This plugin provides UI and APIs for interactive setup mode a.k.a "enrollment flow". + +## How to run interactive setup locally + +Kibana does not start interactive setup mode if it detects that an Elasticsearch connection has already been configured. This is always the case when running `yarn start` so in order to trigger interactive setup we need to run Elasticsearch manually and pass a special command line flag to the Kibana start command. + +1. Start a clean copy of Elasticsearch from inside your Kibana working directory: + + ``` + cd /.es/cache + tar -xzf elasticsearch-8.10.0-SNAPSHOT-darwin-aarch64.tar.gz + cd ./elasticsearch-8.10.0-SNAPSHOT + ./bin/elasticsearch + ``` + + You should see the enrollment token get logged: + + ``` + Elasticsearch security features have been automatically configured! + + • Copy the following enrollment token and paste it into Kibana in your browser: + eyJ2ZXIiOiI4LjEwLjAiLCJhZHIiOlsiMTkyLjE2OC4xLjIxMTo5MjAwIl0sImZnciI6ImZiYWZjOTgxODM0MjAwNzQ0M2ZhMzNmNTQ2N2QzMTM0YTk1NzU2NjEwOTcxNmJmMjdlYWViZWNlYTE3NmM3MTkiLCJrZXkiOiJxdVVQallrQkhOTkFxOVBqNEY0ejpZUkVMaFR5ZlNlZTZGZW9PQVZwaDRnIn0= + ``` + +2. Start Kibana without dev credentials and config: + + ``` + yarn start --no-dev-credentials --no-dev-config + ``` + + You should see the magic link get logged: + + ``` + i Kibana has not been configured. + + Go to http://localhost:5601/tcu/?code=651822 to get started. + ``` + +3. Open the link and copy the enrollment token from Elasticsearch when prompted to complete setup. + +Note: If you want to go through the enrollment flow again you will need to clear all Elasticsearch settings (`elasticsearch.*`) from your `kibana.yml` file and trash your Elasticsearch folder before starting with Step 1. diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts index 32ce4ed1980380..6014c3b3acf4c5 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import moment, { type MomentInput } from 'moment'; import type { Logger, ISavedObjectsRepository, SavedObject } from '@kbn/core/server'; import { type TestElasticsearchUtils, @@ -13,7 +14,7 @@ import { createTestServers, createRootWithCorePlugins, } from '@kbn/core-test-helpers-kbn-server'; -import { rollDailyData } from '../daily'; + import { metricsServiceMock } from '@kbn/core/server/mocks'; import { @@ -21,10 +22,21 @@ import { serializeSavedObjectId, EventLoopDelaysDaily, } from '../../saved_objects'; -import moment from 'moment'; +import { rollDailyData } from '../daily'; const eventLoopDelaysMonitor = metricsServiceMock.createEventLoopDelaysMonitor(); +/* + * Mocking the constructor of moment, so we can control the time of the day. + * This is to avoid flaky tests when starting to run before midnight and ending the test after midnight + * because the logic might remove one extra document since we moved to the next day. + */ +jest.doMock('moment', () => { + const mockedMoment = (date?: MomentInput) => moment(date ?? '2023-07-04T10:00:00.000Z'); + Object.setPrototypeOf(mockedMoment, moment); // inherit the prototype of `moment` so it has all the same methods. + return mockedMoment; +}); + function createRawObject(date: moment.MomentInput): SavedObject { const pid = Math.round(Math.random() * 10000); const instanceUuid = 'mock_instance'; @@ -45,8 +57,8 @@ function createRawObject(date: moment.MomentInput): SavedObject { +describe(`daily rollups integration test`, () => { let esServer: TestElasticsearchUtils; let root: TestKibanaUtils['root']; let internalRepository: ISavedObjectsRepository; @@ -82,15 +93,6 @@ describe.skip(`daily rollups integration test`, () => { logger = root.logger.get('test daily rollups'); internalRepository = start.savedObjects.createInternalRepository([SAVED_OBJECTS_DAILY_TYPE]); - // If we are less than 1 second away from midnight, let's wait 1 second before creating the docs. - // Otherwise, we may receive 1 document less than the expected ones. - if (moment().endOf('day').diff(moment(), 's', true) < 1) { - logger.info( - 'Delaying the creation of the docs 1s, just in case we create them before midnight and run the tests on the following day.' - ); - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - // Create the docs now const rawDailyDocs = createRawEventLoopDelaysDailyDocs(); rawEventLoopDelaysDaily = rawDailyDocs.rawEventLoopDelaysDaily; 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 84079703affc99..27fef2a0178243 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -110,6 +110,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'securitySolution:enableExpandableFlyout': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'securitySolution:enableCcsWarning': { 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 a48a3905c87056..81f7b0eb957f1d 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -63,6 +63,7 @@ export interface UsageStats { 'securitySolution:defaultAnomalyScore': number; 'securitySolution:refreshIntervalDefaults': string; 'securitySolution:enableNewsFeed': boolean; + 'securitySolution:enableExpandableFlyout': boolean; 'securitySolution:enableCcsWarning': boolean; 'search:includeFrozen': boolean; 'courier:maxConcurrentShardRequests': number; diff --git a/src/plugins/presentation_util/public/__stories__/fixtures/flights.ts b/src/plugins/presentation_util/public/__stories__/fixtures/flights.ts index 17cedff90ad23b..7b6d6efc880c86 100644 --- a/src/plugins/presentation_util/public/__stories__/fixtures/flights.ts +++ b/src/plugins/presentation_util/public/__stories__/fixtures/flights.ts @@ -61,14 +61,19 @@ const numberFields = [ const getConfig = (() => {}) as FieldFormatsGetConfigFn; export const flightFieldByName: { [key: string]: DataViewField } = {}; -flightFieldNames.forEach( - (flightFieldName) => - (flightFieldByName[flightFieldName] = { - name: flightFieldName, - type: numberFields.includes(flightFieldName) ? 'number' : 'string', - aggregatable: true, - } as unknown as DataViewField) -); +flightFieldNames.forEach((flightFieldName) => { + const fieldBase = { + name: flightFieldName, + type: numberFields.includes(flightFieldName) ? 'number' : 'string', + aggregatable: true, + }; + flightFieldByName[flightFieldName] = { + ...fieldBase, + toSpec: () => { + return fieldBase; + }, + } as unknown as DataViewField; +}); flightFieldByName.Cancelled = { name: 'Cancelled', type: 'boolean' } as DataViewField; flightFieldByName.timestamp = { name: 'timestamp', type: 'date' } as DataViewField; diff --git a/src/plugins/share/public/url_service/redirect/components/page.tsx b/src/plugins/share/public/url_service/redirect/components/page.tsx index 38aeafa5920d59..b64c87cd374cc2 100644 --- a/src/plugins/share/public/url_service/redirect/components/page.tsx +++ b/src/plugins/share/public/url_service/redirect/components/page.tsx @@ -8,9 +8,9 @@ import * as React from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { EuiPageTemplate_Deprecated as EuiPageTemplate } from '@elastic/eui'; +import { EuiPageTemplate } from '@elastic/eui'; import { ThemeServiceSetup } from '@kbn/core/public'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { CustomBrandingStart } from '@kbn/core-custom-branding-browser'; import { Error } from './error'; import { RedirectManager } from '../redirect_manager'; @@ -28,13 +28,8 @@ export const Page: React.FC = ({ manager, theme, customBranding }) => if (error) { return ( - - + + @@ -42,13 +37,8 @@ export const Page: React.FC = ({ manager, theme, customBranding }) => } return ( - - + + diff --git a/src/plugins/share/tsconfig.json b/src/plugins/share/tsconfig.json index 396c751fccbcc8..47fa82eae4c976 100644 --- a/src/plugins/share/tsconfig.json +++ b/src/plugins/share/tsconfig.json @@ -14,6 +14,7 @@ "@kbn/config-schema", "@kbn/core-custom-branding-browser", "@kbn/core-saved-objects-utils-server", + "@kbn/react-kibana-context-theme", ], "exclude": [ "target/**/*", diff --git a/src/plugins/telemetry/common/routes.ts b/src/plugins/telemetry/common/routes.ts index 06d6f746bf2c1b..ce4db9c87b5ea8 100644 --- a/src/plugins/telemetry/common/routes.ts +++ b/src/plugins/telemetry/common/routes.ts @@ -6,7 +6,49 @@ * Side Public License, v 1. */ +const BASE_INTERNAL_PATH = '/internal/telemetry'; + +export const INTERNAL_VERSION = { version: '2' }; + +/** + * Fetch Telemetry Config + * @public Kept public and path-based because we know other Elastic products fetch the opt-in status via this endpoint. + */ +export const FetchTelemetryConfigRoutePathBasedV2 = '/api/telemetry/v2/config'; + /** * Fetch Telemetry Config + * @internal + */ +export const FetchTelemetryConfigRoute = `${BASE_INTERNAL_PATH}/config`; + +/** + * GET/PUT Last reported date for Snapshot telemetry + * @internal + */ +export const LastReportedRoute = `${BASE_INTERNAL_PATH}/last_reported`; + +/** + * Set user has seen notice + * @internal + */ +export const UserHasSeenNoticeRoute = `${BASE_INTERNAL_PATH}/userHasSeenNotice`; + +/** + * Set opt-in/out status + * @internal + */ +export const OptInRoute = `${BASE_INTERNAL_PATH}/optIn`; + +/** + * Fetch the Snapshot telemetry report + * @internal + */ +export const FetchSnapshotTelemetry = `${BASE_INTERNAL_PATH}/clusters/_stats`; + +/** + * Get Opt-in stats + * @internal + * @deprecated */ -export const FetchTelemetryConfigRoute = '/api/telemetry/v2/config'; +export const GetOptInStatsRoutePathBasedV2 = '/api/telemetry/v2/clusters/_opt_in_stats'; diff --git a/src/plugins/telemetry/common/types/index.ts b/src/plugins/telemetry/common/types/index.ts index 14b2d3cbefcf48..f03d8f821b1ebc 100644 --- a/src/plugins/telemetry/common/types/index.ts +++ b/src/plugins/telemetry/common/types/index.ts @@ -8,4 +8,5 @@ export * from './latest'; +export * as v1 from './v2'; // Just so v1 can also be used (but for some reason telemetry endpoints have always been v2 :shrug:) export * as v2 from './v2'; diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index cf879472a2bb09..c0d0faf0819b0e 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -24,7 +24,7 @@ import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser'; import { of } from 'rxjs'; -import { FetchTelemetryConfigRoute } from '../common/routes'; +import { FetchTelemetryConfigRoute, INTERNAL_VERSION } from '../common/routes'; import type { v2 } from '../common/types'; import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services'; import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice'; @@ -329,7 +329,7 @@ export class TelemetryPlugin */ private async fetchUpdatedConfig(http: HttpStart | HttpSetup): Promise { const { allowChangingOptInStatus, optIn, sendUsageFrom, telemetryNotifyUserAboutOptInDefault } = - await http.get(FetchTelemetryConfigRoute); + await http.get(FetchTelemetryConfigRoute, INTERNAL_VERSION); return { ...this.config, diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts index 4934495d57d8bf..d072d654cceaaa 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.test.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -10,6 +10,12 @@ /* eslint-disable dot-notation */ import { mockTelemetryService } from '../mocks'; +import { + FetchSnapshotTelemetry, + INTERNAL_VERSION, + OptInRoute, + UserHasSeenNoticeRoute, +} from '../../common/routes'; describe('TelemetryService', () => { describe('fetchTelemetry', () => { @@ -17,7 +23,8 @@ describe('TelemetryService', () => { const telemetryService = mockTelemetryService(); await telemetryService.fetchTelemetry(); - expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/clusters/_stats', { + expect(telemetryService['http'].post).toBeCalledWith(FetchSnapshotTelemetry, { + ...INTERNAL_VERSION, body: JSON.stringify({ unencrypted: false, refreshCache: false }), }); }); @@ -64,7 +71,8 @@ describe('TelemetryService', () => { const optedIn = true; await telemetryService.setOptIn(optedIn); - expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', { + expect(telemetryService['http'].post).toBeCalledWith(OptInRoute, { + ...INTERNAL_VERSION, body: JSON.stringify({ enabled: optedIn }), }); }); @@ -77,7 +85,8 @@ describe('TelemetryService', () => { const optedIn = false; await telemetryService.setOptIn(optedIn); - expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', { + expect(telemetryService['http'].post).toBeCalledWith(OptInRoute, { + ...INTERNAL_VERSION, body: JSON.stringify({ enabled: optedIn }), }); }); @@ -110,7 +119,7 @@ describe('TelemetryService', () => { config: { allowChangingOptInStatus: true }, }); telemetryService['http'].post = jest.fn().mockImplementation((url: string) => { - if (url === '/api/telemetry/v2/optIn') { + if (url === OptInRoute) { throw Error('failed to update opt in.'); } }); @@ -203,7 +212,7 @@ describe('TelemetryService', () => { }); telemetryService['http'].put = jest.fn().mockImplementation((url: string) => { - if (url === '/api/telemetry/v2/userHasSeenNotice') { + if (url === UserHasSeenNoticeRoute) { throw Error('failed to update opt in.'); } }); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts index 0629630fea48a7..ec67a4e675e269 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -8,11 +8,19 @@ import { i18n } from '@kbn/i18n'; import type { CoreSetup, CoreStart } from '@kbn/core/public'; +import { + LastReportedRoute, + INTERNAL_VERSION, + OptInRoute, + FetchSnapshotTelemetry, + UserHasSeenNoticeRoute, +} from '../../common/routes'; import type { TelemetryPluginConfig } from '../plugin'; -import { getTelemetryChannelEndpoint } from '../../common/telemetry_config/get_telemetry_channel_endpoint'; +import { getTelemetryChannelEndpoint } from '../../common/telemetry_config'; import type { UnencryptedTelemetryPayload, EncryptedTelemetryPayload, + FetchLastReportedResponse, } from '../../common/types/latest'; import { PAYLOAD_CONTENT_ENCODING } from '../../common/constants'; @@ -93,8 +101,7 @@ export class TelemetryService { /** Is the cluster allowed to change the opt-in/out status **/ public getCanChangeOptInStatus = () => { - const allowChangingOptInStatus = this.config.allowChangingOptInStatus; - return allowChangingOptInStatus; + return this.config.allowChangingOptInStatus; }; /** Retrieve the opt-in/out notification URL **/ @@ -156,17 +163,18 @@ export class TelemetryService { }; public fetchLastReported = async (): Promise => { - const response = await this.http.get<{ lastReported?: number }>( - '/api/telemetry/v2/last_reported' + const response = await this.http.get( + LastReportedRoute, + INTERNAL_VERSION ); return response?.lastReported; }; public updateLastReported = async (): Promise => { - return this.http.put('/api/telemetry/v2/last_reported'); + return this.http.put(LastReportedRoute); }; - /** Fetches an unencrypted telemetry payload so we can show it to the user **/ + /** Fetches an unencrypted telemetry payload, so we can show it to the user **/ public fetchExample = async (): Promise => { return await this.fetchTelemetry({ unencrypted: true, refreshCache: true }); }; @@ -174,12 +182,14 @@ export class TelemetryService { /** * Fetches telemetry payload * @param unencrypted Default `false`. Whether the returned payload should be encrypted or not. + * @param refreshCache Default `false`. Set to `true` to force the regeneration of the telemetry report. */ public fetchTelemetry = async ({ unencrypted = false, refreshCache = false, } = {}): Promise => { - return this.http.post('/api/telemetry/v2/clusters/_stats', { + return this.http.post(FetchSnapshotTelemetry, { + ...INTERNAL_VERSION, body: JSON.stringify({ unencrypted, refreshCache }), }); }; @@ -198,12 +208,10 @@ export class TelemetryService { try { // Report the option to the Kibana server to store the settings. // It returns the encrypted update to send to the telemetry cluster [{cluster_uuid, opt_in_status}] - const optInStatusPayload = await this.http.post( - '/api/telemetry/v2/optIn', - { - body: JSON.stringify({ enabled: optedIn }), - } - ); + const optInStatusPayload = await this.http.post(OptInRoute, { + ...INTERNAL_VERSION, + body: JSON.stringify({ enabled: optedIn }), + }); if (this.reportOptInStatusChange) { // Use the response to report about the change to the remote telemetry cluster. // If it's opt-out, this will be the last communication to the remote service. @@ -231,7 +239,7 @@ export class TelemetryService { */ public setUserHasSeenNotice = async (): Promise => { try { - await this.http.put('/api/telemetry/v2/userHasSeenNotice'); + await this.http.put(UserHasSeenNoticeRoute, INTERNAL_VERSION); this.userHasSeenOptedInNotice = true; } catch (error) { this.notifications.toasts.addError(error, { @@ -248,7 +256,7 @@ export class TelemetryService { /** * Pushes the encrypted payload [{cluster_uuid, opt_in_status}] to the remote telemetry service - * @param optInPayload [{cluster_uuid, opt_in_status}] encrypted by the server into an array of strings + * @param optInStatusPayload [{cluster_uuid, opt_in_status}] encrypted by the server into an array of strings */ private reportOptInStatus = async ( optInStatusPayload: EncryptedTelemetryPayload diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 52a54eb0335b10..00d8a96a59e0e5 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -9207,6 +9207,12 @@ "description": "Non-default value of setting." } }, + "securitySolution:enableExpandableFlyout": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "securitySolution:enableCcsWarning": { "type": "boolean", "_meta": { diff --git a/src/plugins/telemetry/server/routes/telemetry_config.ts b/src/plugins/telemetry/server/routes/telemetry_config.ts index 60a34d80aad2e6..37daef537b568f 100644 --- a/src/plugins/telemetry/server/routes/telemetry_config.ts +++ b/src/plugins/telemetry/server/routes/telemetry_config.ts @@ -8,9 +8,14 @@ import { type Observable, firstValueFrom } from 'rxjs'; import type { IRouter, SavedObjectsClient } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from '@kbn/core-http-server'; import type { TelemetryConfigType } from '../config'; import { v2 } from '../../common/types'; -import { FetchTelemetryConfigRoute } from '../../common/routes'; +import { + FetchTelemetryConfigRoutePathBasedV2, + FetchTelemetryConfigRoute, +} from '../../common/routes'; import { getTelemetrySavedObject } from '../saved_objects'; import { getNotifyUserAboutOptInDefault, @@ -25,54 +30,74 @@ interface RegisterTelemetryConfigRouteOptions { currentKibanaVersion: string; savedObjectsInternalClient$: Observable; } + export function registerTelemetryConfigRoutes({ router, config$, currentKibanaVersion, savedObjectsInternalClient$, }: RegisterTelemetryConfigRouteOptions) { - // GET to retrieve - router.get( - { - path: FetchTelemetryConfigRoute, - validate: false, - }, - async (context, req, res) => { - const config = await firstValueFrom(config$); - const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$); - const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient); - const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({ - configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus, - telemetrySavedObject, - }); + const v2Handler: RequestHandler = async (context, req, res) => { + const config = await firstValueFrom(config$); + const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$); + const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient); + const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({ + configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus, + telemetrySavedObject, + }); + + const optIn = getTelemetryOptIn({ + configTelemetryOptIn: config.optIn, + allowChangingOptInStatus, + telemetrySavedObject, + currentKibanaVersion, + }); - const optIn = getTelemetryOptIn({ - configTelemetryOptIn: config.optIn, - allowChangingOptInStatus, - telemetrySavedObject, - currentKibanaVersion, - }); + const sendUsageFrom = getTelemetrySendUsageFrom({ + configTelemetrySendUsageFrom: config.sendUsageFrom, + telemetrySavedObject, + }); - const sendUsageFrom = getTelemetrySendUsageFrom({ - configTelemetrySendUsageFrom: config.sendUsageFrom, - telemetrySavedObject, - }); + const telemetryNotifyUserAboutOptInDefault = getNotifyUserAboutOptInDefault({ + telemetrySavedObject, + allowChangingOptInStatus, + configTelemetryOptIn: config.optIn, + telemetryOptedIn: optIn, + }); - const telemetryNotifyUserAboutOptInDefault = getNotifyUserAboutOptInDefault({ - telemetrySavedObject, - allowChangingOptInStatus, - configTelemetryOptIn: config.optIn, - telemetryOptedIn: optIn, - }); + const body: v2.FetchTelemetryConfigResponse = { + allowChangingOptInStatus, + optIn, + sendUsageFrom, + telemetryNotifyUserAboutOptInDefault, + }; + + return res.ok({ body }); + }; + + const v2Validations = { + response: { + 200: { + body: schema.object({ + allowChangingOptInStatus: schema.boolean(), + optIn: schema.oneOf([schema.boolean(), schema.literal(null)]), + sendUsageFrom: schema.oneOf([schema.literal('server'), schema.literal('browser')]), + telemetryNotifyUserAboutOptInDefault: schema.boolean(), + }), + }, + }, + }; - const body: v2.FetchTelemetryConfigResponse = { - allowChangingOptInStatus, - optIn, - sendUsageFrom, - telemetryNotifyUserAboutOptInDefault, - }; + // Register the internal versioned API + router.versioned + .get({ access: 'internal', path: FetchTelemetryConfigRoute }) + // Just because it used to be /v2/, we are creating identical v1 and v2. + .addVersion({ version: '1', validate: v2Validations }, v2Handler) + .addVersion({ version: '2', validate: v2Validations }, v2Handler); - return res.ok({ body }); - } - ); + // Register the deprecated public and path-based for BWC + // as we know this one is used by other Elastic products to fetch the opt-in status. + router.versioned + .get({ access: 'public', path: FetchTelemetryConfigRoutePathBasedV2 }) + .addVersion({ version: '2023-10-31', validate: v2Validations }, v2Handler); } diff --git a/src/plugins/telemetry/server/routes/telemetry_last_reported.ts b/src/plugins/telemetry/server/routes/telemetry_last_reported.ts index 2e21785b9296de..23c15521d870da 100644 --- a/src/plugins/telemetry/server/routes/telemetry_last_reported.ts +++ b/src/plugins/telemetry/server/routes/telemetry_last_reported.ts @@ -6,9 +6,12 @@ * Side Public License, v 1. */ +import { schema } from '@kbn/config-schema'; import type { IRouter, SavedObjectsClient } from '@kbn/core/server'; import type { Observable } from 'rxjs'; import { firstValueFrom } from 'rxjs'; +import { RequestHandler } from '@kbn/core-http-server'; +import { LastReportedRoute } from '../../common/routes'; import { v2 } from '../../common/types'; import { getTelemetrySavedObject, updateTelemetrySavedObject } from '../saved_objects'; @@ -17,38 +20,38 @@ export function registerTelemetryLastReported( savedObjectsInternalClient$: Observable ) { // GET to retrieve - router.get( - { - path: '/api/telemetry/v2/last_reported', - validate: false, - }, - async (context, req, res) => { - const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$); - const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient); + const v2GetValidations = { + response: { 200: { body: schema.object({ lastReported: schema.maybe(schema.number()) }) } }, + }; - const body: v2.FetchLastReportedResponse = { - lastReported: telemetrySavedObject && telemetrySavedObject?.lastReported, - }; + const v2GetHandler: RequestHandler = async (context, req, res) => { + const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$); + const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient); - return res.ok({ - body, - }); - } - ); + const body: v2.FetchLastReportedResponse = { + lastReported: telemetrySavedObject && telemetrySavedObject?.lastReported, + }; + return res.ok({ body }); + }; + + router.versioned + .get({ access: 'internal', path: LastReportedRoute }) + // Just because it used to be /v2/, we are creating identical v1 and v2. + .addVersion({ version: '1', validate: v2GetValidations }, v2GetHandler) + .addVersion({ version: '2', validate: v2GetValidations }, v2GetHandler); // PUT to update - router.put( - { - path: '/api/telemetry/v2/last_reported', - validate: false, - }, - async (context, req, res) => { - const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$); - await updateTelemetrySavedObject(savedObjectsInternalClient, { - lastReported: Date.now(), - }); + const v2PutHandler: RequestHandler = async (context, req, res) => { + const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$); + await updateTelemetrySavedObject(savedObjectsInternalClient, { + lastReported: Date.now(), + }); + return res.ok(); + }; - return res.ok(); - } - ); + router.versioned + .put({ access: 'internal', path: LastReportedRoute }) + // Just because it used to be /v2/, we are creating identical v1 and v2. + .addVersion({ version: '1', validate: false }, v2PutHandler) + .addVersion({ version: '2', validate: false }, v2PutHandler); } diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts index f523031e181ed8..689e0cd06fad8d 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts @@ -9,12 +9,14 @@ import { firstValueFrom, type Observable } from 'rxjs'; import { schema } from '@kbn/config-schema'; import type { IRouter, Logger } from '@kbn/core/server'; -import { SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { RequestHandlerContext, SavedObjectsErrorHelpers } from '@kbn/core/server'; import type { StatsGetterConfig, TelemetryCollectionManagerPluginSetup, } from '@kbn/telemetry-collection-manager-plugin/server'; -import { v2 } from '../../common/types'; +import { RequestHandler } from '@kbn/core-http-server'; +import { OptInRoute } from '../../common/routes'; +import { OptInBody, v2 } from '../../common/types'; import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats'; import { getTelemetrySavedObject, @@ -41,78 +43,91 @@ export function registerTelemetryOptInRoutes({ currentKibanaVersion, telemetryCollectionManager, }: RegisterOptInRoutesParams) { - router.post( - { - path: '/api/telemetry/v2/optIn', - validate: { - body: schema.object({ enabled: schema.boolean() }), - }, - }, - async (context, req, res) => { - const newOptInStatus = req.body.enabled; - const soClient = (await context.core).savedObjects.getClient({ - includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE], - }); - const attributes: TelemetrySavedObject = { - enabled: newOptInStatus, - lastVersionChecked: currentKibanaVersion, - }; - const config = await firstValueFrom(config$); + const v2Handler: RequestHandler = async ( + context, + req, + res + ) => { + const newOptInStatus = req.body.enabled; + const soClient = (await context.core).savedObjects.getClient({ + includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE], + }); + const attributes: TelemetrySavedObject = { + enabled: newOptInStatus, + lastVersionChecked: currentKibanaVersion, + }; + const config = await firstValueFrom(config$); - let telemetrySavedObject: TelemetrySavedObject | undefined; - try { - telemetrySavedObject = await getTelemetrySavedObject(soClient); - } catch (err) { - if (SavedObjectsErrorHelpers.isForbiddenError(err)) { - // If we couldn't get the saved object due to lack of permissions, - // we can assume the user won't be able to update it either - return res.forbidden(); - } + let telemetrySavedObject: TelemetrySavedObject | undefined; + try { + telemetrySavedObject = await getTelemetrySavedObject(soClient); + } catch (err) { + if (SavedObjectsErrorHelpers.isForbiddenError(err)) { + // If we couldn't get the saved object due to lack of permissions, + // we can assume the user won't be able to update it either + return res.forbidden(); } + } - const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({ - configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus, - telemetrySavedObject, + const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({ + configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus, + telemetrySavedObject, + }); + if (!allowChangingOptInStatus) { + return res.badRequest({ + body: JSON.stringify({ error: 'Not allowed to change Opt-in Status.' }), }); - if (!allowChangingOptInStatus) { - return res.badRequest({ - body: JSON.stringify({ error: 'Not allowed to change Opt-in Status.' }), - }); - } + } - const statsGetterConfig: StatsGetterConfig = { - unencrypted: false, - }; + const statsGetterConfig: StatsGetterConfig = { + unencrypted: false, + }; - const optInStatus = await telemetryCollectionManager.getOptInStats( - newOptInStatus, + const optInStatus = await telemetryCollectionManager.getOptInStats( + newOptInStatus, + statsGetterConfig + ); + + if (config.sendUsageFrom === 'server') { + const { appendServerlessChannelsSuffix, sendUsageTo } = config; + sendTelemetryOptInStatus( + telemetryCollectionManager, + { appendServerlessChannelsSuffix, sendUsageTo, newOptInStatus, currentKibanaVersion }, statsGetterConfig - ); + ).catch((err) => { + // The server is likely behind a firewall and can't reach the remote service + logger.warn( + `Failed to notify the telemetry endpoint about the opt-in selection. Possibly blocked by a firewall? - Error: ${err.message}` + ); + }); + } - if (config.sendUsageFrom === 'server') { - const { appendServerlessChannelsSuffix, sendUsageTo } = config; - sendTelemetryOptInStatus( - telemetryCollectionManager, - { appendServerlessChannelsSuffix, sendUsageTo, newOptInStatus, currentKibanaVersion }, - statsGetterConfig - ).catch((err) => { - // The server is likely behind a firewall and can't reach the remote service - logger.warn( - `Failed to notify the telemetry endpoint about the opt-in selection. Possibly blocked by a firewall? - Error: ${err.message}` - ); - }); + try { + await updateTelemetrySavedObject(soClient, attributes); + } catch (e) { + if (SavedObjectsErrorHelpers.isForbiddenError(e)) { + return res.forbidden(); } + } - try { - await updateTelemetrySavedObject(soClient, attributes); - } catch (e) { - if (SavedObjectsErrorHelpers.isForbiddenError(e)) { - return res.forbidden(); - } - } + const body: v2.OptInResponse = optInStatus; + return res.ok({ body }); + }; - const body: v2.OptInResponse = optInStatus; - return res.ok({ body }); - } - ); + const v2Validations = { + request: { body: schema.object({ enabled: schema.boolean() }) }, + response: { + 200: { + body: schema.arrayOf( + schema.object({ clusterUuid: schema.string(), stats: schema.string() }) + ), + }, + }, + }; + + router.versioned + .post({ access: 'internal', path: OptInRoute }) + // Just because it used to be /v2/, we are creating identical v1 and v2. + .addVersion({ version: '1', validate: v2Validations }, v2Handler) + .addVersion({ version: '2', validate: v2Validations }, v2Handler); } diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts index f715c84fc93413..378dbdcc9e4951 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts @@ -14,6 +14,7 @@ import type { TelemetryCollectionManagerPluginSetup, StatsGetterConfig, } from '@kbn/telemetry-collection-manager-plugin/server'; +import { GetOptInStatsRoutePathBasedV2 } from '../../common/routes'; import type { v2 } from '../../common/types'; import { EncryptedTelemetryPayload, UnencryptedTelemetryPayload } from '../../common/types'; import { getTelemetryChannelEndpoint } from '../../common/telemetry_config'; @@ -62,43 +63,64 @@ export function registerTelemetryOptInStatsRoutes( router: IRouter, telemetryCollectionManager: TelemetryCollectionManagerPluginSetup ) { - router.post( - { - path: '/api/telemetry/v2/clusters/_opt_in_stats', - validate: { - body: schema.object({ - enabled: schema.boolean(), - unencrypted: schema.boolean({ defaultValue: true }), - }), + router.versioned + .post({ + access: 'public', // It's not used across Kibana, and I didn't want to remove it in this PR just in case. + path: GetOptInStatsRoutePathBasedV2, + }) + .addVersion( + { + version: '2023-10-31', + validate: { + request: { + body: schema.object({ + enabled: schema.boolean(), + unencrypted: schema.boolean({ defaultValue: true }), + }), + }, + response: { + 200: { + body: schema.arrayOf( + schema.object({ + clusterUuid: schema.string(), + stats: schema.object({ + cluster_uuid: schema.string(), + opt_in_status: schema.boolean(), + }), + }) + ), + }, + 503: { body: schema.string() }, + }, + }, }, - }, - async (context, req, res) => { - try { - const newOptInStatus = req.body.enabled; - const unencrypted = req.body.unencrypted; + async (context, req, res) => { + try { + const newOptInStatus = req.body.enabled; + const unencrypted = req.body.unencrypted; - if (!(await telemetryCollectionManager.shouldGetTelemetry())) { - // We probably won't reach here because there is a license check in the auth phase of the HTTP requests. - // But let's keep it here should that changes at any point. - return res.customError({ - statusCode: 503, - body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`, - }); - } + if (!(await telemetryCollectionManager.shouldGetTelemetry())) { + // We probably won't reach here because there is a license check in the auth phase of the HTTP requests. + // But let's keep it here should that changes at any point. + return res.customError({ + statusCode: 503, + body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`, + }); + } - const statsGetterConfig: StatsGetterConfig = { - unencrypted, - }; + const statsGetterConfig: StatsGetterConfig = { + unencrypted, + }; - const optInStatus = await telemetryCollectionManager.getOptInStats( - newOptInStatus, - statsGetterConfig - ); - const body: v2.OptInStatsResponse = optInStatus; - return res.ok({ body }); - } catch (err) { - return res.ok({ body: [] }); + const optInStatus = await telemetryCollectionManager.getOptInStats( + newOptInStatus, + statsGetterConfig + ); + const body: v2.OptInStatsResponse = optInStatus; + return res.ok({ body }); + } catch (err) { + return res.ok({ body: [] }); + } } - } - ); + ); } diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts index 152aaa4c9eed9f..4cbb1381c0566c 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts @@ -16,8 +16,9 @@ async function runRequest( mockRouter: IRouter, body?: { unencrypted?: boolean; refreshCache?: boolean } ) { - expect(mockRouter.post).toBeCalled(); - const [, handler] = (mockRouter.post as jest.Mock).mock.calls[0]; + expect(mockRouter.versioned.post).toBeCalled(); + const [, handler] = (mockRouter.versioned.post as jest.Mock).mock.results[0].value.addVersion.mock + .calls[0]; const mockResponse = httpServerMock.createResponseFactory(); const mockRequest = httpServerMock.createKibanaRequest({ body }); await handler(null, mockRequest, mockResponse); @@ -49,10 +50,10 @@ describe('registerTelemetryUsageStatsRoutes', () => { describe('clusters/_stats POST route', () => { it('registers _stats POST route and accepts body configs', () => { registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); - expect(mockRouter.post).toBeCalledTimes(1); - const [routeConfig, handler] = (mockRouter.post as jest.Mock).mock.calls[0]; - expect(routeConfig.path).toMatchInlineSnapshot(`"/api/telemetry/v2/clusters/_stats"`); - expect(Object.keys(routeConfig.validate.body.props)).toEqual(['unencrypted', 'refreshCache']); + expect(mockRouter.versioned.post).toBeCalledTimes(1); + const [routeConfig, handler] = (mockRouter.versioned.post as jest.Mock).mock.results[0].value + .addVersion.mock.calls[0]; + expect(routeConfig.version).toMatchInlineSnapshot(`"1"`); expect(handler).toBeInstanceOf(Function); }); diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts index 600c6d3ef2c703..a828de911c29ad 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts @@ -13,7 +13,9 @@ import type { StatsGetterConfig, } from '@kbn/telemetry-collection-manager-plugin/server'; import type { SecurityPluginStart } from '@kbn/security-plugin/server'; -import { v2 } from '../../common/types'; +import { RequestHandler } from '@kbn/core-http-server'; +import { FetchSnapshotTelemetry } from '../../common/routes'; +import { UsageStatsBody, v2 } from '../../common/types'; export type SecurityGetter = () => SecurityPluginStart | undefined; @@ -23,64 +25,75 @@ export function registerTelemetryUsageStatsRoutes( isDev: boolean, getSecurity: SecurityGetter ) { - router.post( - { - path: '/api/telemetry/v2/clusters/_stats', - validate: { - body: schema.object({ - unencrypted: schema.boolean({ defaultValue: false }), - refreshCache: schema.boolean({ defaultValue: false }), - }), - }, - }, - async (context, req, res) => { - const { unencrypted, refreshCache } = req.body; + const v2Handler: RequestHandler = async ( + context, + req, + res + ) => { + const { unencrypted, refreshCache } = req.body; - if (!(await telemetryCollectionManager.shouldGetTelemetry())) { - // We probably won't reach here because there is a license check in the auth phase of the HTTP requests. - // But let's keep it here should that changes at any point. - return res.customError({ - statusCode: 503, - body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`, - }); - } + if (!(await telemetryCollectionManager.shouldGetTelemetry())) { + // We probably won't reach here because there is a license check in the auth phase of the HTTP requests. + // But let's keep it here should that changes at any point. + return res.customError({ + statusCode: 503, + body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`, + }); + } - const security = getSecurity(); - // We need to check useRbacForRequest to figure out if ES has security enabled before making the privileges check - if (security && unencrypted && security.authz.mode.useRbacForRequest(req)) { - // Normally we would use `options: { tags: ['access:decryptedTelemetry'] }` in the route definition to check authorization for an - // API action, however, we want to check this conditionally based on the `unencrypted` parameter. In this case we need to use the - // security API directly to check privileges for this action. Note that the 'decryptedTelemetry' API privilege string is only - // granted to users that have "Global All" or "Global Read" privileges in Kibana. - const { checkPrivilegesWithRequest, actions } = security.authz; - const privileges = { kibana: actions.api.get('decryptedTelemetry') }; - const { hasAllRequested } = await checkPrivilegesWithRequest(req).globally(privileges); - if (!hasAllRequested) { - return res.forbidden(); - } + const security = getSecurity(); + // We need to check useRbacForRequest to figure out if ES has security enabled before making the privileges check + if (security && unencrypted && security.authz.mode.useRbacForRequest(req)) { + // Normally we would use `options: { tags: ['access:decryptedTelemetry'] }` in the route definition to check authorization for an + // API action, however, we want to check this conditionally based on the `unencrypted` parameter. In this case we need to use the + // security API directly to check privileges for this action. Note that the 'decryptedTelemetry' API privilege string is only + // granted to users that have "Global All" or "Global Read" privileges in Kibana. + const { checkPrivilegesWithRequest, actions } = security.authz; + const privileges = { kibana: actions.api.get('decryptedTelemetry') }; + const { hasAllRequested } = await checkPrivilegesWithRequest(req).globally(privileges); + if (!hasAllRequested) { + return res.forbidden(); } + } - try { - const statsConfig: StatsGetterConfig = { - unencrypted, - refreshCache: unencrypted || refreshCache, - }; + try { + const statsConfig: StatsGetterConfig = { + unencrypted, + refreshCache: unencrypted || refreshCache, + }; - const body: v2.UnencryptedTelemetryPayload = await telemetryCollectionManager.getStats( - statsConfig - ); - return res.ok({ body }); - } catch (err) { - if (isDev) { - // don't ignore errors when running in dev mode - throw err; - } - if (unencrypted && err.status === 403) { - return res.forbidden(); - } - // ignore errors and return empty set - return res.ok({ body: [] }); + const body: v2.UnencryptedTelemetryPayload = await telemetryCollectionManager.getStats( + statsConfig + ); + return res.ok({ body }); + } catch (err) { + if (isDev) { + // don't ignore errors when running in dev mode + throw err; } + if (unencrypted && err.status === 403) { + return res.forbidden(); + } + // ignore errors and return empty set + return res.ok({ body: [] }); } - ); + }; + + const v2Validations = { + request: { + body: schema.object({ + unencrypted: schema.boolean({ defaultValue: false }), + refreshCache: schema.boolean({ defaultValue: false }), + }), + }, + }; + + router.versioned + .post({ + access: 'internal', + path: FetchSnapshotTelemetry, + }) + // Just because it used to be /v2/, we are creating identical v1 and v2. + .addVersion({ version: '1', validate: v2Validations }, v2Handler) + .addVersion({ version: '2', validate: v2Validations }, v2Handler); } diff --git a/src/plugins/telemetry/server/routes/telemetry_user_has_seen_notice.ts b/src/plugins/telemetry/server/routes/telemetry_user_has_seen_notice.ts index d9cb0b981b0a92..b59ada443054d4 100644 --- a/src/plugins/telemetry/server/routes/telemetry_user_has_seen_notice.ts +++ b/src/plugins/telemetry/server/routes/telemetry_user_has_seen_notice.ts @@ -7,6 +7,9 @@ */ import type { IRouter } from '@kbn/core/server'; +import { RequestHandler } from '@kbn/core-http-server'; +import { RequestHandlerContext } from '@kbn/core/server'; +import { UserHasSeenNoticeRoute } from '../../common/routes'; import { TELEMETRY_SAVED_OBJECT_TYPE } from '../saved_objects'; import { v2 } from '../../common/types'; import { @@ -16,38 +19,42 @@ import { } from '../saved_objects'; export function registerTelemetryUserHasSeenNotice(router: IRouter, currentKibanaVersion: string) { - router.put( - { - path: '/api/telemetry/v2/userHasSeenNotice', - validate: false, - }, - async (context, req, res) => { - const soClient = (await context.core).savedObjects.getClient({ - includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE], - }); - const telemetrySavedObject = await getTelemetrySavedObject(soClient); + const v2Handler: RequestHandler = async ( + context, + req, + res + ) => { + const soClient = (await context.core).savedObjects.getClient({ + includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE], + }); + const telemetrySavedObject = await getTelemetrySavedObject(soClient); - // update the object with a flag stating that the opt-in notice has been seen - const updatedAttributes: TelemetrySavedObjectAttributes = { - ...telemetrySavedObject, - userHasSeenNotice: true, - // We need to store that the user was notified in this version. - // Otherwise, it'll continuously show the banner if previously opted-out. - lastVersionChecked: currentKibanaVersion, - }; - await updateTelemetrySavedObject(soClient, updatedAttributes); + // update the object with a flag stating that the opt-in notice has been seen + const updatedAttributes: TelemetrySavedObjectAttributes = { + ...telemetrySavedObject, + userHasSeenNotice: true, + // We need to store that the user was notified in this version. + // Otherwise, it'll continuously show the banner if previously opted-out. + lastVersionChecked: currentKibanaVersion, + }; + await updateTelemetrySavedObject(soClient, updatedAttributes); - const body: v2.Telemetry = { - allowChangingOptInStatus: updatedAttributes.allowChangingOptInStatus, - enabled: updatedAttributes.enabled, - lastReported: updatedAttributes.lastReported, - lastVersionChecked: updatedAttributes.lastVersionChecked, - reportFailureCount: updatedAttributes.reportFailureCount, - reportFailureVersion: updatedAttributes.reportFailureVersion, - sendUsageFrom: updatedAttributes.sendUsageFrom, - userHasSeenNotice: updatedAttributes.userHasSeenNotice, - }; - return res.ok({ body }); - } - ); + const body: v2.Telemetry = { + allowChangingOptInStatus: updatedAttributes.allowChangingOptInStatus, + enabled: updatedAttributes.enabled, + lastReported: updatedAttributes.lastReported, + lastVersionChecked: updatedAttributes.lastVersionChecked, + reportFailureCount: updatedAttributes.reportFailureCount, + reportFailureVersion: updatedAttributes.reportFailureVersion, + sendUsageFrom: updatedAttributes.sendUsageFrom, + userHasSeenNotice: updatedAttributes.userHasSeenNotice, + }; + return res.ok({ body }); + }; + + router.versioned + .put({ access: 'internal', path: UserHasSeenNoticeRoute }) + // Just because it used to be /v2/, we are creating identical v1 and v2. + .addVersion({ version: '1', validate: false }, v2Handler) + .addVersion({ version: '2', validate: false }, v2Handler); } diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index 7da7e89bae02f8..638bfb4f722a7d 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -34,6 +34,7 @@ "@kbn/std", "@kbn/core-http-browser-mocks", "@kbn/core-http-browser", + "@kbn/core-http-server", ], "exclude": [ "target/**/*", diff --git a/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts b/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts index e4ac2346b5893e..02e1e7656bf787 100644 --- a/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts +++ b/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts @@ -62,7 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(event.properties.value4).to.be.a('number'); expect(event.properties.value5).to.be.a('number'); - if (browser.isChromium) { + if (browser.isChromium()) { // Kibana Loaded memory expect(meta).to.have.property('jsHeapSizeLimit'); expect(meta.jsHeapSizeLimit).to.be.a('number'); diff --git a/test/api_integration/apis/data_views/swap_references/main.ts b/test/api_integration/apis/data_views/swap_references/main.ts index 93247f090a9dad..404d9e58ab477a 100644 --- a/test/api_integration/apis/data_views/swap_references/main.ts +++ b/test/api_integration/apis/data_views/swap_references/main.ts @@ -20,6 +20,7 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const title = 'logs-*'; const prevDataViewId = '91200a00-9efd-11e7-acb3-3dab96693fab'; + const PREVIEW_PATH = `${DATA_VIEW_SWAP_REFERENCES_PATH}/_preview`; let dataViewId = ''; describe('main', () => { @@ -49,23 +50,23 @@ export default function ({ getService }: FtrProviderContext) { it('can preview', async () => { const res = await supertest - .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .post(PREVIEW_PATH) .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) .send({ - from_id: prevDataViewId, - to_id: dataViewId, + fromId: prevDataViewId, + toId: dataViewId, }); expect(res).to.have.property('status', 200); }); it('can preview specifying type', async () => { const res = await supertest - .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .post(PREVIEW_PATH) .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) .send({ - from_id: prevDataViewId, - from_type: 'index-pattern', - to_id: dataViewId, + fromId: prevDataViewId, + fromType: 'index-pattern', + toId: dataViewId, }); expect(res).to.have.property('status', 200); }); @@ -75,13 +76,11 @@ export default function ({ getService }: FtrProviderContext) { .post(DATA_VIEW_SWAP_REFERENCES_PATH) .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) .send({ - from_id: prevDataViewId, - to_id: dataViewId, - preview: false, + fromId: prevDataViewId, + toId: dataViewId, }); expect(res).to.have.property('status', 200); expect(res.body.result.length).to.equal(1); - expect(res.body.preview).to.equal(false); expect(res.body.result[0].id).to.equal('dd7caf20-9efd-11e7-acb3-3dab96693fab'); expect(res.body.result[0].type).to.equal('visualization'); }); @@ -91,13 +90,14 @@ export default function ({ getService }: FtrProviderContext) { .post(DATA_VIEW_SWAP_REFERENCES_PATH) .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) .send({ - from_id: prevDataViewId, - to_id: dataViewId, - preview: false, + fromId: prevDataViewId, + toId: dataViewId, delete: true, }); expect(res).to.have.property('status', 200); expect(res.body.result.length).to.equal(1); + expect(res.body.deleteStatus.remainingRefs).to.equal(0); + expect(res.body.deleteStatus.deletePerformed).to.equal(true); const res2 = await supertest .get(SPECIFIC_DATA_VIEW_PATH.replace('{id}', prevDataViewId)) @@ -118,13 +118,29 @@ export default function ({ getService }: FtrProviderContext) { ); }); + it("won't delete if reference remains", async () => { + const res = await supertest + .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION) + .send({ + fromId: '8963ca30-3224-11e8-a572-ffca06da1357', + toId: '91200a00-9efd-11e7-acb3-3dab96693fab', + forId: ['960372e0-3224-11e8-a572-ffca06da1357'], + delete: true, + }); + expect(res).to.have.property('status', 200); + expect(res.body.result.length).to.equal(1); + expect(res.body.deleteStatus.remainingRefs).to.equal(1); + expect(res.body.deleteStatus.deletePerformed).to.equal(false); + }); + it('can limit by id', async () => { // confirm this will find two items const res = await supertest - .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .post(PREVIEW_PATH) .send({ - from_id: '8963ca30-3224-11e8-a572-ffca06da1357', - to_id: '91200a00-9efd-11e7-acb3-3dab96693fab', + fromId: '8963ca30-3224-11e8-a572-ffca06da1357', + toId: '91200a00-9efd-11e7-acb3-3dab96693fab', }) .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); expect(res).to.have.property('status', 200); @@ -134,10 +150,9 @@ export default function ({ getService }: FtrProviderContext) { const res2 = await supertest .post(DATA_VIEW_SWAP_REFERENCES_PATH) .send({ - from_id: '8963ca30-3224-11e8-a572-ffca06da1357', - to_id: '91200a00-9efd-11e7-acb3-3dab96693fab', - for_id: ['960372e0-3224-11e8-a572-ffca06da1357'], - preview: false, + fromId: '8963ca30-3224-11e8-a572-ffca06da1357', + toId: '91200a00-9efd-11e7-acb3-3dab96693fab', + forId: ['960372e0-3224-11e8-a572-ffca06da1357'], }) .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); expect(res2).to.have.property('status', 200); @@ -147,10 +162,10 @@ export default function ({ getService }: FtrProviderContext) { it('can limit by type', async () => { // confirm this will find two items const res = await supertest - .post(DATA_VIEW_SWAP_REFERENCES_PATH) + .post(PREVIEW_PATH) .send({ - from_id: '8963ca30-3224-11e8-a572-ffca06da1357', - to_id: '91200a00-9efd-11e7-acb3-3dab96693fab', + fromId: '8963ca30-3224-11e8-a572-ffca06da1357', + toId: '91200a00-9efd-11e7-acb3-3dab96693fab', }) .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); expect(res).to.have.property('status', 200); @@ -160,10 +175,9 @@ export default function ({ getService }: FtrProviderContext) { const res2 = await supertest .post(DATA_VIEW_SWAP_REFERENCES_PATH) .send({ - from_id: '8963ca30-3224-11e8-a572-ffca06da1357', - to_id: '91200a00-9efd-11e7-acb3-3dab96693fab', - for_type: 'search', - preview: false, + fromId: '8963ca30-3224-11e8-a572-ffca06da1357', + toId: '91200a00-9efd-11e7-acb3-3dab96693fab', + forType: 'search', }) .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION); expect(res2).to.have.property('status', 200); diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts index 943d7534acc0a2..c7d8a42c6e392a 100644 --- a/test/api_integration/apis/telemetry/opt_in.ts +++ b/test/api_integration/apis/telemetry/opt_in.ts @@ -11,6 +11,10 @@ import expect from '@kbn/expect'; import SuperTest from 'supertest'; import type { KbnClient } from '@kbn/test'; import type { TelemetrySavedObjectAttributes } from '@kbn/telemetry-plugin/server/saved_objects'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import type { FtrProviderContext } from '../../ftr_provider_context'; export default function optInTest({ getService }: FtrProviderContext) { @@ -18,7 +22,7 @@ export default function optInTest({ getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); - describe('/api/telemetry/v2/optIn API', () => { + describe('/internal/telemetry/optIn API', () => { let defaultAttributes: TelemetrySavedObjectAttributes; let kibanaVersion: string; before(async () => { @@ -88,8 +92,10 @@ async function postTelemetryV2OptIn( statusCode: number ): Promise { const { body } = await supertest - .post('/api/telemetry/v2/optIn') + .post('/internal/telemetry/optIn') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ enabled: value }) .expect(statusCode); diff --git a/test/api_integration/apis/telemetry/telemetry_config.ts b/test/api_integration/apis/telemetry/telemetry_config.ts index f6dd12b0c2a9dc..a9a04a3986ba72 100644 --- a/test/api_integration/apis/telemetry/telemetry_config.ts +++ b/test/api_integration/apis/telemetry/telemetry_config.ts @@ -7,6 +7,10 @@ */ import { AxiosError } from 'axios'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import type { FtrProviderContext } from '../../ftr_provider_context'; const TELEMETRY_SO_TYPE = 'telemetry'; @@ -16,110 +20,146 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext) const kbnClient = getService('kibanaServer'); const supertest = getService('supertest'); - describe('/api/telemetry/v2/config API Telemetry config', () => { - before(async () => { - try { - await kbnClient.savedObjects.delete({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID }); - } catch (err) { - const is404Error = err instanceof AxiosError && err.response?.status === 404; - if (!is404Error) { - throw err; - } - } - }); - - it('GET should get the default config', async () => { - await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, { - allowChangingOptInStatus: true, - optIn: null, // the config.js for this FTR sets it to `false`, we are bound to ask again. - sendUsageFrom: 'server', - telemetryNotifyUserAboutOptInDefault: false, // it's not opted-in by default (that's what this flag is about) - }); - }); - - it('GET should get `true` when opted-in', async () => { - // Opt-in - await supertest - .post('/api/telemetry/v2/optIn') - .set('kbn-xsrf', 'xxx') - .send({ enabled: true }) - .expect(200); - - await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, { - allowChangingOptInStatus: true, - optIn: true, - sendUsageFrom: 'server', - telemetryNotifyUserAboutOptInDefault: false, - }); - }); - - it('GET should get false when opted-out', async () => { - // Opt-in - await supertest - .post('/api/telemetry/v2/optIn') - .set('kbn-xsrf', 'xxx') - .send({ enabled: false }) - .expect(200); - - await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, { - allowChangingOptInStatus: true, - optIn: false, - sendUsageFrom: 'server', - telemetryNotifyUserAboutOptInDefault: false, - }); - }); - - describe('From a previous version', function () { - this.tags(['skipCloud']); - - // Get current values - let attributes: Record; - let currentVersion: string; - let previousMinor: string; - - before(async () => { - [{ attributes }, currentVersion] = await Promise.all([ - kbnClient.savedObjects.get({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID }), - kbnClient.version.get(), - ]); - - const [major, minor, patch] = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/)!.map(parseInt); - previousMinor = `${minor === 0 ? major - 1 : major}.${ - minor === 0 ? minor : minor - 1 - }.${patch}`; - }); + describe('API Telemetry config', () => { + ['/api/telemetry/v2/config', '/internal/telemetry/config'].forEach((api) => { + describe(`GET ${api}`, () => { + const apiVersion = api === '/api/telemetry/v2/config' ? '2023-10-31' : '2'; + before(async () => { + try { + await kbnClient.savedObjects.delete({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID }); + } catch (err) { + const is404Error = err instanceof AxiosError && err.response?.status === 404; + if (!is404Error) { + throw err; + } + } + }); - it('GET should get `true` when opted-in in the current version', async () => { - // Opt-in from a previous version - await kbnClient.savedObjects.create({ - overwrite: true, - type: TELEMETRY_SO_TYPE, - id: TELEMETRY_SO_ID, - attributes: { ...attributes, enabled: true, lastVersionChecked: previousMinor }, + it('GET should get the default config', async () => { + await supertest + .get(api) + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, apiVersion) + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200, { + allowChangingOptInStatus: true, + optIn: null, // the config.js for this FTR sets it to `false`, we are bound to ask again. + sendUsageFrom: 'server', + telemetryNotifyUserAboutOptInDefault: false, // it's not opted-in by default (that's what this flag is about) + }); }); - await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, { - allowChangingOptInStatus: true, - optIn: true, - sendUsageFrom: 'server', - telemetryNotifyUserAboutOptInDefault: false, + it('GET should get `true` when opted-in', async () => { + // Opt-in + await supertest + .post('/internal/telemetry/optIn') + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send({ enabled: true }) + .expect(200); + + await supertest + .get(api) + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, apiVersion) + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200, { + allowChangingOptInStatus: true, + optIn: true, + sendUsageFrom: 'server', + telemetryNotifyUserAboutOptInDefault: false, + }); }); - }); - it('GET should get `null` when opted-out in a previous version', async () => { - // Opt-out from previous version - await kbnClient.savedObjects.create({ - overwrite: true, - type: TELEMETRY_SO_TYPE, - id: TELEMETRY_SO_ID, - attributes: { ...attributes, enabled: false, lastVersionChecked: previousMinor }, + it('GET should get false when opted-out', async () => { + // Opt-in + await supertest + .post('/internal/telemetry/optIn') + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send({ enabled: false }) + .expect(200); + + await supertest + .get(api) + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, apiVersion) + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200, { + allowChangingOptInStatus: true, + optIn: false, + sendUsageFrom: 'server', + telemetryNotifyUserAboutOptInDefault: false, + }); }); - await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, { - allowChangingOptInStatus: true, - optIn: null, - sendUsageFrom: 'server', - telemetryNotifyUserAboutOptInDefault: false, + describe('From a previous version', function () { + this.tags(['skipCloud']); + + // Get current values + let attributes: Record; + let currentVersion: string; + let previousMinor: string; + + before(async () => { + [{ attributes }, currentVersion] = await Promise.all([ + kbnClient.savedObjects.get({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID }), + kbnClient.version.get(), + ]); + + const [major, minor, patch] = currentVersion + .match(/^(\d+)\.(\d+)\.(\d+)/)! + .map(parseInt); + previousMinor = `${minor === 0 ? major - 1 : major}.${ + minor === 0 ? minor : minor - 1 + }.${patch}`; + }); + + it('GET should get `true` when opted-in in the current version', async () => { + // Opt-in from a previous version + await kbnClient.savedObjects.create({ + overwrite: true, + type: TELEMETRY_SO_TYPE, + id: TELEMETRY_SO_ID, + attributes: { ...attributes, enabled: true, lastVersionChecked: previousMinor }, + }); + + await supertest + .get(api) + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, apiVersion) + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200, { + allowChangingOptInStatus: true, + optIn: true, + sendUsageFrom: 'server', + telemetryNotifyUserAboutOptInDefault: false, + }); + }); + + it('GET should get `null` when opted-out in a previous version', async () => { + // Opt-out from previous version + await kbnClient.savedObjects.create({ + overwrite: true, + type: TELEMETRY_SO_TYPE, + id: TELEMETRY_SO_ID, + attributes: { ...attributes, enabled: false, lastVersionChecked: previousMinor }, + }); + + await supertest + .get(api) + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, apiVersion) + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200, { + allowChangingOptInStatus: true, + optIn: null, + sendUsageFrom: 'server', + telemetryNotifyUserAboutOptInDefault: false, + }); + }); }); }); }); diff --git a/test/api_integration/apis/telemetry/telemetry_last_reported.ts b/test/api_integration/apis/telemetry/telemetry_last_reported.ts index e553fa0218aa1a..6d077dd2857d9c 100644 --- a/test/api_integration/apis/telemetry/telemetry_last_reported.ts +++ b/test/api_integration/apis/telemetry/telemetry_last_reported.ts @@ -7,23 +7,37 @@ */ import expect from '@kbn/expect'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import type { FtrProviderContext } from '../../ftr_provider_context'; export default function optInTest({ getService }: FtrProviderContext) { const client = getService('kibanaServer'); const supertest = getService('supertest'); - describe('/api/telemetry/v2/last_reported API Telemetry lastReported', () => { + describe('/internal/telemetry/last_reported API Telemetry lastReported', () => { before(async () => { await client.savedObjects.delete({ type: 'telemetry', id: 'telemetry' }); }); it('GET should return undefined when there is no stored telemetry.lastReported value', async () => { - await supertest.get('/api/telemetry/v2/last_reported').set('kbn-xsrf', 'xxx').expect(200, {}); + await supertest + .get('/internal/telemetry/last_reported') + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200, {}); }); it('PUT should update telemetry.lastReported to now', async () => { - await supertest.put('/api/telemetry/v2/last_reported').set('kbn-xsrf', 'xxx').expect(200); + await supertest + .put('/internal/telemetry/last_reported') + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200); const { attributes: { lastReported }, @@ -46,8 +60,10 @@ export default function optInTest({ getService }: FtrProviderContext) { expect(lastReported).to.be.a('number'); await supertest - .get('/api/telemetry/v2/last_reported') + .get('/internal/telemetry/last_reported') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .expect(200, { lastReported }); }); }); diff --git a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts index 53b0d2cadca64b..5310e32b87fed9 100644 --- a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts +++ b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts @@ -7,17 +7,26 @@ */ import expect from '@kbn/expect'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import type { FtrProviderContext } from '../../ftr_provider_context'; export default function optInTest({ getService }: FtrProviderContext) { const client = getService('kibanaServer'); const supertest = getService('supertest'); - describe('/api/telemetry/v2/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => { + describe('/internal/telemetry/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => { it('should update telemetry setting field via PUT', async () => { await client.savedObjects.delete({ type: 'telemetry', id: 'telemetry' }); - await supertest.put('/api/telemetry/v2/userHasSeenNotice').set('kbn-xsrf', 'xxx').expect(200); + await supertest + .put('/internal/telemetry/userHasSeenNotice') + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .expect(200); const { attributes: { userHasSeenNotice }, diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index 393f093d541898..fd46e5ac1448b8 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -7,8 +7,9 @@ */ import { setTimeout as setTimeoutAsync } from 'timers/promises'; -import { cloneDeepWith } from 'lodash'; +import { cloneDeepWith, isString } from 'lodash'; import { Key, Origin, WebDriver } from 'selenium-webdriver'; +import { Driver as ChromiumWebDriver } from 'selenium-webdriver/chrome'; import { modifyUrl } from '@kbn/std'; import sharp from 'sharp'; @@ -16,6 +17,7 @@ import { NoSuchSessionError } from 'selenium-webdriver/lib/error'; import { WebElementWrapper } from '../lib/web_element_wrapper'; import { FtrProviderContext, FtrService } from '../../ftr_provider_context'; import { Browsers } from '../remote/browsers'; +import { NetworkOptions, NetworkProfile, NETWORK_PROFILES } from '../remote/network_profiles'; export type Browser = BrowserService; @@ -25,19 +27,20 @@ class BrowserService extends FtrService { */ public readonly keys = Key; public readonly isFirefox: boolean; - public readonly isChromium: boolean; private readonly log = this.ctx.getService('log'); constructor( ctx: FtrProviderContext, public readonly browserType: string, - private readonly driver: WebDriver + protected readonly driver: WebDriver | ChromiumWebDriver ) { super(ctx); this.isFirefox = this.browserType === Browsers.Firefox; - this.isChromium = - this.browserType === Browsers.Chrome || this.browserType === Browsers.ChromiumEdge; + } + + public isChromium(): this is { driver: ChromiumWebDriver } { + return this.driver instanceof ChromiumWebDriver; } /** @@ -661,6 +664,68 @@ class BrowserService extends FtrService { } } } + + /** + * Get the network simulation for chromium browsers if available. + * https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/chrome_exports_Driver.html#getNetworkConditions + * + * @return {Promise} + */ + public async getNetworkConditions() { + if (this.isChromium()) { + return this.driver.getNetworkConditions().catch(() => undefined); // Return undefined instead of throwing if no conditions are set. + } else { + const message = + 'WebDriver does not support the .getNetworkConditions method.\nProbably the browser in used is not chromium based.'; + this.log.error(message); + throw new Error(message); + } + } + + /** + * Delete the network simulation for chromium browsers if available. + * + * @return {Promise} + */ + public async restoreNetworkConditions() { + this.log.debug('Restore network conditions simulation.'); + return this.setNetworkConditions('NO_THROTTLING'); + } + + /** + * Set the network conditions for chromium browsers if available. + * + * __Sample Usage:__ + * + * browser.setNetworkConditions('FAST_3G') + * browser.setNetworkConditions('SLOW_3G') + * browser.setNetworkConditions('OFFLINE') + * browser.setNetworkConditions({ + * offline: false, + * latency: 5, // Additional latency (ms). + * download_throughput: 500 * 1024, // Maximal aggregated download throughput. + * upload_throughput: 500 * 1024, // Maximal aggregated upload throughput. + * }); + * + * https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/chrome_exports_Driver.html#setNetworkConditions + * + * @return {Promise} + */ + public async setNetworkConditions(profileOrOptions: NetworkProfile | NetworkOptions) { + const networkOptions = isString(profileOrOptions) + ? NETWORK_PROFILES[profileOrOptions] + : profileOrOptions; + + if (this.isChromium()) { + this.log.debug(`Set network conditions with profile "${profileOrOptions}".`); + return this.driver.setNetworkConditions(networkOptions); + } else { + const message = + 'WebDriver does not support the .setNetworkCondition method.\nProbably the browser in used is not chromium based.'; + this.log.error(message); + throw new Error(message); + } + } } export async function BrowserProvider(ctx: FtrProviderContext) { diff --git a/test/functional/services/remote/network_profiles.ts b/test/functional/services/remote/network_profiles.ts index cb4076686270c2..29e4a0feeaaceb 100644 --- a/test/functional/services/remote/network_profiles.ts +++ b/test/functional/services/remote/network_profiles.ts @@ -6,10 +6,13 @@ * Side Public License, v 1. */ -interface NetworkOptions { - DOWNLOAD: number; - UPLOAD: number; - LATENCY: number; +export type NetworkProfile = 'NO_THROTTLING' | 'FAST_3G' | 'SLOW_3G' | 'OFFLINE' | 'CLOUD_USER'; + +export interface NetworkOptions { + offline: boolean; + latency: number; + download_throughput: number; + upload_throughput: number; } const sec = 10 ** 3; @@ -17,6 +20,36 @@ const MBps = 10 ** 6 / 8; // megabyte per second (MB/s) (can be abbreviated as M // Selenium uses B/s (bytes) for network throttling // Download (B/s) Upload (B/s) Latency (ms) -export const NETWORK_PROFILES: { [key: string]: NetworkOptions } = { - CLOUD_USER: { DOWNLOAD: 6 * MBps, UPLOAD: 6 * MBps, LATENCY: 0.1 * sec }, + +export const NETWORK_PROFILES: Record = { + NO_THROTTLING: { + offline: false, + latency: 0, + download_throughput: -1, + upload_throughput: -1, + }, + FAST_3G: { + offline: false, + latency: 0.56 * sec, + download_throughput: 1.44 * MBps, + upload_throughput: 0.7 * MBps, + }, + SLOW_3G: { + offline: false, + latency: 2 * sec, + download_throughput: 0.4 * MBps, + upload_throughput: 0.4 * MBps, + }, + OFFLINE: { + offline: true, + latency: 0, + download_throughput: 0, + upload_throughput: 0, + }, + CLOUD_USER: { + offline: false, + latency: 0.1 * sec, + download_throughput: 6 * MBps, + upload_throughput: 6 * MBps, + }, }; diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 1486d02656d12c..702f674b3c10d0 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -30,7 +30,7 @@ import { createStdoutSocket } from './create_stdout_stream'; import { preventParallelCalls } from './prevent_parallel_calls'; import { Browsers } from './browsers'; -import { NETWORK_PROFILES } from './network_profiles'; +import { NetworkProfile, NETWORK_PROFILES } from './network_profiles'; const throttleOption: string = process.env.TEST_THROTTLE_NETWORK as string; const headlessBrowser: string = process.env.TEST_BROWSER_HEADLESS as string; @@ -300,22 +300,17 @@ async function attemptToCreateCommand( const { session, consoleLog$ } = await buildDriverInstance(); if (throttleOption === '1' && browserType === 'chrome') { - const { KBN_NETWORK_TEST_PROFILE = 'CLOUD_USER' } = process.env; + const KBN_NETWORK_TEST_PROFILE = (process.env.KBN_NETWORK_TEST_PROFILE ?? + 'CLOUD_USER') as NetworkProfile; const profile = - KBN_NETWORK_TEST_PROFILE in Object.keys(NETWORK_PROFILES) - ? KBN_NETWORK_TEST_PROFILE - : 'CLOUD_USER'; + KBN_NETWORK_TEST_PROFILE in NETWORK_PROFILES ? KBN_NETWORK_TEST_PROFILE : 'CLOUD_USER'; - const { - DOWNLOAD: downloadThroughput, - UPLOAD: uploadThroughput, - LATENCY: latency, - } = NETWORK_PROFILES[`${profile}`]; + const networkProfileOptions = NETWORK_PROFILES[profile]; // Only chrome supports this option. log.debug( - `NETWORK THROTTLED with profile ${profile}: ${downloadThroughput} B/s down, ${uploadThroughput} B/s up, ${latency} ms latency.` + `NETWORK THROTTLED with profile ${profile}: ${networkProfileOptions.download_throughput} B/s down, ${networkProfileOptions.upload_throughput} B/s up, ${networkProfileOptions.latency} ms latency.` ); if (noCache) { @@ -326,12 +321,7 @@ async function attemptToCreateCommand( } // @ts-expect-error - session.setNetworkConditions({ - offline: false, - latency, - download_throughput: downloadThroughput, - upload_throughput: uploadThroughput, - }); + session.setNetworkConditions(networkProfileOptions); } if (attemptId !== attemptCounter) { diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 66a2e385d3e6c6..c0573c10c10b16 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -240,6 +240,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.ilm.ui.enabled (boolean)', 'xpack.index_management.ui.enabled (boolean)', 'xpack.index_management.enableIndexActions (any)', + 'xpack.index_management.enableLegacyTemplates (any)', 'xpack.infra.sources.default.fields.message (array)', /** * xpack.infra.logs is conditional and will resolve to an object of properties diff --git a/test/plugin_functional/test_suites/telemetry/telemetry.ts b/test/plugin_functional/test_suites/telemetry/telemetry.ts index cbba01a9ddcb54..a998e139eb5c67 100644 --- a/test/plugin_functional/test_suites/telemetry/telemetry.ts +++ b/test/plugin_functional/test_suites/telemetry/telemetry.ts @@ -8,6 +8,10 @@ import expect from '@kbn/expect'; import { KBN_SCREENSHOT_MODE_ENABLED_KEY } from '@kbn/screenshot-mode-plugin/public'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { PluginFunctionalProviderContext } from '../../services'; const TELEMETRY_SO_TYPE = 'telemetry'; @@ -83,8 +87,10 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide it('does not show the banner if opted-in', async () => { await supertest - .post('/api/telemetry/v2/optIn') + .post('/internal/telemetry/optIn') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ enabled: true }) .expect(200); @@ -95,8 +101,10 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide it('does not show the banner if opted-out in this version', async () => { await supertest - .post('/api/telemetry/v2/optIn') + .post('/internal/telemetry/optIn') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ enabled: false }) .expect(200); diff --git a/tsconfig.base.json b/tsconfig.base.json index 8efe5c62421dee..cc2a1a7d3efb2b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -914,6 +914,8 @@ "@kbn/kubernetes-security-plugin/*": ["x-pack/plugins/kubernetes_security/*"], "@kbn/language-documentation-popover": ["packages/kbn-language-documentation-popover"], "@kbn/language-documentation-popover/*": ["packages/kbn-language-documentation-popover/*"], + "@kbn/lens-embeddable-utils": ["packages/kbn-lens-embeddable-utils"], + "@kbn/lens-embeddable-utils/*": ["packages/kbn-lens-embeddable-utils/*"], "@kbn/lens-plugin": ["x-pack/plugins/lens"], "@kbn/lens-plugin/*": ["x-pack/plugins/lens/*"], "@kbn/license-api-guard-plugin": ["x-pack/plugins/license_api_guard"], @@ -1170,6 +1172,8 @@ "@kbn/screenshotting-example-plugin/*": ["x-pack/examples/screenshotting_example/*"], "@kbn/screenshotting-plugin": ["x-pack/plugins/screenshotting"], "@kbn/screenshotting-plugin/*": ["x-pack/plugins/screenshotting/*"], + "@kbn/search-api-panels": ["packages/kbn-search-api-panels"], + "@kbn/search-api-panels/*": ["packages/kbn-search-api-panels/*"], "@kbn/search-examples-plugin": ["examples/search_examples"], "@kbn/search-examples-plugin/*": ["examples/search_examples/*"], "@kbn/search-response-warnings": ["packages/kbn-search-response-warnings"], @@ -1498,6 +1502,8 @@ "@kbn/usage-collection-plugin/*": ["src/plugins/usage_collection/*"], "@kbn/usage-collection-test-plugin": ["test/plugin_functional/plugins/usage_collection"], "@kbn/usage-collection-test-plugin/*": ["test/plugin_functional/plugins/usage_collection/*"], + "@kbn/use-tracked-promise": ["packages/kbn-use-tracked-promise"], + "@kbn/use-tracked-promise/*": ["packages/kbn-use-tracked-promise/*"], "@kbn/user-profile-components": ["packages/kbn-user-profile-components"], "@kbn/user-profile-components/*": ["packages/kbn-user-profile-components/*"], "@kbn/user-profile-examples-plugin": ["examples/user_profile_examples"], diff --git a/x-pack/dev-tools/api_debug/apis/telemetry/index.js b/x-pack/dev-tools/api_debug/apis/telemetry/index.js index bd9ffb5ed6c0cc..1b2e622d91c779 100644 --- a/x-pack/dev-tools/api_debug/apis/telemetry/index.js +++ b/x-pack/dev-tools/api_debug/apis/telemetry/index.js @@ -8,6 +8,6 @@ export const name = 'telemetry'; export const description = 'Get the clusters stats from the Kibana server'; export const method = 'POST'; -export const path = '/api/telemetry/v2/clusters/_stats'; +export const path = '/internal/telemetry/clusters/_stats'; export const body = { unencrypted: true, refreshCache: true }; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/components/page/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/components/page/index.tsx index 54b930fdb982db..66b68b877da1ca 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/components/page/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/components/page/index.tsx @@ -6,14 +6,7 @@ */ import * as React from 'react'; -import { - EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiPageHeader, - EuiPageHeaderSection, - EuiTitle, -} from '@elastic/eui'; +import { EuiPageBody, EuiPageTemplate, EuiPageSection, EuiPageHeader } from '@elastic/eui'; export interface PageProps { title?: React.ReactNode; @@ -22,18 +15,12 @@ export interface PageProps { export const Page: React.FC = ({ title = 'Untitled', children }) => { return ( - - - -

{title}

-
-
-
- - - {children} - - + + + + + {children} +
); }; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.tsx index eacccf22bc54b5..e21bfd19b66ce9 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/body/index.tsx @@ -82,7 +82,10 @@ const BodyComponent: React.FC = ({ totalSizeInBytes, updatePatternIndexNames, updatePatternRollup, - } = useResultsRollup({ ilmPhases, patterns }); + } = useResultsRollup({ + ilmPhases, + patterns, + }); return ( diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.test.tsx index 2ce0b3d1a6ad01..1628705efb78a5 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.test.tsx @@ -10,9 +10,17 @@ import React from 'react'; import { DataQualityProvider, useDataQualityContext } from '.'; +const mockReportDataQualityIndexChecked = jest.fn(); +const mockReportDataQualityCheckAllClicked = jest.fn(); const mockHttpFetch = jest.fn(); +const mockTelemetryEvents = { + reportDataQualityIndexChecked: mockReportDataQualityIndexChecked, + reportDataQualityCheckAllCompleted: mockReportDataQualityCheckAllClicked, +}; const ContextWrapper: React.FC = ({ children }) => ( - {children} + + {children} + ); describe('DataQualityContext', () => { @@ -37,4 +45,11 @@ describe('DataQualityContext', () => { expect(mockHttpFetch).toBeCalledWith(path); }); + + test('it should return the telemetry events', async () => { + const { result } = renderHook(useDataQualityContext, { wrapper: ContextWrapper }); + const telemetryEvents = await result.current.telemetryEvents; + + expect(telemetryEvents).toEqual(mockTelemetryEvents); + }); }); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.tsx index b98761515e33c9..8456c890688294 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_context/index.tsx @@ -7,9 +7,11 @@ import type { HttpHandler } from '@kbn/core-http-browser'; import React, { useMemo } from 'react'; +import { TelemetryEvents } from '../../types'; interface DataQualityProviderProps { httpFetch: HttpHandler; + telemetryEvents: TelemetryEvents; } const DataQualityContext = React.createContext(undefined); @@ -17,12 +19,14 @@ const DataQualityContext = React.createContext = ({ children, httpFetch, + telemetryEvents, }) => { const value = useMemo( () => ({ httpFetch, + telemetryEvents, }), - [httpFetch] + [httpFetch, telemetryEvents] ); return {children}; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.test.ts index 81e1b88be54709..abd1945e95c837 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.test.ts @@ -97,11 +97,14 @@ describe('checkIndex', () => { await checkIndex({ abortController: new AbortController(), + batchId: 'batch-id', + checkAllStartTime: Date.now(), ecsMetadata, formatBytes, formatNumber, httpFetch, indexName, + isLastCheck: false, onCheckCompleted, pattern, version: EcsVersion, @@ -144,11 +147,14 @@ describe('checkIndex', () => { await checkIndex({ abortController, + batchId: 'batch-id', + checkAllStartTime: Date.now(), ecsMetadata, formatBytes, formatNumber, httpFetch, indexName, + isLastCheck: false, onCheckCompleted, pattern, version: EcsVersion, @@ -166,11 +172,14 @@ describe('checkIndex', () => { await checkIndex({ abortController: new AbortController(), + batchId: 'batch-id', + checkAllStartTime: Date.now(), ecsMetadata: null, // <-- formatBytes, formatNumber, httpFetch, indexName, + isLastCheck: false, onCheckCompleted, pattern, version: EcsVersion, @@ -219,11 +228,14 @@ describe('checkIndex', () => { await checkIndex({ abortController: new AbortController(), + batchId: 'batch-id', + checkAllStartTime: Date.now(), ecsMetadata, formatBytes, formatNumber, httpFetch, indexName, + isLastCheck: false, onCheckCompleted, pattern, version: EcsVersion, @@ -270,11 +282,14 @@ describe('checkIndex', () => { await checkIndex({ abortController: new AbortController(), + batchId: 'batch-id', + checkAllStartTime: Date.now(), ecsMetadata, formatBytes, formatNumber, httpFetch, indexName, + isLastCheck: false, onCheckCompleted, pattern, version: EcsVersion, @@ -329,11 +344,14 @@ describe('checkIndex', () => { await checkIndex({ abortController, + batchId: 'batch-id', + checkAllStartTime: Date.now(), ecsMetadata, formatBytes, formatNumber, httpFetch, indexName, + isLastCheck: false, onCheckCompleted, pattern, version: EcsVersion, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.ts index 145ed31bf5a1b3..7371d74851a413 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.ts @@ -25,26 +25,33 @@ export const EMPTY_PARTITIONED_FIELD_METADATA: PartitionedFieldMetadata = { export async function checkIndex({ abortController, + batchId, + checkAllStartTime, ecsMetadata, formatBytes, formatNumber, httpFetch, indexName, + isLastCheck, onCheckCompleted, pattern, version, }: { abortController: AbortController; + batchId: string; + checkAllStartTime: number; ecsMetadata: Record | null; formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; httpFetch: HttpHandler; indexName: string; + isLastCheck: boolean; onCheckCompleted: OnCheckCompleted; pattern: string; version: string; }) { try { + const startTime = Date.now(); const indexes = await fetchMappings({ abortController, httpFetch, @@ -83,18 +90,24 @@ export async function checkIndex({ if (!abortController.signal.aborted) { onCheckCompleted({ + checkAllStartTime, + batchId, error: null, formatBytes, formatNumber, indexName, partitionedFieldMetadata, pattern, + requestTime: Date.now() - startTime, version, + isLastCheck, }); } } catch (error) { if (!abortController.signal.aborted) { onCheckCompleted({ + checkAllStartTime, + batchId, error: error != null ? error.message : i18n.AN_ERROR_OCCURRED_CHECKING_INDEX(indexName), formatBytes, formatNumber, @@ -102,6 +115,7 @@ export async function checkIndex({ partitionedFieldMetadata: null, pattern, version, + isLastCheck, }); } } diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx index f2aa7a2666c33d..236e7dc3eb9bd0 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx @@ -310,7 +310,7 @@ describe('CheckAll', () => { describe('when all checks have completed', () => { const setIndexToCheck = jest.fn(); - + const onCheckCompleted = jest.fn(); beforeEach(async () => { jest.clearAllMocks(); jest.useFakeTimers(); @@ -322,7 +322,7 @@ describe('CheckAll', () => { formatNumber={mockFormatNumber} ilmPhases={ilmPhases} incrementCheckAllIndiciesChecked={jest.fn()} - onCheckCompleted={jest.fn()} + onCheckCompleted={onCheckCompleted} patternIndexNames={patternIndexNames} patterns={[]} setCheckAllIndiciesChecked={jest.fn()} @@ -359,6 +359,10 @@ describe('CheckAll', () => { expect(setIndexToCheck).toBeCalledWith(null); }); + test('it invokes onCheckAllCompleted after all the checks have completed', () => { + expect(onCheckCompleted).toHaveBeenCalled(); + }); + // test all the patterns Object.entries(patternIndexNames).forEach((pattern) => { const [patternName, indexNames] = pattern; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx index 83cc9b5ade1a9a..9f43af65bb0dd7 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx @@ -10,6 +10,7 @@ import { EcsFlat, EcsVersion } from '@kbn/ecs'; import { EuiButton } from '@elastic/eui'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; +import { v4 as uuidv4 } from 'uuid'; import { checkIndex } from './check_index'; import { useDataQualityContext } from '../../../data_quality_context'; @@ -78,6 +79,10 @@ const CheckAllComponent: React.FC = ({ const onClick = useCallback(() => { async function beginCheck() { const allIndicesToCheck = getAllIndicesToCheck(patternIndexNames); + const startTime = Date.now(); + const batchId = uuidv4(); + let checked = 0; + setCheckAllIndiciesChecked(0); setCheckAllTotalIndiciesToCheck(allIndicesToCheck.length); @@ -90,11 +95,15 @@ const CheckAllComponent: React.FC = ({ await checkIndex({ abortController: abortController.current, + batchId, + checkAllStartTime: startTime, ecsMetadata: EcsFlat as unknown as Record, formatBytes, formatNumber, httpFetch, indexName, + isLastCheck: + allIndicesToCheck.length > 0 ? checked === allIndicesToCheck.length - 1 : true, onCheckCompleted, pattern, version: EcsVersion, @@ -103,6 +112,7 @@ const CheckAllComponent: React.FC = ({ if (!abortController.current.signal.aborted) { await wait(DELAY_AFTER_EVERY_CHECK_COMPLETES); incrementCheckAllIndiciesChecked(); + checked++; } } } diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx index 688c94ab66fff5..973f5dde95e454 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx @@ -109,6 +109,7 @@ const defaultProps: Props = { formatBytes, formatNumber, getGroupByFieldsOnClick: jest.fn(), + indexId: '1xxx', ilmPhase: 'hot', indexName: 'auditbeat-custom-index-1', isAssistantEnabled: true, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx index 5bb5cc26ab9673..4ff6edd007a279 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EcsFlat } from '@kbn/ecs'; +import { EcsFlat, EcsVersion } from '@kbn/ecs'; import type { FlameElementEvent, HeatmapElementEvent, @@ -18,6 +18,7 @@ import type { } from '@elastic/charts'; import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; import { getUnallowedValueRequestItems } from '../allowed_values/helpers'; import { ErrorEmptyPrompt } from '../error_empty_prompt'; @@ -31,12 +32,18 @@ import { import { LoadingEmptyPrompt } from '../loading_empty_prompt'; import { getIndexPropertiesContainerId } from '../pattern/helpers'; import { getTabs } from '../tabs/helpers'; -import { getAllIncompatibleMarkdownComments } from '../tabs/incompatible_tab/helpers'; +import { + getAllIncompatibleMarkdownComments, + getIncompatibleValuesFields, + getIncompatibleMappingsFields, +} from '../tabs/incompatible_tab/helpers'; import * as i18n from './translations'; import type { EcsMetadata, IlmPhase, PartitionedFieldMetadata, PatternRollup } from '../../types'; import { useAddToNewCase } from '../../use_add_to_new_case'; import { useMappings } from '../../use_mappings'; import { useUnallowedValues } from '../../use_unallowed_values'; +import { useDataQualityContext } from '../data_quality_context'; +import { getSizeInBytes } from '../../helpers'; const EMPTY_MARKDOWN_COMMENTS: string[] = []; @@ -60,6 +67,7 @@ export interface Props { groupByField1: string; }; ilmPhase: IlmPhase | undefined; + indexId: string | null | undefined; indexName: string; isAssistantEnabled: boolean; openCreateCaseFlyout: ({ @@ -78,22 +86,24 @@ export interface Props { const IndexPropertiesComponent: React.FC = ({ addSuccessToast, + baseTheme, canUserCreateAndReadCases, + docsCount, formatBytes, formatNumber, - docsCount, getGroupByFieldsOnClick, ilmPhase, + indexId, indexName, isAssistantEnabled, openCreateCaseFlyout, pattern, patternRollup, theme, - baseTheme, updatePatternRollup, }) => { const { error: mappingsError, indexes, loading: loadingMappings } = useMappings(indexName); + const { telemetryEvents } = useDataQualityContext(); const requestItems = useMemo( () => @@ -108,6 +118,7 @@ const IndexPropertiesComponent: React.FC = ({ error: unallowedValuesError, loading: loadingUnallowedValues, unallowedValues, + requestTime, } = useUnallowedValues({ indexName, requestItems }); const mappingsProperties = useMemo( @@ -246,6 +257,29 @@ const IndexPropertiesComponent: React.FC = ({ }, }, }); + + if (indexId && requestTime != null && requestTime > 0 && partitionedFieldMetadata) { + telemetryEvents.reportDataQualityIndexChecked?.({ + batchId: uuidv4(), + ecsVersion: EcsVersion, + errorCount: error ? 1 : 0, + ilmPhase, + indexId, + isCheckAll: false, + numberOfDocuments: docsCount, + numberOfIncompatibleFields: indexIncompatible, + numberOfIndices: 1, + numberOfIndicesChecked: 1, + sizeInBytes: getSizeInBytes({ stats: patternRollup.stats, indexName }), + timeConsumedMs: requestTime, + unallowedMappingFields: getIncompatibleMappingsFields( + partitionedFieldMetadata.incompatible + ), + unallowedValueFields: getIncompatibleValuesFields( + partitionedFieldMetadata.incompatible + ), + }); + } } } }, [ @@ -253,6 +287,7 @@ const IndexPropertiesComponent: React.FC = ({ formatBytes, formatNumber, ilmPhase, + indexId, indexName, loadingMappings, loadingUnallowedValues, @@ -260,6 +295,8 @@ const IndexPropertiesComponent: React.FC = ({ partitionedFieldMetadata, pattern, patternRollup, + requestTime, + telemetryEvents, unallowedValuesError, updatePatternRollup, ]); diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx index 70a8c97f94f03a..b37cea23010fdb 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx @@ -33,6 +33,7 @@ import { } from './helpers'; import { getDocsCount, + getIndexId, getIndexNames, getTotalDocsCount, getTotalPatternIncompatible, @@ -98,7 +99,7 @@ interface Props { indexNames: string[]; pattern: string; }) => void; - updatePatternRollup: (patternRollup: PatternRollup) => void; + updatePatternRollup: (patternRollup: PatternRollup, requestTime?: number) => void; } const PatternComponent: React.FC = ({ @@ -154,6 +155,7 @@ const PatternComponent: React.FC = ({ docsCount={getDocsCount({ stats, indexName })} getGroupByFieldsOnClick={getGroupByFieldsOnClick} ilmPhase={ilmExplain != null ? getIlmPhase(ilmExplain[indexName]) : undefined} + indexId={getIndexId({ stats, indexName })} indexName={indexName} isAssistantEnabled={isAssistantEnabled} openCreateCaseFlyout={openCreateCaseFlyout} @@ -169,18 +171,18 @@ const PatternComponent: React.FC = ({ } }, [ + itemIdToExpandedRowMap, addSuccessToast, canUserCreateAndReadCases, formatBytes, formatNumber, + stats, getGroupByFieldsOnClick, ilmExplain, isAssistantEnabled, - itemIdToExpandedRowMap, openCreateCaseFlyout, pattern, patternRollup, - stats, theme, baseTheme, updatePatternRollup, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.test.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.test.ts index 54babce560f250..7c690ef143f6bc 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.test.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.test.ts @@ -15,7 +15,9 @@ import { getIncompatibleFieldsMarkdownComment, getIncompatibleFieldsMarkdownTablesComment, getIncompatibleMappings, + getIncompatibleMappingsFields, getIncompatibleValues, + getIncompatibleValuesFields, showInvalidCallout, } from './helpers'; import { EMPTY_STAT } from '../../../helpers'; @@ -112,6 +114,19 @@ ${MAPPINGS_THAT_CONFLICT_WITH_ECS} }); }); + describe('getIncompatibleMappingsFields', () => { + test('it (only) returns the fields where type !== indexFieldType', () => { + expect(getIncompatibleMappingsFields(mockPartitionedFieldMetadata.incompatible)).toEqual([ + 'host.name', + 'source.ip', + ]); + }); + + test('it filters-out ECS complaint fields', () => { + expect(getIncompatibleMappingsFields(mockPartitionedFieldMetadata.ecsCompliant)).toEqual([]); + }); + }); + describe('getIncompatibleValues', () => { test('it (only) returns the mappings with indexInvalidValues', () => { expect(getIncompatibleValues(mockPartitionedFieldMetadata.incompatible)).toEqual([ @@ -279,6 +294,18 @@ ${MAPPINGS_THAT_CONFLICT_WITH_ECS} }); }); + describe('getIncompatibleValuesFields', () => { + test('it (only) returns the fields with indexInvalidValues', () => { + expect(getIncompatibleValuesFields(mockPartitionedFieldMetadata.incompatible)).toEqual([ + 'event.category', + ]); + }); + + test('it filters-out ECS complaint fields', () => { + expect(getIncompatibleValuesFields(mockPartitionedFieldMetadata.ecsCompliant)).toEqual([]); + }); + }); + describe('getIncompatibleFieldsMarkdownTablesComment', () => { test('it returns the expected comment when the index has `incompatibleMappings` and `incompatibleValues`', () => { expect( diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts index 1f4e0b62b1c58b..c354b4ef1e8db4 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts @@ -16,6 +16,7 @@ import { getMarkdownTable, getSummaryTableMarkdownComment, getTabCountsMarkdownComment, + escape, } from '../../index_properties/markdown/helpers'; import { getFillColor } from '../summary_tab/helpers'; import * as i18n from '../../index_properties/translations'; @@ -65,11 +66,37 @@ export const getIncompatibleMappings = ( ): EnrichedFieldMetadata[] => enrichedFieldMetadata.filter((x) => !x.isEcsCompliant && x.type !== x.indexFieldType); +export const getIncompatibleMappingsFields = ( + enrichedFieldMetadata: EnrichedFieldMetadata[] +): string[] => + enrichedFieldMetadata.reduce((acc, x) => { + if (!x.isEcsCompliant && x.type !== x.indexFieldType) { + const field = escape(x.indexFieldName); + if (field != null) { + return [...acc, field]; + } + } + return acc; + }, []); + export const getIncompatibleValues = ( enrichedFieldMetadata: EnrichedFieldMetadata[] ): EnrichedFieldMetadata[] => enrichedFieldMetadata.filter((x) => !x.isEcsCompliant && x.indexInvalidValues.length > 0); +export const getIncompatibleValuesFields = ( + enrichedFieldMetadata: EnrichedFieldMetadata[] +): string[] => + enrichedFieldMetadata.reduce((acc, x) => { + if (!x.isEcsCompliant && x.indexInvalidValues.length > 0) { + const field = escape(x.indexFieldName); + if (field != null) { + return [...acc, field]; + } + } + return acc; + }, []); + export const getIncompatibleFieldsMarkdownTablesComment = ({ incompatibleMappings, incompatibleValues, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.ts index 7cb638ad11550d..6b0d75e1308c9e 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/helpers.ts @@ -255,6 +255,14 @@ export const getDocsCount = ({ stats: Record | null; }): number => (stats && stats[indexName]?.primaries?.docs?.count) ?? 0; +export const getIndexId = ({ + indexName, + stats, +}: { + indexName: string; + stats: Record | null; +}): string | null | undefined => stats && stats[indexName]?.uuid; + export const getSizeInBytes = ({ indexName, stats, diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.test.tsx index e1a836a2f049f3..d9cccb4259caf8 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.test.tsx @@ -31,6 +31,7 @@ describe('DataQualityPanel', () => { lastChecked={''} openCreateCaseFlyout={jest.fn()} patterns={[]} + reportDataQualityIndexChecked={jest.fn()} setLastChecked={jest.fn()} baseTheme={DARK_THEME} /> @@ -65,6 +66,7 @@ describe('DataQualityPanel', () => { lastChecked={''} openCreateCaseFlyout={jest.fn()} patterns={[]} + reportDataQualityIndexChecked={jest.fn()} setLastChecked={jest.fn()} baseTheme={DARK_THEME} /> diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.tsx index 2afd1de99a3d64..0a6e0d6a0ccd15 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/index.tsx @@ -17,11 +17,12 @@ import type { WordCloudElementEvent, XYChartElementEvent, } from '@elastic/charts'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Body } from './data_quality_panel/body'; import { DataQualityProvider } from './data_quality_panel/data_quality_context'; import { EMPTY_STAT } from './helpers'; +import { ReportDataQualityCheckAllCompleted, ReportDataQualityIndexChecked } from './types'; interface Props { addSuccessToast: (toast: { title: string }) => void; @@ -53,6 +54,8 @@ interface Props { headerContent?: React.ReactNode; }) => void; patterns: string[]; + reportDataQualityIndexChecked?: ReportDataQualityIndexChecked; + reportDataQualityCheckAllCompleted?: ReportDataQualityCheckAllCompleted; setLastChecked: (lastChecked: string) => void; theme?: PartialTheme; baseTheme: Theme; @@ -61,6 +64,7 @@ interface Props { /** Renders the `Data Quality` dashboard content */ const DataQualityPanelComponent: React.FC = ({ addSuccessToast, + baseTheme, canUserCreateAndReadCases, defaultBytesFormat, defaultNumberFormat, @@ -71,9 +75,10 @@ const DataQualityPanelComponent: React.FC = ({ lastChecked, openCreateCaseFlyout, patterns, + reportDataQualityIndexChecked, + reportDataQualityCheckAllCompleted, setLastChecked, theme, - baseTheme, }) => { const formatBytes = useCallback( (value: number | undefined): string => @@ -87,8 +92,13 @@ const DataQualityPanelComponent: React.FC = ({ [defaultNumberFormat] ); + const telemetryEvents = useMemo( + () => ({ reportDataQualityCheckAllCompleted, reportDataQualityIndexChecked }), + [reportDataQualityCheckAllCompleted, reportDataQualityIndexChecked] + ); + return ( - + = ({ children }) => { const mockGetInitialConversations = jest.fn(() => ({})); const mockGetComments = jest.fn(() => []); const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' }); - + const mockTelemetryEvents = { + reportDataQualityIndexChecked: jest.fn(), + reportDataQualityCheckAllCompleted: jest.fn(), + }; return ( ({ eui: euiDarkVars, darkMode: true })}> @@ -50,7 +53,9 @@ export const TestProvidersComponent: React.FC = ({ children }) => { setDefaultAllowReplacement={jest.fn()} http={mockHttp} > - {children} + + {children} + diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/types.ts b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/types.ts index d51e908bd7b381..02a9fba7f3fbf1 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/types.ts +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/types.ts @@ -140,21 +140,29 @@ export interface IndexToCheck { } export type OnCheckCompleted = ({ + batchId, + checkAllStartTime, error, formatBytes, formatNumber, indexName, + isLastCheck, partitionedFieldMetadata, pattern, version, + requestTime, }: { + batchId: string; + checkAllStartTime: number; error: string | null; formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; indexName: string; + isLastCheck: boolean; partitionedFieldMetadata: PartitionedFieldMetadata | null; pattern: string; version: string; + requestTime?: number; }) => void; export interface ErrorSummary { @@ -174,3 +182,33 @@ export interface SelectedIndex { indexName: string; pattern: string; } + +export type DataQualityIndexCheckedParams = DataQualityCheckAllCompletedParams & { + errorCount?: number; + ilmPhase?: string; + indexId: string; + unallowedMappingFields?: string[]; + unallowedValueFields?: string[]; +}; + +export interface DataQualityCheckAllCompletedParams { + batchId: string; + ecsVersion?: string; + isCheckAll?: boolean; + numberOfDocuments?: number; + numberOfIncompatibleFields?: number; + numberOfIndices?: number; + numberOfIndicesChecked?: number; + sizeInBytes?: number; + timeConsumedMs?: number; +} + +export type ReportDataQualityIndexChecked = (params: DataQualityIndexCheckedParams) => void; +export type ReportDataQualityCheckAllCompleted = ( + params: DataQualityCheckAllCompletedParams +) => void; + +export interface TelemetryEvents { + reportDataQualityIndexChecked?: ReportDataQualityIndexChecked; + reportDataQualityCheckAllCompleted?: ReportDataQualityCheckAllCompleted; +} diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.test.tsx index 8d2e80830724b8..cff820a4c532ca 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_ilm_explain/index.test.tsx @@ -14,8 +14,16 @@ import { ERROR_LOADING_ILM_EXPLAIN } from '../translations'; import { useIlmExplain, UseIlmExplain } from '.'; const mockHttpFetch = jest.fn(); +const mockReportDataQualityIndexChecked = jest.fn(); +const mockReportDataQualityCheckAllClicked = jest.fn(); +const mockTelemetryEvents = { + reportDataQualityIndexChecked: mockReportDataQualityIndexChecked, + reportDataQualityCheckAllCompleted: mockReportDataQualityCheckAllClicked, +}; const ContextWrapper: React.FC = ({ children }) => ( - {children} + + {children} + ); const pattern = 'packetbeat-*'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.test.tsx index 9f80cee1bae52a..cb0165c68d9427 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_mappings/index.test.tsx @@ -14,8 +14,17 @@ import { ERROR_LOADING_MAPPINGS } from '../translations'; import { useMappings, UseMappings } from '.'; const mockHttpFetch = jest.fn(); +const mockReportDataQualityIndexChecked = jest.fn(); +const mockReportDataQualityCheckAllClicked = jest.fn(); +const mockTelemetryEvents = { + reportDataQualityIndexChecked: mockReportDataQualityIndexChecked, + reportDataQualityCheckAllCompleted: mockReportDataQualityCheckAllClicked, +}; + const ContextWrapper: React.FC = ({ children }) => ( - {children} + + {children} + ); const pattern = 'auditbeat-*'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_results_rollup/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_results_rollup/index.tsx index 1976b5e150de35..44eb238da61354 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_results_rollup/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_results_rollup/index.tsx @@ -6,11 +6,7 @@ */ import { useCallback, useEffect, useMemo, useState } from 'react'; - -interface Props { - ilmPhases: string[]; - patterns: string[]; -} +import { EcsVersion } from '@kbn/ecs'; import { getTotalDocsCount, @@ -22,8 +18,19 @@ import { updateResultOnCheckCompleted, } from './helpers'; -import type { OnCheckCompleted, PartitionedFieldMetadata, PatternRollup } from '../types'; +import type { OnCheckCompleted, PatternRollup } from '../types'; +import { getDocsCount, getIndexId, getSizeInBytes } from '../helpers'; +import { getIlmPhase, getIndexIncompatible } from '../data_quality_panel/pattern/helpers'; +import { useDataQualityContext } from '../data_quality_panel/data_quality_context'; +import { + getIncompatibleMappingsFields, + getIncompatibleValuesFields, +} from '../data_quality_panel/tabs/incompatible_tab/helpers'; +interface Props { + ilmPhases: string[]; + patterns: string[]; +} interface UseResultsRollup { onCheckCompleted: OnCheckCompleted; patternIndexNames: Record; @@ -46,7 +53,7 @@ interface UseResultsRollup { export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRollup => { const [patternIndexNames, setPatternIndexNames] = useState>({}); const [patternRollups, setPatternRollups] = useState>({}); - + const { telemetryEvents } = useDataQualityContext(); const updatePatternRollup = useCallback((patternRollup: PatternRollup) => { setPatternRollups((current) => onPatternRollupUpdated({ patternRollup, patternRollups: current }) @@ -74,22 +81,22 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll const onCheckCompleted: OnCheckCompleted = useCallback( ({ + batchId, + checkAllStartTime, error, formatBytes, formatNumber, indexName, partitionedFieldMetadata, pattern, - }: { - error: string | null; - formatBytes: (value: number | undefined) => string; - formatNumber: (value: number | undefined) => string; - indexName: string; - partitionedFieldMetadata: PartitionedFieldMetadata | null; - pattern: string; + requestTime, + isLastCheck, }) => { - setPatternRollups((current) => - updateResultOnCheckCompleted({ + const indexId = getIndexId({ indexName, stats: patternRollups[pattern].stats }); + const ilmExplain = patternRollups[pattern].ilmExplain; + + setPatternRollups((current) => { + const updated = updateResultOnCheckCompleted({ error, formatBytes, formatNumber, @@ -97,10 +104,59 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll partitionedFieldMetadata, pattern, patternRollups: current, - }) - ); + }); + + if ( + indexId != null && + updated[pattern].stats && + updated[pattern].results && + requestTime != null && + requestTime > 0 && + partitionedFieldMetadata && + ilmExplain + ) { + telemetryEvents.reportDataQualityIndexChecked?.({ + batchId, + ecsVersion: EcsVersion, + errorCount: error ? 1 : 0, + ilmPhase: getIlmPhase(ilmExplain[indexName]), + indexId, + isCheckAll: true, + numberOfDocuments: getDocsCount({ indexName, stats: updated[pattern].stats }), + numberOfIncompatibleFields: getIndexIncompatible({ + indexName, + results: updated[pattern].results, + }), + numberOfIndices: 1, + numberOfIndicesChecked: 1, + sizeInBytes: getSizeInBytes({ stats: updated[pattern].stats, indexName }), + timeConsumedMs: requestTime, + unallowedMappingFields: getIncompatibleMappingsFields( + partitionedFieldMetadata.incompatible + ), + unallowedValueFields: getIncompatibleValuesFields( + partitionedFieldMetadata.incompatible + ), + }); + } + + if (isLastCheck) { + telemetryEvents.reportDataQualityCheckAllCompleted?.({ + batchId, + ecsVersion: EcsVersion, + isCheckAll: true, + numberOfDocuments: getTotalDocsCount(updated), + numberOfIncompatibleFields: getTotalIncompatible(updated), + numberOfIndices: getTotalIndices(updated), + numberOfIndicesChecked: getTotalIndicesChecked(updated), + sizeInBytes: getTotalSizeInBytes(updated), + timeConsumedMs: Date.now() - checkAllStartTime, + }); + } + return updated; + }); }, - [] + [patternRollups, telemetryEvents] ); useEffect(() => { diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.test.tsx index b05fd0a4c3c240..30960a7daa8745 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_stats/index.test.tsx @@ -14,8 +14,17 @@ import { ERROR_LOADING_STATS } from '../translations'; import { useStats, UseStats } from '.'; const mockHttpFetch = jest.fn(); +const mockReportDataQualityIndexChecked = jest.fn(); +const mockReportDataQualityCheckAllClicked = jest.fn(); +const mockTelemetryEvents = { + reportDataQualityIndexChecked: mockReportDataQualityIndexChecked, + reportDataQualityCheckAllCompleted: mockReportDataQualityCheckAllClicked, +}; + const ContextWrapper: React.FC = ({ children }) => ( - {children} + + {children} + ); const pattern = 'auditbeat-*'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.test.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.test.tsx index 138a8335792326..b0d55edaf91291 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.test.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.test.tsx @@ -17,8 +17,17 @@ import { EcsMetadata, UnallowedValueRequestItem } from '../types'; import { useUnallowedValues, UseUnallowedValues } from '.'; const mockHttpFetch = jest.fn(); +const mockReportDataQualityIndexChecked = jest.fn(); +const mockReportDataQualityCheckAllClicked = jest.fn(); +const mockTelemetryEvents = { + reportDataQualityIndexChecked: mockReportDataQualityIndexChecked, + reportDataQualityCheckAllCompleted: mockReportDataQualityCheckAllClicked, +}; + const ContextWrapper: React.FC = ({ children }) => ( - {children} + + {children} + ); const ecsMetadata = EcsFlat as unknown as Record; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.tsx index 0d256f0ec9ebb9..de0ce82fb85272 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/use_unallowed_values/index.tsx @@ -15,6 +15,7 @@ export interface UseUnallowedValues { unallowedValues: Record | null; error: string | null; loading: boolean; + requestTime: number | undefined; } export const useUnallowedValues = ({ @@ -31,7 +32,7 @@ export const useUnallowedValues = ({ const { httpFetch } = useDataQualityContext(); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); - + const [requestTime, setRequestTime] = useState(); useEffect(() => { if (requestItems.length === 0) { return; @@ -40,6 +41,8 @@ export const useUnallowedValues = ({ const abortController = new AbortController(); async function fetchData() { + const startTime = Date.now(); + try { const searchResults = await fetchUnallowedValues({ abortController, @@ -59,10 +62,12 @@ export const useUnallowedValues = ({ } catch (e) { if (!abortController.signal.aborted) { setError(e.message); + setRequestTime(Date.now() - startTime); } } finally { if (!abortController.signal.aborted) { setLoading(false); + setRequestTime(Date.now() - startTime); } } } @@ -74,5 +79,5 @@ export const useUnallowedValues = ({ }; }, [httpFetch, indexName, requestItems, setError]); - return { unallowedValues, error, loading }; + return { unallowedValues, error, loading, requestTime }; }; diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.mock.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.mock.ts index 9f455d53700e51..7f5e0b0c6f4553 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.mock.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.mock.ts @@ -30,9 +30,11 @@ const createPublicAlertsClientMock = () => { return jest.fn().mockImplementation(() => { return { create: jest.fn(), - getAlertLimitValue: jest.fn(), + report: jest.fn(), + getAlertLimitValue: jest.fn().mockReturnValue(1000), setAlertLimitReached: jest.fn(), - getRecoveredAlerts: jest.fn(), + getRecoveredAlerts: jest.fn().mockReturnValue([]), + setAlertData: jest.fn(), }; }); }; diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/index.ts b/x-pack/plugins/alerting/server/alerts_client/lib/index.ts index e0276e527515c6..a566c3be9ea7eb 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/index.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/index.ts @@ -14,4 +14,5 @@ export { getHitsWithCount, getLifecycleAlertsQueries, getContinualAlertsQuery, + expandFlattenedAlert, } from './get_summarized_alerts_query'; diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.ts b/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.ts index 9d91d7ec1beae0..bc55f72147d62e 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/strip_framework_fields.ts @@ -6,11 +6,16 @@ */ import { omit } from 'lodash'; -import { ALERT_REASON, ALERT_WORKFLOW_STATUS, TAGS } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_WORKFLOW_STATUS, TAGS, ALERT_URL } from '@kbn/rule-data-utils'; import { alertFieldMap } from '@kbn/alerts-as-data-utils'; import { RuleAlertData } from '../../types'; -const allowedFrameworkFields = new Set([ALERT_REASON, ALERT_WORKFLOW_STATUS, TAGS]); +const allowedFrameworkFields = new Set([ + ALERT_REASON, + ALERT_WORKFLOW_STATUS, + TAGS, + ALERT_URL, +]); /** * Remove framework fields from the alert payload reported by diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.ts index 5e33dd8ddb0145..f4d8a3151ff2e1 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.ts @@ -292,7 +292,7 @@ export class ExecutionHandler< alertActionGroupName: this.ruleTypeActionGroups!.get(actionGroup)!, context: alert.getContext(), actionId: action.id, - state: alert.getScheduledActionOptions()?.state || {}, + state: alert.getState(), kibanaBaseUrl: this.taskRunnerContext.kibanaBaseUrl, alertParams: this.rule.params, actionParams: action.params, diff --git a/x-pack/plugins/apm/common/assistant/constants.ts b/x-pack/plugins/apm/common/assistant/constants.ts new file mode 100644 index 00000000000000..a87fcf01e4c23a --- /dev/null +++ b/x-pack/plugins/apm/common/assistant/constants.ts @@ -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. + */ + +export enum CorrelationsEventType { + Transaction = 'transaction', + ExitSpan = 'exit_span', + Error = 'error', +} diff --git a/x-pack/plugins/apm/common/connections.ts b/x-pack/plugins/apm/common/connections.ts index 78c0f5bedca5b4..6d87e3517e8c3b 100644 --- a/x-pack/plugins/apm/common/connections.ts +++ b/x-pack/plugins/apm/common/connections.ts @@ -21,6 +21,7 @@ export interface ServiceNode extends NodeBase { serviceName: string; agentName: AgentName; environment: string; + dependencyName?: string; } export interface DependencyNode extends NodeBase { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/service_map/service_map.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/service_map/service_map.cy.ts new file mode 100644 index 00000000000000..1ff28bfb46cbf8 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/service_map/service_map.cy.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 url from 'url'; +import { synthtrace } from '../../../../synthtrace'; +import { opbeans } from '../../../fixtures/synthtrace/opbeans'; + +const start = '2021-10-10T00:00:00.000Z'; +const end = '2021-10-10T00:15:00.000Z'; + +const serviceMapHref = url.format({ + pathname: '/app/apm/service-map', + query: { + environment: 'ENVIRONMENT_ALL', + rangeFrom: start, + rangeTo: end, + }, +}); + +const detailedServiceMap = url.format({ + pathname: '/app/apm/services/opbeans-java/service-map', + query: { + environment: 'ENVIRONMENT_ALL', + rangeFrom: start, + rangeTo: end, + }, +}); + +describe('Service map', () => { + before(() => { + synthtrace.index( + opbeans({ + from: new Date(start).getTime(), + to: new Date(end).getTime(), + }) + ); + }); + + after(() => { + synthtrace.clean(); + }); + + beforeEach(() => { + cy.loginAsViewerUser(); + }); + + describe('When navigating to service map', () => { + it('opens service map', () => { + cy.visitKibana(serviceMapHref); + cy.contains('h1', 'Services'); + }); + + it('opens detailed service map', () => { + cy.visitKibana(detailedServiceMap); + cy.contains('h1', 'opbeans-java'); + }); + + describe('When there is no data', () => { + it('shows empty state', () => { + cy.visitKibana(serviceMapHref); + // we need to dismiss the service-group call out first + cy.contains('Dismiss').click(); + cy.getByTestSubj('apmUnifiedSearchBar').type('_id : foo{enter}'); + cy.contains('No services available'); + // search bar is still visible + cy.getByTestSubj('apmUnifiedSearchBar'); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/service_map/index.test.tsx b/x-pack/plugins/apm/public/components/app/service_map/index.test.tsx deleted file mode 100644 index 2fe42bd7692bd7..00000000000000 --- a/x-pack/plugins/apm/public/components/app/service_map/index.test.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import { createMemoryHistory, MemoryHistory } from 'history'; -import { CoreStart } from '@kbn/core/public'; -import React, { ReactNode } from 'react'; -import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { License } from '@kbn/licensing-plugin/common/license'; -import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; -import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; -import { LicenseContext } from '../../../context/license/license_context'; -import * as useFetcherModule from '../../../hooks/use_fetcher'; -import { ServiceMap } from '.'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -let history: MemoryHistory; - -const KibanaReactContext = createKibanaReactContext({ - usageCollection: { reportUiCounter: () => {} }, -} as Partial); - -const activeLicense = new License({ - signature: 'active test signature', - license: { - expiryDateInMillis: 0, - mode: 'platinum', - status: 'active', - type: 'platinum', - uid: '1', - }, -}); - -const expiredLicense = new License({ - signature: 'expired test signature', - license: { - expiryDateInMillis: 0, - mode: 'platinum', - status: 'expired', - type: 'platinum', - uid: '1', - }, -}); - -function createWrapper(license: License | null) { - history = createMemoryHistory(); - history.replace('/service-map?rangeFrom=now-15m&rangeTo=now'); - - return ({ children }: { children?: ReactNode }) => { - return ( - - - - - {children} - - - - - ); - }; -} - -describe('ServiceMap', () => { - describe('with no license', () => { - it('renders null', async () => { - expect( - await render( - , - { - wrapper: createWrapper(null), - } - ).queryByTestId('ServiceMap') - ).not.toBeInTheDocument(); - }); - }); - - describe('with an expired license', () => { - it('renders the license banner', async () => { - expect( - await render( - , - { - wrapper: createWrapper(expiredLicense), - } - ).findAllByText(/Platinum/) - ).toHaveLength(1); - }); - }); - - describe('with an active license', () => { - describe('with an empty response', () => { - it('renders the empty banner', async () => { - jest.spyOn(useFetcherModule, 'useFetcher').mockReturnValueOnce({ - data: { elements: [] }, - refetch: () => {}, - status: useFetcherModule.FETCH_STATUS.SUCCESS, - }); - - expect( - await render( - , - { - wrapper: createWrapper(activeLicense), - } - ).findAllByText(/No services available/) - ).toHaveLength(1); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/service_map/index.tsx b/x-pack/plugins/apm/public/components/app/service_map/index.tsx index f9b848b816d4c6..d3422fac11546a 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/index.tsx @@ -40,7 +40,7 @@ import { DisabledPrompt } from './disabled_prompt'; function PromptContainer({ children }: { children: ReactNode }) { return ( <> - + { + return callApmApi('POST /internal/apm/assistant/get_correlation_values', { + signal, + params: { + body: args, + }, + }); + } + ); +} diff --git a/x-pack/plugins/apm/public/components/assistant_functions/get_apm_downstream_dependencies.ts b/x-pack/plugins/apm/public/components/assistant_functions/get_apm_downstream_dependencies.ts new file mode 100644 index 00000000000000..d313cf527eb5ab --- /dev/null +++ b/x-pack/plugins/apm/public/components/assistant_functions/get_apm_downstream_dependencies.ts @@ -0,0 +1,65 @@ +/* + * 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 { RegisterFunctionDefinition } from '@kbn/observability-ai-assistant-plugin/common/types'; +import { callApmApi } from '../../services/rest/create_call_apm_api'; + +export function registerGetApmDownstreamDependenciesFunction({ + registerFunction, +}: { + registerFunction: RegisterFunctionDefinition; +}) { + registerFunction( + { + name: 'get_apm_downstream_dependencies', + contexts: ['apm'], + description: `Get the downstream dependencies (services or uninstrumented backends) for a + service. This allows you to map the dowstream dependency name to a service, by + returning both span.destination.service.resource and service.name. Use this to + drilldown further if needed.`, + descriptionForUser: `Get the downstream dependencies (services or uninstrumented backends) for a + service. This allows you to map the dowstream dependency name to a service, by + returning both span.destination.service.resource and service.name. Use this to + drilldown further if needed.`, + parameters: { + type: 'object', + properties: { + 'service.name': { + type: 'string', + description: 'The name of the service', + }, + 'service.environment': { + type: 'string', + description: 'The environment that the service is running in', + }, + start: { + type: 'string', + description: + 'The start of the time range, in Elasticsearch date math, like `now`.', + }, + end: { + type: 'string', + description: + 'The end of the time range, in Elasticsearch date math, like `now-24h`.', + }, + }, + required: ['service.name', 'start', 'end'], + } as const, + }, + async ({ arguments: args }, signal) => { + return callApmApi( + 'GET /internal/apm/assistant/get_downstream_dependencies', + { + signal, + params: { + query: args, + }, + } + ); + } + ); +} diff --git a/x-pack/plugins/apm/public/components/assistant_functions/get_apm_error_document.ts b/x-pack/plugins/apm/public/components/assistant_functions/get_apm_error_document.ts new file mode 100644 index 00000000000000..c89c94879b05b0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/assistant_functions/get_apm_error_document.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RegisterFunctionDefinition } from '@kbn/observability-ai-assistant-plugin/common/types'; +import { callApmApi } from '../../services/rest/create_call_apm_api'; + +export function registerGetApmErrorDocumentFunction({ + registerFunction, +}: { + registerFunction: RegisterFunctionDefinition; +}) { + registerFunction( + { + name: 'get_apm_error_document', + contexts: ['apm'], + description: `Get a sample error document based on its grouping name. This also includes the + stacktrace of the error, which might give you a hint as to what the cause is. + ONLY use this for error events.`, + descriptionForUser: `Get a sample error document based on its grouping name. This also includes the + stacktrace of the error, which might give you a hint as to what the cause is.`, + parameters: { + type: 'object', + properties: { + 'error.grouping_name': { + type: 'string', + description: + 'The grouping name of the error. Use the field value returned by get_apm_chart or get_correlation_values.', + }, + start: { + type: 'string', + description: + 'The start of the time range, in Elasticsearch date math, lik e `now`.', + }, + end: { + type: 'string', + description: + 'The end of the time range, in Elasticsearch date math, like `now-24h`.', + }, + }, + required: ['start', 'end', 'error.grouping_name'], + } as const, + }, + async ({ arguments: args }, signal) => { + return callApmApi('GET /internal/apm/assistant/get_error_document', { + signal, + params: { + query: args, + }, + }); + } + ); +} diff --git a/x-pack/plugins/apm/public/components/assistant_functions/get_apm_service_summary.ts b/x-pack/plugins/apm/public/components/assistant_functions/get_apm_service_summary.ts new file mode 100644 index 00000000000000..049ac97acf03b8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/assistant_functions/get_apm_service_summary.ts @@ -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 type { RegisterFunctionDefinition } from '@kbn/observability-ai-assistant-plugin/common/types'; +import { callApmApi } from '../../services/rest/create_call_apm_api'; + +export function registerGetApmServiceSummaryFunction({ + registerFunction, +}: { + registerFunction: RegisterFunctionDefinition; +}) { + registerFunction( + { + name: 'get_apm_service_summary', + contexts: ['apm'], + description: `Gets a summary of a single service, including: the language, service version, +deployments, and the infrastructure that it is running in, for instance on how +many pods, and a list of its downstream dependencies. It also returns active +alerts and anomalies.`, + descriptionForUser: `Gets a summary of a single service, including: the language, service version, +deployments, and the infrastructure that it is running in, for instance on how +many pods, and a list of its downstream dependencies. It also returns active +alerts and anomalies.`, + parameters: { + type: 'object', + properties: { + 'service.name': { + type: 'string', + description: 'The name of the service that should be summarized.', + }, + 'service.environment': { + type: 'string', + description: 'The environment that the service is running in', + }, + start: { + type: 'string', + description: + 'The start of the time range, in Elasticsearch date math, like `now`.', + }, + end: { + type: 'string', + description: + 'The end of the time range, in Elasticsearch date math, like `now-24h`.', + }, + }, + required: ['service.name', 'start', 'end'], + } as const, + }, + async ({ arguments: args }, signal) => { + return callApmApi('GET /internal/apm/assistant/get_service_summary', { + signal, + params: { + query: args, + }, + }); + } + ); +} diff --git a/x-pack/plugins/apm/public/components/assistant_functions/get_apm_timeseries.tsx b/x-pack/plugins/apm/public/components/assistant_functions/get_apm_timeseries.tsx new file mode 100644 index 00000000000000..4e42236bd6a3ba --- /dev/null +++ b/x-pack/plugins/apm/public/components/assistant_functions/get_apm_timeseries.tsx @@ -0,0 +1,295 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import type { RegisterFunctionDefinition } from '@kbn/observability-ai-assistant-plugin/common/types'; +import { groupBy } from 'lodash'; +import React from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { FETCH_STATUS } from '../../hooks/use_fetcher'; +import { callApmApi } from '../../services/rest/create_call_apm_api'; +import { getTimeZone } from '../shared/charts/helper/timezone'; +import { TimeseriesChart } from '../shared/charts/timeseries_chart'; +import { ChartPointerEventContextProvider } from '../../context/chart_pointer_event/chart_pointer_event_context'; +import { ApmThemeProvider } from '../routing/app_root'; +import { Coordinate, TimeSeries } from '../../../typings/timeseries'; +import { + ChartType, + getTimeSeriesColor, +} from '../shared/charts/helper/get_timeseries_color'; +import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; +import { + asPercent, + asTransactionRate, + getDurationFormatter, +} from '../../../common/utils/formatters'; +import { + getMaxY, + getResponseTimeTickFormatter, +} from '../shared/charts/transaction_charts/helper'; + +export function registerGetApmTimeseriesFunction({ + registerFunction, +}: { + registerFunction: RegisterFunctionDefinition; +}) { + registerFunction( + { + contexts: ['apm'], + name: 'get_apm_timeseries', + descriptionForUser: `Display different APM metrics, like throughput, failure rate, or latency, for any service or all services, or any or all of its dependencies, both as a timeseries and as a single statistic. Additionally, the function will return any changes, such as spikes, step and trend changes, or dips. You can also use it to compare data by requesting two different time ranges, or for instance two different service versions`, + description: `Display different APM metrics, like throughput, failure rate, or latency, for any service or all services, or any or all of its dependencies, both as a timeseries and as a single statistic. Additionally, the function will return any changes, such as spikes, step and trend changes, or dips. You can also use it to compare data by requesting two different time ranges, or for instance two different service versions. In KQL, escaping happens with double quotes, not single quotes. Some characters that need escaping are: ':()\\\/\". Always put a field value in double quotes. Best: service.name:\"opbeans-go\". Wrong: service.name:opbeans-go. This is very important!`, + parameters: { + type: 'object', + properties: { + start: { + type: 'string', + description: + 'The start of the time range, in Elasticsearch date math, like `now`.', + }, + end: { + type: 'string', + description: + 'The end of the time range, in Elasticsearch date math, like `now-24h`.', + }, + stats: { + type: 'array', + items: { + type: 'object', + properties: { + timeseries: { + description: 'The metric to be displayed', + oneOf: [ + { + type: 'object', + properties: { + name: { + type: 'string', + enum: [ + 'transaction_throughput', + 'transaction_failure_rate', + ], + }, + 'transaction.type': { + type: 'string', + description: 'The transaction type', + }, + }, + required: ['name'], + }, + { + type: 'object', + properties: { + name: { + type: 'string', + enum: [ + 'exit_span_throughput', + 'exit_span_failure_rate', + 'exit_span_latency', + ], + }, + 'span.destination.service.resource': { + type: 'string', + description: + 'The name of the downstream dependency for the service', + }, + }, + required: ['name'], + }, + { + type: 'object', + properties: { + name: { + type: 'string', + const: 'error_event_rate', + }, + }, + required: ['name'], + }, + { + type: 'object', + properties: { + name: { + type: 'string', + const: 'transaction_latency', + }, + 'transaction.type': { + type: 'string', + }, + function: { + type: 'string', + enum: ['avg', 'p95', 'p99'], + }, + }, + required: ['name', 'function'], + }, + ], + }, + 'service.name': { + type: 'string', + description: 'The name of the service', + }, + 'service.environment': { + type: 'string', + description: + "The environment that the service is running in. If you don't know this, use ENVIRONMENT_ALL.", + }, + filter: { + type: 'string', + description: + 'a KQL query to filter the data by. If no filter should be applied, leave it empty.', + }, + title: { + type: 'string', + description: + 'A unique, human readable, concise title for this specific group series.', + }, + offset: { + type: 'string', + description: + 'The offset. Right: 15m. 8h. 1d. Wrong: -15m. -8h. -1d.', + }, + }, + required: [ + 'service.name', + 'service.environment', + 'timeseries', + 'title', + ], + }, + }, + }, + required: ['stats', 'start', 'end'], + } as const, + }, + async ({ arguments: { stats, start, end } }, signal) => { + const response = await callApmApi( + 'POST /internal/apm/assistant/get_apm_timeseries', + { + signal, + params: { + body: { stats: stats as any, start, end }, + }, + } + ); + + return response; + }, + ({ arguments: args, response }) => { + const groupedSeries = groupBy(response.data, (series) => series.group); + + const { + services: { uiSettings }, + } = useKibana(); + + const timeZone = getTimeZone(uiSettings); + + return ( + + + + {Object.values(groupedSeries).map((groupSeries) => { + const groupId = groupSeries[0].group; + + const maxY = getMaxY(groupSeries); + const latencyFormatter = getDurationFormatter(maxY); + + let yLabelFormat: (value: number) => string; + + const firstStat = groupSeries[0].stat; + + switch (firstStat.timeseries.name) { + case 'transaction_throughput': + case 'exit_span_throughput': + case 'error_event_rate': + yLabelFormat = asTransactionRate; + break; + + case 'transaction_latency': + case 'exit_span_latency': + yLabelFormat = + getResponseTimeTickFormatter(latencyFormatter); + break; + + case 'transaction_failure_rate': + case 'exit_span_failure_rate': + yLabelFormat = (y) => asPercent(y || 0, 100); + break; + } + + const timeseries: Array> = + groupSeries.map((series): TimeSeries => { + let chartType: ChartType; + + switch (series.stat.timeseries.name) { + case 'transaction_throughput': + case 'exit_span_throughput': + chartType = ChartType.THROUGHPUT; + break; + + case 'transaction_failure_rate': + case 'exit_span_failure_rate': + chartType = ChartType.FAILED_TRANSACTION_RATE; + break; + + case 'transaction_latency': + if ( + series.stat.timeseries.function === + LatencyAggregationType.p99 + ) { + chartType = ChartType.LATENCY_P99; + } else if ( + series.stat.timeseries.function === + LatencyAggregationType.p95 + ) { + chartType = ChartType.LATENCY_P95; + } else { + chartType = ChartType.LATENCY_AVG; + } + break; + + case 'exit_span_latency': + chartType = ChartType.LATENCY_AVG; + break; + + case 'error_event_rate': + chartType = ChartType.ERROR_OCCURRENCES; + break; + } + + return { + title: series.id, + type: 'line', + color: getTimeSeriesColor(chartType!).currentPeriodColor, + data: series.data, + }; + }); + + return ( + + + + {groupId} + + + + + ); + })} + + + + ); + } + ); +} diff --git a/x-pack/plugins/apm/public/components/assistant_functions/index.ts b/x-pack/plugins/apm/public/components/assistant_functions/index.ts new file mode 100644 index 00000000000000..61f25d5ee3546f --- /dev/null +++ b/x-pack/plugins/apm/public/components/assistant_functions/index.ts @@ -0,0 +1,132 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import { + RegisterContextDefinition, + RegisterFunctionDefinition, +} from '@kbn/observability-ai-assistant-plugin/common/types'; +import { ApmPluginStartDeps } from '../../plugin'; +import { createCallApmApi } from '../../services/rest/create_call_apm_api'; +import { registerGetApmCorrelationsFunction } from './get_apm_correlations'; +import { registerGetApmDownstreamDependenciesFunction } from './get_apm_downstream_dependencies'; +import { registerGetApmErrorDocumentFunction } from './get_apm_error_document'; +import { registerGetApmServiceSummaryFunction } from './get_apm_service_summary'; +import { registerGetApmTimeseriesFunction } from './get_apm_timeseries'; + +export function registerAssistantFunctions({ + pluginsStart, + coreStart, + registerContext, + registerFunction, +}: { + pluginsStart: ApmPluginStartDeps; + coreStart: CoreStart; + registerFunction: RegisterFunctionDefinition; + registerContext: RegisterContextDefinition; +}) { + createCallApmApi(coreStart); + + registerGetApmTimeseriesFunction({ + registerFunction, + }); + + registerGetApmErrorDocumentFunction({ + registerFunction, + }); + + registerGetApmCorrelationsFunction({ + registerFunction, + }); + + registerGetApmDownstreamDependenciesFunction({ + registerFunction, + }); + + registerGetApmServiceSummaryFunction({ + registerFunction, + }); + + registerContext({ + name: 'apm', + description: ` +There are four important data types in Elastic APM. Each of them have the +following fields: +- service.name: the name of the service +- service.node.name: the id of the service instance (often the hostname) +- service.environment: the environment (often production, development) +- agent.name: the name of the agent (go, java, etc) + +The four data types are transactions, exit spans, error events, and application +metrics. + +Transactions have three metrics: throughput, failure rate, and latency. The +fields are: + +- transaction.type: often request or page-load (the main transaction types), +but can also be worker, or route-change. +- transaction.name: The name of the transaction group, often something like +'GET /api/product/:productId' +- transaction.result: The result. Used to capture HTTP response codes +(2xx,3xx,4xx,5xx) for request transactions. +- event.outcome: whether the transaction was succesful or not. success, +failure, or unknown. + +Exit spans have three metrics: throughput, failure rate and latency. The fields +are: +- span.type: db, external +- span.subtype: the type of database (redis, postgres) or protocol (http, grpc) +- span.destination.service.resource: the address of the destination of the call +- event.outcome: whether the transaction was succesful or not. success, +failure, or unknown. + +Error events have one metric, error event rate. The fields are: +- error.grouping_name: a human readable keyword that identifies the error group + +For transaction metrics we also collect anomalies. These are scored 0 (low) to +100 (critical). + +For root cause analysis, locate a change point in the relevant metrics for a +service or downstream dependency. You can locate a change point by using a +sliding window, e.g. start with a small time range, like 30m, and make it +bigger until you identify a change point. It's very important to identify a +change point. If you don't have a change point, ask the user for next steps. +You can also use an anomaly or a deployment as a change point. Then, compare +data before the change with data after the change. You can either use the +groupBy parameter in get_apm_chart to get the most occuring values in a certain +data set, or you can use correlations to see for which field and value the +frequency has changed when comparing the foreground set to the background set. +This is useful when comparing data from before the change point with after the +change point. For instance, you might see a specific error pop up more often +after the change point. + +When comparing anomalies and changes in timeseries, first, zoom in to a smaller +time window, at least 30 minutes before and 30 minutes after the change +occured. E.g., if the anomaly occured at 2023-07-05T08:15:00.000Z, request a +time window that starts at 2023-07-05T07:45:00.000Z and ends at +2023-07-05T08:45:00.000Z. When comparing changes in different timeseries and +anomalies to determine a correlation, make sure to compare the timestamps. If +in doubt, rate the likelihood of them being related, given the time difference, +between 1 and 10. If below 5, assume it's not related. Mention this likelihood +(and the time difference) to the user. + +Your goal is to help the user determine the root cause of an issue quickly and +transparently. If you see a change or +anomaly in a metric for a service, try to find similar changes in the metrics +for the traffic to its downstream dependencies, by comparing transaction +metrics to span metrics. To inspect the traffic from one service to a +downstream dependency, first get the downstream dependencies for a service, +then get the span metrics from that service (\`service.name\`) to its +downstream dependency (\`span.destination.service.resource\`). For instance, +for an anomaly in throughput, first inspect \`transaction_throughput\` for +\`service.name\`. Then, inspect \`exit_span_throughput\` for its downstream +dependencies, by grouping by \`span.destination.service.resource\`. Repeat this +process over the next service its downstream dependencies until you identify a +root cause. If you can not find any similar changes, use correlations or +grouping to find attributes that could be causes for the change.`, + }); +} diff --git a/x-pack/plugins/apm/public/components/routing/app_root/index.tsx b/x-pack/plugins/apm/public/components/routing/app_root/index.tsx index a9a0331d1c8c7a..a49beaa2215663 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root/index.tsx @@ -127,7 +127,7 @@ function MountApmHeaderActionMenu() { ); } -function ApmThemeProvider({ children }: { children: React.ReactNode }) { +export function ApmThemeProvider({ children }: { children: React.ReactNode }) { const [darkMode] = useUiSetting$('theme:darkMode'); return ( diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 357ab12668d250..a66a2f7a69f8da 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; -import { from } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import type { + PluginSetupContract as AlertingPluginPublicSetup, + PluginStartContract as AlertingPluginPublicStart, +} from '@kbn/alerting-plugin/public'; +import { ChartsPluginStart } from '@kbn/charts-plugin/public'; import { AppMountParameters, AppNavLinkStatus, @@ -19,58 +20,58 @@ import { PluginInitializerContext, } from '@kbn/core/public'; import type { - DataPublicPluginStart, DataPublicPluginSetup, + DataPublicPluginStart, } from '@kbn/data-plugin/public'; -import { LensPublicStart } from '@kbn/lens-plugin/public'; -import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import type { ExploratoryViewPublicSetup } from '@kbn/exploratory-view-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { + DiscoverSetup, + DiscoverStart, +} from '@kbn/discover-plugin/public/plugin'; import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import type { ExploratoryViewPublicSetup } from '@kbn/exploratory-view-plugin/public'; +import type { FeaturesPluginSetup } from '@kbn/features-plugin/public'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { FleetStart } from '@kbn/fleet-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { InfraClientStartExports } from '@kbn/infra-plugin/public'; import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public'; -import type { - PluginSetupContract as AlertingPluginPublicSetup, - PluginStartContract as AlertingPluginPublicStart, -} from '@kbn/alerting-plugin/public'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; -import type { FeaturesPluginSetup } from '@kbn/features-plugin/public'; -import type { FleetStart } from '@kbn/fleet-plugin/public'; +import { LensPublicStart } from '@kbn/lens-plugin/public'; +import { LicenseManagementUIPluginSetup } from '@kbn/license-management-plugin/public'; import type { LicensingPluginSetup } from '@kbn/licensing-plugin/public'; import type { MapsStartApi } from '@kbn/maps-plugin/public'; import type { MlPluginSetup, MlPluginStart } from '@kbn/ml-plugin/public'; -import type { SharePluginSetup } from '@kbn/share-plugin/public'; -import type { - ObservabilitySharedPluginSetup, - ObservabilitySharedPluginStart, -} from '@kbn/observability-shared-plugin/public'; +import type { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public'; import { FetchDataParams, ObservabilityPublicSetup, ObservabilityPublicStart, } from '@kbn/observability-plugin/public'; -import { METRIC_TYPE } from '@kbn/observability-shared-plugin/public'; -import type { - TriggersAndActionsUIPublicPluginSetup, - TriggersAndActionsUIPublicPluginStart, -} from '@kbn/triggers-actions-ui-plugin/public'; -import type { SecurityPluginStart } from '@kbn/security-plugin/public'; -import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import { InfraClientStartExports } from '@kbn/infra-plugin/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { ChartsPluginStart } from '@kbn/charts-plugin/public'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public'; import { ObservabilityTriggerId } from '@kbn/observability-shared-plugin/common'; -import { LicenseManagementUIPluginSetup } from '@kbn/license-management-plugin/public'; +import type { + ObservabilitySharedPluginSetup, + ObservabilitySharedPluginStart, +} from '@kbn/observability-shared-plugin/public'; +import { METRIC_TYPE } from '@kbn/observability-shared-plugin/public'; import { ProfilingPluginSetup, ProfilingPluginStart, } from '@kbn/profiling-plugin/public'; -import { - DiscoverStart, - DiscoverSetup, -} from '@kbn/discover-plugin/public/plugin'; -import type { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { SharePluginSetup } from '@kbn/share-plugin/public'; +import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; +import type { + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart, +} from '@kbn/triggers-actions-ui-plugin/public'; +import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import { from } from 'rxjs'; +import { map } from 'rxjs/operators'; +import type { ConfigSchema } from '.'; import { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types'; import { getApmEnrollmentFlyoutData, @@ -80,7 +81,6 @@ import { getLazyApmAgentsTabExtension } from './components/fleet_integration/laz import { getLazyAPMPolicyCreateExtension } from './components/fleet_integration/lazy_apm_policy_create_extension'; import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/lazy_apm_policy_edit_extension'; import { featureCatalogueEntry } from './feature_catalogue_entry'; -import type { ConfigSchema } from '.'; import { APMServiceDetailLocator } from './locator/service_detail_locator'; export type ApmPluginSetup = ReturnType; @@ -413,6 +413,20 @@ export class ApmPlugin implements Plugin { public start(core: CoreStart, plugins: ApmPluginStartDeps) { const { fleet } = plugins; + + plugins.observabilityAIAssistant.register( + async ({ signal, registerFunction, registerContext }) => { + const mod = await import('./components/assistant_functions'); + + mod.registerAssistantFunctions({ + coreStart: core, + pluginsStart: plugins, + registerFunction, + registerContext, + }); + } + ); + if (fleet) { const agentEnrollmentExtensionData = getApmEnrollmentFlyoutData(); diff --git a/x-pack/plugins/apm/server/lib/connections/get_connection_stats/get_destination_map.ts b/x-pack/plugins/apm/server/lib/connections/get_connection_stats/get_destination_map.ts index f39a5a3264449f..de03052e5b9c56 100644 --- a/x-pack/plugins/apm/server/lib/connections/get_connection_stats/get_destination_map.ts +++ b/x-pack/plugins/apm/server/lib/connections/get_connection_stats/get_destination_map.ts @@ -208,6 +208,7 @@ export const getDestinationMap = ({ environment: mergedDestination.environment, id: objectHash({ serviceName: mergedDestination.serviceName }), type: NodeType.service, + dependencyName: mergedDestination.dependencyName, }; } else { node = { diff --git a/x-pack/plugins/apm/server/lib/helpers/get_apm_alerts_client.ts b/x-pack/plugins/apm/server/lib/helpers/get_apm_alerts_client.ts index 44fdaaf941521e..5be010b6133835 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_apm_alerts_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_apm_alerts_client.ts @@ -6,6 +6,8 @@ */ import { isEmpty } from 'lodash'; +import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; import { APMRouteHandlerResources } from '../../routes/typings'; export type ApmAlertsClient = Awaited>; @@ -26,17 +28,19 @@ export async function getApmAlertsClient({ throw Error('No alert indices exist for "apm"'); } - type ApmAlertsClientSearchParams = Omit< - Parameters[0], - 'index' - >; + type RequiredParams = ESSearchRequest & { + size: number; + track_total_hits: boolean | number; + }; return { - search(searchParams: ApmAlertsClientSearchParams) { + search( + searchParams: TParams + ): Promise> { return alertsClient.find({ ...searchParams, index: apmAlertsIndices.join(','), - }); + }) as Promise; }, }; } diff --git a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts index 6ace413484e27e..afeb0a60219ec9 100644 --- a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts @@ -44,6 +44,7 @@ import { suggestionsRouteRepository } from '../suggestions/route'; import { timeRangeMetadataRoute } from '../time_range_metadata/route'; import { traceRouteRepository } from '../traces/route'; import { transactionRouteRepository } from '../transactions/route'; +import { assistantRouteRepository } from '../assistant_functions/route'; function getTypedGlobalApmServerRouteRepository() { const repository = { @@ -81,6 +82,7 @@ function getTypedGlobalApmServerRouteRepository() { ...agentExplorerRouteRepository, ...mobileRouteRepository, ...diagnosticsRepository, + ...assistantRouteRepository, }; return repository; diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_correlation_values/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_correlation_values/index.ts new file mode 100644 index 00000000000000..7ced5c1206d7b8 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_correlation_values/index.ts @@ -0,0 +1,176 @@ +/* + * 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 datemath from '@elastic/datemath'; +import { AggregationsSignificantTermsAggregation } from '@elastic/elasticsearch/lib/api/types'; +import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; +import * as t from 'io-ts'; +import { CorrelationsEventType } from '../../../../common/assistant/constants'; +import { + SERVICE_NAME, + SERVICE_NODE_NAME, + SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_NAME, + TRANSACTION_NAME, + TRANSACTION_RESULT, +} from '../../../../common/es_fields/apm'; +import { termQuery } from '../../../../common/utils/term_query'; +import { + APMEventClient, + APMEventESSearchRequest, +} from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { environmentRt } from '../../default_api_types'; + +const setRt = t.intersection([ + t.type({ + start: t.string, + end: t.string, + 'service.name': t.string, + label: t.string, + }), + t.partial({ + filter: t.string, + 'service.environment': environmentRt.props.environment, + }), +]); + +export const correlationValuesRouteRt = t.type({ + sets: t.array( + t.type({ + foreground: setRt, + background: setRt, + event: t.union([ + t.literal(CorrelationsEventType.Transaction), + t.literal(CorrelationsEventType.ExitSpan), + t.literal(CorrelationsEventType.Error), + ]), + }) + ), +}); + +export interface CorrelationValue { + foreground: string; + background: string; + fieldName: string; + fields: Array<{ value: string; score: number }>; +} + +export async function getApmCorrelationValues({ + arguments: args, + apmEventClient, +}: { + arguments: t.TypeOf; + apmEventClient: APMEventClient; +}): Promise { + const getQueryForSet = (set: t.TypeOf) => { + const start = datemath.parse(set.start)?.valueOf()!; + const end = datemath.parse(set.end)?.valueOf()!; + + return { + bool: { + filter: [ + ...rangeQuery(start, end), + ...termQuery(SERVICE_NAME, set['service.name']), + ...kqlQuery(set.filter), + ], + }, + }; + }; + + const allCorrelations = await Promise.all( + args.sets.map(async (set) => { + const query = getQueryForSet(set.foreground); + + let apm: APMEventESSearchRequest['apm']; + + let fields: string[] = []; + + switch (set.event) { + case CorrelationsEventType.Transaction: + apm = { + events: [ProcessorEvent.transaction], + }; + fields = [TRANSACTION_NAME, SERVICE_NODE_NAME, TRANSACTION_RESULT]; + break; + + case CorrelationsEventType.ExitSpan: + apm = { + events: [ProcessorEvent.span], + }; + fields = [SPAN_NAME, SPAN_DESTINATION_SERVICE_RESOURCE]; + query.bool.filter.push({ + exists: { + field: SPAN_DESTINATION_SERVICE_RESOURCE, + }, + }); + break; + + case CorrelationsEventType.Error: + apm = { + events: [ProcessorEvent.error], + }; + fields = ['error.grouping_name']; + break; + } + + const sigTermsAggs: Record< + string, + { significant_terms: AggregationsSignificantTermsAggregation } + > = {}; + + fields.forEach((field) => { + sigTermsAggs[field] = { + significant_terms: { + field, + background_filter: getQueryForSet(set.background), + gnd: { + background_is_superset: false, + }, + }, + }; + }); + + const response = await apmEventClient.search('get_significant_terms', { + apm, + body: { + size: 0, + track_total_hits: false, + query, + aggs: sigTermsAggs, + }, + }); + + const correlations: Array<{ + foreground: string; + background: string; + fieldName: string; + fields: Array<{ value: string; score: number }>; + }> = []; + + if (!response.aggregations) { + return { correlations: [] }; + } + + // eslint-disable-next-line guard-for-in + for (const fieldName in response.aggregations) { + correlations.push({ + foreground: set.foreground.label, + background: set.background.label, + fieldName, + fields: response.aggregations[fieldName].buckets.map((bucket) => ({ + score: bucket.score, + value: String(bucket.key), + })), + }); + } + + return { correlations }; + }) + ); + + return allCorrelations.flatMap((_) => _.correlations); +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_downstream_dependencies/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_downstream_dependencies/index.ts new file mode 100644 index 00000000000000..8d478cab20083a --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_downstream_dependencies/index.ts @@ -0,0 +1,79 @@ +/* + * 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 datemath from '@elastic/datemath'; +import * as t from 'io-ts'; +import { termQuery } from '@kbn/observability-plugin/server'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { SERVICE_NAME } from '../../../../common/es_fields/apm'; +import { environmentQuery } from '../../../../common/utils/environment_query'; +import { getDestinationMap } from '../../../lib/connections/get_connection_stats/get_destination_map'; +import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { NodeType } from '../../../../common/connections'; + +export const downstreamDependenciesRouteRt = t.intersection([ + t.type({ + 'service.name': t.string, + start: t.string, + end: t.string, + }), + t.partial({ + 'service.environment': t.string, + }), +]); + +export interface APMDownstreamDependency { + 'service.name'?: string | undefined; + 'span.destination.service.resource': string; + 'span.type'?: string | undefined; + 'span.subtype'?: string | undefined; +} + +export async function getAssistantDownstreamDependencies({ + arguments: args, + apmEventClient, +}: { + arguments: t.TypeOf; + apmEventClient: APMEventClient; +}): Promise { + const start = datemath.parse(args.start)?.valueOf()!; + const end = datemath.parse(args.end)?.valueOf()!; + + const map = await getDestinationMap({ + start, + end, + apmEventClient, + filter: [ + ...termQuery(SERVICE_NAME, args['service.name']), + ...environmentQuery(args['service.environment'] ?? ENVIRONMENT_ALL.value), + ], + }); + + const items: Array<{ + 'service.name'?: string; + 'span.destination.service.resource': string; + 'span.type'?: string; + 'span.subtype'?: string; + }> = []; + + for (const [_, node] of map) { + if (node.type === NodeType.service) { + items.push({ + 'service.name': node.serviceName, + // this should be set, as it's a downstream dependency, and there should be a connection + 'span.destination.service.resource': node.dependencyName!, + }); + } else { + items.push({ + 'span.destination.service.resource': node.dependencyName, + 'span.type': node.spanType, + 'span.subtype': node.spanSubtype, + }); + } + } + + return items; +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_error_document/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_error_document/index.ts new file mode 100644 index 00000000000000..c460acf4c3b3fc --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_error_document/index.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as t from 'io-ts'; +import { rangeQuery } from '@kbn/observability-plugin/server'; +import datemath from '@elastic/datemath'; +import { pick } from 'lodash'; +import { ApmDocumentType } from '../../../../common/document_type'; +import { RollupInterval } from '../../../../common/rollup'; +import { termQuery } from '../../../../common/utils/term_query'; +import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { APMError } from '../../../../typings/es_schemas/ui/apm_error'; + +export const errorRouteRt = t.type({ + start: t.string, + end: t.string, + 'error.grouping_name': t.string, +}); + +export async function getApmErrorDocument({ + arguments: args, + apmEventClient, +}: { + arguments: t.TypeOf; + apmEventClient: APMEventClient; +}) { + const start = datemath.parse(args.start)?.valueOf()!; + const end = datemath.parse(args.end)?.valueOf()!; + + const response = await apmEventClient.search('get_error', { + apm: { + sources: [ + { + documentType: ApmDocumentType.ErrorEvent, + rollupInterval: RollupInterval.None, + }, + ], + }, + body: { + track_total_hits: false, + size: 1, + terminate_after: 1, + query: { + bool: { + filter: [ + ...rangeQuery(start, end), + ...termQuery('error.grouping_name', args['error.grouping_name']), + ], + }, + }, + }, + }); + + const error = response.hits.hits[0]?._source as APMError; + + if (!error) { + return undefined; + } + + return pick( + error, + 'message', + 'error', + '@timestamp', + 'transaction.name', + 'transaction.type', + 'span.name', + 'span.type', + 'span.subtype' + ); +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts new file mode 100644 index 00000000000000..23969c44f2ee15 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts @@ -0,0 +1,319 @@ +/* + * 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 datemath from '@elastic/datemath'; +import { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { + rangeQuery, + ScopedAnnotationsClient, +} from '@kbn/observability-plugin/server'; +import { + ALERT_RULE_PRODUCER, + ALERT_STATUS, + ALERT_STATUS_ACTIVE, +} from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; +import * as t from 'io-ts'; +import { compact, keyBy } from 'lodash'; +import { + ApmMlDetectorType, + getApmMlDetectorType, +} from '../../../../common/anomaly_detection/apm_ml_detectors'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { Environment } from '../../../../common/environment_rt'; +import { SERVICE_NAME } from '../../../../common/es_fields/apm'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; +import { environmentQuery } from '../../../../common/utils/environment_query'; +import { maybe } from '../../../../common/utils/maybe'; +import { termQuery } from '../../../../common/utils/term_query'; +import { anomalySearch } from '../../../lib/anomaly_detection/anomaly_search'; +import { apmMlAnomalyQuery } from '../../../lib/anomaly_detection/apm_ml_anomaly_query'; +import { apmMlJobsQuery } from '../../../lib/anomaly_detection/apm_ml_jobs_query'; +import { getMlJobsWithAPMGroup } from '../../../lib/anomaly_detection/get_ml_jobs_with_apm_group'; +import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client'; +import { MlClient } from '../../../lib/helpers/get_ml_client'; +import { getServiceAnnotations } from '../../services/annotations'; +import { getServiceMetadataDetails } from '../../services/get_service_metadata_details'; + +export const serviceSummaryRouteRt = t.intersection([ + t.type({ + 'service.name': t.string, + start: t.string, + end: t.string, + }), + t.partial({ + 'service.environment': t.string, + 'transaction.type': t.string, + }), +]); + +async function getAnomalies({ + serviceName, + transactionType, + environment, + start, + end, + mlClient, + logger, +}: { + serviceName: string; + transactionType?: string; + environment?: string; + start: number; + end: number; + mlClient?: MlClient; + logger: Logger; +}) { + if (!mlClient) { + return []; + } + + const mlJobs = ( + await getMlJobsWithAPMGroup(mlClient.anomalyDetectors) + ).filter((job) => job.environment !== environment); + + if (!mlJobs.length) { + return []; + } + + const anomaliesResponse = await anomalySearch( + mlClient.mlSystem.mlAnomalySearch, + { + body: { + size: 0, + query: { + bool: { + filter: [ + ...apmMlAnomalyQuery({ + serviceName, + transactionType, + }), + ...rangeQuery(start, end, 'timestamp'), + ...apmMlJobsQuery(mlJobs), + ], + }, + }, + aggs: { + by_timeseries_id: { + composite: { + size: 5000, + sources: asMutableArray([ + { + jobId: { + terms: { + field: 'job_id', + }, + }, + }, + { + detectorIndex: { + terms: { + field: 'detector_index', + }, + }, + }, + { + serviceName: { + terms: { + field: 'partition_field_value', + }, + }, + }, + { + transactionType: { + terms: { + field: 'by_field_value', + }, + }, + }, + ] as const), + }, + aggs: { + record_scores: { + filter: { + term: { + result_type: 'record', + }, + }, + aggs: { + top_anomaly: { + top_metrics: { + metrics: asMutableArray([ + { field: 'record_score' }, + { field: 'actual' }, + { field: 'timestamp' }, + ] as const), + size: 1, + sort: { + record_score: 'desc', + }, + }, + }, + }, + }, + model_lower: { + min: { + field: 'model_lower', + }, + }, + model_upper: { + max: { + field: 'model_upper', + }, + }, + }, + }, + }, + }, + } + ); + + const jobsById = keyBy(mlJobs, (job) => job.jobId); + + const anomalies = + anomaliesResponse.aggregations?.by_timeseries_id.buckets.map((bucket) => { + const jobId = bucket.key.jobId as string; + const job = maybe(jobsById[jobId]); + + if (!job) { + logger.warn(`Could not find job for id ${jobId}`); + return undefined; + } + + const type = getApmMlDetectorType(Number(bucket.key.detectorIndex)); + + // ml failure rate is stored as 0-100, we calculate failure rate as 0-1 + const divider = type === ApmMlDetectorType.txFailureRate ? 100 : 1; + + const metrics = bucket.record_scores.top_anomaly.top[0]?.metrics; + + if (!metrics) { + return undefined; + } + + return { + '@timestamp': new Date(metrics.timestamp as number).toISOString(), + metricName: type.replace('tx', 'transaction'), + 'service.name': bucket.key.serviceName as string, + 'service.environment': job.environment, + 'transaction.type': bucket.key.transactionType as string, + anomalyScore: metrics.record_score, + actualValue: Number(metrics.actual) / divider, + expectedBoundsLower: Number(bucket.model_lower.value) / divider, + expectedBoundsUpper: Number(bucket.model_upper.value) / divider, + }; + }); + + return compact(anomalies); +} + +export interface ServiceSummary { + 'service.name': string; + 'agent.name'?: string; + 'service.version'?: string[]; + 'language.name'?: string; + 'service.framework'?: string; + instances: number; + anomalies: Array<{ + '@timestamp': string; + metricName: string; + 'service.name': string; + 'service.environment': Environment; + 'transaction.type': string; + anomalyScore: string | number | null; + actualValue: number; + expectedBoundsLower: number; + expectedBoundsUpper: number; + }>; + alerts: Array<{ type?: string; started: string }>; + deployments: Array<{ '@timestamp': string }>; +} + +export async function getApmServiceSummary({ + arguments: args, + apmEventClient, + mlClient, + esClient, + annotationsClient, + apmAlertsClient, + logger, +}: { + arguments: t.TypeOf; + apmEventClient: APMEventClient; + mlClient?: MlClient; + esClient: ElasticsearchClient; + annotationsClient?: ScopedAnnotationsClient; + apmAlertsClient: ApmAlertsClient; + logger: Logger; +}): Promise { + const start = datemath.parse(args.start)?.valueOf()!; + const end = datemath.parse(args.end)?.valueOf()!; + + const serviceName = args['service.name']; + const environment = args['service.environment'] || ENVIRONMENT_ALL.value; + const transactionType = args['transaction.type']; + + const [metadataDetails, anomalies, annotations, alerts] = await Promise.all([ + getServiceMetadataDetails({ + apmEventClient, + start, + end, + serviceName, + }), + getAnomalies({ + serviceName, + start, + end, + environment, + mlClient, + logger, + transactionType, + }), + getServiceAnnotations({ + apmEventClient, + start, + end, + searchAggregatedTransactions: true, + client: esClient, + annotationsClient, + environment, + logger, + serviceName, + }), + apmAlertsClient.search({ + size: 100, + // @ts-expect-error types for apm alerts client needs to be reviewed + query: { + bool: { + filter: [ + ...termQuery(ALERT_RULE_PRODUCER, 'apm'), + ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), + ...rangeQuery(start, end), + ...termQuery(SERVICE_NAME, serviceName), + ...environmentQuery(environment), + ], + }, + }, + }), + ]); + + return { + 'service.name': serviceName, + 'agent.name': metadataDetails.service?.agent.name, + 'service.version': metadataDetails.service?.versions, + 'language.name': metadataDetails.service?.agent.name, + 'service.framework': metadataDetails.service?.framework, + instances: metadataDetails.container?.totalNumberInstances ?? 1, + anomalies, + alerts: alerts.hits.hits.map((alert) => ({ + type: alert._source?.['kibana.alert.rule.type'], + started: new Date(alert._source?.['kibana.alert.start']!).toISOString(), + })), + deployments: annotations.annotations.map((annotation) => ({ + '@timestamp': new Date(annotation['@timestamp']).toISOString(), + })), + }; +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/fetch_timeseries.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/fetch_timeseries.ts new file mode 100644 index 00000000000000..cbb9a54d31354a --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/fetch_timeseries.ts @@ -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 type { + AggregationsAggregationContainer, + QueryDslQueryContainer, +} from '@elastic/elasticsearch/lib/api/types'; +import { AggregationResultOf, AggregationResultOfMap } from '@kbn/es-types'; +import { Unionize } from 'utility-types'; +import { ApmDocumentType } from '../../../../common/document_type'; +import { RollupInterval } from '../../../../common/rollup'; +import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; + +type ChangePointResult = AggregationResultOf<{ change_point: any }, unknown>; + +type ValueAggregationMap = Record< + 'value', + Unionize< + Pick< + Required, + 'min' | 'max' | 'sum' | 'bucket_script' | 'avg' + > + > +>; + +interface ApmFetchedTimeseries { + groupBy: string; + data: Array< + { + key: number; + key_as_string: string; + doc_count: number; + } & AggregationResultOfMap + >; + change_point: ChangePointResult; + value: number | null; + unit: string; +} + +export interface FetchSeriesProps { + apmEventClient: APMEventClient; + operationName: string; + documentType: ApmDocumentType; + rollupInterval: RollupInterval; + intervalString: string; + start: number; + end: number; + filter?: QueryDslQueryContainer[]; + groupBy?: string; + aggs: T; + unit: 'ms' | 'rpm' | '%'; +} + +export async function fetchSeries({ + apmEventClient, + operationName, + documentType, + rollupInterval, + intervalString, + start, + end, + filter, + groupBy, + aggs, + unit, +}: FetchSeriesProps): Promise>> { + const response = await apmEventClient.search(operationName, { + apm: { + sources: [{ documentType, rollupInterval }], + }, + body: { + size: 0, + track_total_hits: false, + query: { + bool: { + filter, + }, + }, + aggs: { + groupBy: { + ...(groupBy + ? { + terms: { + field: groupBy, + size: 20, + }, + } + : { + terms: { + field: 'non_existing_field', + missing: '', + }, + }), + aggs: { + ...aggs, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs, + }, + change_point: { + change_point: { + buckets_path: 'timeseries>value', + }, + }, + }, + }, + }, + }, + }); + + if (!response.aggregations?.groupBy) { + return []; + } + + return response.aggregations.groupBy.buckets.map((bucket) => { + return { + groupBy: bucket.key_as_string || String(bucket.key), + data: bucket.timeseries.buckets, + value: + bucket.value?.value === undefined || bucket.value?.value === null + ? null + : Math.round(bucket.value.value), + change_point: bucket.change_point, + unit, + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_error_event_rate.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_error_event_rate.ts new file mode 100644 index 00000000000000..85debffb41742e --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_error_event_rate.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { rangeQuery } from '@kbn/observability-plugin/server'; +import { ApmDocumentType } from '../../../../common/document_type'; +import { RollupInterval } from '../../../../common/rollup'; +import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { fetchSeries } from './fetch_timeseries'; + +export async function getErrorEventRate({ + apmEventClient, + start, + end, + intervalString, + bucketSize, + filter, +}: { + apmEventClient: APMEventClient; + start: number; + end: number; + intervalString: string; + bucketSize: number; + filter: QueryDslQueryContainer[]; +}) { + const bucketSizeInMinutes = bucketSize / 60; + const rangeInMinutes = (end - start) / 1000 / 60; + + return ( + await fetchSeries({ + apmEventClient, + start, + end, + operationName: 'assistant_get_error_event_rate', + unit: 'rpm', + documentType: ApmDocumentType.ErrorEvent, + rollupInterval: RollupInterval.None, + intervalString, + filter: filter.concat(...rangeQuery(start, end)), + aggs: { + value: { + bucket_script: { + buckets_path: { + count: '_count', + }, + script: { + lang: 'painless', + params: { + bucketSizeInMinutes, + }, + source: 'params.count / params.bucketSizeInMinutes', + }, + }, + }, + }, + }) + ).map((fetchedSerie) => { + return { + ...fetchedSerie, + value: + fetchedSerie.value !== null + ? fetchedSerie.value / rangeInMinutes + : null, + data: fetchedSerie.data.map((bucket) => { + return { + x: bucket.key, + y: bucket.value?.value as number, + }; + }), + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_exit_span_failure_rate.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_exit_span_failure_rate.ts new file mode 100644 index 00000000000000..655d03dd87448b --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_exit_span_failure_rate.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ApmDocumentType } from '../../../../common/document_type'; +import { + EVENT_OUTCOME, + SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, +} from '../../../../common/es_fields/apm'; +import { EventOutcome } from '../../../../common/event_outcome'; +import { RollupInterval } from '../../../../common/rollup'; +import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { fetchSeries } from './fetch_timeseries'; + +export async function getExitSpanFailureRate({ + apmEventClient, + start, + end, + intervalString, + filter, + spanDestinationServiceResource, +}: { + apmEventClient: APMEventClient; + start: number; + end: number; + intervalString: string; + bucketSize: number; + filter: QueryDslQueryContainer[]; + spanDestinationServiceResource?: string; +}) { + return ( + await fetchSeries({ + apmEventClient, + start, + end, + operationName: 'assistant_get_exit_span_failure_rate', + unit: '%', + documentType: ApmDocumentType.ServiceDestinationMetric, + rollupInterval: RollupInterval.OneMinute, + intervalString, + filter: filter.concat( + ...rangeQuery(start, end), + ...termQuery( + SPAN_DESTINATION_SERVICE_RESOURCE, + spanDestinationServiceResource + ) + ), + groupBy: SPAN_DESTINATION_SERVICE_RESOURCE, + aggs: { + successful: { + filter: { + terms: { + [EVENT_OUTCOME]: [EventOutcome.success], + }, + }, + aggs: { + count: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + }, + }, + }, + }, + successful_or_failed: { + filter: { + terms: { + [EVENT_OUTCOME]: [EventOutcome.success, EventOutcome.failure], + }, + }, + aggs: { + count: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + }, + }, + }, + }, + value: { + bucket_script: { + buckets_path: { + successful_or_failed: `successful_or_failed>count`, + successful: `successful>count`, + }, + script: + '100 * (1 - (params.successful / params.successful_or_failed))', + }, + }, + }, + }) + ).map((fetchedSerie) => { + return { + ...fetchedSerie, + data: fetchedSerie.data.map((bucket) => { + return { + x: bucket.key, + y: bucket.value?.value as number | null, + }; + }), + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_exit_span_latency.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_exit_span_latency.ts new file mode 100644 index 00000000000000..1d5bcd04cd35c8 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_exit_span_latency.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ApmDocumentType } from '../../../../common/document_type'; +import { + SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, +} from '../../../../common/es_fields/apm'; +import { RollupInterval } from '../../../../common/rollup'; +import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { fetchSeries } from './fetch_timeseries'; + +export async function getExitSpanLatency({ + apmEventClient, + start, + end, + intervalString, + filter, + spanDestinationServiceResource, +}: { + apmEventClient: APMEventClient; + start: number; + end: number; + intervalString: string; + bucketSize: number; + filter: QueryDslQueryContainer[]; + spanDestinationServiceResource?: string; +}) { + return ( + await fetchSeries({ + apmEventClient, + start, + end, + operationName: 'assistant_get_exit_span_latency', + unit: 'rpm', + documentType: ApmDocumentType.ServiceDestinationMetric, + rollupInterval: RollupInterval.OneMinute, + intervalString, + filter: filter.concat( + ...rangeQuery(start, end), + ...termQuery( + SPAN_DESTINATION_SERVICE_RESOURCE, + spanDestinationServiceResource + ) + ), + groupBy: SPAN_DESTINATION_SERVICE_RESOURCE, + aggs: { + count: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + }, + }, + latency: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, + }, + }, + value: { + bucket_script: { + buckets_path: { + latency: 'latency', + count: 'count', + }, + script: '(params.latency / params.count) / 1000', + }, + }, + }, + }) + ).map((fetchedSerie) => { + return { + ...fetchedSerie, + data: fetchedSerie.data.map((bucket) => { + return { + x: bucket.key, + y: bucket.value?.value as number | null, + }; + }), + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_exit_span_throughput.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_exit_span_throughput.ts new file mode 100644 index 00000000000000..dd49c4c7d79f4c --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_exit_span_throughput.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ApmDocumentType } from '../../../../common/document_type'; +import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/es_fields/apm'; +import { RollupInterval } from '../../../../common/rollup'; +import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { fetchSeries } from './fetch_timeseries'; + +export async function getExitSpanThroughput({ + apmEventClient, + start, + end, + intervalString, + bucketSize, + filter, + spanDestinationServiceResource, +}: { + apmEventClient: APMEventClient; + start: number; + end: number; + intervalString: string; + bucketSize: number; + filter: QueryDslQueryContainer[]; + spanDestinationServiceResource?: string; +}) { + const bucketSizeInMinutes = bucketSize / 60; + const rangeInMinutes = (end - start) / 1000 / 60; + + return ( + await fetchSeries({ + apmEventClient, + start, + end, + operationName: 'assistant_get_exit_span_throughput', + unit: 'rpm', + documentType: ApmDocumentType.ServiceDestinationMetric, + rollupInterval: RollupInterval.OneMinute, + intervalString, + filter: filter.concat( + ...rangeQuery(start, end), + ...termQuery( + SPAN_DESTINATION_SERVICE_RESOURCE, + spanDestinationServiceResource + ) + ), + groupBy: SPAN_DESTINATION_SERVICE_RESOURCE, + aggs: { + value: { + bucket_script: { + buckets_path: { + count: '_count', + }, + script: { + lang: 'painless', + params: { + bucketSizeInMinutes, + }, + source: 'params.count / params.bucketSizeInMinutes', + }, + }, + }, + }, + }) + ).map((fetchedSerie) => { + return { + ...fetchedSerie, + value: + fetchedSerie.value !== null + ? fetchedSerie.value / rangeInMinutes + : null, + data: fetchedSerie.data.map((bucket) => { + return { + x: bucket.key, + y: bucket.value?.value as number, + }; + }), + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_transaction_failure_rate.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_transaction_failure_rate.ts new file mode 100644 index 00000000000000..b4b0704ce6ea92 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_transaction_failure_rate.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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ApmDocumentType } from '../../../../common/document_type'; +import { TRANSACTION_TYPE } from '../../../../common/es_fields/apm'; +import { RollupInterval } from '../../../../common/rollup'; +import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { getOutcomeAggregation } from '../../../lib/helpers/transaction_error_rate'; +import { fetchSeries } from './fetch_timeseries'; + +export async function getTransactionFailureRate({ + apmEventClient, + start, + end, + intervalString, + filter, + transactionType, +}: { + apmEventClient: APMEventClient; + start: number; + end: number; + intervalString: string; + bucketSize: number; + filter: QueryDslQueryContainer[]; + transactionType?: string; +}) { + return ( + await fetchSeries({ + apmEventClient, + start, + end, + operationName: 'assistant_get_transaction_failure_rate', + unit: '%', + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + intervalString, + filter: filter.concat( + ...rangeQuery(start, end), + ...termQuery(TRANSACTION_TYPE, transactionType) + ), + groupBy: 'transaction.type', + aggs: { + ...getOutcomeAggregation(ApmDocumentType.TransactionMetric), + value: { + bucket_script: { + buckets_path: { + successful_or_failed: 'successful_or_failed>_count', + successful: 'successful>_count', + }, + script: + '100 * (1 - (params.successful / params.successful_or_failed))', + }, + }, + }, + }) + ).map((fetchedSerie) => { + return { + ...fetchedSerie, + data: fetchedSerie.data.map((bucket) => { + return { + x: bucket.key, + y: bucket.value?.value as number | null, + }; + }), + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_transaction_latency.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_transaction_latency.ts new file mode 100644 index 00000000000000..8cbb6dd74d2d9f --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_transaction_latency.ts @@ -0,0 +1,80 @@ +/* + * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ApmDocumentType } from '../../../../common/document_type'; +import { + TRANSACTION_DURATION_HISTOGRAM, + TRANSACTION_TYPE, +} from '../../../../common/es_fields/apm'; +import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; +import { RollupInterval } from '../../../../common/rollup'; +import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { getLatencyAggregation } from '../../../lib/helpers/latency_aggregation_type'; +import { fetchSeries } from './fetch_timeseries'; + +export async function getTransactionLatency({ + apmEventClient, + start, + end, + intervalString, + filter, + transactionType, + latencyAggregationType, +}: { + apmEventClient: APMEventClient; + start: number; + end: number; + intervalString: string; + bucketSize: number; + filter: QueryDslQueryContainer[]; + transactionType?: string; + latencyAggregationType: LatencyAggregationType; +}) { + return ( + await fetchSeries({ + apmEventClient, + start, + end, + operationName: 'assistant_get_transaction_latencyu', + unit: 'rpm', + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + intervalString, + filter: filter.concat( + ...rangeQuery(start, end), + ...termQuery(TRANSACTION_TYPE, transactionType) + ), + groupBy: 'transaction.type', + aggs: { + ...getLatencyAggregation( + latencyAggregationType, + TRANSACTION_DURATION_HISTOGRAM + ), + value: { + bucket_script: { + buckets_path: { + latency: 'latency', + }, + script: 'params.latency / 1000', + }, + }, + }, + }) + ).map((fetchedSerie) => { + return { + ...fetchedSerie, + data: fetchedSerie.data.map((bucket) => { + return { + x: bucket.key, + y: bucket.value?.value as number | null, + }; + }), + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_transaction_throughput.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_transaction_throughput.ts new file mode 100644 index 00000000000000..919f2b63e51659 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/get_transaction_throughput.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ApmDocumentType } from '../../../../common/document_type'; +import { TRANSACTION_TYPE } from '../../../../common/es_fields/apm'; +import { RollupInterval } from '../../../../common/rollup'; +import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { fetchSeries } from './fetch_timeseries'; + +export async function getTransactionThroughput({ + apmEventClient, + start, + end, + intervalString, + bucketSize, + filter, + transactionType, +}: { + apmEventClient: APMEventClient; + start: number; + end: number; + intervalString: string; + bucketSize: number; + filter: QueryDslQueryContainer[]; + transactionType?: string; +}) { + const bucketSizeInMinutes = bucketSize / 60; + const rangeInMinutes = (end - start) / 1000 / 60; + + return ( + await fetchSeries({ + apmEventClient, + start, + end, + operationName: 'assistant_get_transaction_throughput', + unit: 'rpm', + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + intervalString, + filter: filter.concat( + ...rangeQuery(start, end), + ...termQuery(TRANSACTION_TYPE, transactionType) + ), + groupBy: 'transaction.type', + aggs: { + value: { + bucket_script: { + buckets_path: { + count: '_count', + }, + script: { + lang: 'painless', + params: { + bucketSizeInMinutes, + }, + source: 'params.count / params.bucketSizeInMinutes', + }, + }, + }, + }, + }) + ).map((fetchedSerie) => { + return { + ...fetchedSerie, + value: + fetchedSerie.value !== null + ? fetchedSerie.value / rangeInMinutes + : null, + data: fetchedSerie.data.map((bucket) => { + return { + x: bucket.key, + y: bucket.value?.value as number, + }; + }), + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/index.ts new file mode 100644 index 00000000000000..0c95cf02313680 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_timeseries/index.ts @@ -0,0 +1,234 @@ +/* + * 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 datemath from '@elastic/datemath'; +import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; +import * as t from 'io-ts'; +import { SERVICE_NAME } from '../../../../common/es_fields/apm'; +import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; +import { environmentQuery } from '../../../../common/utils/environment_query'; +import { getBucketSize } from '../../../../common/utils/get_bucket_size'; +import { termQuery } from '../../../../common/utils/term_query'; +import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { environmentRt } from '../../default_api_types'; +import { getErrorEventRate } from './get_error_event_rate'; +import { getExitSpanFailureRate } from './get_exit_span_failure_rate'; +import { getExitSpanLatency } from './get_exit_span_latency'; +import { getExitSpanThroughput } from './get_exit_span_throughput'; +import { getTransactionFailureRate } from './get_transaction_failure_rate'; +import { getTransactionLatency } from './get_transaction_latency'; +import { getTransactionThroughput } from './get_transaction_throughput'; + +export enum ApmTimeseriesType { + transactionThroughput = 'transaction_throughput', + transactionLatency = 'transaction_latency', + transactionFailureRate = 'transaction_failure_rate', + exitSpanThroughput = 'exit_span_throughput', + exitSpanLatency = 'exit_span_latency', + exitSpanFailureRate = 'exit_span_failure_rate', + errorEventRate = 'error_event_rate', +} + +export const getApmTimeseriesRt = t.type({ + stats: t.array( + t.intersection([ + t.type({ + 'service.environment': environmentRt.props.environment, + 'service.name': t.string, + title: t.string, + timeseries: t.union([ + t.intersection([ + t.type({ + name: t.union([ + t.literal(ApmTimeseriesType.transactionThroughput), + t.literal(ApmTimeseriesType.transactionFailureRate), + ]), + }), + t.partial({ + 'transaction.type': t.string, + }), + ]), + t.intersection([ + t.type({ + name: t.union([ + t.literal(ApmTimeseriesType.exitSpanThroughput), + t.literal(ApmTimeseriesType.exitSpanFailureRate), + t.literal(ApmTimeseriesType.exitSpanLatency), + ]), + }), + t.partial({ + 'span.destination.service.resource': t.string, + }), + ]), + t.intersection([ + t.type({ + name: t.literal(ApmTimeseriesType.transactionLatency), + function: t.union([ + t.literal(LatencyAggregationType.avg), + t.literal(LatencyAggregationType.p95), + t.literal(LatencyAggregationType.p99), + ]), + }), + t.partial({ + 'transaction.type': t.string, + }), + ]), + t.type({ + name: t.literal(ApmTimeseriesType.errorEventRate), + }), + ]), + }), + t.partial({ + filter: t.string, + offset: t.string, + }), + ]) + ), + start: t.string, + end: t.string, +}); + +type ApmTimeseriesArgs = t.TypeOf; + +export interface ApmTimeseries { + stat: ApmTimeseriesArgs['stats'][number]; + group: string; + id: string; + data: Array<{ x: number; y: number | null }>; + value: number | null; + start: number; + end: number; + unit: string; + changes: Array<{ + change_point?: number | undefined; + r_value?: number | undefined; + trend?: string | undefined; + p_value: number; + date: string | undefined; + type: string; + }>; +} + +export async function getApmTimeseries({ + arguments: args, + apmEventClient, +}: { + arguments: t.TypeOf; + apmEventClient: APMEventClient; +}): Promise { + const start = datemath.parse(args.start)!.valueOf(); + const end = datemath.parse(args.end)!.valueOf(); + + const { bucketSize, intervalString } = getBucketSize({ + start, + end, + numBuckets: 100, + }); + + const sharedParameters = { + apmEventClient, + start, + end, + bucketSize, + intervalString, + }; + + return ( + await Promise.all( + args.stats.map(async (stat) => { + const parameters = { + ...sharedParameters, + filter: [ + ...rangeQuery(start, end), + ...termQuery(SERVICE_NAME, stat['service.name']), + ...kqlQuery(stat.filter), + ...environmentQuery(stat['service.environment']), + ], + }; + const name = stat.timeseries.name; + + async function fetchSeriesForStat() { + switch (name) { + case ApmTimeseriesType.transactionThroughput: + return await getTransactionThroughput({ + ...parameters, + transactionType: stat.timeseries['transaction.type'], + }); + + case ApmTimeseriesType.transactionFailureRate: + return await getTransactionFailureRate({ + ...parameters, + transactionType: stat.timeseries['transaction.type'], + }); + + case ApmTimeseriesType.transactionLatency: + return await getTransactionLatency({ + ...parameters, + transactionType: stat.timeseries['transaction.type'], + latencyAggregationType: stat.timeseries.function, + }); + + case ApmTimeseriesType.exitSpanThroughput: + return await getExitSpanThroughput({ + ...parameters, + spanDestinationServiceResource: + stat.timeseries['span.destination.service.resource'], + }); + + case ApmTimeseriesType.exitSpanFailureRate: + return await getExitSpanFailureRate({ + ...parameters, + spanDestinationServiceResource: + stat.timeseries['span.destination.service.resource'], + }); + + case ApmTimeseriesType.exitSpanLatency: + return await getExitSpanLatency({ + ...parameters, + spanDestinationServiceResource: + stat.timeseries['span.destination.service.resource'], + }); + + case ApmTimeseriesType.errorEventRate: + return await getErrorEventRate(parameters); + } + } + + const allFetchedSeries = await fetchSeriesForStat(); + return allFetchedSeries.map((series) => ({ ...series, stat })); + }) + ) + ).flatMap((statResults) => + statResults.flatMap((statResult) => { + const changePointType = Object.keys( + statResult.change_point?.type ?? {} + )?.[0]; + + return { + stat: statResult.stat, + group: statResult.stat.title, + id: statResult.groupBy, + data: statResult.data, + value: statResult.value, + start, + end, + unit: statResult.unit, + changes: [ + ...(changePointType && changePointType !== 'indeterminable' + ? [ + { + date: statResult.change_point.bucket?.key, + type: changePointType, + ...statResult.change_point.type[changePointType], + }, + ] + : []), + ], + }; + }) + ); +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/route.ts b/x-pack/plugins/apm/server/routes/assistant_functions/route.ts new file mode 100644 index 00000000000000..bd2d32c90907ed --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/route.ts @@ -0,0 +1,193 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; +import * as t from 'io-ts'; +import { omit } from 'lodash'; +import type { APMError } from '../../../typings/es_schemas/ui/apm_error'; +import { getApmAlertsClient } from '../../lib/helpers/get_apm_alerts_client'; +import { getApmEventClient } from '../../lib/helpers/get_apm_event_client'; +import { getMlClient } from '../../lib/helpers/get_ml_client'; +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; +import { + CorrelationValue, + correlationValuesRouteRt, + getApmCorrelationValues, +} from './get_apm_correlation_values'; +import { + type APMDownstreamDependency, + downstreamDependenciesRouteRt, + getAssistantDownstreamDependencies, +} from './get_apm_downstream_dependencies'; +import { errorRouteRt, getApmErrorDocument } from './get_apm_error_document'; +import { + getApmServiceSummary, + type ServiceSummary, + serviceSummaryRouteRt, +} from './get_apm_service_summary'; +import { + type ApmTimeseries, + getApmTimeseries, + getApmTimeseriesRt, +} from './get_apm_timeseries'; + +const getApmTimeSeriesRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/assistant/get_apm_timeseries', + options: { + tags: ['access:apm', 'access:ai_assistant'], + }, + params: t.type({ + body: getApmTimeseriesRt, + }), + handler: async ( + resources + ): Promise<{ + content: Array>; + data: ApmTimeseries[]; + }> => { + const body = resources.params.body; + + const apmEventClient = await getApmEventClient(resources); + + const timeseries = await getApmTimeseries({ + apmEventClient, + arguments: body, + }); + + return { + content: timeseries.map( + (series): Omit => omit(series, 'data') + ), + data: timeseries, + }; + }, +}); + +const getApmServiceSummaryRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/assistant/get_service_summary', + options: { + tags: ['access:apm', 'access:ai_assistant'], + }, + params: t.type({ + query: serviceSummaryRouteRt, + }), + handler: async ( + resources + ): Promise<{ + content: ServiceSummary; + }> => { + const args = resources.params.query; + + const { context, request, plugins, logger } = resources; + + const [ + apmEventClient, + annotationsClient, + esClient, + apmAlertsClient, + mlClient, + ] = await Promise.all([ + getApmEventClient(resources), + plugins.observability.setup.getScopedAnnotationsClient(context, request), + context.core.then( + (coreContext): ElasticsearchClient => + coreContext.elasticsearch.client.asCurrentUser + ), + getApmAlertsClient(resources), + getMlClient(resources), + ]); + + return { + content: await getApmServiceSummary({ + apmEventClient, + annotationsClient, + esClient, + apmAlertsClient, + mlClient, + logger, + arguments: args, + }), + }; + }, +}); + +const getDownstreamDependenciesRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/assistant/get_downstream_dependencies', + params: t.type({ + query: downstreamDependenciesRouteRt, + }), + options: { + tags: ['access:apm'], + }, + handler: async ( + resources + ): Promise<{ content: APMDownstreamDependency[] }> => { + const { params } = resources; + const apmEventClient = await getApmEventClient(resources); + const { query } = params; + + return { + content: await getAssistantDownstreamDependencies({ + arguments: query, + apmEventClient, + }), + }; + }, +}); + +const getApmCorrelationValuesRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/assistant/get_correlation_values', + params: t.type({ + body: correlationValuesRouteRt, + }), + options: { + tags: ['access:apm'], + }, + handler: async (resources): Promise<{ content: CorrelationValue[] }> => { + const { params } = resources; + const apmEventClient = await getApmEventClient(resources); + const { body } = params; + + return { + content: await getApmCorrelationValues({ + arguments: body, + apmEventClient, + }), + }; + }, +}); + +const getApmErrorDocRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/assistant/get_error_document', + params: t.type({ + query: errorRouteRt, + }), + options: { + tags: ['access:apm'], + }, + handler: async ( + resources + ): Promise<{ content: Partial | undefined }> => { + const { params } = resources; + const apmEventClient = await getApmEventClient(resources); + const { query } = params; + + return { + content: await getApmErrorDocument({ + apmEventClient, + arguments: query, + }), + }; + }, +}); + +export const assistantRouteRepository = { + ...getApmTimeSeriesRoute, + ...getApmServiceSummaryRoute, + ...getApmErrorDocRoute, + ...getApmCorrelationValuesRoute, + ...getDownstreamDependenciesRoute, +}; diff --git a/x-pack/plugins/apm/server/routes/service_groups/get_service_group_alerts.ts b/x-pack/plugins/apm/server/routes/service_groups/get_service_group_alerts.ts index 06ccd8f057a3ef..f38eeefc7ccfa7 100644 --- a/x-pack/plugins/apm/server/routes/service_groups/get_service_group_alerts.ts +++ b/x-pack/plugins/apm/server/routes/service_groups/get_service_group_alerts.ts @@ -7,11 +7,7 @@ import { kqlQuery } from '@kbn/observability-plugin/server'; import { ALERT_RULE_PRODUCER, ALERT_STATUS } from '@kbn/rule-data-utils'; -import { - AggregationsCardinalityAggregate, - AggregationsFilterAggregate, - QueryDslQueryContainer, -} from '@elastic/elasticsearch/lib/api/types'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { Logger } from '@kbn/core/server'; import { ApmPluginRequestHandlerContext } from '../typings'; import { SavedServiceGroup } from '../../../common/service_groups'; @@ -41,6 +37,7 @@ export async function getServiceGroupAlerts({ }, {}); const params = { size: 0, + track_total_hits: false, query: { bool: { filter: [ @@ -66,17 +63,7 @@ export async function getServiceGroupAlerts({ }; const result = await apmAlertsClient.search(params); - interface ServiceGroupsAggResponse { - buckets: Record< - string, - AggregationsFilterAggregate & { - alerts_count: AggregationsCardinalityAggregate; - } - >; - } - - const { buckets: filterAggBuckets } = (result.aggregations - ?.service_groups ?? { buckets: {} }) as ServiceGroupsAggResponse; + const filterAggBuckets = result.aggregations?.service_groups.buckets ?? {}; const serviceGroupAlertsCount = Object.keys(filterAggBuckets).reduce< Record diff --git a/x-pack/plugins/apm/server/routes/services/get_service_transaction_groups_alerts.ts b/x-pack/plugins/apm/server/routes/services/get_service_transaction_groups_alerts.ts index 4f1efb6e46a671..9ad556df126ba6 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_transaction_groups_alerts.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_transaction_groups_alerts.ts @@ -32,20 +32,13 @@ export type ServiceTransactionGroupAlertsResponse = Array<{ alertsCount: number; }>; -interface ServiceTransactionGroupAlertsAggResponse { - buckets: Array<{ - key: string; - doc_count: number; - }>; -} - const RuleAggregationType = { [LatencyAggregationType.avg]: AggregationType.Avg, [LatencyAggregationType.p99]: AggregationType.P99, [LatencyAggregationType.p95]: AggregationType.P95, } as const; -export async function getServiceTranactionGroupsAlerts({ +export async function getServiceTransactionGroupsAlerts({ apmAlertsClient, kuery, transactionType, @@ -68,6 +61,7 @@ export async function getServiceTranactionGroupsAlerts({ const params = { size: 0, + track_total_hits: false, query: { bool: { filter: [ @@ -117,8 +111,7 @@ export async function getServiceTranactionGroupsAlerts({ const response = await apmAlertsClient.search(params); - const { buckets } = response.aggregations - ?.transaction_groups as ServiceTransactionGroupAlertsAggResponse; + const buckets = response?.aggregations?.transaction_groups.buckets ?? []; const servicesTransactionGroupsAlertsCount = buckets.map((bucket) => ({ diff --git a/x-pack/plugins/apm/server/routes/services/get_services/get_service_alerts.ts b/x-pack/plugins/apm/server/routes/services/get_services/get_service_alerts.ts index a8968f9bbb3904..e47d9b61124cce 100644 --- a/x-pack/plugins/apm/server/routes/services/get_services/get_service_alerts.ts +++ b/x-pack/plugins/apm/server/routes/services/get_services/get_service_alerts.ts @@ -5,10 +5,6 @@ * 2.0. */ -import { - AggregationsCardinalityAggregate, - AggregationsFilterAggregate, -} from '@elastic/elasticsearch/lib/api/types'; import { kqlQuery, termQuery, @@ -27,15 +23,6 @@ import { environmentQuery } from '../../../../common/utils/environment_query'; import { MAX_NUMBER_OF_SERVICES } from './get_services_items'; import { serviceGroupWithOverflowQuery } from '../../../lib/service_group_query_with_overflow'; -interface ServiceAggResponse { - buckets: Array< - AggregationsFilterAggregate & { - key: string; - alerts_count: AggregationsCardinalityAggregate; - } - >; -} - export type ServiceAlertsResponse = Array<{ serviceName: string; alertsCount: number; @@ -62,6 +49,7 @@ export async function getServicesAlerts({ }): Promise { const params = { size: 0, + track_total_hits: false, query: { bool: { filter: [ @@ -94,9 +82,7 @@ export async function getServicesAlerts({ const result = await apmAlertsClient.search(params); - const { buckets: filterAggBuckets } = (result.aggregations?.services ?? { - buckets: [], - }) as ServiceAggResponse; + const filterAggBuckets = result.aggregations?.services.buckets ?? []; const servicesAlertsCount: Array<{ serviceName: string; diff --git a/x-pack/plugins/apm/server/routes/transactions/route.ts b/x-pack/plugins/apm/server/routes/transactions/route.ts index e6c28523b0d7a3..888cb82820cc1f 100644 --- a/x-pack/plugins/apm/server/routes/transactions/route.ts +++ b/x-pack/plugins/apm/server/routes/transactions/route.ts @@ -32,7 +32,7 @@ import { getServiceTransactionGroups, ServiceTransactionGroupsResponse, } from '../services/get_service_transaction_groups'; -import { getServiceTranactionGroupsAlerts } from '../services/get_service_transaction_groups_alerts'; +import { getServiceTransactionGroupsAlerts } from '../services/get_service_transaction_groups_alerts'; import { getServiceTransactionGroupDetailedStatisticsPeriods, ServiceTransactionGroupDetailedStatisticsResponse, @@ -128,7 +128,7 @@ const transactionGroupsMainStatisticsRoute = createApmServerRoute({ useDurationSummary, ...commonProps, }), - getServiceTranactionGroupsAlerts({ + getServiceTransactionGroupsAlerts({ apmAlertsClient, ...commonProps, }), diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index b1d2625b5340b8..6e83404b2c9055 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -39,6 +39,7 @@ export interface APMRouteCreateOptions { | 'access:ml:canGetJobs' | 'access:ml:canCreateJob' | 'access:ml:canCloseJob' + | 'access:ai_assistant' >; body?: { accepts: Array<'application/json' | 'multipart/form-data'> }; disableTelemetry?: boolean; diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 627b5ccd2e7b4e..50ea8e402ce833 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -24,18 +24,18 @@ import { useCaseConfigureResponse } from '../configure_cases/__mock__'; import { TestProviders } from '../../common/mock'; import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import { useGetTags } from '../../containers/use_get_tags'; +import { useAvailableCasesOwners } from '../app/use_available_owners'; jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_get_supported_action_connectors'); jest.mock('../../containers/configure/use_configure'); jest.mock('../markdown_editor/plugins/lens/use_lens_draft_comment'); -jest.mock('../app/use_available_owners', () => ({ - useAvailableCasesOwners: () => ['securitySolution', 'observability'], -})); +jest.mock('../app/use_available_owners'); const useGetTagsMock = useGetTags as jest.Mock; const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; +const useAvailableOwnersMock = useAvailableCasesOwners as jest.Mock; const initialCaseValue: FormProps = { description: '', @@ -77,6 +77,7 @@ describe('CreateCaseForm', () => { beforeEach(() => { jest.clearAllMocks(); + useAvailableOwnersMock.mockReturnValue(['securitySolution', 'observability']); useGetTagsMock.mockReturnValue({ data: ['test'] }); useGetConnectorsMock.mockReturnValue({ isLoading: false, data: connectorsMock }); useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); @@ -138,6 +139,18 @@ describe('CreateCaseForm', () => { expect(wrapper.find(`[data-test-subj="caseOwnerSelector"]`).exists()).toBeTruthy(); }); + it('does not render solution picker when only one owner is available', async () => { + useAvailableOwnersMock.mockReturnValue(['securitySolution']); + + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="caseOwnerSelector"]`).exists()).toBeFalsy(); + }); + it('hides the sync alerts toggle', () => { const { queryByText } = render( diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index c36f6a119a5f9f..1c1668ac528fbf 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -86,9 +86,8 @@ export const CreateCaseFormFields: React.FC = React.m ({ connectors, isLoadingConnectors, withSteps, owner, draftStorageKey }) => { const { isSubmitting } = useFormContext(); const { isSyncAlertsEnabled, caseAssignmentAuthorized } = useCasesFeatures(); - const availableOwners = useAvailableCasesOwners(); - const canShowCaseSolutionSelection = !owner.length && availableOwners.length; + const canShowCaseSolutionSelection = !owner.length && availableOwners.length > 1; const firstStep = useMemo( () => ({ diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index fea7f7a019a864..f8c1a503928892 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -26,6 +26,7 @@ import { getConnectorsFormDeserializer, getConnectorsFormSerializer, } from '../utils'; +import { useAvailableCasesOwners } from '../app/use_available_owners'; import type { CaseAttachmentsWithoutOwner } from '../../types'; import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import { useCreateCaseWithAttachmentsTransaction } from '../../common/apm/use_cases_transactions'; @@ -68,6 +69,7 @@ export const FormContext: React.FC = ({ const { mutateAsync: createAttachments } = useCreateAttachments(); const { mutateAsync: pushCaseToExternalService } = usePostPushToService(); const { startTransaction } = useCreateCaseWithAttachmentsTransaction(); + const availableOwners = useAvailableCasesOwners(); const submitCase = useCallback( async ( @@ -82,6 +84,7 @@ export const FormContext: React.FC = ({ if (isValid) { const { selectedOwner, ...userFormData } = dataWithoutConnectorId; const caseConnector = getConnectorById(dataConnectorId, connectors); + const defaultOwner = owner[0] ?? availableOwners[0]; startTransaction({ appId, attachments }); @@ -94,7 +97,7 @@ export const FormContext: React.FC = ({ ...userFormData, connector: connectorToUpdate, settings: { syncAlerts }, - owner: selectedOwner ?? owner[0], + owner: selectedOwner ?? defaultOwner, }, }); @@ -131,6 +134,7 @@ export const FormContext: React.FC = ({ attachments, postCase, owner, + availableOwners, afterCaseCreated, onSuccess, createAttachments, 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 a08ebb48fdd019..6ef38730dcf34d 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -59,6 +59,7 @@ export interface CloudPostureIntegrationProps { disabled?: boolean; icon?: string; tooltip?: string; + isBeta?: boolean; }>; } @@ -91,6 +92,7 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = { defaultMessage: 'CIS GCP', }), icon: 'logoGCP', + isBeta: true, }, { type: CLOUDBEAT_AZURE, diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx index 97018a072abd0e..c9ba38eccb2e62 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/csp_boxed_radio_group.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useEuiTheme, EuiButton, EuiRadio, EuiToolTip } from '@elastic/eui'; +import { useEuiTheme, EuiButton, EuiRadio, EuiToolTip, EuiBetaBadge } from '@elastic/eui'; import { css } from '@emotion/react'; export interface CspRadioGroupProps { @@ -23,6 +23,7 @@ interface CspRadioOption { label: string; icon?: string; tooltip?: string; + isBeta?: boolean; } export const RadioGroup = ({ @@ -57,7 +58,7 @@ export const RadioGroup = ({ content={option.tooltip} anchorProps={{ style: { - flexGrow: 1, + flex: '1 1 0', }, }} > @@ -105,6 +106,15 @@ export const RadioGroup = ({ checked={isChecked} onChange={() => {}} /> + {option.isBeta && ( +
+ +
+ )} ); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx index 9d699ea31107fa..88f6ff77fde21f 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx @@ -335,8 +335,8 @@ export const GcpCredentialsForm = ({ updatePolicy( getPosturePolicy(newPolicy, input.type, { setup_access: { - // Restoring last manual credentials type or defaulting to the first option - value: lastSetupAccessType.current || SETUP_ACCESS_MANUAL, + // Restoring last manual credentials type + value: SETUP_ACCESS_MANUAL, type: 'text', }, // Restoring fields from manual setup format if any diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts index 323ab6aa7ef053..b710741652b6c7 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.test.ts @@ -5,8 +5,14 @@ * 2.0. */ -import { getMaxPackageName, getPostureInputHiddenVars, getPosturePolicy } from './utils'; +import { + getMaxPackageName, + getPostureInputHiddenVars, + getPosturePolicy, + getCspmCloudShellDefaultValue, +} from './utils'; import { getMockPolicyAWS, getMockPolicyK8s, getMockPolicyEKS } from './mocks'; +import type { PackageInfo } from '@kbn/fleet-plugin/common'; describe('getPosturePolicy', () => { for (const [name, getPolicy, expectedVars] of [ @@ -123,3 +129,126 @@ describe('getMaxPackageName', () => { expect(result).toBe('kspm-1'); }); }); + +describe('getCspmCloudShellDefaultValue', () => { + it('should return empty string when policy_templates is missing', () => { + const packagePolicy = { name: 'test' } as PackageInfo; + + const result = getCspmCloudShellDefaultValue(packagePolicy); + + expect(result).toBe(''); + }); + + it('should return empty string when policy_templates.name is not cspm', () => { + const packagePolicy = { name: 'test', policy_templates: [{ name: 'kspm' }] } as PackageInfo; + + const result = getCspmCloudShellDefaultValue(packagePolicy); + + expect(result).toBe(''); + }); + + it('should return empty string when policy_templates.inputs is missing', () => { + const packagePolicy = { name: 'test', policy_templates: [{ name: 'cspm' }] } as PackageInfo; + + const result = getCspmCloudShellDefaultValue(packagePolicy); + + expect(result).toBe(''); + }); + + it('should return empty string when policy_templates.inputs is empty', () => { + const packagePolicy = { + name: 'test', + policy_templates: [ + { + title: '', + description: '', + name: 'cspm', + inputs: [{}], + }, + ], + } as PackageInfo; + + const result = getCspmCloudShellDefaultValue(packagePolicy); + + expect(result).toBe(''); + }); + + it('should return empty string when policy_templates.inputs is undefined', () => { + const packagePolicy = { + name: 'test', + policy_templates: [ + { + title: '', + description: '', + name: 'cspm', + inputs: undefined, + }, + ], + } as PackageInfo; + + const result = getCspmCloudShellDefaultValue(packagePolicy); + + expect(result).toBe(''); + }); + + it('should return empty string when policy_templates.inputs.vars does not have cloud_shell_url', () => { + const packagePolicy = { + name: 'test', + policy_templates: [ + { + title: '', + description: '', + name: 'cspm', + inputs: [{ vars: [{ name: 'cloud_shell_url_FAKE' }] }], + }, + ], + } as PackageInfo; + + const result = getCspmCloudShellDefaultValue(packagePolicy); + + expect(result).toBe(''); + }); + + it('should return empty string when policy_templates.inputs.varshave cloud_shell_url but no default', () => { + const packagePolicy = { + name: 'test', + policy_templates: [ + { + title: '', + description: '', + name: 'cspm', + inputs: [{ vars: [{ name: 'cloud_shell_url' }] }], + }, + ], + } as PackageInfo; + + const result = getCspmCloudShellDefaultValue(packagePolicy); + + expect(result).toBe(''); + }); + + it('should cloud shell url when policy_templates.inputs.vars have cloud_shell_url', () => { + const packagePolicy = { + name: 'test', + policy_templates: [ + { + title: '', + description: '', + name: 'cspm', + inputs: [ + { + vars: [ + { name: 'cloud_shell_url_FAKE', default: 'URL_FAKE' }, + { name: 'cloud_shell_url', default: 'URL' }, + ], + }, + ], + }, + ], + } as PackageInfo; + + const result = getCspmCloudShellDefaultValue(packagePolicy); + + expect(result).toBe('URL'); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts index 6376f2b8286703..7d4233b8016df6 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts @@ -207,6 +207,7 @@ export const getPolicyTemplateInputOptions = (policyTemplate: CloudSecurityPolic label: o.name, icon: o.icon, disabled: o.disabled, + isBeta: o.isBeta, })); export const getMaxPackageName = ( diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/dataset_selector.tsx b/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/dataset_selector.tsx index aa1fb057bbd322..444ec69c9d2662 100644 --- a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/dataset_selector.tsx +++ b/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/dataset_selector.tsx @@ -168,6 +168,7 @@ export function DatasetSelector({ onPanelChange={changePanel} className="eui-yScroll" css={contextMenuStyles} + data-test-subj="datasetSelectorContextMenu" size="s" /> diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_list.tsx b/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_list.tsx index 9decd3732fda4f..134bd7616ac8a8 100644 --- a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_list.tsx +++ b/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_list.tsx @@ -44,6 +44,7 @@ export const DatasetsList = ({ if (hasError) { return ( {noDatasetsLabel}} diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_popover.tsx b/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_popover.tsx index d6c9771da019df..7428ffbd476123 100644 --- a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_popover.tsx +++ b/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_popover.tsx @@ -45,7 +45,8 @@ export const DatasetsPopover = ({ return ( {iconType ? ( @@ -74,7 +75,12 @@ export const DatasetsPopover = ({ {...(isMobile && { display: 'block' })} {...props} > - + <span>{selectDatasetLabel}</span> diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_skeleton.tsx b/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_skeleton.tsx index be9464204497d3..8a9ee61d8e434a 100644 --- a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_skeleton.tsx +++ b/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/datasets_skeleton.tsx @@ -10,7 +10,7 @@ import { EuiPanel, EuiSkeletonText } from '@elastic/eui'; import { uncategorizedLabel } from '../constants'; export const DatasetSkeleton = () => ( - + ); diff --git a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/integrations_list_status.tsx b/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/integrations_list_status.tsx index 418ccbefffd000..42ff78fdd3e83f 100644 --- a/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/integrations_list_status.tsx +++ b/x-pack/plugins/discover_log_explorer/public/components/dataset_selector/sub_components/integrations_list_status.tsx @@ -34,6 +34,7 @@ export const IntegrationsListStatus = ({ if (hasError) { return ( + , + 'data-test-subj': integration.id, panel: integration.id, ...(isLastIntegration && { buttonRef: spyRef }), }); @@ -85,6 +86,7 @@ export const createAllLogDatasetsItem = ({ onClick }: { onClick(): void }) => { const allLogDataset = Dataset.createAllLogsDataset(); return { name: allLogDataset.title, + 'data-test-subj': 'allLogDatasets', icon: allLogDataset.iconType && , onClick, }; @@ -93,6 +95,7 @@ export const createAllLogDatasetsItem = ({ onClick }: { onClick(): void }) => { export const createUnmanagedDatasetsItem = ({ onClick }: { onClick: LoadDatasets }) => { return { name: uncategorizedLabel, + 'data-test-subj': 'unmanagedDatasets', icon: , onClick, panel: UNMANAGED_STREAMS_PANEL_ID, @@ -103,5 +106,6 @@ export const createIntegrationStatusItem = (props: IntegrationsListStatusProps) return { disabled: true, name: , + 'data-test-subj': 'integrationStatusItem', }; }; diff --git a/x-pack/plugins/discover_log_explorer/public/customizations/custom_dataset_filters.tsx b/x-pack/plugins/discover_log_explorer/public/customizations/custom_dataset_filters.tsx index f2e1114eeefc76..a669ebea339421 100644 --- a/x-pack/plugins/discover_log_explorer/public/customizations/custom_dataset_filters.tsx +++ b/x-pack/plugins/discover_log_explorer/public/customizations/custom_dataset_filters.tsx @@ -12,6 +12,8 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { useControlPanels } from '../hooks/use_control_panels'; import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; +const DATASET_FILTERS_CUSTOMIZATION_ID = 'datasetFiltersCustomization'; + interface CustomDatasetFiltersProps { logExplorerProfileStateService: LogExplorerProfileStateService; data: DataPublicPluginStart; @@ -27,7 +29,7 @@ const CustomDatasetFilters = ({ ); return ( - + ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/getting_started.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/getting_started.tsx new file mode 100644 index 00000000000000..15d6108de4cd86 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/getting_started.tsx @@ -0,0 +1,404 @@ +/* + * 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 } from 'react'; + +import { css } from '@emotion/react'; +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiSplitPanel, + EuiText, + EuiThemeProvider, + EuiTitle, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { + SelectClientPanel, + LanguageClientPanel, + InstallClientPanel, + OverviewPanel, + CodeBox, +} from '@kbn/search-api-panels'; + +import { LanguageDefinition } from '@kbn/search-api-panels'; + +import { KibanaDeps } from '../../../../../../../common/types'; + +import { icons } from '../../../../../../assets/client_libraries'; +import { useCloudDetails } from '../../../../../shared/cloud_details/cloud_details'; +import { docLinks } from '../../../../../shared/doc_links'; + +import { HttpLogic } from '../../../../../shared/http'; +import { KibanaLogic } from '../../../../../shared/kibana'; +import { IndexViewLogic } from '../../index_view_logic'; +import { OverviewLogic } from '../../overview.logic'; +import { GenerateApiKeyModal } from '../generate_api_key_modal/modal'; + +import { javascriptDefinition } from './languages/javascript'; +import { languageDefinitions } from './languages/languages'; +import { getCodeSnippet, showTryInConsole } from './languages/utils'; + +const DEFAULT_URL = 'https://localhost:9200'; + +export const APIGettingStarted = () => { + const { http } = useValues(HttpLogic); + const { apiKey, isGenerateModalOpen } = useValues(OverviewLogic); + const { openGenerateModal, closeGenerateModal } = useActions(OverviewLogic); + const { indexName } = useValues(IndexViewLogic); + const { services } = useKibana(); + const { isCloud } = useValues(KibanaLogic); + + const cloudContext = useCloudDetails(); + + const codeArgs = { + apiKey, + url: cloudContext.elasticsearchUrl || DEFAULT_URL, + }; + + const [selectedLanguage, setSelectedLanguage] = + useState(javascriptDefinition); + return ( + <> + {isGenerateModalOpen && ( + + )} + +

+ {i18n.translate('xpack.enterpriseSearch.content.overview.gettingStarted.pageTitle', { + defaultMessage: 'Getting Started with Elastic API', + })} +

+
+ + {languageDefinitions.map((language, index) => ( + + + + ))} + + + + + + + +
+ {i18n.translate( + 'xpack.enterpriseSearch.content.overview.gettingStarted.generateApiKeyPanel.apiKeytitle', + { + defaultMessage: 'Generate an API key', + } + )} +
+
+ + + {i18n.translate( + 'xpack.enterpriseSearch.content.overview.gettingStarted.generateApiKeyPanel.apiKeydesc', + { + defaultMessage: + 'Your private, unique identifier for authentication and authorization.', + } + )} + +
+ + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.overview.documementExample.generateApiKeyButton.createNew', + { defaultMessage: 'New' } + )} +

+
+
+
+ + + KibanaLogic.values.navigateToUrl('/app/management/security/api_keys', { + shouldNotCreateHref: true, + }) + } + > + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.overview.documementExample.generateApiKeyButton.viewAll', + { defaultMessage: 'Manage' } + )} +

+
+
+
+
+
+
+
+ } + links={[]} + title={i18n.translate( + 'xpack.enterpriseSearch.content.overview.gettingStarted.generateApiKeyPanel.panelTitle', + { + defaultMessage: 'Generate an API key', + } + )} + overviewPanelProps={{ color: 'plain', hasShadow: false }} + /> + + + + +
+ {isCloud + ? i18n.translate( + 'xpack.enterpriseSearch.content.overview.gettingStarted.cloudId.cloudTitle', + { + defaultMessage: 'Store your unique Cloud ID', + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.content.overview.gettingStarted.cloudId.elasticTitle', + { + defaultMessage: 'Store your elasticsearch URL', + } + )} +
+
+ + {i18n.translate( + 'xpack.enterpriseSearch.content.overview.gettingStarted.cloudId.desc', + { + defaultMessage: 'Unique identifier for your deployment. ', + } + )} + +
+ + + + {codeArgs.url} + + + + + } + links={[]} + title={ + isCloud + ? i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStarted.cloudId.panelTitleCloud', + { + defaultMessage: 'Copy your Cloud ID', + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStarted.cloudId.panelTitleElastic', + { + defaultMessage: 'Copy your elasticsearch URL', + } + ) + } + overviewPanelProps={{ color: 'plain', hasShadow: false }} + /> + + + } + links={[]} + title={i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStarted.configureClient.title', + { + defaultMessage: 'Configure your client', + } + )} + overviewPanelProps={{ color: 'plain', hasShadow: false }} + /> + + + } + links={[]} + title={i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStarted.testConnection.title', + { + defaultMessage: 'Test your connection', + } + )} + overviewPanelProps={{ color: 'plain', hasShadow: false }} + /> + + } + links={[]} + title={i18n.translate('xpack.enterpriseSearch.overview.gettingStarted.ingestData.title', { + defaultMessage: 'Ingest Data', + })} + overviewPanelProps={{ color: 'plain', hasShadow: false }} + /> + + + } + links={[]} + title={i18n.translate('xpack.enterpriseSearch.overview.gettingStarted.searchQuery.title', { + defaultMessage: 'Build your first search query', + })} + overviewPanelProps={{ color: 'plain', hasShadow: false }} + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/console.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/console.ts new file mode 100644 index 00000000000000..afb685441e89fd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/console.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LanguageDefinition } from '@kbn/search-api-panels'; + +export const consoleDefinition: Partial = { + buildSearchQuery: `POST /books/_search?pretty +{ + "query": { + "query_string": { + "query": "snow" + } + } +}`, + ingestData: `POST _bulk?pretty +{ "index" : { "_index" : "books" } } +{"name": "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470} +{ "index" : { "_index" : "books" } } +{"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585} +{ "index" : { "_index" : "books" } } +{"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328} +{ "index" : { "_index" : "books" } } +{"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227} +{ "index" : { "_index" : "books" } } +{"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268} +{ "index" : { "_index" : "books" } } +{"name": "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311}`, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/constants.ts new file mode 100644 index 00000000000000..b0b122fa01b5c8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const API_KEY_PLACEHOLDER = 'your_api_key'; +export const ELASTICSEARCH_URL_PLACEHOLDER = 'https://your_deployment_url'; +export const INDEX_NAME_PLACEHOLDER = 'index_name'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/curl.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/curl.ts new file mode 100644 index 00000000000000..607b85c7c6e7cf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/curl.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 { i18n } from '@kbn/i18n'; +import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; + +import { docLinks } from '../../../../../../shared/doc_links'; + +export const curlDefinition: LanguageDefinition = { + buildSearchQuery: `curl -X POST "\$\{ES_URL\}/books/_search?pretty" \\ + -H "Authorization: ApiKey "\$\{API_KEY\}"" \\ + -H "Content-Type: application/json" \\ + -d' +{ + "query": { + "query_string": { + "query": "snow" + } + } +}'`, + configureClient: ({ apiKey, url }) => `export ES_URL="${url}" +export API_KEY="${apiKey}"`, + docLink: docLinks.restApis, + iconType: 'curl.svg', + id: Languages.CURL, + ingestData: `curl -X POST "\$\{ES_URL\}/_bulk?pretty" \\ + -H "Authorization: ApiKey "\$\{API_KEY\}"" \\ + -H "Content-Type: application/json" \\ + -d' +{ "index" : { "_index" : "books" } } +{"name": "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470} +{ "index" : { "_index" : "books" } } +{"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585} +{ "index" : { "_index" : "books" } } +{"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328} +{ "index" : { "_index" : "books" } } +{"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227} +{ "index" : { "_index" : "books" } } +{"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268} +{ "index" : { "_index" : "books" } } +{"name": "The Handmaid'"'"'s Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311} +'`, + ingestDataIndex: '', + installClient: `# if cURL is not already installed on your system +# then install it with the package manager of your choice + +# example +brew install curl`, + name: i18n.translate('xpack.enterpriseSearch.languages.cURL', { + defaultMessage: 'cURL', + }), + languageStyling: 'shell', + testConnection: `curl "\$\{ES_URL\}" \\ + -H "Authorization: ApiKey "\$\{API_KEY\}"" \\ + -H "Content-Type: application/json"`, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/go.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/go.ts new file mode 100644 index 00000000000000..d75841a9ba1dbb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/go.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; + +import { docLinks } from '../../../../../../shared/doc_links'; + +export const goDefinition: LanguageDefinition = { + buildSearchQuery: `searchResp, err := es.Search(). + Index("books"). + Q("snow"). + Do(context.Background()) + +fmt.Println(searchResp, err)`, + configureClient: ({ url, apiKey }) => `import ( + "context" + "fmt" + "log" + "strings" +​ + "github.com/elastic/elasticsearch-serverless-go" +) + +func main() { + cfg := elasticsearch.Config{ + Address: "${url}", + APIKey: "${apiKey}", + } + es, err := elasticsearch.NewClient(cfg) + if err != nil { + log.Fatalf("Error creating the client: %s", err) + } +}`, + docLink: docLinks.clientsGoIndex, + iconType: 'go.svg', + id: Languages.GO, + ingestData: `ingestResult, err := es.Bulk(). + Index("books"). + Raw(strings.NewReader(\` +{"index":{"_id":"9780553351927"}} +{"name":"Snow Crash","author":"Neal Stephenson","release_date":"1992-06-01","page_count": 470} +{ "index": { "_id": "9780441017225"}} +{"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585} +{ "index": { "_id": "9780451524935"}} +{"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328} +{ "index": { "_id": "9781451673319"}} +{"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227} +{ "index": { "_id": "9780060850524"}} +{"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268} +{ "index": { "_id": "9780385490818"}} +{"name": "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311}\n\`)). + Do(context.Background()) + +fmt.Println(ingestResult, err)`, + ingestDataIndex: '', + installClient: 'go get github.com/elastic/go-elasticsearch/v8@latest', + name: i18n.translate('xpack.enterpriseSearch.languages.go', { + defaultMessage: 'Go', + }), + testConnection: `infores, err := es.Info().Do(context.Background()) + if err != nil { + log.Fatalf("Error getting response: %s", err) + } + + fmt.Println(infores)`, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/javascript.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/javascript.ts new file mode 100644 index 00000000000000..033dc9b3169dfc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/javascript.ts @@ -0,0 +1,86 @@ +/* + * 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 { Languages, LanguageDefinition } from '@kbn/search-api-panels'; + +import { docLinks } from '../../../../../../shared/doc_links'; + +export const javascriptDefinition: LanguageDefinition = { + buildSearchQuery: `// Let's search! +const searchResult = await client.search({ + index: 'my-index-name', + q: '9HY9SWR' +}); + +console.log(searchResult.hits.hits) +`, + configureClient: ({ url, apiKey }) => `const { Client } = require('@elastic/elasticsearch'); +const client = new Client({ + node: '${url}', + auth: { + apiKey: '${apiKey}' + } +});`, + docLink: docLinks.clientsJsIntro, + iconType: 'javascript.svg', + id: Languages.JAVASCRIPT, + ingestData: `// Sample flight data +const dataset = [ + {'flight': '9HY9SWR', 'price': 841.2656419677076, 'delayed': false}, + {'flight': 'X98CCZO', 'price': 882.9826615595518, 'delayed': false}, + {'flight': 'UFK2WIZ', 'price': 190.6369038508356, 'delayed': true}, +]; + +// Index with the bulk helper +const result = await client.helpers.bulk({ + datasource: dataset, + onDocument (doc) { + return { index: { _index: 'my-index-name' }}; + } +}); + +console.log(result); +/** +{ + total: 3, + failed: 0, + retry: 0, + successful: 3, + noop: 0, + time: 421, + bytes: 293, + aborted: false +} +*/`, + ingestDataIndex: '', + installClient: 'npm install @elastic/elasticsearch@8', + name: i18n.translate('xpack.enterpriseSearch.languages.javascript', { + defaultMessage: 'JavaScript', + }), + testConnection: `const resp = await client.info(); + +console.log(resp); +/** +{ + name: 'instance-0000000000', + cluster_name: 'd9dcd35d12fe46dfaa28ec813f65d57b', + cluster_uuid: 'iln8jaivThSezhTkzp0Knw', + version: { + build_flavor: 'default', + build_type: 'docker', + build_hash: 'c94b4700cda13820dad5aa74fae6db185ca5c304', + build_date: '2022-10-24T16:54:16.433628434Z', + build_snapshot: false, + lucene_version: '9.4.1', + minimum_wire_compatibility_version: '7.17.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' +} +*/`, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/languages.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/languages.ts new file mode 100644 index 00000000000000..754b1c3386f8f4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/languages.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 { Languages, LanguageDefinition } from '@kbn/search-api-panels'; + +import { curlDefinition } from './curl'; +import { goDefinition } from './go'; +import { javascriptDefinition } from './javascript'; +import { phpDefinition } from './php'; +import { pythonDefinition } from './python'; +import { rubyDefinition } from './ruby'; + +const languageDefinitionRecords: Partial> = { + [Languages.CURL]: curlDefinition, + [Languages.PYTHON]: pythonDefinition, + [Languages.JAVASCRIPT]: javascriptDefinition, + [Languages.PHP]: phpDefinition, + [Languages.GO]: goDefinition, + [Languages.RUBY]: rubyDefinition, +}; + +export const languageDefinitions: LanguageDefinition[] = Object.values(languageDefinitionRecords); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/php.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/php.ts new file mode 100644 index 00000000000000..6b1abcae279546 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/php.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; + +import { docLinks } from '../../../../../../shared/doc_links'; + +export const phpDefinition: LanguageDefinition = { + buildSearchQuery: `$params = [ + 'index' => 'books', + 'body' => [ + 'q' => 'snow' + ] +]; + +$response = $client->search($params); +print_r($response->asArray());`, + configureClient: ({ url, apiKey }) => `$client = ClientBuilder::create() + ->setHosts(['${url}']) + ->setApiKey('${apiKey}') + ->build();`, + docLink: docLinks.clientsPhpOverview, + iconType: 'php.svg', + id: Languages.PHP, + ingestData: `$params = [ + 'body' => [ + [ + 'index' => [ + '_index' => 'books', + '_id' => '9780553351927', + ], + ], + [ + 'name' => 'Snow Crash', + 'author' => 'Neal Stephenson', + 'release_date' => '1992-06-01', + 'page_count' => 470, + ], + [ + 'index' => [ + '_index' => 'books', + '_id' => '9780441017225', + ], + ], + [ + 'name' => 'Revelation Space', + 'author' => 'Alastair Reynolds', + 'release_date' => '2000-03-15', + 'page_count' => 585, + ], + [ + 'index' => [ + '_index' => 'books', + '_id' => '9780451524935', + ], + ], + [ + 'name' => '1984', + 'author' => 'George Orwell', + 'release_date' => '1985-06-01', + 'page_count' => 328, + ], + [ + 'index' => [ + '_index' => 'books', + '_id' => '9781451673319', + ], + ], + [ + 'name' => 'Fahrenheit 451', + 'author' => 'Ray Bradbury', + 'release_date' => '1953-10-15', + 'page_count' => 227, + ], + [ + 'index' => [ + '_index' => 'books', + '_id' => '9780060850524', + ], + ], + [ + 'name' => 'Brave New World', + 'author' => 'Aldous Huxley', + 'release_date' => '1932-06-01', + 'page_count' => 268, + ], + [ + 'index' => [ + '_index' => 'books', + '_id' => '9780385490818', + ], + ], + [ + 'name' => 'The Handmaid\'s Tale', + 'author' => 'Margaret Atwood', + 'release_date' => '1985-06-01', + 'page_count' => 311, + ], + ], + ]; + + $response = $client->bulk($params); + echo $response->getStatusCode(); + echo (string) $response->getBody();`, + ingestDataIndex: '', + installClient: 'composer require elasticsearch/elasticsearch', + name: i18n.translate('xpack.enterpriseSearch.languages.php', { + defaultMessage: 'PHP', + }), + testConnection: `$response = $client->info(); +echo $response->getStatusCode(); +echo (string) $response->getBody();`, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/python.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/python.ts new file mode 100644 index 00000000000000..c9d5ba67b26e4b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/python.ts @@ -0,0 +1,50 @@ +/* + * 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 { Languages, LanguageDefinition } from '@kbn/search-api-panels'; + +import { docLinks } from '../../../../../../shared/doc_links'; + +export const pythonDefinition: LanguageDefinition = { + buildSearchQuery: `client.search(index="books", q="snow")`, + configureClient: ({ url, apiKey }) => `from elasticsearch import Elasticsearch + +client = Elasticsearch( + "${url}", + api_key="${apiKey}" +)`, + docLink: docLinks.clientsPythonOverview, + iconType: 'python.svg', + id: Languages.PYTHON, + ingestData: `documents = [ + { "index": { "_index": "books", "_id": "9780553351927"}}, + {"name": "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470}, + { "index": { "_index": "books", "_id": "9780441017225"}}, + {"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585}, + { "index": { "_index": "books", "_id": "9780451524935"}}, + {"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328}, + { "index": { "_index": "books", "_id": "9781451673319"}}, + {"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227}, + { "index": { "_index": "books", "_id": "9780060850524"}}, + {"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268}, + { "index": { "_index": "books", "_id": "9780385490818"}}, + {"name": "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311}, +] + +client.bulk(operations=documents)`, + ingestDataIndex: '', + installClient: `python -m pip install elasticsearch + +# If your application uses async/await in Python you can install with the async extra +# python -m pip install elasticsearch[async] + `, + name: i18n.translate('xpack.enterpriseSearch.languages.python', { + defaultMessage: 'Python', + }), + testConnection: `client.info()`, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/ruby.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/ruby.ts new file mode 100644 index 00000000000000..6706323c96772c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/ruby.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 { i18n } from '@kbn/i18n'; +import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; + +import { docLinks } from '../../../../../../shared/doc_links'; + +export const rubyDefinition: LanguageDefinition = { + buildSearchQuery: `client.search(index: 'books', q: 'snow')`, + configureClient: ({ url, apiKey }) => `client = ElasticsearchServerless::Client.new( + api_key: '${apiKey}', + url: '${url}' +) +`, + docLink: docLinks.clientsRubyOverview, + iconType: 'ruby.svg', + id: Languages.RUBY, + ingestData: `documents = [ + { index: { _index: 'books', data: {name: "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470} } }, + { index: { _index: 'books', data: {name: "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585} } }, + { index: { _index: 'books', data: {name: "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328} } }, + { index: { _index: 'books', data: {name: "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227} } }, + { index: { _index: 'books', data: {name: "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268} } }, + { index: { _index: 'books', data: {name: "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311} } } +] +client.bulk(body: documents)`, + ingestDataIndex: '', + installClient: `$ gem install elasticsearch -v x.x.x`, + name: i18n.translate('xpack.enterpriseSearch.languages.ruby', { + defaultMessage: 'Ruby', + }), + testConnection: `client.info`, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/utils.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/utils.ts new file mode 100644 index 00000000000000..f973099a0947e0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/getting_started/languages/utils.ts @@ -0,0 +1,23 @@ +/* + * 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 { LanguageDefinition, LanguageDefinitionSnippetArguments } from '@kbn/search-api-panels'; + +import { consoleDefinition } from './console'; + +export const showTryInConsole = (code: keyof LanguageDefinition) => code in consoleDefinition; + +export const getCodeSnippet = ( + language: Partial, + key: keyof LanguageDefinition, + args: LanguageDefinitionSnippetArguments +): string => { + const snippetVal = language[key]; + if (snippetVal === undefined) return ''; + if (typeof snippetVal === 'string') return snippetVal; + return snippetVal(args); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/manage_api_keys_popover/popover.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/manage_api_keys_popover/popover.tsx deleted file mode 100644 index e6385096c754dc..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/manage_api_keys_popover/popover.tsx +++ /dev/null @@ -1,86 +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 { useActions, useValues } from 'kea'; - -import { - EuiPopover, - EuiButton, - EuiContextMenuPanel, - EuiContextMenuItem, - EuiText, -} from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; - -import { KibanaLogic } from '../../../../../shared/kibana'; - -import { IndexViewLogic } from '../../index_view_logic'; -import { OverviewLogic } from '../../overview.logic'; - -export const ManageKeysPopover: React.FC = () => { - const { isManageKeysPopoverOpen } = useValues(OverviewLogic); - const { ingestionMethod } = useValues(IndexViewLogic); - const { toggleManageApiKeyPopover, openGenerateModal } = useActions(OverviewLogic); - - return ( - - {i18n.translate( - 'xpack.enterpriseSearch.content.overview.documentExample.generateApiKeyButton.label', - { defaultMessage: 'Manage API keys' } - )} - - } - > - - KibanaLogic.values.navigateToUrl('/app/management/security/api_keys', { - shouldNotCreateHref: true, - }) - } - > - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.overview.documementExample.generateApiKeyButton.viewAll', - { defaultMessage: 'View all API keys' } - )} -

-
-
, - - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.overview.documementExample.generateApiKeyButton.createNew', - { defaultMessage: 'Create a new API key' } - )} -

-
-
, - ]} - /> - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/generate_api_key_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/generate_api_key_panel.tsx index 0de71b834b6f00..f1470f37bc8f06 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/generate_api_key_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/generate_api_key_panel.tsx @@ -5,45 +5,24 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { useActions, useValues } from 'kea'; -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiPanel, - EuiSwitch, - EuiText, - EuiTitle, -} from '@elastic/eui'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { docLinks } from '../../../shared/doc_links'; -import { DOCUMENTS_API_JSON_EXAMPLE } from '../new_index/constants'; - -import { SettingsLogic } from '../settings/settings_logic'; - -import { ClientLibrariesPopover } from './components/client_libraries_popover/popover'; -import { CurlRequest } from './components/curl_request/curl_request'; import { GenerateApiKeyModal } from './components/generate_api_key_modal/modal'; -import { ManageKeysPopover } from './components/manage_api_keys_popover/popover'; +import { APIGettingStarted } from './components/getting_started/getting_started'; import { IndexViewLogic } from './index_view_logic'; import { OverviewLogic } from './overview.logic'; export const GenerateApiKeyPanel: React.FC = () => { - const { apiKey, isGenerateModalOpen } = useValues(OverviewLogic); - const { indexName, ingestionMethod, isHiddenIndex } = useValues(IndexViewLogic); + const { isGenerateModalOpen } = useValues(OverviewLogic); + const { indexName, isHiddenIndex } = useValues(IndexViewLogic); const { closeGenerateModal } = useActions(OverviewLogic); - const { defaultPipeline } = useValues(SettingsLogic); - - const [optimizedRequest, setOptimizedRequest] = useState(true); - return ( <> {isGenerateModalOpen && ( @@ -51,7 +30,7 @@ export const GenerateApiKeyPanel: React.FC = () => { )} - + {isHiddenIndex ? ( { } /> ) : ( - - - - - - - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.overview.documentExample.title', - { defaultMessage: 'Adding documents to your index' } - )} -

-
-
- - -

- - {i18n.translate( - 'xpack.enterpriseSearch.content.overview.documentExample.description.clientsLink', - { defaultMessage: 'programming language clients' } - )} - - ), - documentation: ( - - {i18n.translate( - 'xpack.enterpriseSearch.content.overview.documentExample.description.documentationLink', - { defaultMessage: 'documentation' } - )} - - ), - }} - /> -

-
-
-
-
- - - - - - - - - - -
-
- - - setOptimizedRequest(event.target.checked)} - label={i18n.translate( - 'xpack.enterpriseSearch.content.overview.optimizedRequest.label', - { defaultMessage: 'View Search optimized request' } - )} - checked={optimizedRequest} - /> - - - - -
+ )}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/assets/search_header.svg b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/assets/search_header.svg new file mode 100644 index 00000000000000..c11980cc595b89 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/assets/search_header.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.test.tsx index acac9b2df20de1..349b5cb0a935c8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.test.tsx @@ -13,7 +13,7 @@ import { shallow } from 'enzyme'; import { snakeCase } from 'lodash'; -import { EuiListGroup, EuiPanel } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; import { EuiButtonTo, EuiButtonEmptyTo } from '../../../shared/react_router_helpers'; @@ -22,20 +22,10 @@ import { ProductCard, ProductCardProps } from './product_card'; const MOCK_VALUES: ProductCardProps = { cta: 'Click me', description: 'Mock description', - features: ['first feature', 'second feature'], icon: 'logoElasticsearch', name: 'Mock product', productId: 'mockProduct', - resourceLinks: [ - { - label: 'Link one', - to: 'https://www.elastic.co/guide/one', - }, - { - label: 'Link twwo', - to: 'https://www.elastic.co/guide/two', - }, - ], + rightPanelItems: [
,
], url: '/app/mock_app', }; @@ -49,10 +39,8 @@ describe('ProductCard', () => { const card = wrapper.find(EuiPanel); expect(card.find('h3').text()).toEqual(MOCK_VALUES.name); - expect(card.find(EuiListGroup).children()).toHaveLength(MOCK_VALUES.features.length); - expect(card.find('[data-test-subj="productCard-resources"]').text()).toEqual('Resources'); - expect(card.find('[data-test-subj="productCard-resourceLinks"]').children()).toHaveLength( - MOCK_VALUES.resourceLinks.length + expect(card.find('[data-test-subj="productCard-rightPanelItems"]').children()).toHaveLength( + MOCK_VALUES.rightPanelItems?.length ?? -1 ); const button = card.find(EuiButtonEmptyTo); @@ -69,6 +57,26 @@ describe('ProductCard', () => { }); }); + it('renders a product card without panel', () => { + const wrapper = shallow(); + const card = wrapper.find(EuiPanel); + + expect(card.find('[data-test-subj="productCard-rightPanelItems"]')).toHaveLength(0); + + const button = card.find(EuiButtonEmptyTo); + + expect(button).toHaveLength(1); + expect(button.prop('to')).toEqual(MOCK_VALUES.url); + expect(card.find(EuiButtonTo)).toHaveLength(0); + + button.simulate('click'); + + expect(mockTelemetryActions.sendEnterpriseSearchTelemetry).toHaveBeenCalledWith({ + action: 'clicked', + metric: snakeCase(MOCK_VALUES.productId), + }); + }); + it('renders an empty cta', () => { const wrapper = shallow(); const card = wrapper.find(EuiPanel); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.tsx index 959992ca4b270d..3ba5fa2e1f52cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_card/product_card.tsx @@ -14,9 +14,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, - EuiLink, - EuiListGroup, - EuiListGroupItem, EuiPanel, EuiSpacer, EuiText, @@ -25,30 +22,22 @@ import { IconSize, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - import { EuiButtonTo, EuiButtonEmptyTo } from '../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../shared/telemetry'; import './product_card.scss'; -interface ProductResourceLink { - label: string; - to: string; -} - export interface ProductCardProps { cta?: string; description: string; emptyCta?: boolean; - features: string[]; hasBorder?: boolean; hasShadow?: boolean; icon: IconType; iconSize?: IconSize; name: string; productId: string; - resourceLinks: ProductResourceLink[]; + rightPanelItems?: React.ReactNode[]; url?: string; } @@ -56,14 +45,13 @@ export const ProductCard: React.FC = ({ cta, description, emptyCta = false, - features, hasBorder, hasShadow, icon, iconSize, productId, + rightPanelItems, name, - resourceLinks, url, }) => { const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic); @@ -86,8 +74,8 @@ export const ProductCard: React.FC = ({ - {description} - + {description}{' '} + {' '} {cta && url && (
@@ -122,42 +110,19 @@ export const ProductCard: React.FC = ({
)} - - - {features.map((item: string, index: number) => ( - } - /> - ))} - - - - -

- {i18n.translate('xpack.enterpriseSearch.productCard.resourcesTitle', { - defaultMessage: 'Resources', + {rightPanelItems ? ( + + + {rightPanelItems.map((rightPanelItem) => { + return {rightPanelItem}; })} -

-
- - - {resourceLinks.map((resource, index) => ( - - - {resource.label} - - - ))} - -
+ + + ) : null} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/app_search_product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/app_search_product_card.tsx new file mode 100644 index 00000000000000..f8dac98f048ad7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/app_search_product_card.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { ProductCard } from '../product_card'; + +export interface AppSearchProductCardProps { + hasBorder: boolean; + hasShadow: boolean; +} + +export const AppSearchProductCard: React.FC = ({ + hasBorder = true, + hasShadow = true, +}) => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/behavioral_analytics_product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/behavioral_analytics_product_card.tsx index 52d5f1ab3d5d67..31761b2e88502f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/behavioral_analytics_product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/behavioral_analytics_product_card.tsx @@ -10,12 +10,18 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { ANALYTICS_PLUGIN } from '../../../../../common/constants'; -import { docLinks } from '../../../shared/doc_links'; import baLogo from '../../assets/behavioral_analytics_logo.svg'; import { ProductCard } from '../product_card'; -export const BehavioralAnalyticsProductCard = () => ( +export interface BehavioralAnalyticsProductCard { + hasBorder: boolean; + hasShadow: boolean; +} + +export const BehavioralAnalyticsProductCard = ({ hasBorder = true, hasShadow = true }) => ( ( 'Dashboards and tools for visualizing end-user behavior and measuring the performance of your search applications', })} emptyCta - features={[ - i18n.translate('xpack.enterpriseSearch.behavioralAnalytics.features.tracking', { - defaultMessage: "Track users' searching and clicking behavior", - }), - i18n.translate('xpack.enterpriseSearch.behavioralAnalytics.features.dashboard', { - defaultMessage: 'Search management dashboards', - }), - i18n.translate('xpack.enterpriseSearch.behavioralAnalytics.features.contentGaps', { - defaultMessage: 'Identify gaps in your content', - }), - ]} icon={baLogo} iconSize="l" name={ANALYTICS_PLUGIN.NAME} productId={ANALYTICS_PLUGIN.ID} - resourceLinks={[ - { - label: i18n.translate( - 'xpack.enterpriseSearch.behavioralAnalytics.resources.gettingStartedLabel', - { - defaultMessage: 'Getting started with Behavioral Analytics', - } - ), - to: docLinks.behavioralAnalytics, - }, - ]} url={ANALYTICS_PLUGIN.URL} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/elasticsearch_product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/elasticsearch_product_card.tsx index 70d5f2765b4cc2..3c0609bcf5788b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/elasticsearch_product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/elasticsearch_product_card.tsx @@ -7,17 +7,15 @@ import React from 'react'; -import { useValues } from 'kea'; - import { i18n } from '@kbn/i18n'; -import { ELASTICSEARCH_PLUGIN, SEARCH_EXPERIENCES_PLUGIN } from '../../../../../common/constants'; -import { docLinks } from '../../../shared/doc_links'; -import { HttpLogic } from '../../../shared/http'; +import { ELASTICSEARCH_PLUGIN } from '../../../../../common/constants'; import { ProductCard } from '../product_card'; +import { BehavioralAnalyticsProductCard } from './behavioral_analytics_product_card'; +import { SearchApplicationsProductCard } from './search_applications_product_card'; + export const ElasticsearchProductCard = () => { - const { http } = useValues(HttpLogic); return ( { defaultMessage: 'Ideal for bespoke applications, Elasticsearch helps you build highly customizable search and offers many different ingestion methods.', })} - features={[ - i18n.translate('xpack.enterpriseSearch.elasticsearch.features.integrate', { - defaultMessage: 'Integrate with databases, websites, and more', - }), - i18n.translate('xpack.enterpriseSearch.elasticsearch.features.buildTooling', { - defaultMessage: 'Build custom tooling', - }), - i18n.translate('xpack.enterpriseSearch.elasticsearch.features.buildSearchExperiences', { - defaultMessage: 'Build custom search experiences', - }), - i18n.translate('xpack.enterpriseSearch.elasticsearch.features.esre', { - defaultMessage: 'The Elasticsearch Relevance Engine™ (ESRE)', - }), - ]} icon="logoElasticsearch" name={ELASTICSEARCH_PLUGIN.NAME} productId={ELASTICSEARCH_PLUGIN.ID} - resourceLinks={[ - { - label: i18n.translate( - 'xpack.enterpriseSearch.elasticsearch.resources.gettingStartedLabel', - { - defaultMessage: 'Getting started with Elasticsearch', - } - ), - to: docLinks.start, - }, - { - label: i18n.translate( - 'xpack.enterpriseSearch.elasticsearch.resources.createNewIndexLabel', - { - defaultMessage: 'Create a new index', - } - ), - to: docLinks.start, - }, - { - label: i18n.translate( - 'xpack.enterpriseSearch.elasticsearch.resources.languageClientLabel', - { - defaultMessage: 'Set up a language client', - } - ), - to: docLinks.languageClients, - }, - { - label: i18n.translate('xpack.enterpriseSearch.elasticsearch.resources.searchUILabel', { - defaultMessage: 'Search UI for Elasticsearch', - }), - to: docLinks.searchUIElasticsearch, - }, - { - label: i18n.translate('xpack.enterpriseSearch.elasticsearch.resources.elserLabel', { - defaultMessage: 'ELSER text expansion', - }), - to: docLinks.elser, - }, - { - label: i18n.translate( - 'xpack.enterpriseSearch.elasticsearch.resources.searchExperiencesLabel', - { - defaultMessage: 'Search Experiences', - } - ), - to: http.basePath.prepend(SEARCH_EXPERIENCES_PLUGIN.URL), - }, + rightPanelItems={[ + , + , ]} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/enterprise_search_product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/enterprise_search_product_card.tsx new file mode 100644 index 00000000000000..b53203aade931a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/enterprise_search_product_card.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; 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 { + ENTERPRISE_SEARCH_PRODUCT_NAME, + ENTERPRISE_SEARCH_CONTENT_PLUGIN, +} from '../../../../../common/constants'; +import { ProductCard } from '../product_card'; + +import { AppSearchProductCard } from './app_search_product_card'; +import { WorkplaceSearchProductCard } from './workplace_search_product_card'; + +export const EnterpriseSearchProductCard = () => ( + , + , + ]} + /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/ingestion_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/ingestion_selector.tsx new file mode 100644 index 00000000000000..8758506edd9b6f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/ingestion_selector.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { generatePath } from 'react-router-dom'; + +import { EuiButton, EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { + ENTERPRISE_SEARCH_CONTENT_PLUGIN, + INGESTION_METHOD_IDS, +} from '../../../../../common/constants'; + +import apiLogo from '../../../../assets/images/api_cloud.svg'; +import connectorLogo from '../../../../assets/images/search_connector.svg'; +import crawlerLogo from '../../../../assets/images/search_crawler.svg'; + +import { + NEW_API_PATH, + NEW_INDEX_METHOD_PATH, + NEW_INDEX_SELECT_CONNECTOR_PATH, +} from '../../../enterprise_search_content/routes'; +import { EuiLinkTo } from '../../../shared/react_router_helpers'; + +const START_LABEL = i18n.translate('xpack.enterpriseSearch.ingestSelector.startButton', { + defaultMessage: 'Start', +}); + +export const IngestionSelector: React.FC = () => { + return ( + + + } + textAlign="left" + title={i18n.translate('xpack.enterpriseSearch.ingestSelector.method.api', { + defaultMessage: 'API', + })} + description={i18n.translate( + 'xpack.enterpriseSearch.ingestSelector.method.api.description', + { + defaultMessage: + 'Add documents programmatically by connecting with the API using your preferred language client.', + } + )} + footer={ + + {START_LABEL} + + } + /> + + + } + textAlign="left" + title={i18n.translate('xpack.enterpriseSearch.ingestSelector.method.connectors', { + defaultMessage: 'Connectors', + })} + description={i18n.translate( + 'xpack.enterpriseSearch.ingestSelector.method.connectors.description', + { + defaultMessage: + 'Extract, transform, index and sync data from a third-party data source.', + } + )} + footer={ + + {START_LABEL} + + } + /> + + + } + textAlign="left" + title={i18n.translate('xpack.enterpriseSearch.ingestSelector.method.crawler', { + defaultMessage: 'Web Crawler', + })} + description={i18n.translate( + 'xpack.enterpriseSearch.ingestSelector.method.crawler.description', + { + defaultMessage: + 'Discover, extract, and index searchable content from websites and knowledge bases.', + } + )} + footer={ + + {START_LABEL} + + } + /> + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.scss new file mode 100644 index 00000000000000..8d028680083759 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.scss @@ -0,0 +1,8 @@ +.entSearchProductSelectorHeader { + background-color: $euiColorPrimary; +} + +.entSearchProductSelectorHeader > div { + padding-bottom: 0; + padding-top: 0; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.test.tsx index a1b2b80618c3ff..8a37008c2d695e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.test.tsx @@ -11,15 +11,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { AddContentEmptyPrompt } from '../../../shared/add_content_empty_prompt'; import { ErrorStateCallout } from '../../../shared/error_state'; import { SetupGuideCta } from '../setup_guide'; import { TrialCallout } from '../trial_callout'; -import { BehavioralAnalyticsProductCard } from './behavioral_analytics_product_card'; import { ElasticsearchProductCard } from './elasticsearch_product_card'; -import { SearchApplicationsProductCard } from './search_applications_product_card'; +import { EnterpriseSearchProductCard } from './enterprise_search_product_card'; import { ProductSelector } from '.'; @@ -29,8 +27,7 @@ describe('ProductSelector', () => { const wrapper = shallow(); expect(wrapper.find(ElasticsearchProductCard)).toHaveLength(1); - expect(wrapper.find(SearchApplicationsProductCard)).toHaveLength(1); - expect(wrapper.find(BehavioralAnalyticsProductCard)).toHaveLength(1); + expect(wrapper.find(EnterpriseSearchProductCard)).toHaveLength(1); expect(wrapper.find(SetupGuideCta)).toHaveLength(1); }); @@ -58,23 +55,6 @@ describe('ProductSelector', () => { expect(wrapper.find(ErrorStateCallout)).toHaveLength(1); }); - it('renders add content', () => { - setMockValues({ config: { canDeployEntSearch: true, host: 'localhost' } }); - const wrapper = shallow(); - - expect(wrapper.find(AddContentEmptyPrompt)).toHaveLength(1); - }); - - it('does not render add content when theres a connection error', () => { - setMockValues({ - config: { canDeployEntSearch: true, host: 'localhost' }, - errorConnectingMessage: '502 Bad Gateway', - }); - const wrapper = shallow(); - - expect(wrapper.find(AddContentEmptyPrompt)).toHaveLength(0); - }); - describe('access checks when host is set', () => { beforeEach(() => { setMockValues({ config: { canDeployEntSearch: true, host: 'localhost' } }); @@ -84,8 +64,7 @@ describe('ProductSelector', () => { const wrapper = shallow(); expect(wrapper.find(ElasticsearchProductCard)).toHaveLength(1); - expect(wrapper.find(SearchApplicationsProductCard)).toHaveLength(1); - expect(wrapper.find(BehavioralAnalyticsProductCard)).toHaveLength(1); + expect(wrapper.find(EnterpriseSearchProductCard)).toHaveLength(1); expect(wrapper.find(SetupGuideCta)).toHaveLength(0); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx index 400d5f572ae4d5..001d34c1c5ad6d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx @@ -9,90 +9,121 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPageTemplate, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; import { Chat } from '@kbn/cloud-chat-plugin/public'; import { i18n } from '@kbn/i18n'; +import { WelcomeBanner } from '@kbn/search-api-panels'; -import { AddContentEmptyPrompt } from '../../../shared/add_content_empty_prompt'; import { ErrorStateCallout } from '../../../shared/error_state'; import { HttpLogic } from '../../../shared/http'; import { KibanaLogic } from '../../../shared/kibana'; import { SetSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import headerImage from '../../assets/search_header.svg'; + import { EnterpriseSearchOverviewPageTemplate } from '../layout'; import { SetupGuideCta } from '../setup_guide'; import { TrialCallout } from '../trial_callout'; -import { BehavioralAnalyticsProductCard } from './behavioral_analytics_product_card'; import { ElasticsearchProductCard } from './elasticsearch_product_card'; -import { SearchApplicationsProductCard } from './search_applications_product_card'; +import { EnterpriseSearchProductCard } from './enterprise_search_product_card'; +import { IngestionSelector } from './ingestion_selector'; + +import './product_selector.scss'; export const ProductSelector: React.FC = () => { - const { config } = useValues(KibanaLogic); + const { config, userProfile } = useValues(KibanaLogic); const { errorConnectingMessage } = useValues(HttpLogic); const showErrorConnecting = !!(config.host && errorConnectingMessage); // The create index flow does not work without ent-search, when content is updated // to no longer rely on ent-search we can always show the Add Content component - const showAddContent = config.host && !errorConnectingMessage; return ( - - - - - {showAddContent && ( - <> - + + + + + + + + + + + +

+ {i18n.translate('xpack.enterpriseSearch.productSelector.overview.title', { + defaultMessage: 'Ingest your content', + })} +

+
+ + +

+ {i18n.translate('xpack.enterpriseSearch.productSelector.overview.description', { + defaultMessage: + 'The first step in building your search experience is to create a search-optimized Elasticsearch index and import your content into it. Elasticsearch offers several user-friendly options you can choose from that best match your technical expertise and data sources.', })} - buttonLabel={i18n.translate('xpack.enterpriseSearch.overview.emptyPromptButtonLabel', { - defaultMessage: 'Create an Elasticsearch index', +

+
+ + + + + {showErrorConnecting && ( + <> + + + + )} + + + +

+ {i18n.translate('xpack.enterpriseSearch.productSelector.overview.createCustom.title', { + defaultMessage: 'Create a custom search experience', })} - /> - - - )} - {showErrorConnecting && ( - <> - - - - )} - -

- {i18n.translate('xpack.enterpriseSearch.overview.productSelector.title', { - defaultMessage: "What's next?", - })} -

-
- - - - - - - - - - - - {!config.host && config.canDeployEntSearch && ( +

+
+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.productSelector.overview.createCustom.description', + { + defaultMessage: + "Once your index is created and populated, you'll be ready to use the full power of Elasticsearch. Build search applications using our out-of-the-box tools and programming language clients, all backed by a robust set of APIs.", + } + )} +

+
+ + + + - + - )} - - -
+ + + + {!config.host && config.canDeployEntSearch && ( + + + + )} + + +
+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/search_applications_product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/search_applications_product_card.tsx index e85645445449aa..582387697d4263 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/search_applications_product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/search_applications_product_card.tsx @@ -10,12 +10,21 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { APPLICATIONS_PLUGIN } from '../../../../../common/constants'; -import { docLinks } from '../../../shared/doc_links'; import searchAppLogo from '../../assets/search_applications_logo.svg'; import { ProductCard } from '../product_card'; -export const SearchApplicationsProductCard = () => ( +export interface SearchApplicationProductCardProps { + hasBorder: boolean; + hasShadow: boolean; +} + +export const SearchApplicationsProductCard: React.FC = ({ + hasBorder = true, + hasShadow = true, +}) => ( ( 'Search Applications help make your Elasticsearch data easily searchable for end users', })} emptyCta - features={[ - i18n.translate('xpack.enterpriseSearch.searchApplications.features.queries', { - defaultMessage: 'Build queries using search templates and DLS', - }), - i18n.translate('xpack.enterpriseSearch.searchApplications.features.indices', { - defaultMessage: 'Combine your Elasticsearch indices', - }), - i18n.translate('xpack.enterpriseSearch.searchApplications.features.docsExplorer', { - defaultMessage: 'Easily preview your search results', - }), - i18n.translate('xpack.enterpriseSearch.searchApplications.features.api', { - defaultMessage: 'Elasticsearch Search Application API', - }), - ]} icon={searchAppLogo} iconSize="l" name={APPLICATIONS_PLUGIN.NAV_TITLE} productId={APPLICATIONS_PLUGIN.ID} - resourceLinks={[ - { - label: i18n.translate( - 'xpack.enterpriseSearch.searchApplications.resources.gettingStartedLabel', - { - defaultMessage: 'Getting started with Search Applications', - } - ), - to: docLinks.searchApplications, - }, - ]} url={APPLICATIONS_PLUGIN.URL} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/workplace_search_product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/workplace_search_product_card.tsx new file mode 100644 index 00000000000000..6139d7a2f2b2cd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/workplace_search_product_card.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { ProductCard } from '../product_card'; + +export interface WorkplaceSearchProductCardProps { + hasBorder: boolean; + hasShadow: boolean; +} + +export const WorkplaceSearchProductCard: React.FC = ({ + hasBorder = true, + hasShadow = true, +}) => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index 6f8a8df2d9ef89..81f092bb4dea56 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -38,6 +38,7 @@ describe('renderApp', () => { licensing: licensingMock.createStart(), security: securityMock.createStart(), share: sharePluginMock.createStartContract(), + userProfile: { user: {} }, }, } as any; const pluginData = { diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 1d9ae419f5a76a..0cee920d4ff7f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -66,7 +66,7 @@ export const renderApp = ( const { history } = params; const { application, chrome, http, uiSettings } = core; const { capabilities, navigateToUrl } = application; - const { charts, cloud, guidedOnboarding, lens, security, share } = plugins; + const { charts, cloud, guidedOnboarding, lens, security, share, userProfile } = plugins; const entCloudHost = getCloudEnterpriseSearchHost(plugins.cloud); externalUrl.enterpriseSearchUrl = publicUrl || entCloudHost || config.host || ''; @@ -84,7 +84,6 @@ export const renderApp = ( resetContext({ createStore: true }); const store = getContext().store; - const unmountKibanaLogic = mountKibanaLogic({ application, capabilities, @@ -109,6 +108,7 @@ export const renderApp = ( setDocTitle: chrome.docTitle.change, share, uiSettings, + userProfile, }); const unmountLicensingLogic = mountLicensingLogic({ canManageLicense: core.application.capabilities.management?.stack?.license_management, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index c61deecb811d06..1b3492e719792d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -82,6 +82,7 @@ class DocLinks { public connectorsSharepoint: string; public connectorsSharepointOnline: string; public connectorsWorkplaceSearch: string; + public consoleGuide: string; public crawlerExtractionRules: string; public crawlerManaging: string; public crawlerOverview: string; @@ -114,6 +115,7 @@ class DocLinks { public mlDocumentEnrichment: string; public pluginsIngestAttachment: string; public queryDsl: string; + public restApis: string; public rrf: string; public searchApplications: string; public searchApplicationsSearch: string; @@ -238,6 +240,7 @@ class DocLinks { this.connectorsSharepoint = ''; this.connectorsSharepointOnline = ''; this.connectorsWorkplaceSearch = ''; + this.consoleGuide = ''; this.crawlerExtractionRules = ''; this.crawlerManaging = ''; this.crawlerOverview = ''; @@ -270,6 +273,7 @@ class DocLinks { this.mlDocumentEnrichment = ''; this.pluginsIngestAttachment = ''; this.queryDsl = ''; + this.restApis = ''; this.rrf = ''; this.searchUIAppSearch = ''; this.searchUIElasticsearch = ''; @@ -396,6 +400,7 @@ class DocLinks { this.connectorsSharepoint = docLinks.links.enterpriseSearch.connectorsSharepoint; this.connectorsSharepointOnline = docLinks.links.enterpriseSearch.connectorsSharepointOnline; this.connectorsWorkplaceSearch = docLinks.links.enterpriseSearch.connectorsWorkplaceSearch; + this.consoleGuide = docLinks.links.console.guide; this.crawlerExtractionRules = docLinks.links.enterpriseSearch.crawlerExtractionRules; this.crawlerManaging = docLinks.links.enterpriseSearch.crawlerManaging; this.crawlerOverview = docLinks.links.enterpriseSearch.crawlerOverview; @@ -428,6 +433,7 @@ class DocLinks { this.mlDocumentEnrichment = docLinks.links.enterpriseSearch.mlDocumentEnrichment; this.pluginsIngestAttachment = docLinks.links.plugins.ingestAttachment; this.queryDsl = docLinks.links.query.queryDsl; + this.restApis = docLinks.links.apis.restApis; this.rrf = docLinks.links.elasticsearch.rrf; this.searchUIAppSearch = docLinks.links.searchUI.appSearch; this.searchUIElasticsearch = docLinks.links.searchUI.elasticsearch; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index d816c747e50272..c79ad565b2eb73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -21,6 +21,7 @@ import { import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public'; import { LensPublicStart } from '@kbn/lens-plugin/public'; +import { GetUserProfileResponse, UserProfileData } from '@kbn/security-plugin/common'; import { SecurityPluginStart } from '@kbn/security-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; @@ -53,6 +54,7 @@ interface KibanaLogicProps { setDocTitle(title: string): void; share: SharePluginStart; uiSettings: IUiSettingsClient; + userProfile: GetUserProfileResponse; } export interface KibanaValues extends Omit { @@ -93,6 +95,7 @@ export const KibanaLogic = kea>({ setDocTitle: [props.setDocTitle, {}], share: [props.share, {}], uiSettings: [props.uiSettings, {}], + userProfile: [props.userProfile, {}], }), selectors: ({ selectors }) => ({ isCloud: [() => [selectors.cloud], (cloud?: Partial) => !!cloud?.isCloudEnabled], diff --git a/x-pack/plugins/enterprise_search/public/assets/client_libraries/curl.svg b/x-pack/plugins/enterprise_search/public/assets/client_libraries/curl.svg new file mode 100644 index 00000000000000..e922b12283f7d6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/client_libraries/curl.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/client_libraries/github.svg b/x-pack/plugins/enterprise_search/public/assets/client_libraries/github.svg new file mode 100644 index 00000000000000..94fb8d1ae09a9d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/client_libraries/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/client_libraries/index.ts b/x-pack/plugins/enterprise_search/public/assets/client_libraries/index.ts index 0e0e774aa5bba6..87cde9fe972440 100644 --- a/x-pack/plugins/enterprise_search/public/assets/client_libraries/index.ts +++ b/x-pack/plugins/enterprise_search/public/assets/client_libraries/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import curl from './curl.svg'; import dotnet from './dotnet.svg'; import go from './go.svg'; import java from './java.svg'; @@ -16,6 +17,7 @@ import ruby from './ruby.svg'; import rust from './rust.svg'; export const icons = { + curl, dotnet, go, java, diff --git a/x-pack/plugins/enterprise_search/public/assets/images/api_cloud.svg b/x-pack/plugins/enterprise_search/public/assets/images/api_cloud.svg new file mode 100644 index 00000000000000..96ec0e633d734c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/images/api_cloud.svg @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/images/search_connector.svg b/x-pack/plugins/enterprise_search/public/assets/images/search_connector.svg new file mode 100644 index 00000000000000..ea1a85904bd72f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/images/search_connector.svg @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/assets/images/search_crawler.svg b/x-pack/plugins/enterprise_search/public/assets/images/search_crawler.svg new file mode 100644 index 00000000000000..76fc74b74ea57e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/images/search_crawler.svg @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 615805190a3bf2..e0b86110125fbb 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -22,6 +22,7 @@ import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/publi import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { LensPublicStart } from '@kbn/lens-plugin/public'; import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import { GetUserProfileResponse, UserProfileData } from '@kbn/security-plugin/common'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; @@ -65,6 +66,7 @@ export interface PluginsStart { licensing: LicensingPluginStart; security: SecurityPluginStart; share: SharePluginStart; + userProfile: GetUserProfileResponse; } export class EnterpriseSearchPlugin implements Plugin { @@ -100,7 +102,8 @@ export class EnterpriseSearchPlugin implements Plugin { cloudSetup && (pluginsStart as PluginsStart).cloud ? { ...cloudSetup, ...(pluginsStart as PluginsStart).cloud } : undefined; - const plugins = { ...pluginsStart, cloud } as PluginsStart; + const userProfile = await (pluginsStart as PluginsStart).security.userProfiles.getCurrent(); + const plugins = { ...pluginsStart, cloud, userProfile } as PluginsStart; coreStart.chrome .getChromeStyle$() diff --git a/x-pack/plugins/enterprise_search/server/lib/search_applications/fetch_indices_stats.test.ts b/x-pack/plugins/enterprise_search/server/lib/search_applications/fetch_indices_stats.test.ts index 90b24b81dfaa13..c7db48ac0540db 100644 --- a/x-pack/plugins/enterprise_search/server/lib/search_applications/fetch_indices_stats.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/search_applications/fetch_indices_stats.test.ts @@ -15,6 +15,7 @@ describe('fetchIndicesStats lib function', () => { get: jest.fn(), stats: jest.fn(), }, + msearch: jest.fn(), }, asInternalUser: {}, }; @@ -53,6 +54,18 @@ describe('fetchIndicesStats lib function', () => { }, ]; + const msearchResponse = { + responses: [ + { + hits: { + total: { + value: 200, + }, + }, + }, + ], + }; + beforeEach(() => { jest.clearAllMocks(); }); @@ -60,6 +73,7 @@ describe('fetchIndicesStats lib function', () => { it('should return hydrated indices for all available and open indices', async () => { mockClient.asCurrentUser.indices.get.mockResolvedValueOnce(getAllAvailableIndexResponse); mockClient.asCurrentUser.indices.stats.mockResolvedValueOnce(indexStats); + mockClient.asCurrentUser.msearch.mockImplementationOnce(() => msearchResponse); await expect( fetchIndicesStats(mockClient as unknown as IScopedClusterClient, indices) ).resolves.toEqual(fetchIndicesStatsResponse); @@ -77,6 +91,7 @@ describe('fetchIndicesStats lib function', () => { ); mockClient.asCurrentUser.indices.stats.mockImplementationOnce(() => indexStats); + mockClient.asCurrentUser.msearch.mockImplementationOnce(() => msearchResponse); await expect( fetchIndicesStats(mockClient as unknown as IScopedClusterClient, [ @@ -95,6 +110,7 @@ describe('fetchIndicesStats lib function', () => { it('should return count : null, health: unknown for deleted index ', async () => { mockClient.asCurrentUser.indices.get.mockImplementationOnce(() => getAllAvailableIndexResponse); mockClient.asCurrentUser.indices.stats.mockImplementationOnce(() => indexStats); + mockClient.asCurrentUser.msearch.mockImplementationOnce(() => msearchResponse); await expect( fetchIndicesStats(mockClient as unknown as IScopedClusterClient, [ diff --git a/x-pack/plugins/enterprise_search/server/lib/search_applications/fetch_indices_stats.ts b/x-pack/plugins/enterprise_search/server/lib/search_applications/fetch_indices_stats.ts index 2aea20153fcd2f..03ca3b5accc7a6 100644 --- a/x-pack/plugins/enterprise_search/server/lib/search_applications/fetch_indices_stats.ts +++ b/x-pack/plugins/enterprise_search/server/lib/search_applications/fetch_indices_stats.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { MsearchRequestItem, SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; import { IScopedClusterClient } from '@kbn/core-elasticsearch-server/src/client/scoped_cluster_client'; import { EnterpriseSearchApplicationIndex } from '../../../common/types/search_applications'; @@ -17,15 +18,29 @@ export const fetchIndicesStats = async ( ): Promise => { const indicesStats = await client.asCurrentUser.indices.stats({ index: await availableIndices(client, indices), - metric: ['docs'], }); - return indices.map((index) => { - const indexStats = indicesStats.indices?.[index]; + const searches: MsearchRequestItem[] = []; + indices.forEach((index) => { + searches.push({ index }); + searches.push({ size: 0, track_total_hits: true }); + }); + const msearchResponse = await client.asCurrentUser.msearch({ searches }); + const docCounts = msearchResponse.responses.map((response) => { + if ('error' in response) { + return null; + } + + const totalHits = response.hits.total as SearchTotalHits; + return totalHits.value; + }); + + return indices.map((indexName, number) => { + const indexStats = indicesStats.indices?.[indexName]; return { - count: indexStats?.primaries?.docs?.count ?? null, + count: docCounts[number] ?? null, health: indexStats?.health ?? 'unknown', - name: index, + name: indexName, }; }); }; diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json index 39758cb5111031..25cc264924d471 100644 --- a/x-pack/plugins/enterprise_search/tsconfig.json +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -62,5 +62,6 @@ "@kbn/logs-shared-plugin", "@kbn/share-plugin", "@kbn/core-saved-objects-migration-server-internal", + "@kbn/search-api-panels", ] } diff --git a/x-pack/plugins/fleet/common/constants/secrets.ts b/x-pack/plugins/fleet/common/constants/secrets.ts index 06d370ff81323a..7626c36f5c902d 100644 --- a/x-pack/plugins/fleet/common/constants/secrets.ts +++ b/x-pack/plugins/fleet/common/constants/secrets.ts @@ -6,3 +6,5 @@ */ export const SECRETS_ENDPOINT_PATH = '/_fleet/secret'; + +export const SECRETS_MINIMUM_FLEET_SERVER_VERSION = '8.10.0'; diff --git a/x-pack/plugins/fleet/common/experimental_features.ts b/x-pack/plugins/fleet/common/experimental_features.ts index af8bbbc74790b8..cfdb8cf5fd26bb 100644 --- a/x-pack/plugins/fleet/common/experimental_features.ts +++ b/x-pack/plugins/fleet/common/experimental_features.ts @@ -21,7 +21,7 @@ export const allowedExperimentalValues = Object.freeze({ agentFqdnMode: true, showExperimentalShipperOptions: false, agentTamperProtectionEnabled: false, - secretsStorage: false, + secretsStorage: true, kafkaOutput: true, }); diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 200cebba48cc7e..7cc33b7dc96e84 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -5406,7 +5406,6 @@ "name": "kuery", "in": "query", "required": false, - "deprecated": true, "schema": { "type": "string" } diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 6c216e464a1048..c09c37146a37b2 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -3363,7 +3363,6 @@ components: name: kuery in: query required: false - deprecated: true schema: type: string show_inactive: diff --git a/x-pack/plugins/fleet/common/openapi/components/parameters/kuery.yaml b/x-pack/plugins/fleet/common/openapi/components/parameters/kuery.yaml index 0ef22394e51987..b96ffd54d37ce8 100644 --- a/x-pack/plugins/fleet/common/openapi/components/parameters/kuery.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/parameters/kuery.yaml @@ -1,6 +1,5 @@ name: kuery in: query required: false -deprecated: true schema: type: string diff --git a/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts b/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts index 52ce24634886e2..1ebe49ef4ed395 100644 --- a/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts +++ b/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts @@ -5,8 +5,13 @@ * 2.0. */ -import type { AgentPolicy } from '../types'; -import { FLEET_SERVER_PACKAGE, FLEET_APM_PACKAGE, FLEET_SYNTHETICS_PACKAGE } from '../constants'; +import type { NewAgentPolicy, AgentPolicy } from '../types'; +import { + FLEET_SERVER_PACKAGE, + FLEET_APM_PACKAGE, + FLEET_SYNTHETICS_PACKAGE, + FLEET_ENDPOINT_PACKAGE, +} from '../constants'; export function policyHasFleetServer(agentPolicy: AgentPolicy) { if (!agentPolicy.package_policies) { @@ -26,6 +31,10 @@ export function policyHasSyntheticsIntegration(agentPolicy: AgentPolicy) { return policyHasIntegration(agentPolicy, FLEET_SYNTHETICS_PACKAGE); } +export function policyHasEndpointSecurity(agentPolicy: Partial) { + return policyHasIntegration(agentPolicy as AgentPolicy, FLEET_ENDPOINT_PACKAGE); +} + function policyHasIntegration(agentPolicy: AgentPolicy, packageName: string) { if (!agentPolicy.package_policies) { return false; diff --git a/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts b/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts index 70ee2ae631a3cc..e9bd5aa23b5d9f 100644 --- a/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts +++ b/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts @@ -5,15 +5,15 @@ * 2.0. */ +import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; import { pick } from 'lodash'; -import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; +import { createAgentPolicyMock } from '../mocks'; import { isAgentPolicyValidForLicense, unsetAgentPolicyAccordingToLicenseLevel, } from './agent_policy_config'; -import { generateNewAgentPolicyWithDefaults } from './generate_new_agent_policy'; describe('agent policy config and licenses', () => { const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); @@ -34,13 +34,13 @@ describe('agent policy config and licenses', () => { }); describe('unsetAgentPolicyAccordingToLicenseLevel', () => { it('resets all paid features to default if license is gold', () => { - const defaults = pick(generateNewAgentPolicyWithDefaults(), 'is_protected'); + const defaults = pick(createAgentPolicyMock(), 'is_protected'); const partialPolicy = { is_protected: true }; const retPolicy = unsetAgentPolicyAccordingToLicenseLevel(partialPolicy, Gold); expect(retPolicy).toEqual(defaults); }); it('does not change paid features if license is platinum', () => { - const expected = pick(generateNewAgentPolicyWithDefaults(), 'is_protected'); + const expected = pick(createAgentPolicyMock(), 'is_protected'); const partialPolicy = { is_protected: false }; const expected2 = { is_protected: true }; const partialPolicy2 = { is_protected: true }; diff --git a/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts b/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts index 97e63be4bd7013..bc4a6b55f75ee8 100644 --- a/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts @@ -27,7 +27,6 @@ describe('generateNewAgentPolicyWithDefaults', () => { description: 'test description', namespace: 'test-namespace', monitoring_enabled: ['logs'], - is_protected: true, }); expect(newAgentPolicy).toEqual({ @@ -36,7 +35,7 @@ describe('generateNewAgentPolicyWithDefaults', () => { namespace: 'test-namespace', monitoring_enabled: ['logs'], inactivity_timeout: 1209600, - is_protected: true, + is_protected: false, }); }); }); diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index abf06ac54b07cf..04f74404ba382e 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -66,6 +66,7 @@ export { agentStatusesToSummary } from './agent_statuses_to_summary'; export { policyHasFleetServer, policyHasAPMIntegration, + policyHasEndpointSecurity, policyHasSyntheticsIntegration, } from './agent_policies_helpers'; diff --git a/x-pack/plugins/fleet/common/types/models/settings.ts b/x-pack/plugins/fleet/common/types/models/settings.ts index 01f95146e36211..e4175ae3bbfaf7 100644 --- a/x-pack/plugins/fleet/common/types/models/settings.ts +++ b/x-pack/plugins/fleet/common/types/models/settings.ts @@ -14,4 +14,5 @@ export interface BaseSettings { export interface Settings extends BaseSettings { id: string; preconfigured_fields?: Array<'fleet_server_hosts'>; + secret_storage_requirements_met?: boolean; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx index 019395c0cb5f60..fe21159ab347b3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx @@ -17,11 +17,13 @@ import { allowedExperimentalValues } from '../../../../../../../common/experimen import { ExperimentalFeaturesService } from '../../../../../../services/experimental_features'; -import type { NewAgentPolicy, AgentPolicy } from '../../../../../../../common/types'; +import { createAgentPolicyMock, createPackagePolicyMock } from '../../../../../../../common/mocks'; +import type { AgentPolicy, NewAgentPolicy } from '../../../../../../../common/types'; import { useLicense } from '../../../../../../hooks/use_license'; import type { LicenseService } from '../../../../../../../common/services'; +import { generateNewAgentPolicyWithDefaults } from '../../../../../../../common/services'; import type { ValidationResults } from '../agent_policy_validation'; @@ -34,12 +36,7 @@ const mockedUseLicence = useLicense as jest.MockedFunction; describe('Agent policy advanced options content', () => { let testRender: TestRenderer; let renderResult: RenderResult; - - const mockAgentPolicy: Partial = { - name: 'some-agent-policy', - is_managed: false, - }; - + let mockAgentPolicy: Partial; const mockUpdateAgentPolicy = jest.fn(); const mockValidation = jest.fn() as unknown as ValidationResults; const usePlatinumLicense = () => @@ -48,16 +45,34 @@ describe('Agent policy advanced options content', () => { isPlatinum: () => true, } as unknown as LicenseService); - const render = ({ isProtected = false, policyId = 'agent-policy-1' } = {}) => { + const render = ({ + isProtected = false, + policyId = 'agent-policy-1', + newAgentPolicy = false, + packagePolicy = [createPackagePolicyMock()], + } = {}) => { // remove when feature flag is removed ExperimentalFeaturesService.init({ ...allowedExperimentalValues, agentTamperProtectionEnabled: true, }); + if (newAgentPolicy) { + mockAgentPolicy = generateNewAgentPolicyWithDefaults(); + } else { + mockAgentPolicy = { + ...createAgentPolicyMock(), + package_policies: packagePolicy, + id: policyId, + }; + } + renderResult = testRender.render( @@ -118,5 +133,33 @@ describe('Agent policy advanced options content', () => { }); expect(mockUpdateAgentPolicy).toHaveBeenCalledWith({ is_protected: true }); }); + describe('when the defend integration is not installed', () => { + beforeEach(() => { + usePlatinumLicense(); + render({ + packagePolicy: [ + { + ...createPackagePolicyMock(), + package: { name: 'not-endpoint', title: 'Not Endpoint', version: '0.1.0' }, + }, + ], + isProtected: true, + }); + }); + it('should disable the switch and uninstall command link', () => { + expect(renderResult.getByTestId('tamperProtectionSwitch')).toBeDisabled(); + expect(renderResult.getByTestId('uninstallCommandLink')).toBeDisabled(); + }); + it('should show an icon tip explaining why the switch is disabled', () => { + expect(renderResult.getByTestId('tamperMissingIntegrationTooltip')).toBeTruthy(); + }); + }); + describe('when the user is creating a new agent policy', () => { + it('should be disabled, since it has no package policies and therefore elastic defend integration is not installed', async () => { + usePlatinumLicense(); + render({ newAgentPolicy: true }); + expect(renderResult.getByTestId('tamperProtectionSwitch')).toBeDisabled(); + }); + }); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx index 8811a7d97ed610..49288da22c9350 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { EuiDescribedFormGroup, EuiFormRow, @@ -46,6 +46,8 @@ import type { ValidationResults } from '../agent_policy_validation'; import { ExperimentalFeaturesService, policyHasFleetServer } from '../../../../services'; +import { policyHasEndpointSecurity as hasElasticDefend } from '../../../../../../../common/services'; + import { useOutputOptions, useDownloadSourcesOptions, @@ -106,6 +108,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = const { agentTamperProtectionEnabled } = ExperimentalFeaturesService.get(); const licenseService = useLicense(); const [isUninstallCommandFlyoutOpen, setIsUninstallCommandFlyoutOpen] = useState(false); + const policyHasElasticDefend = useMemo(() => hasElasticDefend(agentPolicy), [agentPolicy]); return ( <> @@ -317,13 +320,34 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = } > + {' '} + {!policyHasElasticDefend && ( + + + + )} + + } checked={agentPolicy.is_protected ?? false} onChange={(e) => { updateAgentPolicy({ is_protected: e.target.checked }); }} + disabled={!policyHasElasticDefend} data-test-subj="tamperProtectionSwitch" /> {agentPolicy.id && ( @@ -333,7 +357,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = onClick={() => { setIsUninstallCommandFlyoutOpen(true); }} - disabled={agentPolicy.is_protected === false} + disabled={!agentPolicy.is_protected || !policyHasElasticDefend} data-test-subj="uninstallCommandLink" > {i18n.translate('xpack.fleet.agentPolicyForm.tamperingUninstallLink', { diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 807eef8ba9917a..74af2fe533a9bb 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -79,6 +79,7 @@ export { MESSAGE_SIGNING_SERVICE_API_ROUTES, // secrets SECRETS_ENDPOINT_PATH, + SECRETS_MINIMUM_FLEET_SERVER_VERSION, } from '../../common/constants'; export { diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index a0b6999f0feb60..e3e04820899d45 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -84,6 +84,7 @@ import type { InstallationInfo, } from '../../types'; import { getDataStreams } from '../../services/epm/data_streams'; +import { NamingCollisionError } from '../../services/epm/packages/custom_integrations/validation/check_naming_collision'; const CACHE_CONTROL_10_MINUTES_HEADER: HttpResponseOptions['headers'] = { 'cache-control': 'max-age=600', @@ -419,28 +420,40 @@ export const createCustomIntegrationHandler: FleetRequestHandler< const spaceId = fleetContext.spaceId; const { integrationName, force, datasets } = request.body; - const res = await installPackage({ - installSource: 'custom', - savedObjectsClient, - pkgName: integrationName, - datasets, - esClient, - spaceId, - force, - authorizationHeader, - kibanaVersion, - }); + try { + const res = await installPackage({ + installSource: 'custom', + savedObjectsClient, + pkgName: integrationName, + datasets, + esClient, + spaceId, + force, + authorizationHeader, + kibanaVersion, + }); - if (!res.error) { - const body: InstallPackageResponse = { - items: res.assets || [], - _meta: { - install_source: res.installSource, - }, - }; - return response.ok({ body }); - } else { - return await defaultFleetErrorHandler({ error: res.error, response }); + if (!res.error) { + const body: InstallPackageResponse = { + items: res.assets || [], + _meta: { + install_source: res.installSource, + }, + }; + return response.ok({ body }); + } else { + return await defaultFleetErrorHandler({ error: res.error, response }); + } + } catch (error) { + if (error instanceof NamingCollisionError) { + return response.customError({ + statusCode: 409, + body: { + message: error.message, + }, + }); + } + return await defaultFleetErrorHandler({ error, response }); } }; diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index b7013bf43ff84d..d4f41c03483115 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -90,6 +90,7 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({ fleet_server_hosts: { type: 'keyword' }, has_seen_add_data_notice: { type: 'boolean', index: false }, prerelease_integrations_enabled: { type: 'boolean' }, + secret_storage_requirements_met: { type: 'boolean' }, }, }, migrations: { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 6958ea80c00d60..44635eee45200a 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -22,6 +22,8 @@ import type { BulkResponseItem } from '@elastic/elasticsearch/lib/api/typesWithB import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; +import { policyHasEndpointSecurity } from '../../common/services'; + import { populateAssignedAgentsCount } from '../routes/agent_policy/handlers'; import type { HTTPAuthorizationHeader } from '../../common/http_authorization_header'; @@ -113,7 +115,10 @@ class AgentPolicyService { id: string, agentPolicy: Partial, user?: AuthenticatedUser, - options: { bumpRevision: boolean } = { bumpRevision: true } + options: { bumpRevision: boolean; removeProtection: boolean } = { + bumpRevision: true, + removeProtection: false, + } ): Promise { auditLoggingService.writeCustomSoAuditLog({ action: 'update', @@ -136,6 +141,12 @@ class AgentPolicyService { ); } + const logger = appContextService.getLogger(); + + if (options.removeProtection) { + logger.warn(`Setting tamper protection for Agent Policy ${id} to false`); + } + await validateOutputForPolicy( soClient, agentPolicy, @@ -145,11 +156,14 @@ class AgentPolicyService { await soClient.update(SAVED_OBJECT_TYPE, id, { ...agentPolicy, ...(options.bumpRevision ? { revision: existingAgentPolicy.revision + 1 } : {}), + ...(options.removeProtection + ? { is_protected: false } + : { is_protected: agentPolicy.is_protected }), updated_at: new Date().toISOString(), updated_by: user ? user.username : 'system', }); - if (options.bumpRevision) { + if (options.bumpRevision || options.removeProtection) { await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'updated', id); } @@ -239,6 +253,14 @@ class AgentPolicyService { this.checkTamperProtectionLicense(agentPolicy); + const logger = appContextService.getLogger(); + + if (agentPolicy?.is_protected) { + logger.warn( + 'Agent policy requires Elastic Defend integration to set tamper protection to true' + ); + } + await this.requireUniqueName(soClient, agentPolicy); await validateOutputForPolicy(soClient, agentPolicy); @@ -253,7 +275,7 @@ class AgentPolicyService { updated_at: new Date().toISOString(), updated_by: options?.user?.username || 'system', schema_version: FLEET_AGENT_POLICIES_SCHEMA_VERSION, - is_protected: agentPolicy.is_protected ?? false, + is_protected: false, } as AgentPolicy, options ); @@ -491,6 +513,16 @@ class AgentPolicyService { this.checkTamperProtectionLicense(agentPolicy); + const logger = appContextService.getLogger(); + + if (agentPolicy?.is_protected && !policyHasEndpointSecurity(existingAgentPolicy)) { + logger.warn( + 'Agent policy requires Elastic Defend integration to set tamper protection to true' + ); + // force agent policy to be false if elastic defend is not present + agentPolicy.is_protected = false; + } + if (existingAgentPolicy.is_managed && !options?.force) { Object.entries(agentPolicy) .filter(([key]) => !KEY_EDITABLE_FOR_MANAGED_POLICIES.includes(key)) @@ -586,9 +618,12 @@ class AgentPolicyService { soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, id: string, - options?: { user?: AuthenticatedUser } + options?: { user?: AuthenticatedUser; removeProtection?: boolean } ): Promise { - const res = await this._update(soClient, esClient, id, {}, options?.user); + const res = await this._update(soClient, esClient, id, {}, options?.user, { + bumpRevision: true, + removeProtection: options?.removeProtection ?? false, + }); return res; } diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index fdce358049006b..b6a124d3c32aba 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -444,6 +444,66 @@ export async function getAgentsById( ); } +// given a list of agentPolicyIds, return a map of agent version => count of agents +// this is used to get all fleet server versions +export async function getAgentVersionsForAgentPolicyIds( + esClient: ElasticsearchClient, + agentPolicyIds: string[] +): Promise> { + const versionCount: Record = {}; + + if (!agentPolicyIds.length) { + return versionCount; + } + + try { + const res = esClient.search< + FleetServerAgent, + Record<'agent_versions', { buckets: Array<{ key: string; doc_count: number }> }> + >({ + size: 0, + track_total_hits: false, + body: { + query: { + bool: { + filter: [ + { + terms: { + policy_id: agentPolicyIds, + }, + }, + ], + }, + }, + aggs: { + agent_versions: { + terms: { + field: 'local_metadata.elastic.agent.version.keyword', + size: 1000, + }, + }, + }, + }, + index: AGENTS_INDEX, + ignore_unavailable: true, + }); + + const { aggregations } = await res; + + if (aggregations && aggregations.agent_versions) { + aggregations.agent_versions.buckets.forEach((bucket) => { + versionCount[bucket.key] = bucket.doc_count; + }); + } + } catch (error) { + if (error.statusCode !== 404) { + throw error; + } + } + + return versionCount; +} + export async function getAgentByAccessAPIKeyId( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_naming_collision.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_naming_collision.ts new file mode 100644 index 00000000000000..1b8c135ae129b4 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/validation/check_naming_collision.ts @@ -0,0 +1,88 @@ +/* + * 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 { nodeBuilder } from '@kbn/es-query'; +import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; + +import { i18n } from '@kbn/i18n'; + +import { auditLoggingService } from '../../../../audit_logging'; +import { PACKAGES_SAVED_OBJECT_TYPE, type Installation } from '../../../../../../common'; +import * as Registry from '../../../registry'; + +export const checkForNamingCollision = async ( + savedObjectsClient: SavedObjectsClientContract, + integrationName: string +) => { + await checkForRegistryNamingCollision(savedObjectsClient, integrationName); + await checkForInstallationNamingCollision(savedObjectsClient, integrationName); +}; + +export const checkForRegistryNamingCollision = async ( + savedObjectsClient: SavedObjectsClientContract, + integrationName: string +) => { + const registryOrBundledPackage = await Registry.fetchFindLatestPackageOrUndefined( + integrationName + ); + if (registryOrBundledPackage) { + const registryConflictMessage = i18n.translate( + 'xpack.fleet.customIntegrations.namingCollisionError.registryOrBundle', + { + defaultMessage: + 'Failed to create the integration as an integration with the name {integrationName} already exists in the package registry or as a bundled package.', + values: { + integrationName, + }, + } + ); + throw new NamingCollisionError(registryConflictMessage); + } +}; + +export const checkForInstallationNamingCollision = async ( + savedObjectsClient: SavedObjectsClientContract, + integrationName: string +) => { + const result = await savedObjectsClient.find({ + type: PACKAGES_SAVED_OBJECT_TYPE, + perPage: 1, + filter: nodeBuilder.and([ + nodeBuilder.is(`${PACKAGES_SAVED_OBJECT_TYPE}.attributes.name`, integrationName), + ]), + }); + + if (result.saved_objects.length > 0) { + const installationConflictMessage = i18n.translate( + 'xpack.fleet.customIntegrations.namingCollisionError.installationConflictMessage', + { + defaultMessage: + 'Failed to create the integration as an installation with the name {integrationName} already exists.', + values: { + integrationName, + }, + } + ); + throw new NamingCollisionError(installationConflictMessage); + } + + for (const savedObject of result.saved_objects) { + auditLoggingService.writeCustomSoAuditLog({ + action: 'find', + id: savedObject.id, + savedObjectType: PACKAGES_SAVED_OBJECT_TYPE, + }); + } +}; + +export class NamingCollisionError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = 'NamingCollisionError'; + } +} 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 2efb841b75f3ba..4bb77d03996c75 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -102,6 +102,7 @@ import { INITIAL_VERSION } from './custom_integrations/constants'; import { createAssets } from './custom_integrations'; import { cacheAssets } from './custom_integrations/assets/cache'; import { generateDatastreamEntries } from './custom_integrations/assets/dataset/utils'; +import { checkForNamingCollision } from './custom_integrations/validation/check_naming_collision'; export async function isPackageInstalled(options: { savedObjectsClient: SavedObjectsClientContract; @@ -781,6 +782,9 @@ export async function installCustomPackage( kibanaVersion, } = args; + // Validate that we can create this package, validations will throw if they don't pass + await checkForNamingCollision(savedObjectsClient, pkgName); + // Compose a packageInfo const packageInfo = { format_version: CUSTOM_INTEGRATION_PACKAGE_SPEC_VERSION, diff --git a/x-pack/plugins/fleet/server/services/fleet_server/index.ts b/x-pack/plugins/fleet/server/services/fleet_server/index.ts index 6ba6dfcc249105..3690e86a71f480 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/index.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/index.ts @@ -5,10 +5,14 @@ * 2.0. */ -import type { ElasticsearchClient } from '@kbn/core/server'; +import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; +import semverGte from 'semver/functions/gte'; +import semverCoerce from 'semver/functions/coerce'; import { FLEET_SERVER_SERVERS_INDEX } from '../../constants'; +import { getAgentVersionsForAgentPolicyIds } from '../agents'; +import { packagePolicyService } from '../package_policy'; /** * Check if at least one fleet server is connected */ @@ -23,3 +27,42 @@ export async function hasFleetServers(esClient: ElasticsearchClient) { return (res.hits.total as number) > 0; } + +export async function allFleetServerVersionsAreAtLeast( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract, + version: string +): Promise { + let hasMore = true; + const policyIds = new Set(); + let page = 1; + while (hasMore) { + const res = await packagePolicyService.list(soClient, { + page: page++, + perPage: 20, + kuery: 'ingest-package-policies.package.name:fleet_server', + }); + + for (const item of res.items) { + policyIds.add(item.policy_id); + } + + if (res.items.length === 0) { + hasMore = false; + } + } + + const versionCounts = await getAgentVersionsForAgentPolicyIds(esClient, [...policyIds]); + const versions = Object.keys(versionCounts); + + // there must be at least one fleet server agent for this check to pass + if (versions.length === 0) { + return false; + } + + return _allVersionsAreAtLeast(version, versions); +} + +function _allVersionsAreAtLeast(version: string, versions: string[]) { + return versions.every((v) => semverGte(semverCoerce(v)!, version)); +} diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 72fddd0e5b6747..6d47be75b4fd65 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -117,6 +117,7 @@ import { extractAndUpdateSecrets, extractAndWriteSecrets, deleteSecretsIfNotReferenced as deleteSecrets, + isSecretStorageEnabled, } from './secrets'; export type InputsOverride = Partial & { @@ -243,8 +244,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { } validatePackagePolicyOrThrow(enrichedPackagePolicy, pkgInfo); - const { secretsStorage: secretsStorageEnabled } = appContextService.getExperimentalFeatures(); - if (secretsStorageEnabled) { + if (await isSecretStorageEnabled(esClient, soClient)) { const secretsRes = await extractAndWriteSecrets({ packagePolicy: { ...enrichedPackagePolicy, inputs }, packageInfo: pkgInfo, @@ -747,8 +747,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { }); validatePackagePolicyOrThrow(packagePolicy, pkgInfo); - const { secretsStorage: secretsStorageEnabled } = appContextService.getExperimentalFeatures(); - if (secretsStorageEnabled) { + if (await isSecretStorageEnabled(esClient, soClient)) { const secretsRes = await extractAndUpdateSecrets({ oldPackagePolicy, packagePolicyUpdate: { ...restOfPackagePolicy, inputs }, @@ -913,9 +912,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { ); if (pkgInfo) { validatePackagePolicyOrThrow(packagePolicy, pkgInfo); - const { secretsStorage: secretsStorageEnabled } = - appContextService.getExperimentalFeatures(); - if (secretsStorageEnabled) { + if (await isSecretStorageEnabled(esClient, soClient)) { const secretsRes = await extractAndUpdateSecrets({ oldPackagePolicy, packagePolicyUpdate: { ...restOfPackagePolicy, inputs }, @@ -1161,13 +1158,22 @@ class PackagePolicyClientImpl implements PackagePolicyClient { ...new Set(result.filter((r) => r.success && r.policy_id).map((r) => r.policy_id!)), ]; + const agentPoliciesWithEndpointPackagePolicies = result.reduce((acc, cur) => { + if (cur.success && cur.policy_id && cur.package?.name === 'endpoint') { + return acc.add(cur.policy_id); + } + return acc; + }, new Set()); + const agentPolicies = await agentPolicyService.getByIDs(soClient, uniquePolicyIdsR); for (const policyId of uniquePolicyIdsR) { const agentPolicy = agentPolicies.find((p) => p.id === policyId); if (agentPolicy) { + // is the agent policy attached to package policy with endpoint await agentPolicyService.bumpRevision(soClient, esClient, policyId, { user: options?.user, + removeProtection: agentPoliciesWithEndpointPackagePolicies.has(policyId), }); } } diff --git a/x-pack/plugins/fleet/server/services/secrets.ts b/x-pack/plugins/fleet/server/services/secrets.ts index 7e6efde6c11b03..886e7d7243172a 100644 --- a/x-pack/plugins/fleet/server/services/secrets.ts +++ b/x-pack/plugins/fleet/server/services/secrets.ts @@ -34,7 +34,7 @@ import type { } from '../types'; import { FleetError } from '../errors'; -import { SECRETS_ENDPOINT_PATH } from '../constants'; +import { SECRETS_ENDPOINT_PATH, SECRETS_MINIMUM_FLEET_SERVER_VERSION } from '../constants'; import { retryTransientEsErrors } from './epm/elasticsearch/retry'; @@ -42,6 +42,8 @@ import { auditLoggingService } from './audit_logging'; import { appContextService } from './app_context'; import { packagePolicyService } from './package_policy'; +import { settingsService } from '.'; +import { allFleetServerVersionsAreAtLeast } from './fleet_server'; export async function createSecrets(opts: { esClient: ElasticsearchClient; @@ -270,10 +272,21 @@ export async function extractAndUpdateSecrets(opts: { ...createdSecrets.map(({ id }) => ({ id })), ]; + const secretsToDelete: PolicySecretReference[] = []; + + toDelete.forEach((secretPath) => { + // check if the previous secret is actually a secret refrerence + // it may be that secrets were not enabled at the time of creation + // in which case they are just stored as plain text + if (secretPath.value.value.isSecretRef) { + secretsToDelete.push({ id: secretPath.value.value.id }); + } + }); + return { packagePolicyUpdate: policyWithSecretRefs, secretReferences, - secretsToDelete: toDelete.map((secretPath) => ({ id: secretPath.value.value.id })), + secretsToDelete, }; } @@ -344,6 +357,58 @@ export function getPolicySecretPaths( return [...packageLevelVarPaths, ...inputSecretPaths]; } +export async function isSecretStorageEnabled( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract +): Promise { + const logger = appContextService.getLogger(); + + // first check if the feature flag is enabled, if not secrets are disabled + const { secretsStorage: secretsStorageEnabled } = appContextService.getExperimentalFeatures(); + if (!secretsStorageEnabled) { + logger.debug('Secrets storage is disabled by feature flag'); + return false; + } + + // if serverless then secrets will always be supported + const isFleetServerStandalone = + appContextService.getConfig()?.internal?.fleetServerStandalone ?? false; + + if (isFleetServerStandalone) { + logger.trace('Secrets storage is enabled as fleet server is standalone'); + return true; + } + + // now check the flag in settings to see if the fleet server requirement has already been met + // once the requirement has been met, secrets are always on + const settings = await settingsService.getSettings(soClient); + + if (settings.secret_storage_requirements_met) { + logger.debug('Secrets storage already met, turned on is settings'); + return true; + } + + // otherwise check if we have the minimum fleet server version and enable secrets if so + if ( + await allFleetServerVersionsAreAtLeast(esClient, soClient, SECRETS_MINIMUM_FLEET_SERVER_VERSION) + ) { + logger.debug('Enabling secrets storage as minimum fleet server version has been met'); + try { + await settingsService.saveSettings(soClient, { + secret_storage_requirements_met: true, + }); + } catch (err) { + // we can suppress this error as it will be retried on the next function call + logger.warn(`Failed to save settings after enabling secrets storage: ${err.message}`); + } + + return true; + } + + logger.info('Secrets storage is disabled as minimum fleet server version has not been met'); + return false; +} + function _getPackageLevelSecretPaths( packagePolicy: NewPackagePolicy, packageInfo: PackageInfo diff --git a/x-pack/plugins/fleet/server/types/so_attributes.ts b/x-pack/plugins/fleet/server/types/so_attributes.ts index 941f27cde7c4de..5706a7ec12ff31 100644 --- a/x-pack/plugins/fleet/server/types/so_attributes.ts +++ b/x-pack/plugins/fleet/server/types/so_attributes.ts @@ -202,6 +202,7 @@ export interface SettingsSOAttributes { prerelease_integrations_enabled: boolean; has_seen_add_data_notice?: boolean; fleet_server_hosts?: string[]; + secret_storage_requirements_met?: boolean; } export interface DownloadSourceSOAttributes { diff --git a/x-pack/plugins/graph/config.ts b/x-pack/plugins/graph/config.ts index 44cd9cb32e2636..75daa64230c48c 100644 --- a/x-pack/plugins/graph/config.ts +++ b/x-pack/plugins/graph/config.ts @@ -18,6 +18,12 @@ export const configSchema = schema.object({ { defaultValue: 'configAndData' } ), canEditDrillDownUrls: schema.boolean({ defaultValue: true }), + enabled: schema.conditional( + schema.contextRef('serverless'), + true, + schema.maybe(schema.boolean({ defaultValue: true })), + schema.never() + ), }); export type ConfigSchema = TypeOf; diff --git a/x-pack/plugins/index_management/README.md b/x-pack/plugins/index_management/README.md index fba162259ce915..b50309ac360997 100644 --- a/x-pack/plugins/index_management/README.md +++ b/x-pack/plugins/index_management/README.md @@ -53,7 +53,7 @@ POST %25%7B%5B%40metadata%5D%5Bbeat%5D%7D-%25%7B%5B%40metadata%5D%5Bversion%5D%7 ### Quick steps for testing -By default, **legacy index templates** are not shown in the UI. Make them appear by creating one in Console: +**Legacy index templates** are only shown in the UI on stateful *and* if a user has existing legacy index templates. You can test this functionality by creating one in Console: ``` PUT _template/template_1 @@ -62,6 +62,8 @@ PUT _template/template_1 } ``` +On serverless, Elasticsearch does not support legacy index templates and therefore this functionality is disabled in Kibana via the config `xpack.index_management.enableLegacyTemplates`. For more details, see [#163518](https://github.com/elastic/kibana/pull/163518). + To test **Cloud-managed templates**: 1. Add `cluster.metadata.managed_index_templates` setting via Dev Tools: diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx index 67fa2d99787ba4..b1dd1d748f3095 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx @@ -56,6 +56,11 @@ const appDependencies = { executionContext: executionContextServiceMock.createStartContract(), }, plugins: {}, + // Default stateful configuration + config: { + enableLegacyTemplates: true, + enableIndexActions: true, + }, } as any; export const kibanaVersion = new SemVer(MAJOR_VERSION); @@ -82,7 +87,6 @@ export const WithAppDependencies = (props: any) => { httpService.setup(httpSetup); const mergedDependencies = merge({}, appDependencies, overridingDependencies); - return ( diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index 8b2b1d65682532..16a3e1fd09bbd3 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -225,11 +225,11 @@ describe('', () => { ]); httpRequestsMockHelpers.setReloadIndicesResponse({ indexNames: [indexNameA, indexNameB] }); - testBed = await setup(httpSetup, { - enableIndexActions: true, + await act(async () => { + testBed = await setup(httpSetup); }); - const { component, find } = testBed; + const { component, find } = testBed; component.update(); find('indexTableIndexNameLink').at(0).simulate('click'); @@ -270,8 +270,8 @@ describe('', () => { }); test('should be able to open a closed index', async () => { - testBed = await setup(httpSetup, { - enableIndexActions: true, + await act(async () => { + testBed = await setup(httpSetup); }); const { component, find, actions } = testBed; diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index ad704311dd2103..5d8371010b3fe7 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -168,7 +168,11 @@ describe('index table', () => { }, plugins: {}, url: urlServiceMock, - enableIndexActions: true, + // Default stateful configuration + config: { + enableLegacyTemplates: true, + enableIndexActions: true, + }, }; component = ( @@ -515,8 +519,8 @@ describe('index table', () => { describe('Common index actions', () => { beforeEach(() => { - // Mock initialization of services - setupMockComponent({ enableIndexActions: false }); + // Mock initialization of services; set enableIndexActions=false to verify config behavior + setupMockComponent({ config: { enableIndexActions: false, enableLegacyTemplates: true } }); }); test('Common index actions should be hidden when feature is turned off', async () => { diff --git a/x-pack/plugins/index_management/common/constants/api_base_path.ts b/x-pack/plugins/index_management/common/constants/api_base_path.ts index c923913bb9d836..c111d95dab1928 100644 --- a/x-pack/plugins/index_management/common/constants/api_base_path.ts +++ b/x-pack/plugins/index_management/common/constants/api_base_path.ts @@ -6,3 +6,5 @@ */ export const API_BASE_PATH = '/api/index_management'; + +export const INTERNAL_API_BASE_PATH = '/internal/index_management'; diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts index 6641e6ef67c7d6..786dad4a5e375f 100644 --- a/x-pack/plugins/index_management/common/constants/index.ts +++ b/x-pack/plugins/index_management/common/constants/index.ts @@ -6,7 +6,7 @@ */ export { BASE_PATH } from './base_path'; -export { API_BASE_PATH } from './api_base_path'; +export { API_BASE_PATH, INTERNAL_API_BASE_PATH } from './api_base_path'; export { INVALID_INDEX_PATTERN_CHARS, INVALID_TEMPLATE_NAME_CHARS } from './invalid_characters'; export * from './index_statuses'; diff --git a/x-pack/plugins/index_management/common/index.ts b/x-pack/plugins/index_management/common/index.ts index 127123609b1868..a481d17615d8d8 100644 --- a/x-pack/plugins/index_management/common/index.ts +++ b/x-pack/plugins/index_management/common/index.ts @@ -8,7 +8,7 @@ // TODO: https://github.com/elastic/kibana/issues/110892 /* eslint-disable @kbn/eslint/no_export_all */ -export { API_BASE_PATH, BASE_PATH, MAJOR_VERSION } from './constants'; +export { API_BASE_PATH, INTERNAL_API_BASE_PATH, BASE_PATH, MAJOR_VERSION } from './constants'; export { getTemplateParameter } from './lib'; diff --git a/x-pack/plugins/index_management/common/types/enrich_policies.ts b/x-pack/plugins/index_management/common/types/enrich_policies.ts new file mode 100644 index 00000000000000..4688cb41135f19 --- /dev/null +++ b/x-pack/plugins/index_management/common/types/enrich_policies.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types'; + +export interface SerializedEnrichPolicy { + type: EnrichPolicyType; + name: string; + sourceIndices: string[]; + matchField: string; + enrichFields: string[]; +} diff --git a/x-pack/plugins/index_management/common/types/index.ts b/x-pack/plugins/index_management/common/types/index.ts index 0cc514b47024f0..ce5d96a8423663 100644 --- a/x-pack/plugins/index_management/common/types/index.ts +++ b/x-pack/plugins/index_management/common/types/index.ts @@ -16,3 +16,5 @@ export * from './templates'; export type { DataStreamFromEs, Health, DataStream, DataStreamIndex } from './data_streams'; export * from './component_templates'; + +export * from './enrich_policies'; diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 9acbda3f9685fd..eb52f50d62ecf9 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -44,6 +44,10 @@ export interface AppDependencies { httpService: HttpService; notificationService: NotificationService; }; + config: { + enableIndexActions: boolean; + enableLegacyTemplates: boolean; + }; history: ScopedHistory; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; uiSettings: IUiSettingsClient; @@ -52,7 +56,6 @@ export interface AppDependencies { docLinks: DocLinksStart; kibanaVersion: SemVer; theme$: Observable; - enableIndexActions: boolean; } export const AppContextProvider = ({ diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index 6bb3b834ce85fe..997568a2eb69aa 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -53,7 +53,8 @@ export async function mountManagementSection( extensionsService: ExtensionsService, isFleetEnabled: boolean, kibanaVersion: SemVer, - enableIndexActions: boolean = true + enableIndexActions: boolean = true, + enableLegacyTemplates: boolean = true ) { const { element, setBreadcrumbs, history, theme$ } = params; const [core, startDependencies] = await coreSetup.getStartServices(); @@ -95,7 +96,10 @@ export async function mountManagementSection( uiMetricService, extensionsService, }, - enableIndexActions, + config: { + enableIndexActions, + enableLegacyTemplates, + }, history, setBreadcrumbs, uiSettings, diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js index 4188797431e5d0..4dd22c0a73e13b 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js @@ -49,7 +49,9 @@ export class IndexActionsContextMenu extends Component { this.setState({ isActionConfirmed }); }; panels({ services: { extensionsService }, core: { getUrlForApp } }) { - const { enableIndexActions } = this.context; + const { + config: { enableIndexActions }, + } = this.context; const { closeIndices, diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx index 3a49237a517c9b..eff5cbb5549042 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -19,6 +19,7 @@ import { getTemplateDetailsLink } from '../../services/routing'; import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; import { attemptToURIDecode } from '../../../shared_imports'; +import { useAppContext } from '../../app_context'; interface MatchParams { name: string; @@ -32,7 +33,11 @@ export const TemplateClone: React.FunctionComponent { const decodedTemplateName = attemptToURIDecode(name)!; - const isLegacy = getIsLegacyFromQueryParams(location); + const { + config: { enableLegacyTemplates }, + } = useAppContext(); + // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard + const isLegacy = enableLegacyTemplates && getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx index cb8f29d222d63b..e5422ca93db262 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx @@ -18,12 +18,17 @@ import { TemplateForm } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; import { saveTemplate } from '../../services/api'; import { getTemplateDetailsLink } from '../../services/routing'; +import { useAppContext } from '../../app_context'; export const TemplateCreate: React.FunctionComponent = ({ history }) => { const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); + const { + config: { enableLegacyTemplates }, + } = useAppContext(); const search = parse(useLocation().search.substring(1)); - const isLegacy = Boolean(search.legacy); + // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard + const isLegacy = enableLegacyTemplates && Boolean(search.legacy); const onSave = async (template: TemplateDeserialized) => { const { name } = template; diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index c96502fd15066d..b0a6b953513865 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -23,6 +23,7 @@ import { useLoadIndexTemplate, updateTemplate } from '../../services/api'; import { getTemplateDetailsLink } from '../../services/routing'; import { TemplateForm } from '../../components'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; +import { useAppContext } from '../../app_context'; interface MatchParams { name: string; @@ -36,7 +37,12 @@ export const TemplateEdit: React.FunctionComponent { const decodedTemplateName = attemptToURIDecode(name)!; - const isLegacy = getIsLegacyFromQueryParams(location); + const { + config: { enableLegacyTemplates }, + } = useAppContext(); + + // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the enableLegacyTemplates check as a safeguard + const isLegacy = enableLegacyTemplates && getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); diff --git a/x-pack/plugins/index_management/public/mocks.ts b/x-pack/plugins/index_management/public/mocks.ts index 30e21c80be5b16..69a43b985787ad 100644 --- a/x-pack/plugins/index_management/public/mocks.ts +++ b/x-pack/plugins/index_management/public/mocks.ts @@ -6,12 +6,15 @@ */ import { extensionsServiceMock } from './services/extensions_service.mock'; +import { publicApiServiceMock } from './services/public_api_service.mock'; export { extensionsServiceMock } from './services/extensions_service.mock'; +export { publicApiServiceMock } from './services/public_api_service.mock'; function createIdxManagementSetupMock() { const mock = { extensionsService: extensionsServiceMock, + publicApiService: publicApiServiceMock, }; return mock; diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index fc965a061e0bf1..0771e254fd6aa6 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -11,7 +11,7 @@ import SemVer from 'semver/classes/semver'; import { CoreSetup, PluginInitializerContext } from '@kbn/core/public'; import { setExtensionsService } from './application/store/selectors/extension_service'; -import { ExtensionsService } from './services'; +import { ExtensionsService, PublicApiService } from './services'; import { IndexManagementPluginSetup, @@ -39,6 +39,7 @@ export class IndexMgmtUIPlugin { const { ui: { enabled: isIndexManagementUiEnabled }, enableIndexActions, + enableLegacyTemplates, } = this.ctx.config.get(); if (isIndexManagementUiEnabled) { @@ -57,13 +58,15 @@ export class IndexMgmtUIPlugin { this.extensionsService, Boolean(fleet), kibanaVersion, - enableIndexActions + enableIndexActions, + enableLegacyTemplates ); }, }); } return { + apiService: new PublicApiService(coreSetup.http), extensionsService: this.extensionsService.setup(), }; } diff --git a/x-pack/plugins/index_management/public/services/index.ts b/x-pack/plugins/index_management/public/services/index.ts index f32787a427b891..8f4ddbeffba352 100644 --- a/x-pack/plugins/index_management/public/services/index.ts +++ b/x-pack/plugins/index_management/public/services/index.ts @@ -7,3 +7,6 @@ export type { ExtensionsSetup } from './extensions_service'; export { ExtensionsService } from './extensions_service'; + +export type { PublicApiServiceSetup } from './public_api_service'; +export { PublicApiService } from './public_api_service'; diff --git a/x-pack/plugins/index_management/public/services/public_api_service.mock.ts b/x-pack/plugins/index_management/public/services/public_api_service.mock.ts new file mode 100644 index 00000000000000..85ce1b232c06a8 --- /dev/null +++ b/x-pack/plugins/index_management/public/services/public_api_service.mock.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. + */ + +import { PublicApiServiceSetup } from './public_api_service'; + +export type PublicApiServiceSetupMock = jest.Mocked; + +const createServiceMock = (): PublicApiServiceSetupMock => ({ + getAllEnrichPolicies: jest.fn(), +}); + +export const publicApiServiceMock = { + createSetupContract: createServiceMock, +}; diff --git a/x-pack/plugins/index_management/public/services/public_api_service.ts b/x-pack/plugins/index_management/public/services/public_api_service.ts new file mode 100644 index 00000000000000..33d43b9304fdb2 --- /dev/null +++ b/x-pack/plugins/index_management/public/services/public_api_service.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpSetup } from '@kbn/core/public'; +import { sendRequest, SendRequestResponse } from '../shared_imports'; +import { API_BASE_PATH } from '../../common/constants'; +import { SerializedEnrichPolicy } from '../../common/types'; + +export interface PublicApiServiceSetup { + getAllEnrichPolicies(): Promise>; +} + +/** + * Index Management public API service + */ +export class PublicApiService { + private http: HttpSetup; + + /** + * constructor + * @param http http dependency + */ + constructor(http: HttpSetup) { + this.http = http; + } + + /** + * Gets a list of all the enrich policies + */ + getAllEnrichPolicies() { + return sendRequest(this.http, { + path: `${API_BASE_PATH}/enrich_policies`, + method: 'get', + }); + } +} diff --git a/x-pack/plugins/index_management/public/types.ts b/x-pack/plugins/index_management/public/types.ts index 20d2405a0fa4b0..b3e479b081fb4b 100644 --- a/x-pack/plugins/index_management/public/types.ts +++ b/x-pack/plugins/index_management/public/types.ts @@ -8,9 +8,10 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { ManagementSetup } from '@kbn/management-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; -import { ExtensionsSetup } from './services'; +import { ExtensionsSetup, PublicApiServiceSetup } from './services'; export interface IndexManagementPluginSetup { + apiService: PublicApiServiceSetup; extensionsService: ExtensionsSetup; } @@ -29,4 +30,5 @@ export interface ClientConfigType { enabled: boolean; }; enableIndexActions?: boolean; + enableLegacyTemplates?: boolean; } diff --git a/x-pack/plugins/index_management/server/config.ts b/x-pack/plugins/index_management/server/config.ts index c5d459486a8ef9..f480c7747ca8d1 100644 --- a/x-pack/plugins/index_management/server/config.ts +++ b/x-pack/plugins/index_management/server/config.ts @@ -30,6 +30,14 @@ const schemaLatest = schema.object( schema.boolean({ defaultValue: true }), schema.never() ), + enableLegacyTemplates: schema.conditional( + schema.contextRef('serverless'), + true, + // Legacy templates functionality is disabled in serverless; refer to the serverless.yml file as the source of truth + // We take this approach in order to have a central place (serverless.yml) for serverless config across Kibana + schema.boolean({ defaultValue: true }), + schema.never() + ), }, { defaultValue: undefined } ); @@ -38,6 +46,7 @@ const configLatest: PluginConfigDescriptor = { exposeToBrowser: { ui: true, enableIndexActions: true, + enableLegacyTemplates: true, }, schema: schemaLatest, deprecations: () => [], diff --git a/x-pack/plugins/index_management/server/lib/enrich_policies.test.ts b/x-pack/plugins/index_management/server/lib/enrich_policies.test.ts new file mode 100644 index 00000000000000..15517b2bfc20f2 --- /dev/null +++ b/x-pack/plugins/index_management/server/lib/enrich_policies.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { serializeEnrichmentPolicies } from './enrich_policies'; +import { createTestESEnrichPolicy } from '../test/helpers'; + +describe('serializeEnrichmentPolicies', () => { + it('knows how to serialize a list of policies', async () => { + const mockedESPolicy = createTestESEnrichPolicy('my-policy', 'match'); + expect(serializeEnrichmentPolicies([mockedESPolicy])).toEqual([ + { + name: 'my-policy', + type: 'match', + sourceIndices: ['users'], + matchField: 'email', + enrichFields: ['first_name', 'last_name', 'city'], + }, + ]); + }); +}); diff --git a/x-pack/plugins/index_management/server/lib/enrich_policies.ts b/x-pack/plugins/index_management/server/lib/enrich_policies.ts new file mode 100644 index 00000000000000..ca1748a380c706 --- /dev/null +++ b/x-pack/plugins/index_management/server/lib/enrich_policies.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 { IScopedClusterClient } from '@kbn/core/server'; +import type { EnrichSummary, EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types'; +import type { SerializedEnrichPolicy } from '../../common/types'; + +const getPolicyType = (policy: EnrichSummary): EnrichPolicyType => { + if (policy.config.match) { + return 'match'; + } + + if (policy.config.geo_match) { + return 'geo_match'; + } + + if (policy.config.range) { + return 'range'; + } + + throw new Error('Unknown policy type'); +}; + +export const serializeEnrichmentPolicies = ( + policies: EnrichSummary[] +): SerializedEnrichPolicy[] => { + return policies.map((policy: any) => { + const policyType = getPolicyType(policy); + + return { + name: policy.config[policyType].name, + type: policyType, + sourceIndices: policy.config[policyType].indices, + matchField: policy.config[policyType].match_field, + enrichFields: policy.config[policyType].enrich_fields, + }; + }); +}; + +const fetchAll = async (client: IScopedClusterClient) => { + const res = await client.asCurrentUser.enrich.getPolicy(); + + return serializeEnrichmentPolicies(res.policies); +}; + +export const enrichPoliciesActions = { + fetchAll, +}; diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index a36101ad2911e0..a42216d9f1bb7e 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -12,6 +12,7 @@ import { Dependencies } from './types'; import { ApiRoutes } from './routes'; import { IndexDataEnricher } from './services'; import { handleEsError } from './shared_imports'; +import { IndexManagementConfig } from './config'; export interface IndexManagementPluginSetup { indexDataEnricher: { @@ -22,10 +23,12 @@ export interface IndexManagementPluginSetup { export class IndexMgmtServerPlugin implements Plugin { private readonly apiRoutes: ApiRoutes; private readonly indexDataEnricher: IndexDataEnricher; + private readonly config: IndexManagementConfig; constructor(initContext: PluginInitializerContext) { this.apiRoutes = new ApiRoutes(); this.indexDataEnricher = new IndexDataEnricher(); + this.config = initContext.config.get(); } setup( @@ -51,6 +54,7 @@ export class IndexMgmtServerPlugin implements Plugin security !== undefined && security.license.isEnabled(), + isLegacyTemplatesEnabled: this.config.enableLegacyTemplates, }, indexDataEnricher: this.indexDataEnricher, lib: { diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts index dc4214ae43f73e..601695c64e054f 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts @@ -46,6 +46,7 @@ describe('GET privileges', () => { router, config: { isSecurityEnabled: () => true, + isLegacyTemplatesEnabled: true, }, indexDataEnricher: mockedIndexDataEnricher, lib: { @@ -112,6 +113,7 @@ describe('GET privileges', () => { router, config: { isSecurityEnabled: () => false, + isLegacyTemplatesEnabled: true, }, indexDataEnricher: mockedIndexDataEnricher, lib: { diff --git a/x-pack/plugins/index_management/server/routes/api/enrich_policies/enrich_policies.test.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/enrich_policies.test.ts new file mode 100644 index 00000000000000..57d8f3f05a3d6f --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/enrich_policies.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { addInternalBasePath } from '..'; +import { RouterMock, routeDependencies, RequestMock } from '../../../test/helpers'; +import { serializeEnrichmentPolicies } from '../../../lib/enrich_policies'; +import { createTestESEnrichPolicy } from '../../../test/helpers'; + +import { registerEnrichPoliciesRoute } from './register_enrich_policies_routes'; + +const mockedPolicy = createTestESEnrichPolicy('my-policy', 'match'); + +describe('Enrich policies API', () => { + const router = new RouterMock(); + + beforeEach(() => { + registerEnrichPoliciesRoute({ + ...routeDependencies, + router, + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('Get all policies - GET /internal/index_management/enrich_policies', () => { + const getEnrichPolicies = router.getMockESApiFn('enrich.getPolicy'); + + it('returns all available policies', async () => { + const mockRequest: RequestMock = { + method: 'get', + path: addInternalBasePath('/enrich_policies'), + }; + + getEnrichPolicies.mockResolvedValue({ policies: [mockedPolicy] }); + + const res = await router.runRequest(mockRequest); + + expect(res).toEqual({ + body: serializeEnrichmentPolicies([mockedPolicy]), + }); + }); + + it('should return an error if it fails', async () => { + const mockRequest: RequestMock = { + method: 'get', + path: addInternalBasePath('/enrich_policies'), + }; + + const error = new Error('Oh no!'); + getEnrichPolicies.mockRejectedValue(error); + + await expect(router.runRequest(mockRequest)).rejects.toThrowError(error); + }); + }); +}); diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/index.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/index.ts similarity index 69% rename from x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/index.ts rename to x-pack/plugins/index_management/server/routes/api/enrich_policies/index.ts index b7112840436de8..945728dfff9d87 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/index.ts +++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/index.ts @@ -5,7 +5,4 @@ * 2.0. */ -export { XYChart } from './xy_chart'; -export { MetricChart } from './metric_chart'; - -export * from './layers'; +export { registerEnrichPoliciesRoute } from './register_enrich_policies_routes'; diff --git a/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_enrich_policies_routes.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_enrich_policies_routes.ts new file mode 100644 index 00000000000000..ccafe26a2e68fe --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_enrich_policies_routes.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RouteDependencies } from '../../../types'; + +import { registerListRoute } from './register_list_route'; + +export function registerEnrichPoliciesRoute(dependencies: RouteDependencies) { + registerListRoute(dependencies); +} diff --git a/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_list_route.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_list_route.ts new file mode 100644 index 00000000000000..1df52d8f2ba175 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_list_route.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 { IScopedClusterClient } from '@kbn/core/server'; +import { RouteDependencies } from '../../../types'; +import { addInternalBasePath } from '..'; +import { enrichPoliciesActions } from '../../../lib/enrich_policies'; + +export function registerListRoute({ router, lib: { handleEsError } }: RouteDependencies) { + router.get( + { path: addInternalBasePath('/enrich_policies'), validate: false }, + async (context, request, response) => { + const client = (await context.core).elasticsearch.client as IScopedClusterClient; + try { + const policies = await enrichPoliciesActions.fetchAll(client); + return response.ok({ body: policies }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ); +} diff --git a/x-pack/plugins/index_management/server/routes/api/index.ts b/x-pack/plugins/index_management/server/routes/api/index.ts index 98b16e21913a7b..85d937717f41f2 100644 --- a/x-pack/plugins/index_management/server/routes/api/index.ts +++ b/x-pack/plugins/index_management/server/routes/api/index.ts @@ -5,6 +5,8 @@ * 2.0. */ -import { API_BASE_PATH } from '../../../common'; +import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common'; export const addBasePath = (uri: string): string => API_BASE_PATH + uri; + +export const addInternalBasePath = (uri: string): string => INTERNAL_API_BASE_PATH + uri; diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index 32661bb308876a..ce389af9b13e87 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -17,7 +17,7 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template import { RouteDependencies } from '../../../types'; import { addBasePath } from '..'; -export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDependencies) { +export function registerGetAllRoute({ router, config, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/index_templates'), validate: false }, async (context, request, response) => { @@ -25,17 +25,24 @@ export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDep try { const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client); - - const legacyTemplatesEs = await client.asCurrentUser.indices.getTemplate(); const { index_templates: templatesEs } = await client.asCurrentUser.indices.getIndexTemplate(); + // @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns + const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); + + if (config.isLegacyTemplatesEnabled === false) { + // If isLegacyTemplatesEnabled=false, we do not want to fetch legacy templates and return an empty array; + // we retain the same response format to limit changes required on the client + return response.ok({ body: { templates, legacyTemplates: [] } }); + } + + const legacyTemplatesEs = await client.asCurrentUser.indices.getTemplate(); + const legacyTemplates = deserializeLegacyTemplateList( legacyTemplatesEs, cloudManagedTemplatePrefix ); - // @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns - const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); const body = { templates, @@ -59,7 +66,7 @@ const querySchema = schema.object({ legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); -export function registerGetOneRoute({ router, lib: { handleEsError } }: RouteDependencies) { +export function registerGetOneRoute({ router, config, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/index_templates/{name}'), @@ -68,7 +75,10 @@ export function registerGetOneRoute({ router, lib: { handleEsError } }: RouteDep async (context, request, response) => { const { client } = (await context.core).elasticsearch; const { name } = request.params as TypeOf; - const isLegacy = (request.query as TypeOf).legacy === 'true'; + // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard + const isLegacy = + config.isLegacyTemplatesEnabled !== false && + (request.query as TypeOf).legacy === 'true'; try { const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client); diff --git a/x-pack/plugins/index_management/server/routes/index.ts b/x-pack/plugins/index_management/server/routes/index.ts index e2a2eaf1184f66..79d90762920bfc 100644 --- a/x-pack/plugins/index_management/server/routes/index.ts +++ b/x-pack/plugins/index_management/server/routes/index.ts @@ -15,6 +15,7 @@ import { registerSettingsRoutes } from './api/settings'; import { registerStatsRoute } from './api/stats'; import { registerComponentTemplateRoutes } from './api/component_templates'; import { registerNodesRoute } from './api/nodes'; +import { registerEnrichPoliciesRoute } from './api/enrich_policies'; export class ApiRoutes { setup(dependencies: RouteDependencies) { @@ -26,6 +27,7 @@ export class ApiRoutes { registerMappingRoute(dependencies); registerComponentTemplateRoutes(dependencies); registerNodesRoute(dependencies); + registerEnrichPoliciesRoute(dependencies); } start() {} diff --git a/x-pack/plugins/index_management/server/test/helpers/index.ts b/x-pack/plugins/index_management/server/test/helpers/index.ts index 682b520c12b00f..cfce28a4301981 100644 --- a/x-pack/plugins/index_management/server/test/helpers/index.ts +++ b/x-pack/plugins/index_management/server/test/helpers/index.ts @@ -9,3 +9,5 @@ export type { RequestMock } from './router_mock'; export { RouterMock } from './router_mock'; export { routeDependencies } from './route_dependencies'; + +export { createTestESEnrichPolicy } from './policies_fixtures'; diff --git a/x-pack/plugins/index_management/server/test/helpers/policies_fixtures.ts b/x-pack/plugins/index_management/server/test/helpers/policies_fixtures.ts new file mode 100644 index 00000000000000..235c4ad80a1419 --- /dev/null +++ b/x-pack/plugins/index_management/server/test/helpers/policies_fixtures.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types'; + +export const createTestESEnrichPolicy = (name: string, type: EnrichPolicyType) => ({ + config: { + [type]: { + name, + indices: ['users'], + match_field: 'email', + enrich_fields: ['first_name', 'last_name', 'city'], + }, + }, +}); diff --git a/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts b/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts index 592e7490cdbe23..bfcf2a18a77365 100644 --- a/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts +++ b/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts @@ -12,6 +12,7 @@ import type { RouteDependencies } from '../../types'; export const routeDependencies: Omit = { config: { isSecurityEnabled: jest.fn().mockReturnValue(true), + isLegacyTemplatesEnabled: true, }, indexDataEnricher: new IndexDataEnricher(), lib: { diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts index fc245fb664f9c5..bd3d889f2bce94 100644 --- a/x-pack/plugins/index_management/server/types.ts +++ b/x-pack/plugins/index_management/server/types.ts @@ -23,6 +23,7 @@ export interface RouteDependencies { router: IRouter; config: { isSecurityEnabled: () => boolean; + isLegacyTemplatesEnabled: boolean; }; indexDataEnricher: IndexDataEnricher; lib: { diff --git a/x-pack/plugins/infra/public/common/visualizations/constants.ts b/x-pack/plugins/infra/public/common/visualizations/constants.ts index 09583ef7ae3edf..1ef34848435078 100644 --- a/x-pack/plugins/infra/public/common/visualizations/constants.ts +++ b/x-pack/plugins/infra/public/common/visualizations/constants.ts @@ -42,3 +42,4 @@ export const hostLensFormulas = { }; export const HOST_METRICS_DOC_HREF = 'https://ela.st/docs-infra-host-metrics'; +export const HOST_METRICS_DOTTED_LINES_DOC_HREF = 'https://ela.st/docs-infra-why-dotted'; diff --git a/x-pack/plugins/infra/public/common/visualizations/index.ts b/x-pack/plugins/infra/public/common/visualizations/index.ts index 888bf3d8f8ad6b..7bb1ba597dd792 100644 --- a/x-pack/plugins/infra/public/common/visualizations/index.ts +++ b/x-pack/plugins/infra/public/common/visualizations/index.ts @@ -9,14 +9,6 @@ export type { HostsLensFormulas, HostsLensMetricChartFormulas, HostsLensLineChartFormulas, - LensAttributes, - FormulaConfig, - Chart, - LensVisualizationState, } from './types'; export { hostLensFormulas } from './constants'; - -export * from './lens/visualization_types'; - -export { LensAttributesBuilder } from './lens/lens_attributes_builder'; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/kpi_grid_config.ts b/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/kpi_grid_config.ts index 9dde33f39cbdf0..15efef082200f4 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/kpi_grid_config.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/kpi_grid_config.ts @@ -7,13 +7,10 @@ import { i18n } from '@kbn/i18n'; import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; -import type { Layer } from '../../../../../hooks/use_lens_attributes'; +import { UseLensAttributesMetricLayerConfig } from '../../../../../hooks/use_lens_attributes'; import { hostLensFormulas } from '../../../constants'; import { TOOLTIP } from './translations'; -import type { FormulaConfig } from '../../../types'; -import type { MetricLayerOptions } from '../../visualization_types'; - export const KPI_CHART_HEIGHT = 150; export const AVERAGE_SUBTITLE = i18n.translate( 'xpack.infra.assetDetailsEmbeddable.overview.kpi.subtitle.average', @@ -23,7 +20,7 @@ export const AVERAGE_SUBTITLE = i18n.translate( ); export interface KPIChartProps extends Pick { - layers: Layer; + layers: UseLensAttributesMetricLayerConfig; toolTip: string; } @@ -36,12 +33,14 @@ export const KPI_CHARTS: KPIChartProps[] = [ layers: { data: { ...hostLensFormulas.cpuUsage, - format: { - ...hostLensFormulas.cpuUsage.format, - params: { - decimals: 1, - }, - }, + format: hostLensFormulas.cpuUsage.format + ? { + ...hostLensFormulas.cpuUsage.format, + params: { + decimals: 1, + }, + } + : undefined, }, layerType: 'data', options: { @@ -62,12 +61,14 @@ export const KPI_CHARTS: KPIChartProps[] = [ layers: { data: { ...hostLensFormulas.normalizedLoad1m, - format: { - ...hostLensFormulas.normalizedLoad1m.format, - params: { - decimals: 1, - }, - }, + format: hostLensFormulas.normalizedLoad1m.format + ? { + ...hostLensFormulas.normalizedLoad1m.format, + params: { + decimals: 1, + }, + } + : undefined, }, layerType: 'data', options: { @@ -85,12 +86,14 @@ export const KPI_CHARTS: KPIChartProps[] = [ layers: { data: { ...hostLensFormulas.memoryUsage, - format: { - ...hostLensFormulas.memoryUsage.format, - params: { - decimals: 1, - }, - }, + format: hostLensFormulas.memoryUsage.format + ? { + ...hostLensFormulas.memoryUsage.format, + params: { + decimals: 1, + }, + } + : undefined, }, layerType: 'data', options: { @@ -108,12 +111,14 @@ export const KPI_CHARTS: KPIChartProps[] = [ layers: { data: { ...hostLensFormulas.diskSpaceUsage, - format: { - ...hostLensFormulas.diskSpaceUsage.format, - params: { - decimals: 1, - }, - }, + format: hostLensFormulas.diskSpaceUsage.format + ? { + ...hostLensFormulas.diskSpaceUsage.format, + params: { + decimals: 1, + }, + } + : undefined, }, layerType: 'data', options: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/cpu_usage.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/cpu_usage.ts index b3b40f585d7e03..1364d9a4bd83a7 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/cpu_usage.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/cpu_usage.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const cpuUsage: FormulaConfig = { +export const cpuUsage: FormulaValueConfig = { label: 'CPU Usage', value: '(average(system.cpu.user.pct) + average(system.cpu.system.pct)) / max(system.cpu.cores)', format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_iops.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_iops.ts index 27b288f3a119ec..9b3f22164aacc2 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_iops.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_iops.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const diskIORead: FormulaConfig = { +export const diskIORead: FormulaValueConfig = { label: 'Disk Read IOPS', value: "counter_rate(max(system.diskio.read.count), kql='system.diskio.read.count: *')", format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts index 48fa795e9688a6..5043fb7f94fe18 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const diskReadThroughput: FormulaConfig = { +export const diskReadThroughput: FormulaValueConfig = { label: 'Disk Read Throughput', value: "counter_rate(max(system.diskio.read.bytes), kql='system.diskio.read.bytes: *')", format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_availability.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_availability.ts index aadbd8ccea650f..11d8346c06d28b 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_availability.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_availability.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const diskSpaceAvailability: FormulaConfig = { +export const diskSpaceAvailability: FormulaValueConfig = { label: 'Disk Space Availability', value: '1 - average(system.filesystem.used.pct)', format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_available.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_available.ts index 088e28799ce037..2d55c085deb0b7 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_available.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_available.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const diskSpaceAvailable: FormulaConfig = { +export const diskSpaceAvailable: FormulaValueConfig = { label: 'Disk Space Available', value: 'average(system.filesystem.free)', format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_usage.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_usage.ts index e4cb5851d5241e..24143b58c81e6e 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_usage.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_usage.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const diskSpaceUsage: FormulaConfig = { +export const diskSpaceUsage: FormulaValueConfig = { label: 'Disk Space Usage', value: 'average(system.filesystem.used.pct)', format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_iops.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_iops.ts index 04370c61903ceb..2831957ccb2301 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_iops.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_iops.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const diskIOWrite: FormulaConfig = { +export const diskIOWrite: FormulaValueConfig = { label: 'Disk Write IOPS', value: "counter_rate(max(system.diskio.write.count), kql='system.diskio.write.count: *')", format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts index aed685aa34d8ca..9f0f0937bff37a 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const diskWriteThroughput: FormulaConfig = { +export const diskWriteThroughput: FormulaValueConfig = { label: 'Disk Write Throughput', value: "counter_rate(max(system.diskio.write.bytes), kql='system.diskio.write.bytes: *')", format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/host_count.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/host_count.ts index e642a8cb629f18..f34a9d1913e493 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/host_count.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/host_count.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const hostCount: FormulaConfig = { +export const hostCount: FormulaValueConfig = { label: 'Hosts', value: 'unique_count(host.name)', format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/log_rate.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/log_rate.ts index f6b7ce3cdf0aa1..3365efca35ebbc 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/log_rate.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/log_rate.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const logRate: FormulaConfig = { +export const logRate: FormulaValueConfig = { label: 'Log Rate', value: 'differences(cumulative_sum(count()))', format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_free.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_free.ts index 4406ebd1e820cd..5221faa86b3beb 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_free.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_free.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const memoryFree: FormulaConfig = { +export const memoryFree: FormulaValueConfig = { label: 'Memory Free', value: 'max(system.memory.total) - average(system.memory.actual.used.bytes)', format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_usage.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_usage.ts index f95198756d61e5..d7074968ce8b06 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_usage.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_usage.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const memoryUsage: FormulaConfig = { +export const memoryUsage: FormulaValueConfig = { label: 'Memory Usage', value: 'average(system.memory.actual.used.pct)', format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/normalized_load_1m.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/normalized_load_1m.ts index 32031d07fb8583..3071804f3b5b43 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/normalized_load_1m.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/normalized_load_1m.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const normalizedLoad1m: FormulaConfig = { +export const normalizedLoad1m: FormulaValueConfig = { label: 'Normalized Load', value: 'average(system.load.1) / max(system.load.cores)', format: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/rx.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/rx.ts index 2c5da5cb83988d..92162fad6010f2 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/rx.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/rx.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const rx: FormulaConfig = { +export const rx: FormulaValueConfig = { label: 'Network Inbound (RX)', value: "average(host.network.ingress.bytes) * 8 / (max(metricset.period, kql='host.network.ingress.bytes: *') / 1000)", diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/tx.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/tx.ts index 70aa43a4efaf0c..2b196103619a7a 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/tx.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/tx.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { FormulaConfig } from '../../../types'; +import type { FormulaValueConfig } from '@kbn/lens-embeddable-utils'; -export const tx: FormulaConfig = { +export const tx: FormulaValueConfig = { label: 'Network Outbound (TX)', value: "average(host.network.egress.bytes) * 8 / (max(metricset.period, kql='host.network.egress.bytes: *') / 1000)", diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_data_layer.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_data_layer.ts deleted file mode 100644 index 8da6598c77ae5a..00000000000000 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_data_layer.ts +++ /dev/null @@ -1,108 +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 { SavedObjectReference } from '@kbn/core/server'; -import type { DataView } from '@kbn/data-views-plugin/common'; -import type { - FormulaPublicApi, - FormBasedPersistedState, - PersistedIndexPatternLayer, - XYDataLayerConfig, - SeriesType, -} from '@kbn/lens-plugin/public'; -import type { ChartColumn, ChartLayer, FormulaConfig } from '../../../types'; -import { getDefaultReferences, getHistogramColumn, getTopValuesColumn } from '../../utils'; -import { FormulaColumn } from './column/formula'; - -const BREAKDOWN_COLUMN_NAME = 'aggs_breakdown'; -const HISTOGRAM_COLUMN_NAME = 'x_date_histogram'; - -export interface XYLayerOptions { - breakdown?: { - size: number; - sourceField: string; - }; - seriesType?: SeriesType; -} - -interface XYLayerConfig { - data: FormulaConfig[]; - options?: XYLayerOptions; - formulaAPI: FormulaPublicApi; -} - -export class XYDataLayer implements ChartLayer { - private column: ChartColumn[]; - constructor(private layerConfig: XYLayerConfig) { - this.column = layerConfig.data.map((p) => new FormulaColumn(p, layerConfig.formulaAPI)); - } - - getName(): string | undefined { - return this.column[0].getFormulaConfig().label; - } - - getBaseLayer(dataView: DataView, options?: XYLayerOptions) { - return { - ...getHistogramColumn({ - columnName: HISTOGRAM_COLUMN_NAME, - overrides: { - sourceField: dataView.timeFieldName, - }, - }), - ...(options?.breakdown - ? { - ...getTopValuesColumn({ - columnName: BREAKDOWN_COLUMN_NAME, - overrides: { - sourceField: options?.breakdown.sourceField, - breakdownSize: options?.breakdown.size, - }, - }), - } - : {}), - }; - } - - getLayer( - layerId: string, - accessorId: string, - dataView: DataView - ): FormBasedPersistedState['layers'] { - const baseLayer: PersistedIndexPatternLayer = { - columnOrder: [BREAKDOWN_COLUMN_NAME, HISTOGRAM_COLUMN_NAME], - columns: { - ...this.getBaseLayer(dataView, this.layerConfig.options), - }, - }; - - return { - [layerId]: this.column.reduce( - (acc, curr, index) => ({ - ...acc, - ...curr.getData(`${accessorId}_${index}`, acc, dataView), - }), - baseLayer - ), - }; - } - - getReference(layerId: string, dataView: DataView): SavedObjectReference[] { - return getDefaultReferences(dataView, layerId); - } - - getLayerConfig(layerId: string, accessorId: string): XYDataLayerConfig { - return { - layerId, - seriesType: this.layerConfig.options?.seriesType ?? 'line', - accessors: this.column.map((_, index) => `${accessorId}_${index}`), - yConfig: [], - layerType: 'data', - xAccessor: HISTOGRAM_COLUMN_NAME, - splitAccessor: this.layerConfig.options?.breakdown ? BREAKDOWN_COLUMN_NAME : undefined, - }; - } -} diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/xy_chart.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/xy_chart.ts deleted file mode 100644 index dc6f93683f7951..00000000000000 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/xy_chart.ts +++ /dev/null @@ -1,99 +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 { FormBasedPersistedState, XYLayerConfig, XYState } from '@kbn/lens-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import type { SavedObjectReference } from '@kbn/core/server'; -import { DEFAULT_LAYER_ID } from '../utils'; -import type { Chart, ChartConfig, ChartLayer } from '../../types'; - -const ACCESSOR = 'formula_accessor'; - -export class XYChart implements Chart { - constructor(private chartConfig: ChartConfig>>) {} - - getVisualizationType(): string { - return 'lnsXY'; - } - - getLayers(): FormBasedPersistedState['layers'] { - return this.chartConfig.layers.reduce((acc, curr, index) => { - const layerId = `${DEFAULT_LAYER_ID}_${index}`; - const accessorId = `${ACCESSOR}_${index}`; - return { - ...acc, - ...curr.getLayer(layerId, accessorId, this.chartConfig.dataView), - }; - }, {}); - } - - getVisualizationState(): XYState { - return getXYVisualizationState({ - layers: [ - ...this.chartConfig.layers.map((layerItem, index) => { - const layerId = `${DEFAULT_LAYER_ID}_${index}`; - const accessorId = `${ACCESSOR}_${index}`; - return layerItem.getLayerConfig(layerId, accessorId); - }), - ], - }); - } - - getReferences(): SavedObjectReference[] { - return this.chartConfig.layers.flatMap((p, index) => { - const layerId = `${DEFAULT_LAYER_ID}_${index}`; - return p.getReference(layerId, this.chartConfig.dataView); - }); - } - - getDataView(): DataView { - return this.chartConfig.dataView; - } - - getTitle(): string { - return this.chartConfig.title ?? this.chartConfig.layers[0].getName() ?? ''; - } -} - -export const getXYVisualizationState = ( - custom: Omit, 'layers'> & { layers: XYState['layers'] } -): XYState => ({ - legend: { - isVisible: false, - position: 'right', - showSingleSeries: false, - }, - valueLabels: 'show', - fittingFunction: 'Zero', - curveType: 'LINEAR', - yLeftScale: 'linear', - axisTitlesVisibilitySettings: { - x: false, - yLeft: false, - yRight: true, - }, - tickLabelsVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - labelsOrientation: { - x: 0, - yLeft: 0, - yRight: 0, - }, - gridlinesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - preferredSeriesType: 'line', - valuesInLegend: false, - emphasizeFitting: true, - hideEndzones: true, - ...custom, -}); diff --git a/x-pack/plugins/infra/public/common/visualizations/types.ts b/x-pack/plugins/infra/public/common/visualizations/types.ts index b9e04bf5244773..b12343b0fd9c62 100644 --- a/x-pack/plugins/infra/public/common/visualizations/types.ts +++ b/x-pack/plugins/infra/public/common/visualizations/types.ts @@ -5,75 +5,7 @@ * 2.0. */ -import type { SavedObjectReference } from '@kbn/core/server'; -import type { DataView } from '@kbn/data-views-plugin/common'; -import type { - FormBasedPersistedState, - MetricVisualizationState, - PersistedIndexPatternLayer, - TypedLensByValueInput, - XYState, - FormulaPublicApi, - XYLayerConfig, -} from '@kbn/lens-plugin/public'; import { hostLensFormulas } from './constants'; -export type LensAttributes = TypedLensByValueInput['attributes']; - -// Attributes -export type LensVisualizationState = XYState | MetricVisualizationState; - -export interface VisualizationAttributesBuilder { - build(): LensAttributes; -} - -// Column -export interface ChartColumn { - getData( - id: string, - baseLayer: PersistedIndexPatternLayer, - dataView: DataView - ): PersistedIndexPatternLayer; - getFormulaConfig(): FormulaConfig; -} - -// Layer -export type LensLayerConfig = XYLayerConfig | MetricVisualizationState; - -export interface ChartLayer { - getName(): string | undefined; - getLayer( - layerId: string, - accessorId: string, - dataView: DataView - ): FormBasedPersistedState['layers']; - getReference(layerId: string, dataView: DataView): SavedObjectReference[]; - getLayerConfig(layerId: string, acessorId: string): TLayerConfig; -} - -// Chart -export interface Chart { - getTitle(): string; - getVisualizationType(): string; - getLayers(): FormBasedPersistedState['layers']; - getVisualizationState(): TVisualizationState; - getReferences(): SavedObjectReference[]; - getDataView(): DataView; -} -export interface ChartConfig< - TLayer extends ChartLayer | Array> -> { - dataView: DataView; - layers: TLayer; - title?: string; -} - -// Formula -type LensFormula = Parameters[1]; -export type FormulaConfig = Omit & { - color?: string; - format: NonNullable; - value: string; -}; export type HostsLensFormulas = keyof typeof hostLensFormulas; export type HostsLensMetricChartFormulas = Exclude; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/common/popover.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/common/popover.tsx new file mode 100644 index 00000000000000..263c61d46230b1 --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/common/popover.tsx @@ -0,0 +1,45 @@ +/* + * 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, EuiIcon, IconType } from '@elastic/eui'; +import { css } from '@emotion/react'; +import React from 'react'; +import { useBoolean } from '../../../../hooks/use_boolean'; + +export const Popover = ({ + children, + icon, + ...props +}: { + children: React.ReactNode; + icon: IconType; + 'data-test-subj'?: string; +}) => { + const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false); + return ( + + } + isOpen={isPopoverOpen} + offset={10} + closePopover={closePopover} + repositionOnScroll + anchorPosition="upCenter" + > + {children} + + ); +}; 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 2edac4abbbddab..88c62cfa027c0f 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 @@ -6,7 +6,7 @@ */ import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiPopover, EuiIcon, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useSummaryTimeRange } from '@kbn/observability-plugin/public'; import type { TimeRange } from '@kbn/es-query'; @@ -16,13 +16,13 @@ import type { InventoryItemType } from '../../../../../common/inventory_models/t import { findInventoryFields } from '../../../../../common/inventory_models'; import { createAlertsEsQuery } from '../../../../common/alerts/create_alerts_es_query'; import { infraAlertFeatureIds } from '../../../../pages/metrics/hosts/components/tabs/config'; - import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { LinkToAlertsRule } from '../../links/link_to_alerts'; import { LinkToAlertsPage } from '../../links/link_to_alerts_page'; import { AlertFlyout } from '../../../../alerting/inventory/components/alert_flyout'; import { useBoolean } from '../../../../hooks/use_boolean'; import { ALERT_STATUS_ALL } from '../../../../common/alerts/constants'; +import { Popover } from '../common/popover'; export const AlertsSummaryContent = ({ assetName, @@ -107,10 +107,8 @@ const MemoAlertSummaryWidget = React.memo( ); const AlertsSectionTitle = () => { - const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false); - return ( - +
@@ -122,21 +120,9 @@ const AlertsSectionTitle = () => { - - } - isOpen={isPopoverOpen} - closePopover={closePopover} - repositionOnScroll - anchorPosition="upCenter" - > + - + ); diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx index b6b2d97a9bc098..e19d261094d8fe 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx @@ -5,20 +5,14 @@ * 2.0. */ -import { - EuiDescriptionListTitle, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiPopover, -} from '@elastic/eui'; +import { EuiDescriptionListTitle, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useBoolean } from '../../../../../hooks/use_boolean'; +import { EuiText } from '@elastic/eui'; import type { MetadataData } from './metadata_summary_list'; +import { Popover } from '../../common/popover'; const columnTitles = { hostIp: i18n.translate('xpack.infra.assetDetailsEmbeddable.overview.metadataHostIpHeading', { @@ -51,52 +45,43 @@ interface MetadataSummaryProps { } export const MetadataHeader = ({ metadataValue }: MetadataSummaryProps) => { - const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false); - return ( - + {columnTitles[metadataValue.field as MetadataFields]} - - } - isOpen={isPopoverOpen} - closePopover={closePopover} - repositionOnScroll - anchorPosition="upCenter" + - {metadataValue.tooltipLink ? ( - - {metadataValue.tooltipFieldLabel} - - ), - }} - /> - ) : ( - {metadataValue.tooltipFieldLabel} - )} - + + {metadataValue.tooltipLink ? ( + + {metadataValue.tooltipFieldLabel} + + ), + }} + /> + ) : ( + {metadataValue.tooltipFieldLabel} + )} + + diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx index 5f0bce1b54856b..fcb76d2fa8ef9f 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx @@ -11,19 +11,17 @@ import { i18n } from '@kbn/i18n'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { TimeRange } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; +import type { XYVisualOptions } from '@kbn/lens-embeddable-utils'; +import { UseLensAttributesXYLayerConfig } from '../../../../../hooks/use_lens_attributes'; import { buildCombinedHostsFilter } from '../../../../../utils/filters/build'; -import type { Layer } from '../../../../../hooks/use_lens_attributes'; -import { HostMetricsDocsLink, LensChart, type LensChartProps } from '../../../../lens'; -import { - type FormulaConfig, - hostLensFormulas, - type XYLayerOptions, -} from '../../../../../common/visualizations'; +import { LensChart, type LensChartProps, HostMetricsExplanationContent } from '../../../../lens'; +import { hostLensFormulas } from '../../../../../common/visualizations'; import { METRIC_CHART_HEIGHT } from '../../../constants'; +import { Popover } from '../../common/popover'; type DataViewOrigin = 'logs' | 'metrics'; interface MetricChartConfig extends Pick { - layers: Array>; + layers: UseLensAttributesXYLayerConfig; toolTip: string; } @@ -44,6 +42,11 @@ const LEGEND_SETTINGS: Pick['overrides'] = { }, }; +const XY_VISUAL_OPTIONS: XYVisualOptions = { + showDottedLine: true, + missingValues: 'Linear', +}; + const CHARTS_IN_ORDER: Array< Pick & { dataViewOrigin: DataViewOrigin; @@ -303,17 +306,9 @@ export const MetricsGrid = React.memo( return ( - -
- -
-
+
- { + return ( + + + +
+ +
+
+
+ + + + + +
+ ); +}; diff --git a/x-pack/plugins/infra/public/components/lens/index.tsx b/x-pack/plugins/infra/public/components/lens/index.tsx index 93d050209a2194..ae05bd8e82fe42 100644 --- a/x-pack/plugins/infra/public/components/lens/index.tsx +++ b/x-pack/plugins/infra/public/components/lens/index.tsx @@ -9,3 +9,4 @@ export { LensChart, type LensChartProps } from './lens_chart'; export { ChartPlaceholder } from './chart_placeholder'; export { TooltipContent } from './metric_explanation/tooltip_content'; export { HostMetricsDocsLink } from './metric_explanation/host_metrics_docs_link'; +export { HostMetricsExplanationContent } from './metric_explanation/host_metrics_explanation_content'; diff --git a/x-pack/plugins/infra/public/components/lens/lens_wrapper.tsx b/x-pack/plugins/infra/public/components/lens/lens_wrapper.tsx index f203c9c344797a..043173113c5a56 100644 --- a/x-pack/plugins/infra/public/components/lens/lens_wrapper.tsx +++ b/x-pack/plugins/infra/public/components/lens/lens_wrapper.tsx @@ -11,10 +11,10 @@ import type { TimeRange } from '@kbn/es-query'; import { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { css } from '@emotion/react'; import { useEuiTheme } from '@elastic/eui'; +import { LensAttributes } from '@kbn/lens-embeddable-utils'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import { ChartLoadingProgress, ChartPlaceholder } from './chart_placeholder'; import { parseDateRange } from '../../utils/datemath'; -import { LensAttributes } from '../../common/visualizations'; export type LensWrapperProps = Omit< TypedLensByValueInput, diff --git a/x-pack/plugins/infra/public/components/lens/metric_explanation/host_metrics_docs_link.tsx b/x-pack/plugins/infra/public/components/lens/metric_explanation/host_metrics_docs_link.tsx index 347c2174b9077b..992c899928e69b 100644 --- a/x-pack/plugins/infra/public/components/lens/metric_explanation/host_metrics_docs_link.tsx +++ b/x-pack/plugins/infra/public/components/lens/metric_explanation/host_metrics_docs_link.tsx @@ -7,21 +7,40 @@ import React from 'react'; import { EuiLink, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { HOST_METRICS_DOC_HREF } from '../../../common/visualizations/constants'; +import { i18n } from '@kbn/i18n'; +import { + HOST_METRICS_DOC_HREF, + HOST_METRICS_DOTTED_LINES_DOC_HREF, +} from '../../../common/visualizations/constants'; -export const HostMetricsDocsLink = () => { +const DocLinks = { + metrics: { + href: HOST_METRICS_DOC_HREF, + label: i18n.translate('xpack.infra.hostsViewPage.tooltip.whatAreTheseMetricsLink', { + defaultMessage: 'What are these metrics?', + }), + }, + dottedLines: { + href: HOST_METRICS_DOTTED_LINES_DOC_HREF, + label: i18n.translate('xpack.infra.hostsViewPage.tooltip.whyAmISeeingDottedLines', { + defaultMessage: 'Why am I seeing dotted lines?', + }), + }, +}; + +interface Props { + type: keyof typeof DocLinks; +} + +export const HostMetricsDocsLink = ({ type }: Props) => { return ( - + {DocLinks[type].label} ); diff --git a/x-pack/plugins/infra/public/components/lens/metric_explanation/host_metrics_explanation_content.tsx b/x-pack/plugins/infra/public/components/lens/metric_explanation/host_metrics_explanation_content.tsx new file mode 100644 index 00000000000000..63a9f8160fe439 --- /dev/null +++ b/x-pack/plugins/infra/public/components/lens/metric_explanation/host_metrics_explanation_content.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { HostMetricsDocsLink } from './host_metrics_docs_link'; + +export const HostMetricsExplanationContent = () => { + return ( + +

+ +

+

+ +

+

+ +

+
+ ); +}; diff --git a/x-pack/plugins/infra/public/hooks/use_lens_attributes.test.ts b/x-pack/plugins/infra/public/hooks/use_lens_attributes.test.ts index a3222210dec242..3058663f3a6fef 100644 --- a/x-pack/plugins/infra/public/hooks/use_lens_attributes.test.ts +++ b/x-pack/plugins/infra/public/hooks/use_lens_attributes.test.ts @@ -57,9 +57,15 @@ describe('useHostTable hook', () => { data: [normalizedLoad1m], layerType: 'data', options: { + buckets: { + type: 'date_histogram', + }, breakdown: { - size: 10, - sourceField: 'host.name', + field: 'host.name', + type: 'top_values', + params: { + size: 10, + }, }, }, }, diff --git a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts index 6f3ae0cf37affb..373060c9452d87 100644 --- a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts @@ -12,59 +12,64 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { Action, ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; import { i18n } from '@kbn/i18n'; import useAsync from 'react-use/lib/useAsync'; -import { FormulaPublicApi, LayerType as LensLayerType } from '@kbn/lens-plugin/public'; -import { InfraClientSetupDeps } from '../types'; +import { FormulaPublicApi } from '@kbn/lens-plugin/public'; import { type XYLayerOptions, type MetricLayerOptions, - type FormulaConfig, + type FormulaValueConfig, type LensAttributes, + type StaticValueConfig, + type LensVisualizationState, + type XYVisualOptions, + type Chart, LensAttributesBuilder, XYDataLayer, MetricLayer, XYChart, MetricChart, XYReferenceLinesLayer, - Chart, - LensVisualizationState, -} from '../common/visualizations'; +} from '@kbn/lens-embeddable-utils'; + +import { InfraClientSetupDeps } from '../types'; import { useLazyRef } from './use_lazy_ref'; type Options = XYLayerOptions | MetricLayerOptions; -type ChartType = 'lnsXY' | 'lnsMetric'; -export type LayerType = Exclude; -export interface Layer< + +interface StaticValueLayer { + data: StaticValueConfig[]; + layerType: 'referenceLine'; +} + +interface FormulaValueLayer< TOptions extends Options, - TFormulaConfig extends FormulaConfig | FormulaConfig[], - TLayerType extends LayerType = LayerType + TData extends FormulaValueConfig[] | FormulaValueConfig > { - layerType: TLayerType; - data: TFormulaConfig; options?: TOptions; + data: TData; + layerType: 'data'; } -interface UseLensAttributesBaseParams< - TOptions extends Options, - TLayers extends Array> | Layer -> { +type XYLayerConfig = StaticValueLayer | FormulaValueLayer; + +export type UseLensAttributesXYLayerConfig = XYLayerConfig | XYLayerConfig[]; +export type UseLensAttributesMetricLayerConfig = FormulaValueLayer< + MetricLayerOptions, + FormulaValueConfig +>; + +interface UseLensAttributesBaseParams { dataView?: DataView; - layers: TLayers; title?: string; } -interface UseLensAttributesXYChartParams - extends UseLensAttributesBaseParams< - XYLayerOptions, - Array> - > { +interface UseLensAttributesXYChartParams extends UseLensAttributesBaseParams { + layers: UseLensAttributesXYLayerConfig; visualizationType: 'lnsXY'; + visualOptions?: XYVisualOptions; } -interface UseLensAttributesMetricChartParams - extends UseLensAttributesBaseParams< - MetricLayerOptions, - Layer - > { +interface UseLensAttributesMetricChartParams extends UseLensAttributesBaseParams { + layers: UseLensAttributesMetricLayerConfig; visualizationType: 'lnsMetric'; } @@ -72,12 +77,7 @@ export type UseLensAttributesParams = | UseLensAttributesXYChartParams | UseLensAttributesMetricChartParams; -export const useLensAttributes = ({ - dataView, - layers, - title, - visualizationType, -}: UseLensAttributesParams) => { +export const useLensAttributes = ({ dataView, ...params }: UseLensAttributesParams) => { const { services: { lens }, } = useKibana(); @@ -94,9 +94,7 @@ export const useLensAttributes = ({ visualization: chartFactory({ dataView, formulaAPI, - layers, - title, - visualizationType, + ...params, }), }); @@ -157,9 +155,9 @@ export const useLensAttributes = ({ ); const getFormula = () => { - const firstDataLayer = [...(Array.isArray(layers) ? layers : [layers])].find( - (p) => p.layerType === 'data' - ); + const firstDataLayer = [ + ...(Array.isArray(params.layers) ? params.layers : [params.layers]), + ].find((p) => p.layerType === 'data'); if (!firstDataLayer) { return ''; @@ -175,77 +173,70 @@ export const useLensAttributes = ({ return { formula: getFormula(), attributes: attributes.current, getExtraActions, error }; }; -const chartFactory = < - TOptions, - TLayers extends Array> | Layer ->({ +const chartFactory = ({ dataView, formulaAPI, - layers, - title, - visualizationType, + ...params }: { dataView: DataView; formulaAPI: FormulaPublicApi; - visualizationType: ChartType; - layers: TLayers; - title?: string; -}): Chart => { - switch (visualizationType) { +} & UseLensAttributesParams): Chart => { + switch (params.visualizationType) { case 'lnsXY': - if (!Array.isArray(layers)) { + if (!Array.isArray(params.layers)) { throw new Error(`Invalid layers type. Expected an array of layers.`); } - const getLayerClass = (layerType: LayerType) => { - switch (layerType) { + const xyLayerFactory = (layer: XYLayerConfig) => { + switch (layer.layerType) { case 'data': { - return XYDataLayer; + return new XYDataLayer({ + data: layer.data, + options: layer.options, + }); } case 'referenceLine': { - return XYReferenceLinesLayer; + return new XYReferenceLinesLayer({ + data: layer.data, + }); } default: - throw new Error(`Invalid layerType: ${layerType}`); + throw new Error(`Invalid layerType`); } }; return new XYChart({ dataView, - layers: layers.map((layerItem) => { - const Layer = getLayerClass(layerItem.layerType); - return new Layer({ - data: layerItem.data, - formulaAPI, - options: layerItem.options, - }); + formulaAPI, + layers: params.layers.map((layerItem) => { + return xyLayerFactory(layerItem); }), - title, + title: params.title, + visualOptions: params.visualOptions, }); case 'lnsMetric': - if (Array.isArray(layers)) { + if (Array.isArray(params.layers)) { throw new Error(`Invalid layers type. Expected a single layer object.`); } return new MetricChart({ dataView, + formulaAPI, layers: new MetricLayer({ - data: layers.data, - formulaAPI, - options: layers.options, + data: params.layers.data, + options: params.layers.options, }), - title, + title: params.title, }); default: - throw new Error(`Unsupported chart type: ${visualizationType}`); + throw new Error(`Unsupported chart type`); } }; const getOpenInLensAction = (onExecute: () => void): Action => { return { id: 'openInLens', - getDisplayName(_context: ActionExecutionContext): string { return i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.actions.openInLines', { defaultMessage: 'Open in Lens', diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx index d7720b820cf64b..94a71302629317 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx @@ -20,7 +20,7 @@ import { export const KPIGrid = () => { return ( - + diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/column_header.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/column_header.tsx index e865fad0827312..04f5ae78ded548 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/column_header.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/column_header.tsx @@ -4,115 +4,38 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useRef, useCallback, useLayoutEffect } from 'react'; -import { EuiPopover, EuiIcon, EuiFlexGroup, useEuiTheme } from '@elastic/eui'; +import React from 'react'; +import { EuiFlexGroup } from '@elastic/eui'; import { css } from '@emotion/react'; -import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import { TooltipContent } from '../../../../../components/lens/metric_explanation/tooltip_content'; -import { useBoolean } from '../../../../../hooks/use_boolean'; +import { Popover } from './popover'; interface Props { label: string; toolTip?: string; formula?: string; - popoverContainerRef?: React.RefObject; } -const SEARCH_BAR_OFFSET = 250; -const ANCHOR_SPACING = 10; - -const findTableParentElement = (element: HTMLElement | null): HTMLElement | null => { - let currentElement = element; - - while (currentElement && currentElement.className !== APP_WRAPPER_CLASS) { - currentElement = currentElement.parentElement; - } - return currentElement; -}; - -export const ColumnHeader = React.memo( - ({ label, toolTip, formula, popoverContainerRef }: Props) => { - const buttonRef = useRef(null); - const containerRef = useRef(null); - const [offset, setOffset] = useState(0); - const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false); - - const { euiTheme } = useEuiTheme(); - - useLayoutEffect(() => { - containerRef.current = findTableParentElement(buttonRef.current); - }, []); - - const calculateHeaderOffset = () => { - const { top: containerTop = 0 } = containerRef.current?.getBoundingClientRect() ?? {}; - const headerOffset = containerTop + window.scrollY; - - return headerOffset; - }; - - const onButtonClick = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - const { top: buttonTop = 0 } = buttonRef.current?.getBoundingClientRect() ?? {}; - - // gets the actual page position, discounting anything above the page content (e.g: header, dismissible banner) - const headerOffset = calculateHeaderOffset(); - // determines if the scroll position is close to overlapping with the button - const scrollPosition = buttonTop - headerOffset - SEARCH_BAR_OFFSET; - const isAboveElement = scrollPosition <= 0; - - // offset to be taken into account when positioning the popover - setOffset(headerOffset * (isAboveElement ? -1 : 1) + ANCHOR_SPACING); - togglePopover(); - }, - [togglePopover] - ); - - return ( - -
- {label} -
- - {toolTip && ( - (buttonRef.current = el)} - button={ - - } - insert={ - popoverContainerRef && popoverContainerRef?.current - ? { - sibling: popoverContainerRef.current, - position: 'after', - } - : undefined - } - offset={offset} - anchorPosition={offset <= 0 ? 'downCenter' : 'upCenter'} - isOpen={isPopoverOpen} - closePopover={closePopover} - zIndex={Number(euiTheme.levels.header) - 1} - panelStyle={{ maxWidth: 350 }} - > - - - )} -
- ); - } -); +export const ColumnHeader = React.memo(({ label, toolTip, formula }: Props) => { + return ( + +
+ {label} +
+ + {toolTip && ( + + + + )} +
+ ); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/popover.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/popover.tsx new file mode 100644 index 00000000000000..678d6b4d53fce7 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/popover.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; +import { EuiPopover, EuiIcon, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { APP_WRAPPER_CLASS } from '@kbn/core/public'; +import { useBoolean } from '../../../../../hooks/use_boolean'; +import { useHostsTableContext } from '../../hooks/use_hosts_table'; + +const SEARCH_BAR_OFFSET = 250; +const ANCHOR_SPACING = 10; + +const findTableParentElement = (element: HTMLElement | null): HTMLElement | null => { + let currentElement = element; + + while (currentElement && currentElement.className !== APP_WRAPPER_CLASS) { + currentElement = currentElement.parentElement; + } + return currentElement; +}; + +export const Popover = ({ children }: { children: React.ReactNode }) => { + const buttonRef = useRef(null); + const containerRef = useRef(null); + const [offset, setOffset] = useState(0); + const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false); + const { + refs: { popoverContainerRef }, + } = useHostsTableContext(); + + const { euiTheme } = useEuiTheme(); + + useLayoutEffect(() => { + containerRef.current = findTableParentElement(buttonRef.current); + }, []); + + const calculateHeaderOffset = () => { + const { top: containerTop = 0 } = containerRef.current?.getBoundingClientRect() ?? {}; + const headerOffset = containerTop + window.scrollY; + + return headerOffset; + }; + + const onButtonClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const { top: buttonTop = 0 } = buttonRef.current?.getBoundingClientRect() ?? {}; + + // gets the actual page position, discounting anything above the page content (e.g: header, dismissible banner) + const headerOffset = calculateHeaderOffset(); + // determines if the scroll position is close to overlapping with the button + const scrollPosition = buttonTop - headerOffset - SEARCH_BAR_OFFSET; + const isAboveElement = scrollPosition <= 0; + + // offset to be taken into account when positioning the popover + setOffset(headerOffset * (isAboveElement ? -1 : 1) + ANCHOR_SPACING); + togglePopover(); + }, + [togglePopover] + ); + + return ( + (buttonRef.current = el)} + button={ + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + offset={offset} + anchorPosition={offset <= 0 ? 'downCenter' : 'upCenter'} + insert={ + popoverContainerRef && popoverContainerRef?.current + ? { + sibling: popoverContainerRef.current, + position: 'after', + } + : undefined + } + zIndex={Number(euiTheme.levels.header) - 1} + panelStyle={{ maxWidth: 350 }} + > + {children} + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx index c0f05c55d1e9ef..2d77bdfbcecfc9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx @@ -6,11 +6,11 @@ */ import React, { useMemo } from 'react'; import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import type { XYVisualOptions } from '@kbn/lens-embeddable-utils'; import { LensChart } from '../../../../../../components/lens'; -import type { Layer } from '../../../../../../hooks/use_lens_attributes'; +import type { UseLensAttributesXYLayerConfig } from '../../../../../../hooks/use_lens_attributes'; import { useMetricsDataViewContext } from '../../../hooks/use_data_view'; import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; -import type { FormulaConfig, XYLayerOptions } from '../../../../../../common/visualizations'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; import { buildCombinedHostsFilter } from '../../../../../../utils/filters/build'; import { useHostsTableContext } from '../../../hooks/use_hosts_table'; @@ -19,10 +19,11 @@ import { METRIC_CHART_HEIGHT } from '../../../constants'; export interface MetricChartProps extends Pick { title: string; - layers: Array>; + layers: UseLensAttributesXYLayerConfig; + visualOptions?: XYVisualOptions; } -export const MetricChart = ({ id, title, layers, overrides }: MetricChartProps) => { +export const MetricChart = ({ id, title, layers, visualOptions, overrides }: MetricChartProps) => { const { searchCriteria } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); const { requestTs, loading } = useHostsViewContext(); @@ -58,6 +59,7 @@ export const MetricChart = ({ id, title, layers, overrides }: MetricChartProps) dateRange={afterLoadedState.dateRange} height={METRIC_CHART_HEIGHT} layers={layers} + visualOptions={visualOptions} lastReloadRequestTime={afterLoadedState.lastReloadRequestTime} loading={loading} filters={filters} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx index 7dfa9b63b87b24..07abe5fa98d090 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx @@ -6,21 +6,30 @@ */ import React from 'react'; -import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGrid, EuiFlexItem, EuiText, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer } from '@elastic/eui'; -import { hostLensFormulas, type XYLayerOptions } from '../../../../../../common/visualizations'; -import { HostMetricsDocsLink } from '../../../../../../components/lens'; +import type { XYLayerOptions, XYVisualOptions } from '@kbn/lens-embeddable-utils'; +import { hostLensFormulas } from '../../../../../../common/visualizations'; +import { HostMetricsExplanationContent } from '../../../../../../components/lens'; import { MetricChart, MetricChartProps } from './metric_chart'; +import { Popover } from '../../table/popover'; const DEFAULT_BREAKDOWN_SIZE = 20; const XY_LAYER_OPTIONS: XYLayerOptions = { breakdown: { - size: DEFAULT_BREAKDOWN_SIZE, - sourceField: 'host.name', + type: 'top_values', + field: 'host.name', + params: { + size: DEFAULT_BREAKDOWN_SIZE, + }, }, }; +const XY_VISUAL_OPTIONS: XYVisualOptions = { + showDottedLine: true, + missingValues: 'Linear', +}; + const PERCENT_LEFT_AXIS: Pick['overrides'] = { axisLeft: { domain: { @@ -28,6 +37,7 @@ const PERCENT_LEFT_AXIS: Pick['overrides'] = { max: 1, }, }, + settings: {}, }; const CHARTS_IN_ORDER: MetricChartProps[] = [ @@ -210,12 +220,22 @@ const CHARTS_IN_ORDER: MetricChartProps[] = [ export const MetricsGrid = React.memo(() => { return ( <> - + + + Learn more about metrics + + + + + + + + {CHARTS_IN_ORDER.map((chartProp, index) => ( - + ))} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 19c0bcdaa1f47e..6052c1221fe69d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -256,7 +256,6 @@ export const useHostsTable = () => { label={TABLE_COLUMN_LABEL.cpuUsage} toolTip={TOOLTIP.cpuUsage} formula={hostLensFormulas.cpuUsage.value} - popoverContainerRef={popoverContainerRef} /> ), field: 'cpu', @@ -271,7 +270,6 @@ export const useHostsTable = () => { label={TABLE_COLUMN_LABEL.normalizedLoad1m} toolTip={TOOLTIP.normalizedLoad1m} formula={hostLensFormulas.normalizedLoad1m.value} - popoverContainerRef={popoverContainerRef} /> ), field: 'normalizedLoad1m', @@ -286,7 +284,6 @@ export const useHostsTable = () => { label={TABLE_COLUMN_LABEL.memoryUsage} toolTip={TOOLTIP.memoryUsage} formula={hostLensFormulas.memoryUsage.value} - popoverContainerRef={popoverContainerRef} /> ), field: 'memory', @@ -301,7 +298,6 @@ export const useHostsTable = () => { label={TABLE_COLUMN_LABEL.memoryFree} toolTip={TOOLTIP.memoryFree} formula={hostLensFormulas.memoryFree.value} - popoverContainerRef={popoverContainerRef} /> ), field: 'memoryFree', @@ -316,7 +312,6 @@ export const useHostsTable = () => { label={TABLE_COLUMN_LABEL.diskSpaceUsage} toolTip={TOOLTIP.diskSpaceUsage} formula={hostLensFormulas.diskSpaceUsage.value} - popoverContainerRef={popoverContainerRef} /> ), field: 'diskSpaceUsage', @@ -331,7 +326,6 @@ export const useHostsTable = () => { label={TABLE_COLUMN_LABEL.rx} toolTip={TOOLTIP.rx} formula={hostLensFormulas.rx.value} - popoverContainerRef={popoverContainerRef} /> ), field: 'rx', @@ -347,7 +341,6 @@ export const useHostsTable = () => { label={TABLE_COLUMN_LABEL.tx} toolTip={TOOLTIP.tx} formula={hostLensFormulas.tx.value} - popoverContainerRef={popoverContainerRef} /> ), field: 'tx', @@ -358,13 +351,7 @@ export const useHostsTable = () => { width: '120px', }, ], - [ - hostFlyoutState?.itemId, - reportHostEntryClick, - searchCriteria.dateRange, - setHostFlyoutState, - popoverContainerRef, - ] + [hostFlyoutState?.itemId, reportHostEntryClick, searchCriteria.dateRange, setHostFlyoutState] ); const selection: EuiTableSelectionType = { diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index c0cc1cbf1b3c96..519e2a8a1e9411 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -69,6 +69,7 @@ "@kbn/logs-shared-plugin", "@kbn/licensing-plugin", "@kbn/aiops-utils", + "@kbn/lens-embeddable-utils" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts index 04e51848dd63a4..591cfc322a8a07 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts @@ -2291,7 +2291,7 @@ describe('IndexPattern Data Source', () => { disabled: { kuery: [], lucene: [] }, }); }); - it('shuold collect top values fields as kuery existence filters if no data is provided', () => { + it('should collect top values fields as kuery existence filters if no data is provided', () => { publicAPI = FormBasedDatasource.getPublicAPI({ state: { ...baseState, @@ -2335,10 +2335,10 @@ describe('IndexPattern Data Source', () => { expect(publicAPI.getFilters()).toEqual({ enabled: { kuery: [ - [{ language: 'kuery', query: 'geo.src: *' }], + [{ language: 'kuery', query: '"geo.src": *' }], [ - { language: 'kuery', query: 'geo.dest: *' }, - { language: 'kuery', query: 'myField: *' }, + { language: 'kuery', query: '"geo.dest": *' }, + { language: 'kuery', query: '"myField": *' }, ], ], lucene: [], @@ -2903,8 +2903,8 @@ describe('IndexPattern Data Source', () => { { language: 'kuery', query: 'memory > 500000' }, ], [ - { language: 'kuery', query: 'geo.src: *' }, - { language: 'kuery', query: 'myField: *' }, + { language: 'kuery', query: '"geo.src": *' }, + { language: 'kuery', query: '"myField": *' }, ], ], lucene: [ diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/filters/filters.test.tsx index 5f27106a5ab44d..15ccced9533090 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/filters/filters.test.tsx @@ -233,7 +233,7 @@ describe('filters', () => { filters: [ { input: { - query: 'bytes : *', + query: '"bytes" : *', language: 'kuery', }, label: '', @@ -272,14 +272,14 @@ describe('filters', () => { filters: [ { input: { - query: 'bytes : *', + query: '"bytes" : *', language: 'kuery', }, label: '', }, { input: { - query: 'dest : *', + query: '"dest" : *', language: 'kuery', }, label: '', diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/filters/filters.tsx index f84e023b493e19..e514e9d6c08d4f 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/filters/filters.tsx @@ -25,6 +25,8 @@ import { updateColumnParam } from '../../layer_helpers'; import type { OperationDefinition } from '..'; import type { BaseIndexPatternColumn } from '../column_types'; import { FilterPopover } from './filter_popover'; +import { TermsIndexPatternColumn } from '../terms'; +import { isColumnOfType } from '../helpers'; const generateId = htmlIdGenerator(); const OPERATION_NAME = 'filters'; @@ -79,13 +81,13 @@ export const filtersOperation: OperationDefinition< getDefaultLabel: () => filtersLabel, buildColumn({ previousColumn }, columnParams) { let params = { filters: columnParams?.filters ?? [defaultFilter] }; - if (previousColumn?.operationType === 'terms' && 'sourceField' in previousColumn) { + if (previousColumn && isColumnOfType('terms', previousColumn)) { params = { filters: columnParams?.filters ?? [ { label: '', input: { - query: `${previousColumn.sourceField} : *`, + query: `"${previousColumn.sourceField}" : *`, language: 'kuery', }, }, @@ -94,7 +96,7 @@ export const filtersOperation: OperationDefinition< ).params?.secondaryFields?.map((field) => ({ label: '', input: { - query: `${field} : *`, + query: `"${field}" : *`, language: 'kuery', }, })) ?? []), diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/helpers.tsx index 2fb83af48271eb..6dabd0dc07556f 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/helpers.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { isEqual } from 'lodash'; -import { LastValueColumn } from '@kbn/visualizations-plugin/common'; +import { Query } from '@kbn/es-query'; import type { IndexPattern, IndexPatternField } from '../../../../types'; import { type FieldBasedOperationErrorMessage, @@ -192,13 +192,19 @@ export function getFormatFromPreviousColumn( : undefined; } -export function getExistsFilter(field: string) { +// Check the escape argument when used for transitioning comparisons +export function getExistsFilter(field: string, escape: boolean = true) { return { - query: `${field}: *`, + query: escape ? `"${field}": *` : `${field}: *`, language: 'kuery', }; } +// Useful utility to compare for escape and unescaped exist filters +export function comparePreviousColumnFilter(filter: Query | undefined, field: string) { + return isEqual(filter, getExistsFilter(field)) || isEqual(filter, getExistsFilter(field, false)); +} + export function getFilter( previousColumn: GenericIndexPatternColumn | undefined, columnParams: { kql?: string | undefined; lucene?: string | undefined } | undefined @@ -207,7 +213,7 @@ export function getFilter( if ( previousColumn && isColumnOfType('last_value', previousColumn) && - isEqual(filter, getExistsFilter((previousColumn as LastValueColumn)?.sourceField)) + comparePreviousColumnFilter(filter, previousColumn.sourceField) ) { return; } diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx index 47f8d6f2bd2e02..a246aa5d95ef51 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.test.tsx @@ -175,7 +175,7 @@ describe('last_value', () => { expect(column).toEqual( expect.objectContaining({ - filter: { language: 'kuery', query: 'bytes: *' }, + filter: { language: 'kuery', query: '"bytes": *' }, }) ); }); @@ -442,7 +442,7 @@ describe('last_value', () => { }, layer: { columns: {}, columnOrder: [], indexPatternId: '' }, }); - expect(lastValueColumn.filter).toEqual({ language: 'kuery', query: 'test: *' }); + expect(lastValueColumn.filter).toEqual({ language: 'kuery', query: '"test": *' }); }); it('should use indexPattern timeFieldName as a default sortField', () => { diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx index c813fa26cde8f4..bdfb42fe3d6577 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, @@ -29,6 +28,7 @@ import { getSafeName, getFilter, getExistsFilter, + comparePreviousColumnFilter, } from './helpers'; import { adjustTimeScaleLabelSuffix } from '../time_scale_utils'; import { isRuntimeField, isScriptedField } from './terms/helpers'; @@ -188,7 +188,7 @@ export const lastValueOperation: OperationDefinition< params: newParams, scale: getScale(field.type), filter: - oldColumn.filter && isEqual(oldColumn.filter, getExistsFilter(oldColumn.sourceField)) + oldColumn.filter && comparePreviousColumnFilter(oldColumn.filter, oldColumn.sourceField) ? getExistsFilter(field.name) : oldColumn.filter, }; diff --git a/x-pack/plugins/lens/public/datasources/form_based/utils.tsx b/x-pack/plugins/lens/public/datasources/form_based/utils.tsx index 2d6806902bd529..33c60b89f3ce45 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/utils.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/utils.tsx @@ -715,7 +715,7 @@ function extractTimeRangeFromDateHistogram( return [ { language: 'kuery', - query: `${column.sourceField} >= "${timeRange.from}" AND ${column.sourceField} <= "${timeRange.to}"`, + query: `"${column.sourceField}" >= "${timeRange.from}" AND "${column.sourceField}" <= "${timeRange.to}"`, }, ]; } @@ -882,7 +882,7 @@ export function getFiltersInLayer( const fields = operationDefinitionMap[column.operationType]!.getCurrentFields!(column); return { kuery: fields.map((field) => ({ - query: `${field}: *`, + query: `"${field}": *`, language: 'kuery', })), }; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/index.ts b/x-pack/plugins/ml/public/application/model_management/test_models/models/index.ts index 9e4ffeda263546..c9a806bf5b7f35 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/index.ts +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/index.ts @@ -6,6 +6,7 @@ */ import { NerInference } from './ner'; +import { TextExpansionInference } from './text_expansion'; import { QuestionAnsweringInference } from './question_answering'; import { TextClassificationInference, @@ -22,4 +23,5 @@ export type InferrerType = | TextEmbeddingInference | ZeroShotClassificationInference | FillMaskInference - | LangIdentInference; + | LangIdentInference + | TextExpansionInference; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/question_answering/question_answering_inference.ts b/x-pack/plugins/ml/public/application/model_management/test_models/models/question_answering/question_answering_inference.ts index b428442f8908df..fb130d0e3a4195 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/question_answering/question_answering_inference.ts +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/question_answering/question_answering_inference.ts @@ -57,7 +57,7 @@ export class QuestionAnsweringInference extends InferenceBase(''); + private questionText$ = new BehaviorSubject(''); constructor( trainedModelsApi: ReturnType, diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/text_classification/zero_shot_classification_inference.ts b/x-pack/plugins/ml/public/application/model_management/test_models/models/text_classification/zero_shot_classification_inference.ts index 19cc9708268211..651575c1e0b4de 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/text_classification/zero_shot_classification_inference.ts +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/text_classification/zero_shot_classification_inference.ts @@ -31,8 +31,8 @@ export class ZeroShotClassificationInference extends InferenceBase(''); - public multiLabel$ = new BehaviorSubject(false); + private labelsText$ = new BehaviorSubject(''); + private multiLabel$ = new BehaviorSubject(false); constructor( trainedModelsApi: ReturnType, diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/index.ts b/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/index.ts new file mode 100644 index 00000000000000..6c492545932bd9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/index.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 type { + TextExpansionResponse, + FormattedTextExpansionResponse, +} from './text_expansion_inference'; +export { TextExpansionInference } from './text_expansion_inference'; +export { getTextExpansionOutputComponent } from './text_expansion_output'; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/text_expansion_inference.ts b/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/text_expansion_inference.ts new file mode 100644 index 00000000000000..b384bf1c0526c3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/text_expansion_inference.ts @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { i18n } from '@kbn/i18n'; +import { SUPPORTED_PYTORCH_TASKS } from '@kbn/ml-trained-models-utils'; +import { BehaviorSubject } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { trainedModelsApiProvider } from '../../../../services/ml_api_service/trained_models'; +import { InferenceBase, INPUT_TYPE, type InferResponse } from '../inference_base'; +import { getTextExpansionOutputComponent } from './text_expansion_output'; +import { getTextExpansionInput } from './text_expansion_input'; + +export interface TextExpansionPair { + token: string; + value: number; +} + +export interface FormattedTextExpansionResponse { + text: string; + score: number; + originalTokenWeights: TextExpansionPair[]; + adjustedTokenWeights: TextExpansionPair[]; +} + +export type TextExpansionResponse = InferResponse< + FormattedTextExpansionResponse, + estypes.MlInferTrainedModelResponse +>; + +export class TextExpansionInference extends InferenceBase { + protected inferenceType = SUPPORTED_PYTORCH_TASKS.TEXT_EXPANSION; + protected inferenceTypeLabel = i18n.translate( + 'xpack.ml.trainedModels.testModelsFlyout.textExpansion.label', + { defaultMessage: 'Text expansion' } + ); + protected info = [ + i18n.translate('xpack.ml.trainedModels.testModelsFlyout.textExpansion.info', { + defaultMessage: + 'Expand your search to include relevant terms in the results that are not present in the query.', + }), + ]; + + private queryText$ = new BehaviorSubject(''); + private queryResults: Record = {}; + + constructor( + trainedModelsApi: ReturnType, + model: estypes.MlTrainedModelConfig, + inputType: INPUT_TYPE, + deploymentId: string + ) { + super(trainedModelsApi, model, inputType, deploymentId); + + this.initialize( + [this.queryText$.pipe(map((questionText) => questionText !== ''))], + [this.queryText$] + ); + } + + protected async inferText() { + return this.runInfer( + () => {}, + (resp, inputText) => { + return { + response: parseResponse( + resp as unknown as MlInferTrainedModelResponse, + '', + this.queryResults + ), + rawResponse: resp, + inputText, + }; + } + ); + } + + protected async inferIndex() { + const { docs } = await this.trainedModelsApi.trainedModelPipelineSimulate(this.getPipeline(), [ + { + _source: { + text_field: this.getQueryText(), + }, + }, + ]); + + if (docs.length === 0) { + throw new Error( + i18n.translate('xpack.ml.trainedModels.testModelsFlyout.textExpansion.noDocsError', { + defaultMessage: 'No docs loaded', + }) + ); + } + + this.queryResults = docs[0].doc?._source[this.inferenceType].predicted_value ?? {}; + + return this.runPipelineSimulate((doc) => { + return { + response: parseResponse( + { inference_results: [doc._source[this.inferenceType]] }, + doc._source[this.getInputField()], + this.queryResults + ), + rawResponse: doc._source[this.inferenceType], + inputText: doc._source[this.getInputField()], + }; + }); + } + + protected getProcessors() { + return this.getBasicProcessors(); + } + + public setQueryText(text: string) { + this.queryText$.next(text); + } + + public getQueryText$() { + return this.queryText$.asObservable(); + } + + public getQueryText() { + return this.queryText$.getValue(); + } + + public getInputComponent(): JSX.Element | null { + const placeholder = i18n.translate( + 'xpack.ml.trainedModels.testModelsFlyout.textExpansion.inputText', + { + defaultMessage: 'Enter a phrase to test', + } + ); + return getTextExpansionInput(this, placeholder); + } + + public getOutputComponent(): JSX.Element { + return getTextExpansionOutputComponent(this); + } +} + +interface MlInferTrainedModelResponse { + inference_results: TextExpansionPredictedValue[]; +} + +interface TextExpansionPredictedValue { + predicted_value: Record; +} + +function parseResponse( + resp: MlInferTrainedModelResponse, + text: string, + queryResults: Record +): FormattedTextExpansionResponse { + const [{ predicted_value: predictedValue }] = resp.inference_results; + + if (predictedValue === undefined) { + throw new Error( + i18n.translate('xpack.ml.trainedModels.testModelsFlyout.textExpansion.noPredictionError', { + defaultMessage: 'No results found', + }) + ); + } + + // extract token and value pairs + const originalTokenWeights = Object.entries(predictedValue).map(([token, value]) => ({ + token, + value, + })); + let score = 0; + const adjustedTokenWeights = originalTokenWeights.map(({ token, value }) => { + // if token is in query results, multiply value by query result value + const adjustedValue = value * (queryResults[token] ?? 0); + score += adjustedValue; + return { + token, + value: adjustedValue, + }; + }); + + return { + text, + score, + originalTokenWeights, + adjustedTokenWeights, + }; +} diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/text_expansion_input.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/text_expansion_input.tsx new file mode 100644 index 00000000000000..3b1c4cc55a4550 --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/text_expansion_input.tsx @@ -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 React, { FC } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { i18n } from '@kbn/i18n'; + +import { EuiSpacer, EuiFieldText, EuiFormRow } from '@elastic/eui'; + +import { TextInput } from '../text_input'; +import { TextExpansionInference } from './text_expansion_inference'; +import { INPUT_TYPE, RUNNING_STATE } from '../inference_base'; + +const QueryInput: FC<{ + inferrer: TextExpansionInference; +}> = ({ inferrer }) => { + const questionText = useObservable(inferrer.getQueryText$(), inferrer.getQueryText()); + const runningState = useObservable(inferrer.getRunningState$(), inferrer.getRunningState()); + + return ( + + { + inferrer.setQueryText(e.target.value); + }} + /> + + ); +}; + +export const getTextExpansionInput = (inferrer: TextExpansionInference, placeholder?: string) => ( + <> + {inferrer.getInputType() === INPUT_TYPE.TEXT ? ( + <> + + + + ) : null} + + + +); diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/text_expansion_output.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/text_expansion_output.tsx new file mode 100644 index 00000000000000..da048103ac1a31 --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/text_expansion/text_expansion_output.tsx @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { type FC } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { + EuiAccordion, + EuiHorizontalRule, + EuiIcon, + EuiInMemoryTable, + EuiSpacer, + EuiStat, + EuiTextColor, + EuiCallOut, +} from '@elastic/eui'; + +import { roundToDecimalPlace } from '@kbn/ml-number-utils'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useCurrentThemeVars } from '../../../../contexts/kibana'; +import type { TextExpansionInference, FormattedTextExpansionResponse } from '.'; + +const MAX_TOKENS = 5; + +export const getTextExpansionOutputComponent = (inferrer: TextExpansionInference) => ( + +); + +export const TextExpansionOutput: FC<{ + inferrer: TextExpansionInference; +}> = ({ inferrer }) => { + const result = useObservable(inferrer.getInferenceResult$(), inferrer.getInferenceResult()); + if (!result) { + return null; + } + + return ( + <> + + + + + + + {result + .sort((a, b) => b.response.score - a.response.score) + .map(({ response, inputText }) => ( + <> + + + + ))} + + ); +}; + +export const DocumentResult: FC<{ + response: FormattedTextExpansionResponse; +}> = ({ response }) => { + const tokens = response.adjustedTokenWeights + .filter(({ value }) => value > 0) + .sort((a, b) => b.value - a.value) + .slice(0, MAX_TOKENS) + .map(({ token, value }) => ({ token, value: roundToDecimalPlace(value, 3) })); + + const statInfo = useResultStatFormatting(response); + + return ( + <> + {response.text !== undefined ? ( + <> + + + {statInfo.icon !== null ? ( + + ) : null} + {statInfo.text} + + + } + /> + + + {response.text} + + + ) : null} + + {tokens.length > 0 ? ( + + <> + + + + + + + + + ) : null} + + ); +}; + +interface ResultStatFormatting { + color: string; + textColor: string; + text: string | null; + icon: string | null; +} + +const useResultStatFormatting = ( + response: FormattedTextExpansionResponse +): ResultStatFormatting => { + const { + euiTheme: { euiColorMediumShade, euiTextSubduedColor, euiTextColor }, + } = useCurrentThemeVars(); + + if (response.score >= 5) { + return { + color: 'success', + textColor: euiTextColor, + icon: 'check', + text: i18n.translate( + 'xpack.ml.trainedModels.testModelsFlyout.textExpansion.output.goodMatch', + { defaultMessage: 'Good match' } + ), + }; + } + + if (response.score > 0) { + return { + color: euiTextSubduedColor, + textColor: euiTextColor, + text: null, + icon: null, + }; + } + + return { + color: euiColorMediumShade, + textColor: euiColorMediumShade, + text: null, + icon: null, + }; +}; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/selected_model.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/selected_model.tsx index 06ff8128aa1736..4747d9c1491862 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/selected_model.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/selected_model.tsx @@ -25,6 +25,7 @@ import { useMlApiContext } from '../../contexts/kibana'; import { InferenceInputForm } from './models/inference_input_form'; import { InferrerType } from './models'; import { INPUT_TYPE } from './models/inference_base'; +import { TextExpansionInference } from './models/text_expansion'; interface Props { model: estypes.MlTrainedModelConfig; @@ -42,22 +43,18 @@ export const SelectedModel: FC = ({ model, inputType, deploymentId }) => switch (taskType) { case SUPPORTED_PYTORCH_TASKS.NER: return new NerInference(trainedModels, model, inputType, deploymentId); - break; case SUPPORTED_PYTORCH_TASKS.TEXT_CLASSIFICATION: return new TextClassificationInference(trainedModels, model, inputType, deploymentId); - break; case SUPPORTED_PYTORCH_TASKS.ZERO_SHOT_CLASSIFICATION: return new ZeroShotClassificationInference(trainedModels, model, inputType, deploymentId); - break; case SUPPORTED_PYTORCH_TASKS.TEXT_EMBEDDING: return new TextEmbeddingInference(trainedModels, model, inputType, deploymentId); - break; case SUPPORTED_PYTORCH_TASKS.FILL_MASK: return new FillMaskInference(trainedModels, model, inputType, deploymentId); - break; case SUPPORTED_PYTORCH_TASKS.QUESTION_ANSWERING: return new QuestionAnsweringInference(trainedModels, model, inputType, deploymentId); - break; + case SUPPORTED_PYTORCH_TASKS.TEXT_EXPANSION: + return new TextExpansionInference(trainedModels, model, inputType, deploymentId); default: break; } diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/test_flyout.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/test_flyout.tsx index d3fa13b233f5fe..434621d11773be 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/test_flyout.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/test_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useState } from 'react'; +import React, { FC, useState, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -21,6 +21,7 @@ import { useEuiPaddingSize, } from '@elastic/eui'; +import { SUPPORTED_PYTORCH_TASKS } from '@kbn/ml-trained-models-utils'; import { SelectedModel } from './selected_model'; import { INPUT_TYPE } from './models/inference_base'; import { type ModelItem } from '../models_list'; @@ -35,6 +36,12 @@ export const TestTrainedModelFlyout: FC = ({ model, onClose }) => { const [inputType, setInputType] = useState(INPUT_TYPE.TEXT); + const onlyShowTab: INPUT_TYPE | undefined = useMemo(() => { + return (model.type ?? []).includes(SUPPORTED_PYTORCH_TASKS.TEXT_EXPANSION) + ? INPUT_TYPE.INDEX + : undefined; + }, [model]); + return ( <> @@ -79,37 +86,41 @@ export const TestTrainedModelFlyout: FC = ({ model, onClose }) => { ) : null} - - setInputType(INPUT_TYPE.TEXT)} - > - - - setInputType(INPUT_TYPE.INDEX)} - > - - - + {onlyShowTab === undefined ? ( + <> + + setInputType(INPUT_TYPE.TEXT)} + > + + + setInputType(INPUT_TYPE.INDEX)} + > + + + - + + + ) : null} diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/utils.ts b/x-pack/plugins/ml/public/application/model_management/test_models/utils.ts index a9f3d895fca36d..3adecb767f2802 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/utils.ts +++ b/x-pack/plugins/ml/public/application/model_management/test_models/utils.ts @@ -13,9 +13,7 @@ import { } from '@kbn/ml-trained-models-utils'; import type { ModelItem } from '../models_list'; -const PYTORCH_TYPES = Object.values(SUPPORTED_PYTORCH_TASKS).filter( - (taskType) => taskType !== SUPPORTED_PYTORCH_TASKS.TEXT_EXPANSION -); +const PYTORCH_TYPES = Object.values(SUPPORTED_PYTORCH_TASKS); export function isTestable(modelItem: ModelItem, checkForState = false) { if ( diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 10af8c30b3bbed..9459802d820dbe 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -12,6 +12,9 @@ export { asPercent, getDurationFormatter, asDuration, + asDynamicBytes, + asAbsoluteDateTime, + asInteger, } from './utils/formatters'; export { getInspectResponse } from './utils/get_inspect_response'; export { getAlertDetailsUrl, getAlertUrl } from './utils/alerting/alert_url'; diff --git a/x-pack/plugins/observability/dev_docs/composite_slo.md b/x-pack/plugins/observability/dev_docs/composite_slo.md index f3018e33d46dd7..4e34933c8560e2 100644 --- a/x-pack/plugins/observability/dev_docs/composite_slo.md +++ b/x-pack/plugins/observability/dev_docs/composite_slo.md @@ -28,7 +28,7 @@ curl --request POST \ ], "timeWindow": { "duration": "7d", - "isRolling": true + "type": "rolling" }, "budgetingMethod": "occurrences", "objective": { diff --git a/x-pack/plugins/observability/dev_docs/slo.md b/x-pack/plugins/observability/dev_docs/slo.md index ab605e51ffd27b..13e74fef228e85 100644 --- a/x-pack/plugins/observability/dev_docs/slo.md +++ b/x-pack/plugins/observability/dev_docs/slo.md @@ -23,9 +23,9 @@ The **custom Metric** SLI requires an index pattern, an optional filter query, a We support **calendar aligned** and **rolling** time windows. Any duration greater than 1 day can be used: days, weeks, months, quarters, years. -**Rolling time window:** Requires a duration, e.g. `1w` for one week, and `isRolling: true`. SLOs defined with such time window, will only considere the SLI data from the last duration period as a moving window. +**Rolling time window:** Requires a duration, e.g. `1w` for one week, and `type: rolling`. SLOs defined with such time window, will only considere the SLI data from the last duration period as a moving window. -**Calendar aligned time window:** Requires a duration, limited to `1M` for monthly or `1w` for weekly, and `isCalendar: true`. +**Calendar aligned time window:** Requires a duration, limited to `1M` for monthly or `1w` for weekly, and `type: calendarAligned`. ### Budgeting method @@ -46,8 +46,8 @@ If a **timeslices** budgeting method is used, we also need to define the **times The default settings should be sufficient for most users, but if needed, the following properties can be overwritten: -- **syncDelay**: The ingest delay in the source data -- **frequency**: How often do we query the source data +- **syncDelay**: The ingest delay in the source data, defaults to `1m` +- **frequency**: How often do we query the source data, defaults to `1m` ## Example @@ -77,7 +77,7 @@ curl --request POST \ }, "timeWindow": { "duration": "30d", - "isRolling": true + "type": "rolling" }, "budgetingMethod": "occurrences", "objective": { @@ -112,7 +112,7 @@ curl --request POST \ }, "timeWindow": { "duration": "1M", - "isCalendar": true + "type": "calendarAligned" }, "budgetingMethod": "occurrences", "objective": { @@ -146,8 +146,8 @@ curl --request POST \ } }, "timeWindow": { - "duration": "1w", - "isRolling": true + "duration": "7d", + "type": "rolling" }, "budgetingMethod": "timeslices", "objective": { @@ -187,7 +187,7 @@ curl --request POST \ }, "timeWindow": { "duration": "7d", - "isRolling": true + "type": "rolling" }, "budgetingMethod": "occurrences", "objective": { @@ -223,7 +223,7 @@ curl --request POST \ }, "timeWindow": { "duration": "7d", - "isRolling": true + "type": "rolling" }, "budgetingMethod": "timeslices", "objective": { @@ -261,7 +261,7 @@ curl --request POST \ }, "timeWindow": { "duration": "1w", - "isCalendar": true + "type": "calendarAligned" }, "budgetingMethod": "timeslices", "objective": { @@ -300,7 +300,7 @@ curl --request POST \ }, "timeWindow": { "duration": "7d", - "isRolling": true + "type": "rolling" }, "budgetingMethod": "occurrences", "objective": { @@ -355,7 +355,7 @@ curl --request POST \ }, "timeWindow": { "duration": "7d", - "isRolling": true + "type": "rolling" }, "budgetingMethod": "occurrences", "objective": { diff --git a/x-pack/plugins/observability/public/components/threshold/components/__snapshots__/expression_row.test.tsx.snap b/x-pack/plugins/observability/public/components/threshold/components/__snapshots__/expression_row.test.tsx.snap deleted file mode 100644 index 4cf9b2195d554b..00000000000000 --- a/x-pack/plugins/observability/public/components/threshold/components/__snapshots__/expression_row.test.tsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ExpressionRow should render a helpText for the of expression 1`] = ` - - - , - } - } -/> -`; diff --git a/x-pack/plugins/observability/public/components/threshold/components/closable_popover_title.test.tsx b/x-pack/plugins/observability/public/components/threshold/components/closable_popover_title.test.tsx new file mode 100644 index 00000000000000..b101295a6d426f --- /dev/null +++ b/x-pack/plugins/observability/public/components/threshold/components/closable_popover_title.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as React from 'react'; +import { mount } from 'enzyme'; +import { ClosablePopoverTitle } from './closable_popover_title'; + +describe('closable popover title', () => { + it('renders with defined options', () => { + const onClose = jest.fn(); + const children =
; + const wrapper = mount( + {children} + ); + expect(wrapper.contains(
)).toBeTruthy(); + }); + + it('onClose function gets called', () => { + const onClose = jest.fn(); + const children =
; + const wrapper = mount( + {children} + ); + wrapper.find('EuiButtonIcon').simulate('click'); + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/threshold/components/closable_popover_title.tsx b/x-pack/plugins/observability/public/components/threshold/components/closable_popover_title.tsx new file mode 100644 index 00000000000000..7fe5d73783011a --- /dev/null +++ b/x-pack/plugins/observability/public/components/threshold/components/closable_popover_title.tsx @@ -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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiPopoverTitle, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export function ClosablePopoverTitle({ children, onClose }: ClosablePopoverTitleProps) { + return ( + + + {children} + + onClose()} + /> + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/threshold/components/custom_equation/custom_equation_editor.tsx b/x-pack/plugins/observability/public/components/threshold/components/custom_equation/custom_equation_editor.tsx index 68568e51dd29c6..cdb4f7b7155a08 100644 --- a/x-pack/plugins/observability/public/components/threshold/components/custom_equation/custom_equation_editor.tsx +++ b/x-pack/plugins/observability/public/components/threshold/components/custom_equation/custom_equation_editor.tsx @@ -11,12 +11,15 @@ import { EuiFlexGroup, EuiButtonEmpty, EuiSpacer, + EuiExpression, + EuiPopover, } from '@elastic/eui'; import React, { useState, useCallback, useMemo } from 'react'; import { omit, range, first, xor, debounce } from 'lodash'; import { IErrorObject } from '@kbn/triggers-actions-ui-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { DataViewBase } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; import { OMITTED_AGGREGATIONS_FOR_CUSTOM_METRICS } from '../../../../../common/threshold_rule/metrics_explorer'; import { Aggregators, @@ -27,13 +30,8 @@ import { import { MetricExpression } from '../../types'; import { CustomMetrics, AggregationTypes, NormalizedFields } from './types'; import { MetricRowWithAgg } from './metric_row_with_agg'; -import { MetricRowWithCount } from './metric_row_with_count'; -import { - CUSTOM_EQUATION, - EQUATION_HELP_MESSAGE, - LABEL_HELP_MESSAGE, - LABEL_LABEL, -} from '../../i18n_strings'; +import { ClosablePopoverTitle } from '../closable_popover_title'; +import { EQUATION_HELP_MESSAGE } from '../../i18n_strings'; export interface CustomEquationEditorProps { onChange: (expression: MetricExpression) => void; @@ -61,7 +59,7 @@ export function CustomEquationEditor({ const [customMetrics, setCustomMetrics] = useState( expression?.customMetrics ?? [NEW_METRIC] ); - const [label, setLabel] = useState(expression?.label || undefined); + const [customEqPopoverOpen, setCustomEqPopoverOpen] = useState(false); const [equation, setEquation] = useState(expression?.equation || undefined); const debouncedOnChange = useMemo(() => debounce(onChange, 500), [onChange]); @@ -70,48 +68,40 @@ export function CustomEquationEditor({ const currentVars = previous?.map((m) => m.name) ?? []; const name = first(xor(VAR_NAMES, currentVars))!; const nextMetrics = [...(previous || []), { ...NEW_METRIC, name }]; - debouncedOnChange({ ...expression, customMetrics: nextMetrics, equation, label }); + debouncedOnChange({ ...expression, customMetrics: nextMetrics, equation }); return nextMetrics; }); - }, [debouncedOnChange, equation, expression, label]); + }, [debouncedOnChange, equation, expression]); const handleDelete = useCallback( (name: string) => { setCustomMetrics((previous) => { const nextMetrics = previous?.filter((row) => row.name !== name) ?? [NEW_METRIC]; const finalMetrics = (nextMetrics.length && nextMetrics) || [NEW_METRIC]; - debouncedOnChange({ ...expression, customMetrics: finalMetrics, equation, label }); + debouncedOnChange({ ...expression, customMetrics: finalMetrics, equation }); return finalMetrics; }); }, - [equation, expression, debouncedOnChange, label] + [equation, expression, debouncedOnChange] ); const handleChange = useCallback( (metric: MetricExpressionCustomMetric) => { setCustomMetrics((previous) => { const nextMetrics = previous?.map((m) => (m.name === metric.name ? metric : m)); - debouncedOnChange({ ...expression, customMetrics: nextMetrics, equation, label }); + debouncedOnChange({ ...expression, customMetrics: nextMetrics, equation }); return nextMetrics; }); }, - [equation, expression, debouncedOnChange, label] + [equation, expression, debouncedOnChange] ); const handleEquationChange = useCallback( (e: React.ChangeEvent) => { setEquation(e.target.value); - debouncedOnChange({ ...expression, customMetrics, equation: e.target.value, label }); + debouncedOnChange({ ...expression, customMetrics, equation: e.target.value }); }, - [debouncedOnChange, expression, customMetrics, label] - ); - - const handleLabelChange = useCallback( - (e: React.ChangeEvent) => { - setLabel(e.target.value); - debouncedOnChange({ ...expression, customMetrics, equation, label: e.target.value }); - }, - [debouncedOnChange, expression, customMetrics, equation] + [debouncedOnChange, expression, customMetrics] ); const disableAdd = customMetrics?.length === MAX_VARIABLES; @@ -119,42 +109,24 @@ export function CustomEquationEditor({ const filteredAggregationTypes = omit(aggregationTypes, OMITTED_AGGREGATIONS_FOR_CUSTOM_METRICS); - const metricRows = customMetrics?.map((row) => { - if (row.aggType === Aggregators.COUNT) { - return ( - - ); - } - return ( - - ); - }); + const metricRows = customMetrics?.map((row) => ( + + )); const placeholder = useMemo(() => { return customMetrics?.map((row) => row.name).join(' + '); @@ -181,42 +153,69 @@ export function CustomEquationEditor({ - - - - + - - - - - - - - + <> + + { + setCustomEqPopoverOpen(true); + }} + /> + + + } + isOpen={customEqPopoverOpen} + closePopover={() => { + setCustomEqPopoverOpen(false); + }} + display="block" + ownFocus + anchorPosition={'downLeft'} + repositionOnScroll + > +
+ setCustomEqPopoverOpen(false)}> + + + - - - + helpText={EQUATION_HELP_MESSAGE} + isInvalid={errors.equation != null} + > + + +
+ +
); } diff --git a/x-pack/plugins/observability/public/components/threshold/components/custom_equation/metric_row_controls.tsx b/x-pack/plugins/observability/public/components/threshold/components/custom_equation/metric_row_controls.tsx index 7533e23640ece2..5f4ebac04cd343 100644 --- a/x-pack/plugins/observability/public/components/threshold/components/custom_equation/metric_row_controls.tsx +++ b/x-pack/plugins/observability/public/components/threshold/components/custom_equation/metric_row_controls.tsx @@ -21,7 +21,7 @@ export function MetricRowControls({ onDelete, disableDelete }: MetricRowControlP aria-label={DELETE_LABEL} iconType="trash" color="danger" - style={{ marginBottom: '0.2em' }} + style={{ marginBottom: '0.6em' }} onClick={onDelete} disabled={disableDelete} title={DELETE_LABEL} diff --git a/x-pack/plugins/observability/public/components/threshold/components/custom_equation/metric_row_with_agg.tsx b/x-pack/plugins/observability/public/components/threshold/components/custom_equation/metric_row_with_agg.tsx index 10ff425e065255..fcc09399bb3da6 100644 --- a/x-pack/plugins/observability/public/components/threshold/components/custom_equation/metric_row_with_agg.tsx +++ b/x-pack/plugins/observability/public/components/threshold/components/custom_equation/metric_row_with_agg.tsx @@ -7,24 +7,31 @@ import { EuiFormRow, - EuiHorizontalRule, EuiFlexItem, EuiFlexGroup, EuiSelect, EuiComboBox, EuiComboBoxOptionOption, + EuiPopover, + EuiExpression, } from '@elastic/eui'; -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useState } from 'react'; import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ValidNormalizedTypes } from '@kbn/triggers-actions-ui-plugin/public'; +import { DataViewBase } from '@kbn/es-query'; +import { FormattedMessage } from '@kbn/i18n-react'; import { Aggregators, CustomMetricAggTypes } from '../../../../../common/threshold_rule/types'; import { MetricRowControls } from './metric_row_controls'; import { NormalizedFields, MetricRowBaseProps } from './types'; +import { ClosablePopoverTitle } from '../closable_popover_title'; +import { MetricsExplorerKueryBar } from '../kuery_bar'; interface MetricRowWithAggProps extends MetricRowBaseProps { aggType?: CustomMetricAggTypes; field?: string; + dataView: DataViewBase; + filter?: string; fields: NormalizedFields; } @@ -33,6 +40,8 @@ export function MetricRowWithAgg({ aggType = Aggregators.AVERAGE, field, onDelete, + dataView, + filter, disableDelete, fields, aggregationTypes, @@ -43,6 +52,8 @@ export function MetricRowWithAgg({ onDelete(name); }, [name, onDelete]); + const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); + const fieldOptions = useMemo( () => fields.reduce((acc, fieldValue) => { @@ -59,15 +70,6 @@ export function MetricRowWithAgg({ [fields, aggregationTypes, aggType] ); - const aggOptions = useMemo( - () => - Object.values(aggregationTypes).map((a) => ({ - text: a.text, - value: a.value, - })), - [aggregationTypes] - ); - const handleFieldChange = useCallback( (selectedOptions: EuiComboBoxOptionOption[]) => { onChange({ @@ -80,62 +82,141 @@ export function MetricRowWithAgg({ ); const handleAggChange = useCallback( - (el: React.ChangeEvent) => { + (customAggType: string) => { onChange({ name, field, - aggType: el.target.value as CustomMetricAggTypes, + aggType: customAggType as CustomMetricAggTypes, }); }, [name, field, onChange] ); + const handleFilterChange = useCallback( + (filterString: string) => { + onChange({ + name, + filter: filterString, + aggType, + }); + }, + [name, aggType, onChange] + ); + const isAggInvalid = get(errors, ['customMetrics', name, 'aggType']) != null; const isFieldInvalid = get(errors, ['customMetrics', name, 'field']) != null || !field; return ( <> - - + + { + setAggTypePopoverOpen(true); + }} + /> + + } + isOpen={aggTypePopoverOpen} + closePopover={() => { + setAggTypePopoverOpen(false); + }} + display="block" + ownFocus + anchorPosition={'downLeft'} + repositionOnScroll > - - - - - - - +
+ setAggTypePopoverOpen(false)}> + + + + + + + { + handleAggChange(e.target.value); + }} + options={Object.values(aggregationTypes).map(({ text, value }) => { + return { + text, + value, + }; + })} + /> + + + + {aggType === Aggregators.COUNT ? ( + + + + ) : ( + + + + )} + + +
+
- ); } diff --git a/x-pack/plugins/observability/public/components/threshold/components/custom_equation/metric_row_with_count.tsx b/x-pack/plugins/observability/public/components/threshold/components/custom_equation/metric_row_with_count.tsx deleted file mode 100644 index e27efafde504c9..00000000000000 --- a/x-pack/plugins/observability/public/components/threshold/components/custom_equation/metric_row_with_count.tsx +++ /dev/null @@ -1,110 +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 { EuiFormRow, EuiHorizontalRule, EuiFlexItem, EuiFlexGroup, EuiSelect } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { DataViewBase } from '@kbn/es-query'; -import { Aggregators, CustomMetricAggTypes } from '../../../../../common/threshold_rule/types'; -import { MetricRowControls } from './metric_row_controls'; -import { MetricRowBaseProps } from './types'; -import { MetricsExplorerKueryBar } from '../kuery_bar'; - -interface MetricRowWithCountProps extends MetricRowBaseProps { - agg?: Aggregators; - filter?: string; - dataView: DataViewBase; -} - -export function MetricRowWithCount({ - name, - agg, - filter, - onDelete, - disableDelete, - onChange, - aggregationTypes, - dataView, -}: MetricRowWithCountProps) { - const aggOptions = useMemo( - () => - Object.values(aggregationTypes) - .filter((aggType) => aggType.value !== Aggregators.CUSTOM) - .map((aggType) => ({ - text: aggType.text, - value: aggType.value, - })), - [aggregationTypes] - ); - - const handleDelete = useCallback(() => { - onDelete(name); - }, [name, onDelete]); - - const handleAggChange = useCallback( - (el: React.ChangeEvent) => { - onChange({ - name, - filter, - aggType: el.target.value as CustomMetricAggTypes, - }); - }, - [name, filter, onChange] - ); - - const handleFilterChange = useCallback( - (filterString: string) => { - onChange({ - name, - filter: filterString, - aggType: agg as CustomMetricAggTypes, - }); - }, - [name, agg, onChange] - ); - - return ( - <> - - - - - - - - - - - - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/threshold/components/expression_row.test.tsx b/x-pack/plugins/observability/public/components/threshold/components/expression_row.test.tsx index afc1a1d87ff486..7a7536849c1876 100644 --- a/x-pack/plugins/observability/public/components/threshold/components/expression_row.test.tsx +++ b/x-pack/plugins/observability/public/components/threshold/components/expression_row.test.tsx @@ -67,15 +67,16 @@ describe('ExpressionRow', () => { threshold: [0.5], timeSize: 1, timeUnit: 'm', - aggType: 'avg', + aggType: 'custom', }; const { wrapper, update } = await setup(expression as MetricExpression); await update(); const [valueMatch] = wrapper .html() - .match('50') ?? - []; + .match( + '50' + ) ?? []; expect(valueMatch).toBeTruthy(); }); @@ -86,34 +87,15 @@ describe('ExpressionRow', () => { threshold: [0.5], timeSize: 1, timeUnit: 'm', - aggType: 'avg', + aggType: 'custom', }; const { wrapper } = await setup(expression as MetricExpression); const [valueMatch] = wrapper .html() - .match('0.5') ?? - []; + .match( + '0.5' + ) ?? []; expect(valueMatch).toBeTruthy(); }); - - it('should render a helpText for the of expression', async () => { - const expression = { - metric: 'system.load.1', - comparator: Comparator.GT, - threshold: [0.5], - timeSize: 1, - timeUnit: 'm', - aggType: 'avg', - } as MetricExpression; - - const { wrapper } = await setup(expression as MetricExpression); - - const helpText = wrapper - .find('[data-test-subj="thresholdRuleOfExpression"]') - .at(0) - .prop('helpText'); - - expect(helpText).toMatchSnapshot(); - }); }); diff --git a/x-pack/plugins/observability/public/components/threshold/components/expression_row.tsx b/x-pack/plugins/observability/public/components/threshold/components/expression_row.tsx index 3ab71eaf639d19..ac8a4a05092d49 100644 --- a/x-pack/plugins/observability/public/components/threshold/components/expression_row.tsx +++ b/x-pack/plugins/observability/public/components/threshold/components/expression_row.tsx @@ -6,30 +6,28 @@ */ import { EuiButtonIcon, - EuiExpression, + EuiFieldText, EuiFlexGroup, EuiFlexItem, - EuiLink, + EuiFormRow, EuiSpacer, EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { AggregationType, builtInComparators, IErrorObject, - OfExpression, ThresholdExpression, } from '@kbn/triggers-actions-ui-plugin/public'; import { DataViewBase } from '@kbn/es-query'; -import useToggle from 'react-use/lib/useToggle'; -import { Aggregators, Comparator } from '../../../../common/threshold_rule/types'; +import { debounce } from 'lodash'; +import { Comparator } from '../../../../common/threshold_rule/types'; import { AGGREGATION_TYPES, DerivedIndexPattern, MetricExpression } from '../types'; import { CustomEquationEditor } from './custom_equation'; -import { CUSTOM_EQUATION } from '../i18n_strings'; +import { CUSTOM_EQUATION, LABEL_HELP_MESSAGE, LABEL_LABEL } from '../i18n_strings'; import { decimalToPct, pctToDecimal } from '../helpers/corrected_percent_convert'; const customComparators = { @@ -62,14 +60,8 @@ const StyledExpressionRow = euiStyled(EuiFlexGroup)` margin: 0 -4px; `; -const StyledExpression = euiStyled.div` - padding: 0 4px; -`; - // eslint-disable-next-line react/function-component-definition export const ExpressionRow: React.FC = (props) => { - const [isExpanded, toggle] = useToggle(true); - const { dataView, children, @@ -82,21 +74,10 @@ export const ExpressionRow: React.FC = (props) => { canDelete, } = props; - const { - aggType = AGGREGATION_TYPES.MAX, - metric, - comparator = Comparator.GT, - threshold = [], - } = expression; + const { metric, comparator = Comparator.GT, threshold = [] } = expression; const isMetricPct = useMemo(() => Boolean(metric && metric.endsWith('.pct')), [metric]); - - const updateMetric = useCallback( - (m?: MetricExpression['metric']) => { - setRuleParams(expressionId, { ...expression, metric: m }); - }, - [expressionId, expression, setRuleParams] - ); + const [label, setLabel] = useState(expression?.label || undefined); const updateComparator = useCallback( (c?: string) => { @@ -127,6 +108,10 @@ export const ExpressionRow: React.FC = (props) => { }, [expressionId, setRuleParams] ); + const debouncedLabelChange = useMemo( + () => debounce(handleCustomMetricChange, 300), + [handleCustomMetricChange] + ); const criticalThresholdExpression = ( = (props) => { name: f.name, })); + const handleLabelChange = useCallback( + (e: React.ChangeEvent) => { + setLabel(e.target.value); + debouncedLabelChange({ ...expression, label: e.target.value }); + }, + [debouncedLabelChange, expression] + ); return ( <> - - - - - - - - {!['count', 'custom'].includes(aggType) && ( - - - - - ), - }} - /> - } - data-test-subj="thresholdRuleOfExpression" - /> - - )} + + <> + + {criticalThresholdExpression} - - - {aggType === Aggregators.CUSTOM && ( - <> - - - - - - - )} + + + + + + + + + + {canDelete && ( @@ -244,7 +186,7 @@ export const ExpressionRow: React.FC = (props) => { )} - {isExpanded ?
{children}
: null} + {children} ); @@ -266,16 +208,16 @@ const ThresholdElement: React.FC<{ return ( <> - - - + + {isMetricPct && (
- {ruleParams.criteria && ruleParams.criteria.map((e, idx) => { return ( - 1) || false} - fields={derivedIndexPattern.fields as any} - remove={removeExpression} - addExpression={addExpression} - key={idx} // idx's don't usually make good key's but here the index has semantic meaning - expressionId={idx} - setRuleParams={updateParams} - errors={(errors[idx] as IErrorObject) || emptyError} - expression={e || {}} - dataView={derivedIndexPattern} - > - {/* Preview */} - - +
+ {/* index has semantic meaning, we show the condition title starting from the 2nd one */} + {idx >= 1 && ( + +
+ +
+
+ )} + 1) || false} + fields={derivedIndexPattern.fields as any} + remove={removeExpression} + addExpression={addExpression} + key={idx} // idx's don't usually make good key's but here the index has semantic meaning + expressionId={idx} + setRuleParams={updateParams} + errors={(errors[idx] as IErrorObject) || emptyError} + expression={e || {}} + dataView={derivedIndexPattern} + > + {/* Preview */} + + +
); })} -
- -
+ + +
{ expect(alertDetails.queryByTestId('alertDetails')).toBeTruthy(); expect(alertDetails.queryByTestId('alertDetailsError')).toBeFalsy(); - expect(alertDetails.queryByTestId('page-title-container')).toBeTruthy(); + expect(alertDetails.queryByTestId('alertDetailsPageTitle')).toBeTruthy(); expect(alertDetails.queryByTestId('alert-summary-container')).toBeTruthy(); }); diff --git a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx index c988d967fefa10..8ce2278d0823c4 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx @@ -123,7 +123,9 @@ export function AlertDetails() { return ( , + pageTitle: ( + + ), rightSideItems: [ { const defaultProps = { alert, + dataTestSubj: 'ruleTypeId', }; const renderComp = (props: PageTitleProps) => { @@ -28,7 +29,7 @@ describe('Page Title', () => { it('should display Log threshold title', () => { const { getByTestId } = renderComp(defaultProps); - expect(getByTestId('page-title-container').textContent).toContain('Log threshold breached'); + expect(getByTestId('ruleTypeId').textContent).toContain('Log threshold breached'); }); it('should display Anomaly title', () => { @@ -40,11 +41,12 @@ describe('Page Title', () => { [ALERT_RULE_CATEGORY]: 'Anomaly', }, }, + dataTestSubj: defaultProps.dataTestSubj, }; const { getByTestId } = renderComp(props); - expect(getByTestId('page-title-container').textContent).toContain('Anomaly detected'); + expect(getByTestId('ruleTypeId').textContent).toContain('Anomaly detected'); }); it('should display Inventory title', () => { @@ -56,13 +58,12 @@ describe('Page Title', () => { [ALERT_RULE_CATEGORY]: 'Inventory', }, }, + dataTestSubj: defaultProps.dataTestSubj, }; const { getByTestId } = renderComp(props); - expect(getByTestId('page-title-container').textContent).toContain( - 'Inventory threshold breached' - ); + expect(getByTestId('ruleTypeId').textContent).toContain('Inventory threshold breached'); }); it('should display an active badge when active is true', async () => { @@ -71,7 +72,7 @@ describe('Page Title', () => { }); it('should display an inactive badge when active is false', async () => { - const updatedProps = { alert }; + const updatedProps = { alert, dataTestSubj: defaultProps.dataTestSubj }; updatedProps.alert.active = false; const { getByText } = renderComp({ ...updatedProps }); diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx index 86ec39c60dd106..5fc846bb56ad8e 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx @@ -39,6 +39,7 @@ import { export interface PageTitleProps { alert: TopAlert | null; + dataTestSubj: string; } export function pageTitleContent(ruleCategory: string) { @@ -51,7 +52,7 @@ export function pageTitleContent(ruleCategory: string) { }); } -export function PageTitle({ alert }: PageTitleProps) { +export function PageTitle({ alert, dataTestSubj }: PageTitleProps) { const { euiTheme } = useEuiTheme(); if (!alert) return ; @@ -62,7 +63,7 @@ export function PageTitle({ alert }: PageTitleProps) { alert.fields[ALERT_RULE_TYPE_ID] === METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID; return ( -
+
{pageTitleContent(alert.fields[ALERT_RULE_CATEGORY])} {showExperimentalBadge && } diff --git a/x-pack/plugins/observability_ai_assistant/README.md b/x-pack/plugins/observability_ai_assistant/README.md index 1e2a19618825e0..8487cbf13ba109 100644 --- a/x-pack/plugins/observability_ai_assistant/README.md +++ b/x-pack/plugins/observability_ai_assistant/README.md @@ -1,3 +1,73 @@ -# Observability AI Assistant plugin +### **1. Observability AI Assistant Overview** -This plugin provides the Observability AI Assistant service and UI components. +#### **1.1. Introduction** + +This document gives an overview of the features of the Observability AI Assistant at the time of writing, and how to use them. At a high level, the Observability AI Assistant offers contextual insights, and a chat functionality that we enrich with function calling, allowing the LLM to hook into the user's data. We also allow the LLM to store things it considers new information as embeddings into Elasticsearch, and query this knowledge base when it decides it needs more information, using ELSER. + +#### **1.1. Configuration** + +Users can connect to an LLM using [connectors](https://www.elastic.co/guide/en/kibana/current/action-types.html) - specifically the [Generative AI connector](https://www.elastic.co/guide/en/kibana/current/gen-ai-action-type.html), which currently supports both OpenAI and Azure OpenAI as providers. The connector is Enterprise-only. Users can also leverage [preconfigured connectors](https://www.elastic.co/guide/en/kibana/current/pre-configured-connectors.html), in which case the following should be added to `kibana.yml`: + +```yaml +xpack.actions.preconfigured: + open-ai: + actionTypeId: .gen-ai + name: OpenAI + config: + apiUrl: https://api.openai.com/v1/chat/completions + apiProvider: OpenAI + secrets: + apiKey: + azure-open-ai: + actionTypeId: .gen-ai + name: Azure OpenAI + config: + apiUrl: https://.openai.azure.com/openai/deployments//chat/completions?api-version= + apiProvider: Azure OpenAI + secrets: + apiKey: +``` + +**Note**: The configured deployed model should support [function calling](https://platform.openai.com/docs/guides/gpt/function-calling). For OpenAI, this is usually the case. For Azure, the minimum `apiVersion` is `2023-07-01-preview`. We also recommend a model with a pretty sizable token context length. + +#### **1.2. Feature controls** + +Access to the Observability AI Assistant and its APIs is managed through [Kibana privileges](https://www.elastic.co/guide/en/kibana/current/kibana-privileges.html). + +The feature privilege is only available to those with an Enterprise licene. + +#### **1.2. Access Points** + +- **1.2.1. Contextual insights** + +In several places in the Observability apps, the AI Assistant can generate content that helps users understand what they are looking at. We call these contextual insights. Some examples: + +- In Profiling, the AI Assistant explains a displayed function and suggests optimisation opportunities +- In APM, it explains the meaning of a specific error or exception and offers common causes and possible impact +- In Alerting, the AI Assistant takes the results of the log spike analysis, and tries to find a root cause for the spike + +The user can then also continue the conversation in a flyout by clicking "Start chat". + +- **1.2.2. Action Menu Button** + +All Observability apps also have a button in the top action menu, to open the AI Assistant and start a conversation. + +- **1.2.3. Standalone page** + +Users can also access existing conversations and create a new one by navigating to `/app/observabilityAIAssistant/conversations/new`. They can also find this link in the search bar. + +#### **1.3. Chat** + +Conversations with the AI Assistant are powered by three foundational components: the LLM (currently only OpenAI flavors), the knowledge base, and function calling. + +The LLM essentially sits between the product and the user. Its purpose is to interpret both the messages from the user and the response from the functions called, and offer its conclusions and suggest next steps. It can suggest functions on its own, and it has read and write access to the knowledge base. + +The knowledge base is an Elasticsearch index, with an inference processor powered by ELSER. Kibana developers can preload embeddings into this index, and users can access them too, via plain Elasticsearch APIs or specific Kibana APIs. Additionally, the LLM can query the knowledge base for additional context and store things it has learned from a conversation. + +Both the user and the LLM are able to suggest functions, that are executed on behalf (and with the privileges of) the user. Functions allow both the user and the LLM to include relevant context into the conversation. This context can be text, data, or a visual component, like a timeseries graph. Some of the functions that are available are: + +- `recall` and `summarise`: these functions query (with a semantic search) or write to (with a summarisation) the knowledge database. This allows the LLM to create a (partly) user-specific working memory, and access predefined embeddings that help improve its understanding of the Elastic platform. +- `lens`: a function that can be used to create Lens visualisations using Formulas. +- `get_apm_timeseries`, `get_apm_service_summary`, `get_apm_downstream_dependencies` and `get_apm_error_document`: a set of APM functions, some with visual components, that are helpful in performing root cause analysis. + +Function calling is completely transparent to the user - they can edit function suggestions from the LLM, or inspect a function response (but not edit it), or they can request a function themselves. diff --git a/x-pack/plugins/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_ai_assistant/common/types.ts index b98e2555559048..0fad443871add4 100644 --- a/x-pack/plugins/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_ai_assistant/common/types.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { Serializable } from '@kbn/utility-types'; import type { FromSchema } from 'json-schema-to-ts'; import type { JSONSchema } from 'json-schema-to-ts'; import React from 'react'; @@ -23,7 +22,6 @@ export interface Message { message: { content?: string; name?: string; - event?: string; role: MessageRole; function_call?: { name: string; @@ -76,41 +74,44 @@ export interface ContextDefinition { } interface FunctionResponse { - content?: Serializable; - data?: Serializable; + content?: any; + data?: any; } interface FunctionOptions { name: string; description: string; + descriptionForUser: string; parameters: TParameters; contexts: string[]; } -type RespondFunction< - TParameters extends CompatibleJSONSchema, - TResponse extends FunctionResponse -> = (options: { arguments: FromSchema }, signal: AbortSignal) => Promise; +type RespondFunction = ( + options: { arguments: TArguments }, + signal: AbortSignal +) => Promise; -type RenderFunction = (options: { +type RenderFunction = (options: { + arguments: TArguments; response: TResponse; }) => React.ReactNode; export interface FunctionDefinition { options: FunctionOptions; respond: (options: { arguments: any }, signal: AbortSignal) => Promise; - render?: RenderFunction; + render?: RenderFunction; } export type RegisterContextDefinition = (options: ContextDefinition) => void; export type RegisterFunctionDefinition = < TParameters extends CompatibleJSONSchema, - TResponse extends FunctionResponse + TResponse extends FunctionResponse, + TArguments = FromSchema >( options: FunctionOptions, - respond: RespondFunction, - render?: RenderFunction + respond: RespondFunction, + render?: RenderFunction ) => void; export type ContextRegistry = Map; diff --git a/x-pack/plugins/observability_ai_assistant/public/application.tsx b/x-pack/plugins/observability_ai_assistant/public/application.tsx index 869b805e3d3d9d..56270b8ff6002c 100644 --- a/x-pack/plugins/observability_ai_assistant/public/application.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/application.tsx @@ -6,12 +6,13 @@ */ import { EuiErrorBoundary } from '@elastic/eui'; import type { CoreStart, CoreTheme } from '@kbn/core/public'; -import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config'; import type { History } from 'history'; -import React from 'react'; +import React, { useMemo } from 'react'; import type { Observable } from 'rxjs'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { ObservabilityAIAssistantProvider } from './context/observability_ai_assistant_provider'; import { observabilityAIAssistantRouter } from './routes/config'; import type { @@ -32,9 +33,12 @@ export function Application({ pluginsStart: ObservabilityAIAssistantPluginStartDependencies; service: ObservabilityAIAssistantService; }) { + const theme = useMemo(() => { + return { theme$ }; + }, [theme$]); return ( - + (); + const [isOpen, setIsOpen] = useState(false); - const { conversation, displayedMessages, setDisplayedMessages, save } = - useConversation(conversationId); + const chatService = useAbortableAsync( + ({ signal }) => { + if (!isOpen) { + return Promise.resolve(undefined); + } + return service.start({ signal }); + }, + [service, isOpen] + ); - const [isOpen, setIsOpen] = useState(false); + const [conversationId, setConversationId] = useState(); + + const { conversation, displayedMessages, setDisplayedMessages, save } = useConversation({ + conversationId, + }); if (!service.isEnabled()) { return null; @@ -37,7 +50,11 @@ export function ObservabilityAIAssistantActionMenuItem() { > - + {!isOpen || chatService.value ? ( + + ) : ( + + )} {i18n.translate('xpack.observabilityAiAssistant.actionMenuItemLabel', { @@ -46,25 +63,29 @@ export function ObservabilityAIAssistantActionMenuItem() { - { - setIsOpen(() => false); - }} - onChatComplete={(messages) => { - save(messages) - .then((nextConversation) => { - setConversationId(nextConversation.conversation.id); - }) - .catch(() => {}); - }} - onChatUpdate={(nextMessages) => { - setDisplayedMessages(nextMessages); - }} - /> + {chatService.value ? ( + + { + setIsOpen(() => false); + }} + onChatComplete={(messages) => { + save(messages) + .then((nextConversation) => { + setConversationId(nextConversation.conversation.id); + }) + .catch(() => {}); + }} + onChatUpdate={(nextMessages) => { + setDisplayedMessages(nextMessages); + }} + /> + + ) : null} ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.stories.tsx index 0dc50499410063..4b8d749abf9f41 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.stories.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.stories.tsx @@ -5,72 +5,77 @@ * 2.0. */ -import { ComponentStory } from '@storybook/react'; +import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; import React from 'react'; -import { Observable } from 'rxjs'; import { MessageRole } from '../../../common'; -import { getSystemMessage } from '../../service/get_system_message'; -import { ObservabilityAIAssistantService } from '../../types'; +import { getAssistantSetupMessage } from '../../service/get_assistant_setup_message'; import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator'; import { ChatBody as Component } from './chat_body'; -export default { +const meta: ComponentMeta = { component: Component, title: 'app/Organisms/ChatBody', decorators: [KibanaReactStorybookDecorator], }; -type ChatBodyProps = React.ComponentProps; - -const Template: ComponentStory = (props: ChatBodyProps) => { - return ( -
- -
- ); -}; - -const defaultProps: ChatBodyProps = { - title: 'My Conversation', - messages: [ - getSystemMessage(), - { - '@timestamp': new Date().toISOString(), - message: { - role: MessageRole.User, - content: `{"entries":[{"@timestamp":"2023-08-04T06:31:15.160Z","public":false,"confidence":"high","is_correction":false,"namespace":"default","text":"The user's name is Dario.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:53:21.848Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The RENAME command in ES|QL is used to rename a column. The syntax is 'RENAME = '. For example, 'FROM employees | KEEP first_name, last_name, still_hired | RENAME employed = still_hired' will rename the 'still_hired' column to 'employed'. If a column with the new name already exists, it will be replaced by the new column. Multiple columns can be renamed with a single RENAME command.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:52:02.052Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The KEEP command in ES|QL is used to specify what columns are returned and the order in which they are returned. To limit the columns that are returned, a comma-separated list of column names is used. The columns are then returned in the specified order. Wildcards can also be used to return all columns with a name that matches a pattern. For example, 'FROM employees | KEEP h*' will return all columns with a name that starts with an 'h'. The asterisk wildcard (*) by itself translates to all columns that do not match the other arguments.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:55:18.984Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The WHERE command in ES|QL is used to produce a table that contains all the rows from the input table for which the provided condition evaluates to true. For example, 'FROM employees | KEEP first_name, last_name, still_hired | WHERE still_hired == true' will return only the rows where 'still_hired' is true. WHERE supports various operators and functions for calculating values.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:53:57.401Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The SORT command in ES|QL is used to sort rows on one or more fields. The default sort order is ascending, but this can be explicitly set using ASC or DESC. For example, 'FROM employees | KEEP first_name, last_name, height | SORT height DESC' will sort the rows in descending order of height. Additional sort expressions can be provided to act as tie breakers. By default, null values are treated as being larger than any other value, meaning they are sorted last in an ascending order and first in a descending order. This can be changed by providing NULLS FIRST or NULLS LAST.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:50:09.345Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The EVAL command in ES|QL is used to append new columns to a table. For example, 'FROM employees | KEEP first_name, last_name, height | EVAL height_feet = height * 3.281, height_cm = height * 100' will append new columns 'height_feet' and 'height_cm' to the 'employees' table. If the specified column already exists, the existing column will be dropped, and the new column will be appended to the table. EVAL supports various functions for calculating values.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:49:37.882Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The ENRICH command in ES|QL is used to add data from existing indices to incoming records at query time. It requires an enrich policy to be executed, which defines a match field and a set of enrich fields. ENRICH looks for records in the enrich index based on the match field value. The matching key in the input dataset can be defined using 'ON '. If it’s not specified, the match will be performed on a field with the same name as the match field defined in the enrich policy. You can specify which attributes to be added to the result using 'WITH , ...' syntax. Attributes can also be renamed using 'WITH new_name='. By default, ENRICH will add all the enrich fields defined in the enrich policy to the result. In case of name collisions, the newly created fields will override the existing fields.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:50:45.339Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The GROK command in ES|QL enables you to extract structured data out of a string. GROK matches the string against patterns, based on regular expressions, and extracts the specified patterns as columns. For example, 'ROW a = "1953-01-23T12:15:00Z 127.0.0.1 some.email@foo.com 42" | GROK a "%{TIMESTAMP_ISO8601:date} %{IP:ip} %{EMAILADDRESS:email} %{NUMBER:num:int}" | KEEP date, ip, email, num' will extract the date, IP, email, and number from the string into separate columns. Refer to the grok processor documentation for the syntax of grok patterns.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:44:22.647Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The FROM source command in ES|QL returns a table with up to 10,000 documents from a data stream, index, or alias. Each row in the table represents a document, and each column corresponds to a field, which can be accessed by the name of that field. Date math can be used to refer to indices, aliases and data streams, which is useful for time series data. Comma-separated lists or wildcards can be used to query multiple data streams, indices, or aliases.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:42:52.832Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"ES|QL, the Elasticsearch Query Language, is a query language designed for iterative data exploration. An ES|QL query consists of a series of commands, separated by pipes. Each query starts with a source command that produces a table, typically with data from Elasticsearch. This can be followed by one or more processing commands that modify the input table by adding, removing, or changing rows and columns. Processing commands can be chained together, with each command working on the output table of the previous command. The result of a query is the table produced by the final processing command. ES|QL can be used via the _esql endpoint, and results are returned as JSON by default. It can also be used in Kibana's Discover and Lens features for data exploration and visualization. Currently, ES|QL supports field types such as alias, boolean, date, ip, keyword family, double/float/half_float, long int/short/byte, and version.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}}]}`, - }, - }, - ], - connectors: { - connectors: [ +export default meta; +const defaultProps: ComponentStoryObj = { + args: { + title: 'My Conversation', + messages: [ + getAssistantSetupMessage({ contexts: [] }), { - id: 'foo', - referencedByCount: 1, - actionTypeId: 'foo', - name: 'GPT-v8-ultra', - isPreconfigured: true, - isDeprecated: false, - isSystemAction: false, + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.User, + content: `{"entries":[{"@timestamp":"2023-08-04T06:31:15.160Z","public":false,"confidence":"high","is_correction":false,"namespace":"default","text":"The user's name is Dario.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:53:21.848Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The RENAME command in ES|QL is used to rename a column. The syntax is 'RENAME = '. For example, 'FROM employees | KEEP first_name, last_name, still_hired | RENAME employed = still_hired' will rename the 'still_hired' column to 'employed'. If a column with the new name already exists, it will be replaced by the new column. Multiple columns can be renamed with a single RENAME command.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:52:02.052Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The KEEP command in ES|QL is used to specify what columns are returned and the order in which they are returned. To limit the columns that are returned, a comma-separated list of column names is used. The columns are then returned in the specified order. Wildcards can also be used to return all columns with a name that matches a pattern. For example, 'FROM employees | KEEP h*' will return all columns with a name that starts with an 'h'. The asterisk wildcard (*) by itself translates to all columns that do not match the other arguments.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:55:18.984Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The WHERE command in ES|QL is used to produce a table that contains all the rows from the input table for which the provided condition evaluates to true. For example, 'FROM employees | KEEP first_name, last_name, still_hired | WHERE still_hired == true' will return only the rows where 'still_hired' is true. WHERE supports various operators and functions for calculating values.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:53:57.401Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The SORT command in ES|QL is used to sort rows on one or more fields. The default sort order is ascending, but this can be explicitly set using ASC or DESC. For example, 'FROM employees | KEEP first_name, last_name, height | SORT height DESC' will sort the rows in descending order of height. Additional sort expressions can be provided to act as tie breakers. By default, null values are treated as being larger than any other value, meaning they are sorted last in an ascending order and first in a descending order. This can be changed by providing NULLS FIRST or NULLS LAST.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:50:09.345Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The EVAL command in ES|QL is used to append new columns to a table. For example, 'FROM employees | KEEP first_name, last_name, height | EVAL height_feet = height * 3.281, height_cm = height * 100' will append new columns 'height_feet' and 'height_cm' to the 'employees' table. If the specified column already exists, the existing column will be dropped, and the new column will be appended to the table. EVAL supports various functions for calculating values.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:49:37.882Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The ENRICH command in ES|QL is used to add data from existing indices to incoming records at query time. It requires an enrich policy to be executed, which defines a match field and a set of enrich fields. ENRICH looks for records in the enrich index based on the match field value. The matching key in the input dataset can be defined using 'ON '. If it’s not specified, the match will be performed on a field with the same name as the match field defined in the enrich policy. You can specify which attributes to be added to the result using 'WITH , ...' syntax. Attributes can also be renamed using 'WITH new_name='. By default, ENRICH will add all the enrich fields defined in the enrich policy to the result. In case of name collisions, the newly created fields will override the existing fields.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:50:45.339Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The GROK command in ES|QL enables you to extract structured data out of a string. GROK matches the string against patterns, based on regular expressions, and extracts the specified patterns as columns. For example, 'ROW a = "1953-01-23T12:15:00Z 127.0.0.1 some.email@foo.com 42" | GROK a "%{TIMESTAMP_ISO8601:date} %{IP:ip} %{EMAILADDRESS:email} %{NUMBER:num:int}" | KEEP date, ip, email, num' will extract the date, IP, email, and number from the string into separate columns. Refer to the grok processor documentation for the syntax of grok patterns.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:44:22.647Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"The FROM source command in ES|QL returns a table with up to 10,000 documents from a data stream, index, or alias. Each row in the table represents a document, and each column corresponds to a field, which can be accessed by the name of that field. Date math can be used to refer to indices, aliases and data streams, which is useful for time series data. Comma-separated lists or wildcards can be used to query multiple data streams, indices, or aliases.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}},{"@timestamp":"2023-08-03T16:42:52.832Z","public":true,"confidence":"high","is_correction":false,"namespace":"default","text":"ES|QL, the Elasticsearch Query Language, is a query language designed for iterative data exploration. An ES|QL query consists of a series of commands, separated by pipes. Each query starts with a source command that produces a table, typically with data from Elasticsearch. This can be followed by one or more processing commands that modify the input table by adding, removing, or changing rows and columns. Processing commands can be chained together, with each command working on the output table of the previous command. The result of a query is the table produced by the final processing command. ES|QL can be used via the _esql endpoint, and results are returned as JSON by default. It can also be used in Kibana's Discover and Lens features for data exploration and visualization. Currently, ES|QL supports field types such as alias, boolean, date, ip, keyword family, double/float/half_float, long int/short/byte, and version.","user":{"name":"elastic","id":"u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"},"ml":{"model_id":".elser_model_1"}}]}`, + }, }, ], - loading: false, - error: undefined, - selectedConnector: 'foo', - selectConnector: () => {}, + knowledgeBase: { + status: { + loading: false, + value: { + ready: true, + }, + refresh: () => {}, + }, + isInstalling: false, + install: async () => {}, + }, + connectors: { + connectors: [ + { + id: 'foo', + referencedByCount: 1, + actionTypeId: 'foo', + name: 'GPT-v8-ultra', + isPreconfigured: true, + isDeprecated: false, + isSystemAction: false, + }, + ], + loading: false, + error: undefined, + selectedConnector: 'foo', + selectConnector: () => {}, + }, + connectorsManagementHref: '', + currentUser: { + username: 'elastic', + }, + onChatUpdate: () => {}, + onChatComplete: () => {}, }, - connectorsManagementHref: '', - currentUser: { - username: 'elastic', + render: (props) => { + return ( +
+ +
+ ); }, - service: { - chat: () => { - return new Observable(); - }, - } as unknown as ObservabilityAIAssistantService, - onChatUpdate: () => {}, - onChatComplete: () => {}, }; -export const ChatBody = Template.bind({}); -ChatBody.args = defaultProps; +export const ChatBody: ComponentStoryObj = { + ...defaultProps, +}; 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 365db887e8bcbd..90a6b7f7709109 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 @@ -18,12 +18,14 @@ import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import React from 'react'; import type { Message } from '../../../common/types'; import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; +import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; import { useTimeline } from '../../hooks/use_timeline'; -import { ObservabilityAIAssistantService } from '../../types'; +import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service'; import { MissingCredentialsCallout } from '../missing_credentials_callout'; import { ChatHeader } from './chat_header'; import { ChatPromptEditor } from './chat_prompt_editor'; import { ChatTimeline } from './chat_timeline'; +import { KnowledgeBaseCallout } from './knowledge_base_callout'; const containerClassName = css` max-height: 100%; @@ -42,8 +44,8 @@ export function ChatBody({ title, messages, connectors, + knowledgeBase, currentUser, - service, connectorsManagementHref, onChatUpdate, onChatComplete, @@ -51,34 +53,36 @@ export function ChatBody({ title: string; messages: Message[]; connectors: UseGenAIConnectorsResult; + knowledgeBase: UseKnowledgeBaseResult; currentUser?: Pick; - service: ObservabilityAIAssistantService; connectorsManagementHref: string; onChatUpdate: (messages: Message[]) => void; onChatComplete: (messages: Message[]) => void; }) { + const chatService = useObservabilityAIAssistantChatService(); + const timeline = useTimeline({ messages, connectors, currentUser, - service, + chatService, onChatUpdate, onChatComplete, }); let footer: React.ReactNode; - if (connectors.loading || connectors.connectors?.length === 0) { + if (connectors.loading || knowledgeBase.status.loading) { + footer = ( + + + + ); + } else if (connectors.connectors?.length === 0) { footer = ( <> - {connectors.connectors?.length === 0 ? ( - - ) : ( - - - - )} + ); } else { @@ -118,6 +122,9 @@ export function ChatBody({ + + + diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.stories.tsx index bcadebcf4c729c..835d49b7bde6a3 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.stories.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.stories.tsx @@ -7,7 +7,7 @@ import { ComponentStory } from '@storybook/react'; import React from 'react'; -import { getSystemMessage } from '../../service/get_system_message'; +import { getAssistantSetupMessage } from '../../service/get_assistant_setup_message'; import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator'; import { ChatFlyout as Component } from './chat_flyout'; @@ -30,7 +30,7 @@ const Template: ComponentStory = (props: ChatFlyoutProps) => { const defaultProps: ChatFlyoutProps = { isOpen: true, title: 'How is this working', - messages: [getSystemMessage()], + messages: [getAssistantSetupMessage({ contexts: [] })], onClose: () => {}, }; 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 7afbbc8e5b89c6..dbe55b5454da6a 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 @@ -6,13 +6,13 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiFlyout, EuiLink, EuiPanel, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/css'; -import React from 'react'; import { i18n } from '@kbn/i18n'; +import React from 'react'; import type { Message } from '../../../common/types'; import { useCurrentUser } from '../../hooks/use_current_user'; import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; import { useKibana } from '../../hooks/use_kibana'; -import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant'; +import { useKnowledgeBase } from '../../hooks/use_knowledge_base'; import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; import { getConnectorsManagementHref } from '../../utils/get_connectors_management_href'; import { ChatBody } from './chat_body'; @@ -21,6 +21,10 @@ const containerClassName = css` max-height: 100%; `; +const bodyClassName = css` + overflow-y: auto; +`; + export function ChatFlyout({ title, messages, @@ -46,12 +50,12 @@ export function ChatFlyout({ services: { http }, } = useKibana(); - const service = useObservabilityAIAssistant(); - const { euiTheme } = useEuiTheme(); const router = useObservabilityAIAssistantRouter(); + const knowledgeBase = useKnowledgeBase(); + return isOpen ? ( - + { if (onChatUpdate) { onChatUpdate(nextMessages); diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx index ef17ba34ca780b..be41e39067e326 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx @@ -5,191 +5,193 @@ * 2.0. */ +import React, { useState } from 'react'; +import { css } from '@emotion/css'; import { - EuiButtonIcon, + EuiAccordion, EuiComment, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, + EuiErrorBoundary, + EuiPanel, + EuiSpacer, + useGeneratedHtmlId, } from '@elastic/eui'; -import { css } from '@emotion/css'; -import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { MessageRole } from '../../../common/types'; -import { Feedback, FeedbackButtons } from '../feedback_buttons'; -import { MessagePanel } from '../message_panel/message_panel'; -import { MessageText } from '../message_panel/message_text'; -import { RegenerateResponseButton } from '../buttons/regenerate_response_button'; -import { StopGeneratingButton } from '../buttons/stop_generating_button'; +import { ChatItemActions } from './chat_item_actions'; import { ChatItemAvatar } from './chat_item_avatar'; -import { ChatItemTitle } from './chat_item_title'; +import { ChatItemContentInlinePromptEditor } from './chat_item_content_inline_prompt_editor'; +import { ChatItemControls } from './chat_item_controls'; import { ChatTimelineItem } from './chat_timeline'; - -export interface ChatItemAction { - id: string; - label: string; - icon?: string; - handler: () => void; -} +import { getRoleTranslation } from '../../utils/get_role_translation'; +import type { Feedback } from '../feedback_buttons'; +import { Message } from '../../../common'; export interface ChatItemProps extends ChatTimelineItem { - onEditSubmit: (content: string) => void; + onEditSubmit: (message: Message) => Promise; onFeedbackClick: (feedback: Feedback) => void; onRegenerateClick: () => void; onStopGeneratingClick: () => void; } -const euiCommentClassName = css` - .euiCommentEvent__headerEvent { - flex-grow: 1; +const normalMessageClassName = css` + .euiCommentEvent__header { + padding: 4px 8px; + } + + .euiCommentEvent__body { + padding: 0; + } + /* targets .*euiTimelineItemEvent-top, makes sure text properly wraps and doesn't overflow */ + > :last-child { + overflow-x: hidden; + } +`; + +const noPanelMessageClassName = css` + .euiCommentEvent { + border: none; + } + + .euiCommentEvent__header { + background: transparent; + border-block-end: none; } - > div:last-child { - overflow: hidden; + .euiCommentEvent__body { + display: none; + } +`; + +const accordionButtonClassName = css` + .euiAccordion__iconButton { + display: none; } `; export function ChatItem({ - title, + actions: { canCopy, canEdit, canGiveFeedback, canRegenerate }, + display: { collapsed }, content, - canEdit, - canGiveFeedback, - canRegenerate, - role, - loading, - error, currentUser, + element, + error, + function_call: functionCall, + loading, + role, + title, onEditSubmit, + onFeedbackClick, onRegenerateClick, onStopGeneratingClick, - onFeedbackClick, }: ChatItemProps) { - const [isActionsPopoverOpen, setIsActionsPopover] = useState(false); + const accordionId = useGeneratedHtmlId({ prefix: 'chat' }); + + const [editing, setEditing] = useState(false); + const [expanded, setExpanded] = useState(Boolean(element)); - const handleClickActions = () => { - setIsActionsPopover(!isActionsPopoverOpen); + const actions = [canCopy, collapsed, canCopy].filter(Boolean); + + const noBodyMessageClassName = css` + .euiCommentEvent__header { + padding: 4px 8px; + } + + .euiCommentEvent__body { + padding: 0; + height: ${expanded ? 'fit-content' : '0px'}; + overflow: hidden; + } + `; + + const handleToggleExpand = () => { + setExpanded(!expanded); + + if (editing) { + setEditing(false); + } + }; + + const handleToggleEdit = () => { + if (collapsed && !expanded) { + setExpanded(true); + } + setEditing(!editing); + }; + + const handleInlineEditSubmit = (message: Message) => { + handleToggleEdit(); + return onEditSubmit(message); + }; + + const handleCopyToClipboard = () => { + navigator.clipboard.writeText(content || ''); }; - const [_, setEditing] = useState(false); - - const actions: ChatItemAction[] = canEdit - ? [ - { - id: 'edit', - label: i18n.translate('xpack.observabilityAiAssistant.chatTimeline.actions.editMessage', { - defaultMessage: 'Edit message', - }), - handler: () => { - setEditing(false); - setIsActionsPopover(false); - }, - }, - ] - : []; - - let controls: React.ReactNode; - - const displayFeedback = !error && canGiveFeedback; - const displayRegenerate = !loading && canRegenerate; - - if (loading) { - controls = ; - } else if (displayFeedback || displayRegenerate) { - controls = ( - - {displayFeedback ? ( - - - - ) : null} - {displayRegenerate ? ( - - - - ) : null} - + let contentElement: React.ReactNode = + content || error ? ( + + ) : null; + + if (collapsed) { + contentElement = ( + + + {contentElement} + ); } return ( - } - panelPaddingSize="s" - closePopover={handleClickActions} - isOpen={isActionsPopoverOpen} - > - ( - - {label} - - ))} - /> - - ) : null - } - title={title} - /> - } - className={euiCommentClassName} timelineAvatar={ } username={getRoleTranslation(role)} - > - {content || error || controls ? ( - : null - } - error={error} - controls={controls} + event={title} + actions={ + - ) : null} + } + className={ + actions.length === 0 && !content + ? noPanelMessageClassName + : collapsed + ? noBodyMessageClassName + : normalMessageClassName + } + > + + {element ? {element} : null} + + {contentElement} + + + ); } - -const getRoleTranslation = (role: MessageRole) => { - if (role === MessageRole.User) { - return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.user.label', { - defaultMessage: 'You', - }); - } - - if (role === MessageRole.System) { - return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.system.label', { - defaultMessage: 'System', - }); - } - - return i18n.translate( - 'xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.label', - { - defaultMessage: 'Elastic Assistant', - } - ); -}; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_actions.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_actions.tsx new file mode 100644 index 00000000000000..79240a03bb3143 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_actions.tsx @@ -0,0 +1,114 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiPopover, EuiText } from '@elastic/eui'; + +export function ChatItemActions({ + canCopy, + canEdit, + collapsed, + editing, + expanded, + onToggleEdit, + onToggleExpand, + onCopyToClipboard, +}: { + canCopy: boolean; + canEdit: boolean; + collapsed: boolean; + editing: boolean; + expanded: boolean; + onToggleEdit: () => void; + onToggleExpand: () => void; + onCopyToClipboard: () => void; +}) { + const [isPopoverOpen, setIsPopoverOpen] = useState(); + + useEffect(() => { + const timeout = setTimeout(() => { + if (isPopoverOpen) { + setIsPopoverOpen(undefined); + } + }, 800); + + return () => { + clearTimeout(timeout); + }; + }, [isPopoverOpen]); + + return ( + <> + {canEdit ? ( + + ) : null} + + {collapsed ? ( + + ) : null} + + {canCopy ? ( + { + setIsPopoverOpen('copy'); + onCopyToClipboard(); + }} + /> + } + isOpen={isPopoverOpen === 'copy'} + panelPaddingSize="s" + closePopover={() => setIsPopoverOpen(undefined)} + > + +

+ {i18n.translate( + 'xpack.observabilityAiAssistant.chatTimeline.actions.copyMessageSuccessful', + { + defaultMessage: 'Copied message', + } + )} +

+
+
+ ) : null} + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_content_inline_prompt_editor.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_content_inline_prompt_editor.tsx new file mode 100644 index 00000000000000..d017d7d65fc90b --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_content_inline_prompt_editor.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { MessageText } from '../message_panel/message_text'; +import { ChatPromptEditor } from './chat_prompt_editor'; +import { MessageRole, type Message } from '../../../common'; + +interface Props { + content: string | undefined; + functionCall: + | { + name: string; + arguments?: string | undefined; + trigger: MessageRole; + } + | undefined; + loading: boolean; + editing: boolean; + onSubmit: (message: Message) => Promise; +} +export function ChatItemContentInlinePromptEditor({ + content, + functionCall, + editing, + loading, + onSubmit, +}: Props) { + return !editing ? ( + + ) : ( + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_controls.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_controls.tsx new file mode 100644 index 00000000000000..74eeb184cb5c7f --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_controls.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 { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPanel, + EuiSpacer, + useEuiTheme, +} from '@elastic/eui'; +import { Feedback, FeedbackButtons } from '../feedback_buttons'; +import { RegenerateResponseButton } from '../buttons/regenerate_response_button'; +import { StopGeneratingButton } from '../buttons/stop_generating_button'; + +export function ChatItemControls({ + error, + loading, + canRegenerate, + canGiveFeedback, + onFeedbackClick, + onRegenerateClick, + onStopGeneratingClick, +}: { + error: any; + loading: boolean; + canRegenerate: boolean; + canGiveFeedback: boolean; + onFeedbackClick: (feedback: Feedback) => void; + onRegenerateClick: () => void; + onStopGeneratingClick: () => void; +}) { + const { euiTheme } = useEuiTheme(); + + const displayFeedback = !error && canGiveFeedback; + const displayRegenerate = !loading && canRegenerate; + + let controls; + + if (loading) { + controls = ; + } else if (displayFeedback || displayRegenerate) { + controls = ( + + {displayFeedback ? ( + + + + ) : null} + {displayRegenerate ? ( + + + + ) : null} + + ); + } else { + controls = null; + } + + return controls ? ( + <> + + + + {controls} + + + ) : null; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.tsx index a49da7665c9a28..6aa08ba31d818f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_prompt_editor.tsx @@ -5,48 +5,65 @@ * 2.0. */ -import React, { useCallback, useEffect, useRef, useState } from 'react'; import { - EuiButtonIcon, EuiButtonEmpty, - EuiFieldText, + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, - EuiSpacer, EuiPanel, + EuiSpacer, + EuiTextArea, keys, + EuiFocusTrap, } from '@elastic/eui'; -import { CodeEditor } from '@kbn/kibana-react-plugin/public'; import { i18n } from '@kbn/i18n'; -import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant'; +import { CodeEditor } from '@kbn/kibana-react-plugin/public'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { MessageRole, type Message } from '../../../common'; import { useJsonEditorModel } from '../../hooks/use_json_editor_model'; -import { type Message, MessageRole } from '../../../common'; -import type { FunctionDefinition } from '../../../common/types'; import { FunctionListPopover } from './function_list_popover'; export interface ChatPromptEditorProps { disabled: boolean; loading: boolean; + initialPrompt?: string; + initialSelectedFunctionName?: string; + initialFunctionPayload?: string; + trigger?: MessageRole; onSubmit: (message: Message) => Promise; } -export function ChatPromptEditor({ onSubmit, disabled, loading }: ChatPromptEditorProps) { - const { getFunctions } = useObservabilityAIAssistant(); - const functions = getFunctions(); +export function ChatPromptEditor({ + disabled, + loading, + initialPrompt, + initialSelectedFunctionName, + initialFunctionPayload, + onSubmit, +}: ChatPromptEditorProps) { + const isFocusTrapEnabled = Boolean(initialPrompt); + + const [prompt, setPrompt] = useState(initialPrompt); - const [prompt, setPrompt] = useState(''); - const [functionPayload, setFunctionPayload] = useState(''); - const [selectedFunction, setSelectedFunction] = useState(); + const [selectedFunctionName, setSelectedFunctionName] = useState( + initialSelectedFunctionName + ); + const [functionPayload, setFunctionPayload] = useState( + initialFunctionPayload + ); - const { model, initialJsonString } = useJsonEditorModel(selectedFunction); + const { model, initialJsonString } = useJsonEditorModel({ + functionName: selectedFunctionName, + initialJson: initialFunctionPayload, + }); - const ref = useRef(null); + const textAreaRef = useRef(null); useEffect(() => { setFunctionPayload(initialJsonString); - }, [initialJsonString, selectedFunction]); + }, [initialJsonString, selectedFunctionName]); - const handleChange = (event: React.ChangeEvent) => { + const handleChange = (event: React.ChangeEvent) => { setPrompt(event.currentTarget.value); }; @@ -55,8 +72,22 @@ export function ChatPromptEditor({ onSubmit, disabled, loading }: ChatPromptEdit }; const handleClearSelection = () => { - setSelectedFunction(undefined); + setSelectedFunctionName(undefined); + setFunctionPayload(''); + setPrompt(''); + }; + + const handleSelectFunction = (functionName: string) => { + setPrompt(''); setFunctionPayload(''); + setSelectedFunctionName(functionName); + }; + + const handleResizeTextArea = () => { + if (textAreaRef.current) { + textAreaRef.current.style.height = 'auto'; + textAreaRef.current.style.height = textAreaRef.current?.scrollHeight + 'px'; + } }; const handleSubmit = useCallback(async () => { @@ -65,20 +96,25 @@ export function ChatPromptEditor({ onSubmit, disabled, loading }: ChatPromptEdit setPrompt(''); setFunctionPayload(undefined); + handleResizeTextArea(); try { - if (selectedFunction) { + if (selectedFunctionName) { await onSubmit({ '@timestamp': new Date().toISOString(), message: { - role: MessageRole.Function, + role: MessageRole.Assistant, + content: '', function_call: { - name: selectedFunction.options.name, + name: selectedFunctionName, trigger: MessageRole.User, arguments: currentPayload, }, }, }); + + setFunctionPayload(undefined); + setSelectedFunctionName(undefined); } else { await onSubmit({ '@timestamp': new Date().toISOString(), @@ -89,131 +125,144 @@ export function ChatPromptEditor({ onSubmit, disabled, loading }: ChatPromptEdit } catch (_) { setPrompt(currentPrompt); } - }, [functionPayload, onSubmit, prompt, selectedFunction]); + }, [functionPayload, onSubmit, prompt, selectedFunctionName]); useEffect(() => { const keyboardListener = (event: KeyboardEvent) => { - if (event.key === keys.ENTER) { + if (!event.shiftKey && event.key === keys.ENTER && (prompt || selectedFunctionName)) { + event.preventDefault(); handleSubmit(); } }; - window.addEventListener('keyup', keyboardListener); + window.addEventListener('keypress', keyboardListener); return () => { - window.removeEventListener('keyup', keyboardListener); + window.removeEventListener('keypress', keyboardListener); }; - }, [handleSubmit]); + }, [handleSubmit, prompt, selectedFunctionName]); useEffect(() => { - if (ref.current) { - ref.current.focus(); + const textarea = textAreaRef.current; + + if (textarea) { + textarea.focus(); + textarea.addEventListener('input', handleResizeTextArea, false); } + + return () => { + textarea?.removeEventListener('input', handleResizeTextArea, false); + }; }); return ( - - - - - - - - - - {selectedFunction ? ( - - {i18n.translate('xpack.observabilityAiAssistant.prompt.emptySelection', { - defaultMessage: 'Empty selection', - })} - - ) : null} - - - - - {selectedFunction ? ( - - + + + + + + + + + + {selectedFunctionName ? ( + + {i18n.translate('xpack.observabilityAiAssistant.prompt.emptySelection', { + defaultMessage: 'Empty selection', + })} + + ) : null} + + + + + {selectedFunctionName ? ( + + { + editor.focus(); + }} + options={{ + accessibilitySupport: 'off', + acceptSuggestionOnEnter: 'on', + automaticLayout: true, + autoClosingQuotes: 'always', + autoIndent: 'full', + contextmenu: true, + fontSize: 12, + formatOnPaste: true, + formatOnType: true, + inlineHints: { enabled: true }, + lineNumbers: 'on', + minimap: { enabled: false }, + model, + overviewRulerBorder: false, + quickSuggestions: true, + scrollbar: { alwaysConsumeMouseWheel: false }, + scrollBeyondLastLine: false, + suggestOnTriggerCharacters: true, + tabSize: 2, + wordWrap: 'on', + wrappingIndent: 'indent', + }} + transparentBackground + value={functionPayload || ''} + onChange={handleChangeFunctionPayload} + /> + + ) : ( + - - ) : ( - - )} - - - - - - - - + )} +
+
+ + + + + +
+ ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx index 625f6ba28d141e..fb1938b2914c76 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx @@ -77,12 +77,16 @@ const defaultProps: ComponentProps = { arguments: '{ "foo": "bar" }', trigger: MessageRole.Assistant, }, - canEdit: true, + actions: { + canEdit: true, + }, }), buildFunctionChatItem({ content: '{ "message": "The arguments are wrong" }', error: new Error(), - canRegenerate: false, + actions: { + canRegenerate: false, + }, }), buildAssistantChatItem({ content: '', @@ -92,7 +96,9 @@ const defaultProps: ComponentProps = { arguments: '{ "bar": "foo" }', trigger: MessageRole.Assistant, }, - canEdit: true, + actions: { + canEdit: true, + }, }), buildFunctionChatItem({ content: '', @@ -100,7 +106,7 @@ const defaultProps: ComponentProps = { loading: true, }), ], - onEdit: () => {}, + onEdit: async () => {}, onFeedback: () => {}, onRegenerate: () => {}, onStopGenerating: () => {}, 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 cdfbeecc7a56a7..9aee8190dde654 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 @@ -7,26 +7,36 @@ import { EuiCommentList } from '@elastic/eui'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; -import React from 'react'; -import type { Message } from '../../../common'; +import { compact } from 'lodash'; +import React, { ReactNode } from 'react'; +import { type Message } from '../../../common'; + import type { Feedback } from '../feedback_buttons'; import { ChatItem } from './chat_item'; export interface ChatTimelineItem extends Pick { id: string; - title: string; + title: ReactNode; + actions: { + canCopy: boolean; + canEdit: boolean; + canGiveFeedback: boolean; + canRegenerate: boolean; + }; + display: { + collapsed: boolean; + hide?: boolean; + }; loading: boolean; - error?: any; - canEdit: boolean; - canRegenerate: boolean; - canGiveFeedback: boolean; + element?: React.ReactNode; currentUser?: Pick; + error?: any; } export interface ChatTimelineProps { items: ChatTimelineItem[]; - onEdit: (item: ChatTimelineItem, content: string) => void; + onEdit: (item: ChatTimelineItem, message: Message) => Promise; onFeedback: (item: ChatTimelineItem, feedback: Feedback) => void; onRegenerate: (item: ChatTimelineItem) => void; onStopGenerating: () => void; @@ -41,23 +51,27 @@ export function ChatTimeline({ }: ChatTimelineProps) { return ( - {items.map((item, index) => ( - { - onFeedback(item, feedback); - }} - onRegenerateClick={() => { - onRegenerate(item); - }} - onEditSubmit={(content) => { - onEdit(item, content); - }} - onStopGeneratingClick={onStopGenerating} - /> - ))} + {compact( + items.map((item, index) => + !item.display.hide ? ( + { + onFeedback(item, feedback); + }} + onRegenerateClick={() => { + onRegenerate(item); + }} + onEditSubmit={(message) => { + return onEdit(item, message); + }} + onStopGeneratingClick={onStopGenerating} + /> + ) : null + ) + )} ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx index 27b1ff23d5c066..05408f62ea6ba8 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx @@ -37,11 +37,11 @@ export function ConversationList({ onClickDeleteConversation, }: { selected: string; - onClickConversation: (conversationId: string) => void; - onClickNewChat: () => void; loading: boolean; error?: any; conversations?: Array<{ id: string; label: string; href?: string }>; + onClickConversation: (conversationId: string) => void; + onClickNewChat: () => void; onClickDeleteConversation: (id: string) => void; }) { return ( diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.stories.tsx index d4163fabaaa9ae..a37dbb9e59c1bf 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.stories.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.stories.tsx @@ -23,7 +23,6 @@ const Template: ComponentStory = (props: FunctionListPopover) }; const defaultProps: FunctionListPopover = { - functions: [], onSelectFunction: () => {}, }; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx index 9e59167c847136..64e4233ab1d7b8 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react'; import { EuiButtonEmpty, - EuiContextMenuItem, + EuiContextMenu, EuiContextMenuPanel, EuiPopover, EuiSpacer, @@ -16,16 +16,17 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FunctionDefinition } from '../../../common/types'; +import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service'; export function FunctionListPopover({ - functions, - selectedFunction, + selectedFunctionName, onSelectFunction, }: { - functions: FunctionDefinition[]; - selectedFunction?: FunctionDefinition; - onSelectFunction: (func: FunctionDefinition) => void; + selectedFunctionName?: string; + onSelectFunction: (func: string) => void; }) { + const chatService = useObservabilityAIAssistantChatService(); + const [isFunctionListOpen, setIsFunctionListOpen] = useState(false); const handleClickFunctionList = () => { @@ -34,7 +35,7 @@ export function FunctionListPopover({ const handleSelectFunction = (func: FunctionDefinition) => { setIsFunctionListOpen(false); - onSelectFunction(func); + onSelectFunction(func.options.name); }; useEffect(() => { @@ -61,31 +62,44 @@ export function FunctionListPopover({ size="xs" onClick={handleClickFunctionList} > - {selectedFunction - ? selectedFunction.options.name + {selectedFunctionName + ? selectedFunctionName : i18n.translate('xpack.observabilityAiAssistant.prompt.callFunction', { defaultMessage: 'Call function', })} } closePopover={handleClickFunctionList} + css={{ maxWidth: 400 }} panelPaddingSize="none" isOpen={isFunctionListOpen} > - {functions.map((func) => ( - handleSelectFunction(func)}> - -

- {func.options.name} -

-
- - -

{func.options.description}

-
-
- ))} + ({ + name: ( + <> + +

+ {func.options.name} +

+
+ + +

{func.options.descriptionForUser}

+
+ + ), + onClick: () => handleSelectFunction(func), + })), + }, + ]} + />
); diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/knowledge_base_callout.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/knowledge_base_callout.stories.tsx new file mode 100644 index 00000000000000..3122631bb65614 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/knowledge_base_callout.stories.tsx @@ -0,0 +1,70 @@ +/* + * 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 { ComponentMeta, ComponentStoryObj } from '@storybook/react'; +import { merge } from 'lodash'; +import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator'; +import { KnowledgeBaseCallout as Component } from './knowledge_base_callout'; + +const meta: ComponentMeta = { + component: Component, + title: 'app/Molecules/KnowledgeBaseCallout', + decorators: [KibanaReactStorybookDecorator], +}; + +export default meta; +const defaultProps: ComponentStoryObj = { + args: { + knowledgeBase: { + status: { + loading: false, + value: { + ready: false, + }, + refresh: () => {}, + }, + isInstalling: false, + installError: undefined, + install: async () => {}, + }, + }, +}; + +export const StatusError: ComponentStoryObj = merge({}, defaultProps, { + args: { knowledgeBase: { status: { loading: false, error: new Error() } } }, +}); + +export const Loading: ComponentStoryObj = merge({}, defaultProps, { + args: { knowledgeBase: { status: { loading: true } } }, +}); + +export const NotInstalled: ComponentStoryObj = merge({}, defaultProps, { + args: { knowledgeBase: { status: { loading: false, value: { ready: false } } } }, +}); + +export const Installing: ComponentStoryObj = merge({}, defaultProps, { + args: { + knowledgeBase: { status: { loading: false, value: { ready: false } }, isInstalling: true }, + }, +}); + +export const InstallError: ComponentStoryObj = merge({}, defaultProps, { + args: { + knowledgeBase: { + status: { + loading: false, + value: { ready: false }, + }, + isInstalling: false, + installError: new Error(), + }, + }, +}); + +export const Installed: ComponentStoryObj = merge({}, defaultProps, { + args: { knowledgeBase: { status: { loading: false, value: { ready: true } } } }, +}); diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/knowledge_base_callout.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/knowledge_base_callout.tsx new file mode 100644 index 00000000000000..36453b641e3e1a --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/knowledge_base_callout.tsx @@ -0,0 +1,107 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiLoadingSpinner, + EuiPanel, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; + +export function KnowledgeBaseCallout({ knowledgeBase }: { knowledgeBase: UseKnowledgeBaseResult }) { + let content: React.ReactNode; + + let color: 'primary' | 'danger' | 'plain' = 'primary'; + + if (knowledgeBase.status.loading) { + content = ( + + + + + + + {i18n.translate('xpack.observabilityAiAssistant.checkingKbAvailability', { + defaultMessage: 'Checking availability of knowledge base', + })} + + + + ); + } else if (knowledgeBase.status.error) { + color = 'danger'; + content = ( + + {i18n.translate('xpack.observabilityAiAssistant.failedToGetStatus', { + defaultMessage: 'Failed to get model status.', + })} + + ); + } else if (knowledgeBase.status.value?.ready) { + color = 'plain'; + content = ( + + {i18n.translate('xpack.observabilityAiAssistant.poweredByModel', { + defaultMessage: 'Powered by {model}', + values: { + model: 'ELSER', + }, + })} + + ); + } else if (knowledgeBase.isInstalling) { + color = 'primary'; + content = ( + + + + + + + {i18n.translate('xpack.observabilityAiAssistant.installingKb', { + defaultMessage: 'Setting up the knowledge base', + })} + + + + ); + } else if (knowledgeBase.installError) { + color = 'danger'; + content = ( + + {i18n.translate('xpack.observabilityAiAssistant.failedToSetupKnowledgeBase', { + defaultMessage: 'Failed to set up knowledge base.', + })} + + ); + } else if (!knowledgeBase.status.value?.ready && !knowledgeBase.status.error) { + content = ( + { + knowledgeBase.install(); + }} + > + + {i18n.translate('xpack.observabilityAiAssistant.setupKb', { + defaultMessage: 'Improve your experience by setting up the knowledge base.', + })} + + + ); + } + + return ( + + {content} + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx index e34050bf19f467..d695ebc2776692 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx @@ -11,7 +11,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { Subscription } from 'rxjs'; import { MessageRole, type Message } from '../../../common/types'; import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; -import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant'; import type { PendingMessage } from '../../types'; import { ChatFlyout } from '../chat/chat_flyout'; import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base'; @@ -23,6 +22,10 @@ import { StopGeneratingButton } from '../buttons/stop_generating_button'; import { InsightBase } from './insight_base'; import { MissingCredentialsCallout } from '../missing_credentials_callout'; import { getConnectorsManagementHref } from '../../utils/get_connectors_management_href'; +import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service'; +import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant'; +import { useAbortableAsync } from '../../hooks/use_abortable_async'; +import { ObservabilityAIAssistantChatServiceProvider } from '../../context/observability_ai_assistant_chat_service_provider'; function ChatContent({ title, @@ -33,7 +36,7 @@ function ChatContent({ messages: Message[]; connectorId: string; }) { - const service = useObservabilityAIAssistant(); + const chatService = useObservabilityAIAssistantChatService(); const [pendingMessage, setPendingMessage] = useState(); const [loading, setLoading] = useState(false); @@ -42,7 +45,7 @@ function ChatContent({ const reloadReply = useCallback(() => { setLoading(true); - const nextSubscription = service.chat({ messages, connectorId }).subscribe({ + const nextSubscription = chatService.chat({ messages, connectorId }).subscribe({ next: (msg) => { setPendingMessage(() => msg); }, @@ -52,7 +55,7 @@ function ChatContent({ }); setSubscription(nextSubscription); - }, [messages, connectorId, service]); + }, [messages, connectorId, chatService]); useEffect(() => { reloadReply(); @@ -129,6 +132,15 @@ export function Insight({ messages, title }: { messages: Message[]; title: strin const connectors = useGenAIConnectors(); + const service = useObservabilityAIAssistant(); + + const chatService = useAbortableAsync( + ({ signal }) => { + return service.start({ signal }); + }, + [service] + ); + const { services: { http }, } = useKibana(); @@ -152,9 +164,13 @@ export function Insight({ messages, title }: { messages: Message[]; title: strin setHasOpened((prevHasOpened) => prevHasOpened || isOpen); }} controls={} - loading={connectors.loading} + loading={connectors.loading || chatService.loading} > - {children} + {chatService.value ? ( + + {children} + + ) : null} ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx index 96cddb6a3b7ca3..7aeae624a02538 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx @@ -87,7 +87,7 @@ export function MessageText(props: Props) { const containerClassName = css` overflow-wrap: break-word; - code { + pre { background: ${euiThemeVars.euiColorLightestShade}; padding: 0 8px; } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/page_template.tsx b/x-pack/plugins/observability_ai_assistant/public/components/page_template.tsx index dbbb2db235f199..94c36f463aaf41 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/page_template.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/page_template.tsx @@ -14,6 +14,7 @@ const pageSectionContentClassName = css` flex-grow: 1; padding-top: 0; padding-bottom: 0; + max-block-size: calc(100vh - 96px); `; export function ObservabilityAIAssistantPageTemplate({ children }: { children: React.ReactNode }) { 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 new file mode 100644 index 00000000000000..aed661f27a220e --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/render_function.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { Message } from '../../common'; +import { useObservabilityAIAssistantChatService } from '../hooks/use_observability_ai_assistant_chat_service'; + +interface Props { + name: string; + arguments: string | undefined; + response: Message['message']; +} + +export function RenderFunction(props: Props) { + const chatService = useObservabilityAIAssistantChatService(); + + return <>{chatService.renderFunction(props.name, props.arguments, props.response)}; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_chat_service_provider.tsx b/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_chat_service_provider.tsx new file mode 100644 index 00000000000000..9f14464461fabe --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_chat_service_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 { ObservabilityAIAssistantChatService } from '../types'; + +export const ObservabilityAIAssistantChatServiceContext = createContext< + ObservabilityAIAssistantChatService | undefined +>(undefined); + +export const ObservabilityAIAssistantChatServiceProvider = + ObservabilityAIAssistantChatServiceContext.Provider; diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/elasticsearch.ts b/x-pack/plugins/observability_ai_assistant/public/functions/elasticsearch.ts index 154f1ac40c20ab..744c9934da3352 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/elasticsearch.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/elasticsearch.ts @@ -21,6 +21,7 @@ export function registerElasticsearchFunction({ name: 'elasticsearch', contexts: ['core'], description: 'Call Elasticsearch APIs on behalf of the user', + descriptionForUser: 'Call Elasticsearch APIs on behalf of the user', parameters: { type: 'object', properties: { @@ -34,7 +35,7 @@ export function registerElasticsearchFunction({ description: 'The path of the Elasticsearch endpoint, including query parameters', }, }, - required: ['method' as const, 'path' as const], + required: ['method', 'path'] as const, }, }, ({ arguments: { method, path, body } }, signal) => { diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/index.ts b/x-pack/plugins/observability_ai_assistant/public/functions/index.ts index 450793554f19ef..2f1b446de8b1b7 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/index.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/index.ts @@ -5,30 +5,68 @@ * 2.0. */ +import dedent from 'dedent'; import type { RegisterContextDefinition, RegisterFunctionDefinition } from '../../common/types'; +import type { ObservabilityAIAssistantPluginStartDependencies } from '../types'; import type { ObservabilityAIAssistantService } from '../types'; import { registerElasticsearchFunction } from './elasticsearch'; import { registerRecallFunction } from './recall'; -import { registerSetupKbFunction } from './setup_kb'; import { registerSummarisationFunction } from './summarise'; -export function registerFunctions({ +export async function registerFunctions({ registerFunction, registerContext, service, + pluginsStart, + signal, }: { registerFunction: RegisterFunctionDefinition; registerContext: RegisterContextDefinition; service: ObservabilityAIAssistantService; + pluginsStart: ObservabilityAIAssistantPluginStartDependencies; + signal: AbortSignal; }) { - registerContext({ - name: 'core', - description: - 'Core functions, like calling Elasticsearch APIs, storing embeddables for instructions or creating base visualisations.', - }); + return service + .callApi('GET /internal/observability_ai_assistant/functions/kb_status', { + signal, + }) + .then((response) => { + const isReady = response.ready; - registerElasticsearchFunction({ service, registerFunction }); - registerSummarisationFunction({ service, registerFunction }); - registerRecallFunction({ service, registerFunction }); - registerSetupKbFunction({ service, registerFunction }); + let description = `You have the ability to call Elasticsearch APIs or create visualisations using Lens.`; + + if (isReady) { + description += `You can use the "summarise" functions to store new information you have learned in a knowledge database. Once you have established that you did not know the answer to a question, and the user gave you this information, it's important that you create a summarisation of what you have learned and store it in the knowledge database. When you create this summarisation, make sure you craft it in a way that can be recalled with a semantic search later. + + Additionally, you can use the "recall" function to retrieve relevant information from the knowledge database. Using the "recall" function will allow you to create a much more tailored and improved user experience. Assume the user has told you before. + + For every question that the user asks, you, as the assistant, must query the knowledge base, no matter how confident you are in your response. + + When the user starts a conversation, you as the assistant, MUST ALWAYS use the "recall" function to make sure you have enough context from the knowledge database to understand their question. DO NOT assume your knowledge is sufficient to answer the question appropriately. Make sure you mention this to the user in your first reply to them. + + You do have access to private data that the user has shared with you in previous conversations. + + Given this ability to store and recall data, it is very important to not make assumptions about the user's data or preferences, but rather, first query the knowledge database, and if nothing relevant comes up, ask the user for clarification. This is very important! They could be storing their data in any field, and in any data stream or index. + + RIGHT: + User: "What is NASA" + Assistant executes recall function + Assistant answers question with data from recall function response + + WRONG: + User: "What is NASA" + Assistant answers question without querying the knowledge`; + registerSummarisationFunction({ service, registerFunction }); + registerRecallFunction({ service, registerFunction }); + } else { + description += `You do not have a working memory. Don't try to recall information via other functions. If the user expects you to remember the previous conversations, tell them they can set up the knowledge base. A banner is available at the top of the conversation to set this up.`; + } + + registerElasticsearchFunction({ service, registerFunction }); + + registerContext({ + name: 'core', + description: dedent(description), + }); + }); } diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts b/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts index 576eba182c659e..a571287acf8b24 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts @@ -22,6 +22,7 @@ export function registerRecallFunction({ contexts: ['core'], description: 'Use this function to recall earlier learnings. Anything you will summarise can be retrieved again later via this function.', + descriptionForUser: 'This function allows the assistant to recall previous learnings.', parameters: { type: 'object', properties: { diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/setup_kb.ts b/x-pack/plugins/observability_ai_assistant/public/functions/setup_kb.ts deleted file mode 100644 index 9cb498e1a7793b..00000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/functions/setup_kb.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Serializable } from '@kbn/utility-types'; -import type { RegisterFunctionDefinition } from '../../common/types'; -import type { ObservabilityAIAssistantService } from '../types'; - -export function registerSetupKbFunction({ - service, - registerFunction, -}: { - service: ObservabilityAIAssistantService; - registerFunction: RegisterFunctionDefinition; -}) { - registerFunction( - { - name: 'setup_kb', - contexts: ['core'], - description: - 'Use this function to set up the knowledge base. ONLY use this if you got an error from the recall or summarise function, or if the user has explicitly requested it. Note that it might take a while (e.g. ten minutes) until the knowledge base is available. Assume it will not be ready for the rest of the current conversation.', - parameters: { - type: 'object', - properties: {}, - }, - }, - ({}, signal) => { - return service - .callApi('POST /internal/observability_ai_assistant/functions/setup_kb', { - signal, - }) - .then((response) => ({ content: response as unknown as Serializable })); - } - ); -} diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/summarise.ts b/x-pack/plugins/observability_ai_assistant/public/functions/summarise.ts index 723839fd6da6fe..3fe55385a74ff3 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/summarise.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/summarise.ts @@ -21,6 +21,8 @@ export function registerSummarisationFunction({ contexts: ['core'], description: 'Use this function to summarise things learned from the conversation. You can score the learnings with a confidence metric, whether it is a correction on a previous learning. An embedding will be created that you can recall later with a semantic search. There is no need to ask the user for permission to store something you have learned, unless you do not feel confident.', + descriptionForUser: + 'This function allows the Elastic Assistant to summarise things from the conversation.', parameters: { type: 'object', properties: { diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_knowledge_base.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_knowledge_base.ts new file mode 100644 index 00000000000000..bcb1725d35109d --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_knowledge_base.ts @@ -0,0 +1,23 @@ +/* + * 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 { UseKnowledgeBaseResult } from '../use_knowledge_base'; + +export function useKnowledgeBase(): UseKnowledgeBaseResult { + return { + install: async () => {}, + isInstalling: false, + status: { + loading: false, + refresh: () => {}, + error: undefined, + value: { + ready: true, + }, + }, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_observability_ai_assistant.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_observability_ai_assistant.ts index 15aa5d0428ab3d..978ae7671d1e5c 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_observability_ai_assistant.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_observability_ai_assistant.ts @@ -5,6 +5,14 @@ * 2.0. */ +const service = { + start: async () => { + return { + getFunctions: [], + }; + }, +}; + export function useObservabilityAIAssistant() { - return {}; + return service; } diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_observability_ai_assistant_chat_service.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_observability_ai_assistant_chat_service.ts new file mode 100644 index 00000000000000..35f34e8950fce3 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_observability_ai_assistant_chat_service.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function useObservabilityAIAssistantChatService() { + return { + getFunctions: () => { + return []; + }, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_abortable_async.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_abortable_async.ts index 393d7cba2ae35c..b8937161dd22ed 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_abortable_async.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_abortable_async.ts @@ -49,11 +49,11 @@ export function useAbortableAsync( } else { setError(undefined); setValue(response); + setLoading(false); } } catch (err) { setValue(undefined); setError(err); - } finally { setLoading(false); } diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.ts index 39e64c5c95666b..b0793c59e1f6b9 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.ts @@ -9,12 +9,19 @@ import { merge, omit } from 'lodash'; import { Dispatch, SetStateAction, useState } from 'react'; import type { Conversation, Message } from '../../common'; import type { ConversationCreateRequest } from '../../common/types'; +import { ObservabilityAIAssistantChatService } from '../types'; import { useAbortableAsync, type AbortableAsyncState } from './use_abortable_async'; import { useKibana } from './use_kibana'; import { useObservabilityAIAssistant } from './use_observability_ai_assistant'; import { createNewConversation } from './use_timeline'; -export function useConversation(conversationId?: string): { +export function useConversation({ + conversationId, + chatService, +}: { + conversationId?: string; + chatService?: ObservabilityAIAssistantChatService; +}): { conversation: AbortableAsyncState; displayedMessages: Message[]; setDisplayedMessages: Dispatch>; @@ -32,7 +39,9 @@ export function useConversation(conversationId?: string): { useAbortableAsync( ({ signal }) => { if (!conversationId) { - const nextConversation = createNewConversation(); + const nextConversation = createNewConversation({ + contexts: chatService?.getContexts() || [], + }); setDisplayedMessages(nextConversation.messages); return nextConversation; } @@ -51,7 +60,7 @@ export function useConversation(conversationId?: string): { throw error; }); }, - [conversationId] + [conversationId, chatService] ); return { diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts index f04cdf68e9c3e1..466708baad8af4 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_json_editor_model.ts @@ -4,16 +4,29 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useMemo } from 'react'; import { monaco } from '@kbn/monaco'; -import { FunctionDefinition } from '../../common/types'; +import { useMemo } from 'react'; +import { createInitializedObject } from '../utils/create_initialized_object'; +import { useObservabilityAIAssistantChatService } from './use_observability_ai_assistant_chat_service'; const { editor, languages, Uri } = monaco; const SCHEMA_URI = 'http://elastic.co/foo.json'; const modelUri = Uri.parse(SCHEMA_URI); -export const useJsonEditorModel = (functionDefinition?: FunctionDefinition) => { +export const useJsonEditorModel = ({ + functionName, + initialJson, +}: { + functionName: string | undefined; + initialJson?: string | undefined; +}) => { + const chatService = useObservabilityAIAssistantChatService(); + + const functionDefinition = chatService + .getFunctions() + .find((func) => func.options.name === functionName); + return useMemo(() => { if (!functionDefinition) { return {}; @@ -21,14 +34,10 @@ export const useJsonEditorModel = (functionDefinition?: FunctionDefinition) => { const schema = { ...functionDefinition.options.parameters }; - const initialJsonString = functionDefinition.options.parameters.properties - ? Object.keys(functionDefinition.options.parameters.properties).reduce( - (acc, curr, index, arr) => { - const val = `${acc} "${curr}": "",\n`; - return index === arr.length - 1 ? `${val}}` : val; - }, - '{\n' - ) + const initialJsonString = initialJson + ? initialJson + : functionDefinition.options.parameters.properties + ? JSON.stringify(createInitializedObject(functionDefinition.options.parameters), null, 4) : ''; languages.json.jsonDefaults.setDiagnosticsOptions({ @@ -49,5 +58,5 @@ export const useJsonEditorModel = (functionDefinition?: FunctionDefinition) => { } return { model, initialJsonString }; - }, [functionDefinition]); + }, [functionDefinition, initialJson]); }; diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_knowledge_base.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_knowledge_base.ts new file mode 100644 index 00000000000000..41b0c70a9f1bff --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_knowledge_base.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useMemo, useState } from 'react'; +import { AbortableAsyncState, useAbortableAsync } from './use_abortable_async'; +import { useKibana } from './use_kibana'; +import { useObservabilityAIAssistant } from './use_observability_ai_assistant'; + +export interface UseKnowledgeBaseResult { + status: AbortableAsyncState<{ + ready: boolean; + error?: any; + deployment_state?: string; + allocation_state?: string; + }>; + isInstalling: boolean; + installError?: Error; + install: () => Promise; +} + +export function useKnowledgeBase(): UseKnowledgeBaseResult { + const { + notifications: { toasts }, + } = useKibana().services; + const service = useObservabilityAIAssistant(); + + const status = useAbortableAsync(({ signal }) => { + return service.callApi('GET /internal/observability_ai_assistant/functions/kb_status', { + signal, + }); + }, []); + + const [isInstalling, setIsInstalling] = useState(false); + + const [installError, setInstallError] = useState(); + + return useMemo( + () => ({ + status, + isInstalling, + installError, + install: () => { + setIsInstalling(true); + return service + .callApi('POST /internal/observability_ai_assistant/functions/setup_kb', { + signal: null, + }) + .then(() => { + status.refresh(); + }) + .catch((error) => { + setInstallError(error); + toasts.addError(error, { + title: i18n.translate('xpack.observabilityAiAssistant.errorSettingUpKnowledgeBase', { + defaultMessage: 'Could not set up Knowledge Base', + }), + }); + }) + .finally(() => { + setIsInstalling(false); + }); + }, + }), + [status, isInstalling, installError, service, toasts] + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_chat_service.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_chat_service.ts new file mode 100644 index 00000000000000..c0e0301741a32c --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_chat_service.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useContext } from 'react'; +import { ObservabilityAIAssistantChatServiceContext } from '../context/observability_ai_assistant_chat_service_provider'; + +export function useObservabilityAIAssistantChatService() { + const services = useContext(ObservabilityAIAssistantChatServiceContext); + + if (!services) { + throw new Error( + 'ObservabilityAIAssistantChatServiceContext not set. Did you wrap your component in ``?' + ); + } + + return services; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.test.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.test.ts index cb608450fd82ec..5469455eab8d51 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.test.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.test.ts @@ -34,7 +34,7 @@ describe('useTimeline', () => { selectConnector: () => {}, connectors: [{ id: 'OpenAI' }] as FindActionResult[], }, - service: {}, + chatService: {}, messages: [], onChatComplete: jest.fn(), onChatUpdate: jest.fn(), @@ -45,9 +45,16 @@ describe('useTimeline', () => { expect(hookResult.result.current.items.length).toEqual(1); expect(hookResult.result.current.items[0]).toEqual({ - canEdit: false, - canRegenerate: false, - canGiveFeedback: false, + display: { + collapsed: false, + hide: false, + }, + actions: { + canCopy: false, + canEdit: false, + canRegenerate: false, + canGiveFeedback: false, + }, role: MessageRole.User, title: 'started a conversation', loading: false, @@ -62,14 +69,12 @@ describe('useTimeline', () => { initialProps: { messages: [ { - '@timestamp': new Date().toISOString(), message: { role: MessageRole.User, content: 'Hello', }, }, { - '@timestamp': new Date().toISOString(), message: { role: MessageRole.Assistant, content: 'Goodbye', @@ -79,7 +84,7 @@ describe('useTimeline', () => { connectors: { selectedConnector: 'foo', }, - service: { + chatService: { chat: () => {}, }, } as unknown as HookProps, @@ -89,9 +94,16 @@ describe('useTimeline', () => { expect(hookResult.result.current.items.length).toEqual(3); expect(hookResult.result.current.items[1]).toEqual({ - canEdit: true, - canRegenerate: false, - canGiveFeedback: false, + actions: { + canCopy: true, + canEdit: true, + canRegenerate: false, + canGiveFeedback: false, + }, + display: { + collapsed: false, + hide: false, + }, role: MessageRole.User, content: 'Hello', loading: false, @@ -100,9 +112,16 @@ describe('useTimeline', () => { }); expect(hookResult.result.current.items[2]).toEqual({ - canEdit: false, - canRegenerate: true, - canGiveFeedback: true, + display: { + collapsed: false, + hide: false, + }, + actions: { + canCopy: true, + canEdit: false, + canRegenerate: true, + canGiveFeedback: true, + }, role: MessageRole.Assistant, content: 'Goodbye', loading: false, @@ -115,11 +134,11 @@ describe('useTimeline', () => { describe('when submitting a new prompt', () => { let subject: Subject; - let props: Omit & { + let props: Omit & { onChatUpdate: jest.MockedFn; onChatComplete: jest.MockedFn; - service: Omit & { - executeFunction: jest.MockedFn; + chatService: Omit & { + executeFunction: jest.MockedFn; }; }; @@ -129,7 +148,7 @@ describe('useTimeline', () => { connectors: { selectedConnector: 'foo', }, - service: { + chatService: { chat: jest.fn().mockImplementation(() => { subject = new BehaviorSubject({ message: { @@ -179,8 +198,10 @@ describe('useTimeline', () => { role: MessageRole.Assistant, content: '', loading: true, - canRegenerate: false, - canGiveFeedback: false, + actions: { + canRegenerate: false, + canGiveFeedback: false, + }, }); expect(hookResult.result.current.items.length).toBe(3); @@ -189,8 +210,10 @@ describe('useTimeline', () => { role: MessageRole.Assistant, content: '', loading: true, - canRegenerate: false, - canGiveFeedback: false, + actions: { + canRegenerate: false, + canGiveFeedback: false, + }, }); act(() => { @@ -201,8 +224,10 @@ describe('useTimeline', () => { role: MessageRole.Assistant, content: 'Goodbye', loading: true, - canRegenerate: false, - canGiveFeedback: false, + actions: { + canRegenerate: false, + canGiveFeedback: false, + }, }); act(() => { @@ -215,8 +240,10 @@ describe('useTimeline', () => { role: MessageRole.Assistant, content: 'Goodbye', loading: false, - canRegenerate: true, - canGiveFeedback: true, + actions: { + canRegenerate: true, + canGiveFeedback: true, + }, }); }); @@ -240,9 +267,16 @@ describe('useTimeline', () => { expect(hookResult.result.current.items.length).toBe(3); expect(hookResult.result.current.items[2]).toEqual({ - canEdit: false, - canRegenerate: true, - canGiveFeedback: false, + actions: { + canEdit: false, + canRegenerate: true, + canGiveFeedback: false, + canCopy: true, + }, + display: { + collapsed: false, + hide: false, + }, content: 'My partial', id: expect.any(String), loading: false, @@ -262,9 +296,16 @@ describe('useTimeline', () => { it('updates the last item in the array to be loading', () => { expect(hookResult.result.current.items[2]).toEqual({ - canEdit: false, - canRegenerate: false, - canGiveFeedback: false, + display: { + hide: false, + collapsed: false, + }, + actions: { + canCopy: true, + canEdit: false, + canRegenerate: false, + canGiveFeedback: false, + }, content: '', id: expect.any(String), loading: true, @@ -288,9 +329,16 @@ describe('useTimeline', () => { expect(hookResult.result.current.items.length).toBe(3); expect(hookResult.result.current.items[2]).toEqual({ - canEdit: false, - canRegenerate: false, - canGiveFeedback: false, + actions: { + canCopy: true, + canEdit: false, + canRegenerate: false, + canGiveFeedback: false, + }, + display: { + collapsed: false, + hide: false, + }, content: '', id: expect.any(String), loading: true, @@ -308,9 +356,16 @@ describe('useTimeline', () => { expect(hookResult.result.current.items.length).toBe(3); expect(hookResult.result.current.items[2]).toEqual({ - canEdit: false, - canRegenerate: true, - canGiveFeedback: true, + display: { + collapsed: false, + hide: false, + }, + actions: { + canCopy: true, + canEdit: false, + canRegenerate: true, + canGiveFeedback: true, + }, content: 'Regenerated', id: expect.any(String), loading: false, @@ -340,7 +395,7 @@ describe('useTimeline', () => { subject.complete(); }); - props.service.executeFunction.mockResolvedValueOnce({ + props.chatService.executeFunction.mockResolvedValueOnce({ content: { message: 'my-response', }, @@ -364,7 +419,7 @@ describe('useTimeline', () => { expect(props.onChatComplete).not.toHaveBeenCalled(); - expect(props.service.executeFunction).toHaveBeenCalledWith( + expect(props.chatService.executeFunction).toHaveBeenCalledWith( 'my_function', '{}', expect.any(Object) diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts index ffe247ef5d6f9b..00566ce3bcec61 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts @@ -7,21 +7,31 @@ import { AbortError } from '@kbn/kibana-utils-plugin/common'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import { last } from 'lodash'; import { useEffect, useMemo, useRef, useState } from 'react'; import type { Subscription } from 'rxjs'; -import { MessageRole, type ConversationCreateRequest, type Message } from '../../common/types'; +import { + ContextDefinition, + MessageRole, + type ConversationCreateRequest, + type Message, +} from '../../common/types'; import type { ChatPromptEditorProps } from '../components/chat/chat_prompt_editor'; import type { ChatTimelineProps } from '../components/chat/chat_timeline'; import { EMPTY_CONVERSATION_TITLE } from '../i18n'; -import { getSystemMessage } from '../service/get_system_message'; -import type { ObservabilityAIAssistantService, PendingMessage } from '../types'; +import { getAssistantSetupMessage } from '../service/get_assistant_setup_message'; +import type { ObservabilityAIAssistantChatService, PendingMessage } from '../types'; import { getTimelineItemsfromConversation } from '../utils/get_timeline_items_from_conversation'; import type { UseGenAIConnectorsResult } from './use_genai_connectors'; -export function createNewConversation(): ConversationCreateRequest { +export function createNewConversation({ + contexts, +}: { + contexts: ContextDefinition[]; +}): ConversationCreateRequest { return { '@timestamp': new Date().toISOString(), - messages: [getSystemMessage()], + messages: [getAssistantSetupMessage({ contexts })], conversation: { title: EMPTY_CONVERSATION_TITLE, }, @@ -41,14 +51,14 @@ export function useTimeline({ messages, connectors, currentUser, - service, + chatService, onChatUpdate, onChatComplete, }: { messages: Message[]; connectors: UseGenAIConnectorsResult; currentUser?: Pick; - service: ObservabilityAIAssistantService; + chatService: ObservabilityAIAssistantChatService; onChatUpdate: (messages: Message[]) => void; onChatComplete: (messages: Message[]) => void; }): UseTimelineResult { @@ -57,12 +67,15 @@ export function useTimeline({ const hasConnector = !!connectorId; const conversationItems = useMemo(() => { - return getTimelineItemsfromConversation({ + const items = getTimelineItemsfromConversation({ messages, currentUser, hasConnector, + chatService, }); - }, [messages, currentUser, hasConnector]); + + return items; + }, [messages, currentUser, hasConnector, chatService]); const [subscription, setSubscription] = useState(); @@ -73,7 +86,7 @@ export function useTimeline({ function chat(nextMessages: Message[]): Promise { const controller = new AbortController(); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { if (!connectorId) { reject(new Error('Can not add a message without a connector')); return; @@ -81,7 +94,18 @@ export function useTimeline({ onChatUpdate(nextMessages); - const response$ = service.chat({ messages: nextMessages, connectorId }); + const lastMessage = last(nextMessages); + + if (lastMessage?.message.function_call?.name) { + // the user has edited a function suggestion, no need to talk to + resolve(undefined); + return; + } + + const response$ = chatService!.chat({ + messages: nextMessages, + connectorId, + }); let pendingMessageLocal = pendingMessage; @@ -101,31 +125,35 @@ export function useTimeline({ return nextSubscription; }); }).then(async (reply) => { - if (reply.error) { + if (reply?.error) { return nextMessages; } - if (reply.aborted) { + if (reply?.aborted) { return nextMessages; } setPendingMessage(undefined); - const messagesAfterChat = nextMessages.concat({ - '@timestamp': new Date().toISOString(), - message: { - ...reply.message, - }, - }); + const messagesAfterChat = reply + ? nextMessages.concat({ + '@timestamp': new Date().toISOString(), + message: { + ...reply.message, + }, + }) + : nextMessages; onChatUpdate(messagesAfterChat); - if (reply?.message.function_call?.name) { - const name = reply.message.function_call.name; + const lastMessage = last(messagesAfterChat); + + if (lastMessage?.message.function_call?.name) { + const name = lastMessage.message.function_call.name; try { - const message = await service.executeFunction( + const message = await chatService!.executeFunction( name, - reply.message.function_call.arguments, + lastMessage.message.function_call.arguments, controller.signal ); @@ -133,8 +161,8 @@ export function useTimeline({ messagesAfterChat.concat({ '@timestamp': new Date().toISOString(), message: { - role: MessageRole.User, name, + role: MessageRole.User, content: JSON.stringify(message.content), data: JSON.stringify(message.data), }, @@ -149,7 +177,7 @@ export function useTimeline({ name, content: JSON.stringify({ message: error.toString(), - ...error.body, + error: error.body, }), }, }) @@ -165,16 +193,23 @@ export function useTimeline({ if (pendingMessage) { return conversationItems.concat({ id: '', - canEdit: false, - canRegenerate: pendingMessage.aborted || !!pendingMessage.error, - canGiveFeedback: false, - title: '', - role: pendingMessage.message.role, + actions: { + canCopy: true, + canEdit: false, + canGiveFeedback: false, + canRegenerate: pendingMessage.aborted || !!pendingMessage.error, + }, + display: { + collapsed: false, + hide: pendingMessage.message.role === MessageRole.System, + }, content: pendingMessage.message.content, - loading: !pendingMessage.aborted && !pendingMessage.error, - function_call: pendingMessage.message.function_call, currentUser, error: pendingMessage.error, + function_call: pendingMessage.message.function_call, + loading: !pendingMessage.aborted && !pendingMessage.error, + role: pendingMessage.message.role, + title: '', }); } @@ -189,7 +224,12 @@ export function useTimeline({ return { items, - onEdit: (item, content) => {}, + onEdit: async (item, newMessage) => { + const indexOf = items.indexOf(item); + const sliced = messages.slice(0, indexOf - 1); + const nextMessages = await chat(sliced.concat(newMessage)); + onChatComplete(nextMessages); + }, onFeedback: (item, feedback) => {}, onRegenerate: (item) => { const indexOf = items.indexOf(item); diff --git a/x-pack/plugins/observability_ai_assistant/public/plugin.tsx b/x-pack/plugins/observability_ai_assistant/public/plugin.tsx index 4c0b1841315574..efc80a1cd22a08 100644 --- a/x-pack/plugins/observability_ai_assistant/public/plugin.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/plugin.tsx @@ -17,12 +17,6 @@ import { i18n } from '@kbn/i18n'; import type { Logger } from '@kbn/logging'; import React from 'react'; import ReactDOM from 'react-dom'; -import type { - ContextRegistry, - FunctionRegistry, - RegisterContextDefinition, - RegisterFunctionDefinition, -} from '../common/types'; import { registerFunctions } from './functions'; import { createService } from './service/create_service'; import type { @@ -101,35 +95,22 @@ export class ObservabilityAIAssistantPlugin coreStart: CoreStart, pluginsStart: ObservabilityAIAssistantPluginStartDependencies ): ObservabilityAIAssistantPluginStart { - const contextRegistry: ContextRegistry = new Map(); - const functionRegistry: FunctionRegistry = new Map(); - const service = (this.service = createService({ coreStart, securityStart: pluginsStart.security, - contextRegistry, - functionRegistry, enabled: coreStart.application.capabilities.observabilityAIAssistant.show === true, })); - const registerContext: RegisterContextDefinition = (context) => { - contextRegistry.set(context.name, context); - }; - - const registerFunction: RegisterFunctionDefinition = (def, respond, render) => { - functionRegistry.set(def.name, { options: def, respond, render }); - }; - - registerFunctions({ - registerContext, - registerFunction, - service, + service.register(({ signal, registerContext, registerFunction }) => { + return registerFunctions({ + service, + signal, + pluginsStart, + registerContext, + registerFunction, + }); }); - return { - ...service, - registerContext, - registerFunction, - }; + return service; } } 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 2092390aedb909..3573a819da8422 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 @@ -10,12 +10,14 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo, useState } from 'react'; import { ChatBody } from '../../components/chat/chat_body'; import { ConversationList } from '../../components/chat/conversation_list'; +import { ObservabilityAIAssistantChatServiceProvider } from '../../context/observability_ai_assistant_chat_service_provider'; import { useAbortableAsync } from '../../hooks/use_abortable_async'; import { useConfirmModal } from '../../hooks/use_confirm_modal'; import { useConversation } from '../../hooks/use_conversation'; import { useCurrentUser } from '../../hooks/use_current_user'; import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; import { useKibana } from '../../hooks/use_kibana'; +import { useKnowledgeBase } from '../../hooks/use_knowledge_base'; import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant'; import { useObservabilityAIAssistantParams } from '../../hooks/use_observability_ai_assistant_params'; import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; @@ -33,6 +35,8 @@ const chatBodyContainerClassNameWithError = css` export function ConversationView() { const connectors = useGenAIConnectors(); + const knowledgeBase = useKnowledgeBase(); + const currentUser = useCurrentUser(); const service = useObservabilityAIAssistant(); @@ -59,10 +63,19 @@ export function ConversationView() { const [isUpdatingList, setIsUpdatingList] = useState(false); + const chatService = useAbortableAsync( + ({ signal }) => { + return service.start({ signal }); + }, + [service] + ); + const conversationId = 'conversationId' in path ? path.conversationId : undefined; - const { conversation, displayedMessages, setDisplayedMessages, save } = - useConversation(conversationId); + const { conversation, displayedMessages, setDisplayedMessages, save } = useConversation({ + conversationId, + chatService: chatService.value, + }); const conversations = useAbortableAsync( ({ signal }) => { @@ -173,6 +186,7 @@ export function ConversationView() { }); }} /> + ) : null} - {conversation.loading ? : null} - {!conversation.error && conversation.value ? ( - { - save(messages) - .then((nextConversation) => { - conversations.refresh(); - if (!conversationId) { - navigateToConversation(nextConversation.conversation.id); - } - }) - .catch(() => {}); - }} - onChatUpdate={(messages) => { - setDisplayedMessages(messages); - }} - /> + {chatService.loading || conversation.loading ? ( + + + + + + + ) : null} + {!conversation.error && conversation.value && chatService.value ? ( + + { + save(messages) + .then((nextConversation) => { + conversations.refresh(); + if (!conversationId) { + navigateToConversation(nextConversation.conversation.id); + } + }) + .catch(() => {}); + }} + onChatUpdate={(messages) => { + setDisplayedMessages(messages); + }} + /> + ) : null} diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_service.test.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.test.ts similarity index 81% rename from x-pack/plugins/observability_ai_assistant/public/service/create_service.test.ts rename to x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.test.ts index 4e9eb3d61d13b5..49fc3eeb5e56ea 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_service.test.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.test.ts @@ -4,19 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { CoreStart, HttpFetchOptions } from '@kbn/core/public'; -import { ReadableStream } from 'stream/web'; -import type { AuthenticatedUser } from '@kbn/security-plugin/common'; -import type { ObservabilityAIAssistantService } from '../types'; -import { createService } from './create_service'; -import { SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { HttpFetchOptions } from '@kbn/core/public'; import { lastValueFrom } from 'rxjs'; +import { ReadableStream } from 'stream/web'; +import type { ObservabilityAIAssistantChatService } from '../types'; +import { createChatService } from './create_chat_service'; -describe('createService', () => { +describe('createChatService', () => { describe('chat', () => { - let service: ObservabilityAIAssistantService; + let service: ObservabilityAIAssistantChatService; - const httpPostSpy = jest.fn(); + const clientSpy = jest.fn(); function respondWithChunks({ chunks, status = 200 }: { status?: number; chunks: string[] }) { const response = { @@ -33,33 +31,23 @@ describe('createService', () => { }, }; - httpPostSpy.mockResolvedValueOnce(response); + clientSpy.mockResolvedValueOnce(response); } function chat() { return service.chat({ messages: [], connectorId: '' }); } - beforeEach(() => { - service = createService({ - coreStart: { - http: { - post: httpPostSpy, - }, - } as unknown as CoreStart, - securityStart: { - authc: { - getCurrentUser: () => Promise.resolve({ username: 'elastic' } as AuthenticatedUser), - }, - } as unknown as SecurityPluginStart, - contextRegistry: new Map(), - functionRegistry: new Map(), - enabled: true, + beforeEach(async () => { + service = await createChatService({ + client: clientSpy, + registrations: [], + signal: new AbortController().signal, }); }); afterEach(() => { - httpPostSpy.mockReset(); + clientSpy.mockReset(); }); it('correctly parses a stream of JSON lines', async () => { @@ -169,7 +157,7 @@ describe('createService', () => { }); it('cancels a running http request when aborted', async () => { - httpPostSpy.mockImplementationOnce((endpoint: string, options: HttpFetchOptions) => { + clientSpy.mockImplementationOnce((endpoint: string, options: HttpFetchOptions) => { options.signal?.addEventListener('abort', () => { expect(options.signal?.aborted).toBeTruthy(); }); 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 new file mode 100644 index 00000000000000..bb3d3111b43be3 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts @@ -0,0 +1,226 @@ +/* + * 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 { IncomingMessage } from 'http'; +import { cloneDeep, pick } from 'lodash'; +import { + BehaviorSubject, + map, + filter as rxJsFilter, + scan, + catchError, + of, + concatMap, + shareReplay, + finalize, + delay, +} from 'rxjs'; +import { HttpResponse } from '@kbn/core/public'; +import { AbortError } from '@kbn/kibana-utils-plugin/common'; +import { + type RegisterContextDefinition, + type RegisterFunctionDefinition, + Message, + MessageRole, + ContextRegistry, + FunctionRegistry, +} from '../../common/types'; +import { ObservabilityAIAssistantAPIClient } from '../api'; +import type { + ChatRegistrationFunction, + CreateChatCompletionResponseChunk, + ObservabilityAIAssistantChatService, + PendingMessage, +} from '../types'; +import { readableStreamReaderIntoObservable } from '../utils/readable_stream_reader_into_observable'; + +export async function createChatService({ + signal: setupAbortSignal, + registrations, + client, +}: { + signal: AbortSignal; + registrations: ChatRegistrationFunction[]; + client: ObservabilityAIAssistantAPIClient; +}): Promise { + const contextRegistry: ContextRegistry = new Map(); + const functionRegistry: FunctionRegistry = new Map(); + + const registerContext: RegisterContextDefinition = (context) => { + contextRegistry.set(context.name, context); + }; + + const registerFunction: RegisterFunctionDefinition = (def, respond, render) => { + functionRegistry.set(def.name, { options: def, respond, render }); + }; + + const getContexts: ObservabilityAIAssistantChatService['getContexts'] = () => { + return Array.from(contextRegistry.values()); + }; + const getFunctions: ObservabilityAIAssistantChatService['getFunctions'] = ({ + contexts, + filter, + } = {}) => { + const allFunctions = Array.from(functionRegistry.values()); + + return contexts || filter + ? allFunctions.filter((fn) => { + const matchesContext = + !contexts || fn.options.contexts.some((context) => contexts.includes(context)); + const matchesFilter = + !filter || fn.options.name.includes(filter) || fn.options.description.includes(filter); + + return matchesContext && matchesFilter; + }) + : allFunctions; + }; + + await Promise.all( + registrations.map((fn) => fn({ signal: setupAbortSignal, registerContext, registerFunction })) + ); + + return { + executeFunction: async (name, args, signal) => { + const fn = functionRegistry.get(name); + + if (!fn) { + throw new Error(`Function ${name} not found`); + } + + const parsedArguments = args ? JSON.parse(args) : {}; + + // validate + + return await fn.respond({ arguments: parsedArguments }, signal); + }, + renderFunction: (name, args, response) => { + const fn = functionRegistry.get(name); + + if (!fn) { + throw new Error(`Function ${name} not found`); + } + + const parsedArguments = args ? JSON.parse(args) : {}; + + const parsedResponse = { + content: JSON.parse(response.content ?? '{}'), + data: JSON.parse(response.data ?? '{}'), + }; + + // validate + + return fn.render?.({ response: parsedResponse, arguments: parsedArguments }); + }, + getContexts, + getFunctions, + hasRenderFunction: (name: string) => { + return !!getFunctions().find((fn) => fn.options.name === name)?.render; + }, + chat({ connectorId, messages }: { connectorId: string; messages: Message[] }) { + const subject = new BehaviorSubject({ + message: { + role: MessageRole.Assistant, + }, + }); + + const contexts = ['core', 'apm']; + + const functions = getFunctions({ contexts }); + + const controller = new AbortController(); + + client('POST /internal/observability_ai_assistant/chat', { + params: { + body: { + messages, + connectorId, + functions: functions.map((fn) => pick(fn.options, 'name', 'description', 'parameters')), + }, + }, + signal: controller.signal, + asResponse: true, + rawResponse: true, + }) + .then((_response) => { + const response = _response as unknown as HttpResponse; + + const status = response.response?.status; + + if (!status || status >= 400) { + throw new Error(response.response?.statusText || 'Unexpected error'); + } + + const reader = response.response.body?.getReader(); + + if (!reader) { + throw new Error('Could not get reader from response'); + } + + const subscription = readableStreamReaderIntoObservable(reader) + .pipe( + map((line) => line.substring(6)), + rxJsFilter((line) => !!line && line !== '[DONE]'), + map((line) => JSON.parse(line) as CreateChatCompletionResponseChunk), + rxJsFilter((line) => line.object === 'chat.completion.chunk'), + scan( + (acc, { choices }) => { + acc.message.content += choices[0].delta.content ?? ''; + acc.message.function_call.name += choices[0].delta.function_call?.name ?? ''; + acc.message.function_call.arguments += + choices[0].delta.function_call?.arguments ?? ''; + return cloneDeep(acc); + }, + { + message: { + content: '', + function_call: { + name: '', + arguments: '', + trigger: MessageRole.Assistant as const, + }, + role: MessageRole.Assistant, + }, + } + ), + catchError((error) => + of({ + ...subject.value, + error, + aborted: error instanceof AbortError || controller.signal.aborted, + }) + ) + ) + .subscribe(subject); + + controller.signal.addEventListener('abort', () => { + subscription.unsubscribe(); + subject.next({ + ...subject.value, + aborted: true, + }); + subject.complete(); + }); + }) + .catch((err) => { + subject.next({ + ...subject.value, + aborted: false, + error: err, + }); + subject.complete(); + }); + + return subject.pipe( + concatMap((value) => of(value).pipe(delay(50))), + shareReplay(1), + finalize(() => { + controller.abort(); + }) + ); + }, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts index c977b8c3137c6b..0978457ffe28cd 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts @@ -5,200 +5,37 @@ * 2.0. */ -import type { CoreStart, HttpResponse } from '@kbn/core/public'; -import { AbortError } from '@kbn/kibana-utils-plugin/common'; +import type { CoreStart } from '@kbn/core/public'; import { SecurityPluginStart } from '@kbn/security-plugin/public'; -import { IncomingMessage } from 'http'; -import { cloneDeep } from 'lodash'; -import { - BehaviorSubject, - catchError, - concatMap, - delay, - filter as rxJsFilter, - finalize, - map, - of, - scan, - shareReplay, -} from 'rxjs'; -import type { Message } from '../../common'; -import { ContextRegistry, FunctionRegistry, MessageRole } from '../../common/types'; import { createCallObservabilityAIAssistantAPI } from '../api'; -import type { - CreateChatCompletionResponseChunk, - ObservabilityAIAssistantService, - PendingMessage, -} from '../types'; -import { readableStreamReaderIntoObservable } from '../utils/readable_stream_reader_into_observable'; +import type { ChatRegistrationFunction, ObservabilityAIAssistantService } from '../types'; +import { createChatService } from './create_chat_service'; export function createService({ coreStart, securityStart, - functionRegistry, - contextRegistry, enabled, }: { coreStart: CoreStart; securityStart: SecurityPluginStart; - functionRegistry: FunctionRegistry; - contextRegistry: ContextRegistry; enabled: boolean; -}): ObservabilityAIAssistantService { +}): ObservabilityAIAssistantService & { register: (fn: ChatRegistrationFunction) => void } { const client = createCallObservabilityAIAssistantAPI(coreStart); - const getContexts: ObservabilityAIAssistantService['getContexts'] = () => { - return Array.from(contextRegistry.values()); - }; - const getFunctions: ObservabilityAIAssistantService['getFunctions'] = ({ - contexts, - filter, - } = {}) => { - const allFunctions = Array.from(functionRegistry.values()); - - return contexts || filter - ? allFunctions.filter((fn) => { - const matchesContext = - !contexts || fn.options.contexts.some((context) => contexts.includes(context)); - const matchesFilter = - !filter || fn.options.name.includes(filter) || fn.options.description.includes(filter); - - return matchesContext && matchesFilter; - }) - : allFunctions; - }; + const registrations: ChatRegistrationFunction[] = []; return { isEnabled: () => { return enabled; }, - chat({ connectorId, messages }: { connectorId: string; messages: Message[] }) { - const subject = new BehaviorSubject({ - message: { - role: MessageRole.Assistant, - }, - }); - - const contexts = ['core']; - - const functions = getFunctions({ contexts }); - - const controller = new AbortController(); - - client('POST /internal/observability_ai_assistant/chat', { - params: { - body: { - messages, - connectorId, - functions: functions.map((fn) => fn.options), - }, - }, - signal: controller.signal, - asResponse: true, - rawResponse: true, - }) - .then((_response) => { - const response = _response as unknown as HttpResponse; - - const status = response.response?.status; - - if (!status || status >= 400) { - throw new Error(response.response?.statusText || 'Unexpected error'); - } - - const reader = response.response.body?.getReader(); - - if (!reader) { - throw new Error('Could not get reader from response'); - } - - const subscription = readableStreamReaderIntoObservable(reader) - .pipe( - map((line) => line.substring(6)), - rxJsFilter((line) => !!line && line !== '[DONE]'), - map((line) => JSON.parse(line) as CreateChatCompletionResponseChunk), - rxJsFilter((line) => line.object === 'chat.completion.chunk'), - scan( - (acc, { choices }) => { - acc.message.content += choices[0].delta.content ?? ''; - acc.message.function_call.name += choices[0].delta.function_call?.name ?? ''; - acc.message.function_call.arguments += - choices[0].delta.function_call?.arguments ?? ''; - return cloneDeep(acc); - }, - { - message: { - content: '', - function_call: { - name: '', - arguments: '', - trigger: MessageRole.Assistant as const, - }, - role: MessageRole.Assistant, - }, - } - ), - catchError((error) => - of({ - ...subject.value, - error, - aborted: error instanceof AbortError || controller.signal.aborted, - }) - ) - ) - .subscribe(subject); - - controller.signal.addEventListener('abort', () => { - subscription.unsubscribe(); - subject.next({ - ...subject.value, - aborted: true, - }); - subject.complete(); - }); - }) - .catch((err) => { - subject.next({ - ...subject.value, - aborted: false, - error: err, - }); - subject.complete(); - }); - - return subject.pipe( - concatMap((value) => of(value).pipe(delay(50))), - shareReplay(1), - finalize(() => { - controller.abort(); - }) - ); + register: (fn) => { + registrations.push(fn); }, - callApi: client, - getCurrentUser: () => securityStart.authc.getCurrentUser(), - getContexts, - getFunctions, - executeFunction: async (name, args, signal) => { - const fn = functionRegistry.get(name); - - if (!fn) { - throw new Error(`Function ${name} not found`); - } - - const parsedArguments = args ? JSON.parse(args) : {}; - - // validate - - return await fn.respond({ arguments: parsedArguments }, signal); + start: async ({ signal }) => { + return await createChatService({ client, signal, registrations }); }, - renderFunction: (name, response) => { - const fn = functionRegistry.get(name); - - if (!fn) { - throw new Error(`Function ${name} not found`); - } - return fn.render?.({ response }); - }, + callApi: client, + getCurrentUser: () => securityStart.authc.getCurrentUser(), }; } diff --git a/x-pack/plugins/observability_ai_assistant/public/service/get_assistant_setup_message.ts b/x-pack/plugins/observability_ai_assistant/public/service/get_assistant_setup_message.ts new file mode 100644 index 00000000000000..4e4a9e7ed25e66 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/service/get_assistant_setup_message.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 dedent from 'dedent'; +import { MessageRole } from '../../common'; +import { ContextDefinition } from '../../common/types'; + +export function getAssistantSetupMessage({ contexts }: { contexts: ContextDefinition[] }) { + return { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.System as const, + content: [ + dedent( + `You are a helpful assistant for Elastic Observability. Your goal is to help the Elastic Observability users to quickly assess what is happening in their observed systems. You can help them visualise and analyze data, investigate their systems, perform root cause analysis or identify optimisation opportunities` + ), + ] + .concat(contexts.map((context) => context.description)) + .join('\n'), + }, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/service/get_system_message.ts b/x-pack/plugins/observability_ai_assistant/public/service/get_system_message.ts deleted file mode 100644 index 5193d98d9f6b6f..00000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/service/get_system_message.ts +++ /dev/null @@ -1,47 +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 dedent from 'dedent'; -import { MessageRole } from '../../common'; - -export function getSystemMessage() { - return { - '@timestamp': new Date().toISOString(), - message: { - role: MessageRole.System as const, - content: - dedent(`You are a helpful assistant for Elastic Observability. Your goal is to help the Elastic Observability users to quickly assess what is happening in their observed systems. You can help them visualise and analyze data, investigate their systems, perform root cause analysis or identify optimisation opportunities. - - You can use the "summarise" functions to store new information you have learned in a knowledge database. Once you have established that you did not know the answer to a question, and the user gave you this information, it's important that you create a summarisation of what you have learned and store it in the knowledge database. When you create this summarisation, make sure you craft it in a way that can be recalled with a semantic search later. - - Additionally, you can use the "recall" function to retrieve relevant information from the knowledge database. Using the "recall" function will allow you to create a much more tailored and improved user experience. Assume the user has told you before. - - For every question that the user asks, you, as the assistant, must query the knowledge base, no matter how confident you are in your response. - - When the user starts a conversation, you as the assistant, MUST ALWAYS use the "recall" function to make sure you have enough context from the knowledge database to understand their question. DO NOT assume your knowledge is sufficient to answer the question appropriately. Make sure you mention this to the user in your first reply to them. - - You do have access to private data that the user has shared with you in previous conversations. - - Given this ability to store and recall data, it is very important to not make assumptions about the user's data or preferences, but rather, first query the knowledge database, and if nothing relevant comes up, ask the user for clarification. This is very important! They could be storing their data in any field, and in any data stream or index. - - RIGHT: - User: "What is NASA" - Assistant executes recall function - Assistant answers question with data from recall function response - - WRONG: - User: "What is NASA" - Assistant answers question without querying the knowledge - - You should autonomously execute these functions - do not wait on the user's permission, but be proactive. - - Note that any visualisations will be displayed ABOVE your textual response, not below. - - Feel free to use Markdown in your replies, especially for code and query statements.`), - }, - }; -} diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index 02e626185fdb7d..da919295f00e61 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -49,13 +49,11 @@ export interface PendingMessage { error?: any; } -export interface ObservabilityAIAssistantService { - isEnabled: () => boolean; +export interface ObservabilityAIAssistantChatService { chat: (options: { messages: Message[]; connectorId: string }) => Observable; - callApi: ObservabilityAIAssistantAPIClient; - getCurrentUser: () => Promise; getContexts: () => ContextDefinition[]; getFunctions: (options?: { contexts?: string[]; filter?: string }) => FunctionDefinition[]; + hasRenderFunction: (name: string) => boolean; executeFunction: ( name: string, args: string | undefined, @@ -63,13 +61,26 @@ export interface ObservabilityAIAssistantService { ) => Promise<{ content?: Serializable; data?: Serializable }>; renderFunction: ( name: string, - response: { data?: Serializable; content?: Serializable } + args: string | undefined, + response: { data?: string; content?: string } ) => React.ReactNode; } -export interface ObservabilityAIAssistantPluginStart extends ObservabilityAIAssistantService { - registerContext: RegisterContextDefinition; +export type ChatRegistrationFunction = ({}: { + signal: AbortSignal; registerFunction: RegisterFunctionDefinition; + registerContext: RegisterContextDefinition; +}) => Promise; + +export interface ObservabilityAIAssistantService { + isEnabled: () => boolean; + callApi: ObservabilityAIAssistantAPIClient; + getCurrentUser: () => Promise; + start: ({}: { signal: AbortSignal }) => Promise; +} + +export interface ObservabilityAIAssistantPluginStart extends ObservabilityAIAssistantService { + register: (fn: ChatRegistrationFunction) => void; } export interface ObservabilityAIAssistantPluginSetup {} diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts b/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts index 597d28499f9d24..ed318397de73c4 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts +++ b/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts @@ -5,26 +5,39 @@ * 2.0. */ -import { uniqueId } from 'lodash'; +import { merge, uniqueId } from 'lodash'; import { MessageRole, Conversation, FunctionDefinition } from '../../common/types'; import { ChatTimelineItem } from '../components/chat/chat_timeline'; -import { getSystemMessage } from '../service/get_system_message'; +import { getAssistantSetupMessage } from '../service/get_assistant_setup_message'; -type ChatItemBuildProps = Partial & Pick; +type ChatItemBuildProps = Omit, 'actions' | 'display' | 'currentUser'> & { + actions?: Partial; + display?: Partial; + currentUser?: Partial; +} & Pick; export function buildChatItem(params: ChatItemBuildProps): ChatTimelineItem { - return { - id: uniqueId(), - title: '', - canEdit: false, - canGiveFeedback: false, - canRegenerate: params.role === MessageRole.User, - currentUser: { - username: 'elastic', + return merge( + { + id: uniqueId(), + title: '', + actions: { + canCopy: true, + canEdit: false, + canGiveFeedback: false, + canRegenerate: params.role === MessageRole.Assistant, + }, + display: { + collapsed: false, + hide: false, + }, + currentUser: { + username: 'elastic', + }, + loading: false, }, - loading: false, - ...params, - }; + params + ); } export function buildSystemChatItem(params?: Omit) { @@ -38,7 +51,12 @@ export function buildChatInitItem() { return buildChatItem({ role: MessageRole.User, title: 'started a conversation', - canRegenerate: false, + actions: { + canEdit: false, + canCopy: true, + canGiveFeedback: false, + canRegenerate: false, + }, }); } @@ -46,7 +64,12 @@ export function buildUserChatItem(params?: Omit) { return buildChatItem({ role: MessageRole.User, content: "What's a function?", - canEdit: true, + actions: { + canCopy: true, + canEdit: true, + canGiveFeedback: false, + canRegenerate: true, + }, ...params, }); } @@ -56,8 +79,12 @@ export function buildAssistantChatItem(params?: Omit role: MessageRole.Assistant, content: `In computer programming and mathematics, a function is a fundamental concept that represents a relationship between input values and output values. It takes one or more input values (also known as arguments or parameters) and processes them to produce a result, which is the output of the function. The input values are passed to the function, and the function performs a specific set of operations or calculations on those inputs to produce the desired output. A function is often defined with a name, which serves as an identifier to call and use the function in the code. It can be thought of as a reusable block of code that can be executed whenever needed, and it helps in organizing code and making it more modular and maintainable.`, - canRegenerate: true, - canGiveFeedback: true, + actions: { + canCopy: true, + canEdit: false, + canRegenerate: true, + canGiveFeedback: true, + }, ...params, }); } @@ -92,7 +119,7 @@ export function buildConversation(params?: Partial) { title: '', last_updated: '', }, - messages: [getSystemMessage()], + messages: [getAssistantSetupMessage({ contexts: [] })], labels: {}, numeric_labels: {}, namespace: '', @@ -106,6 +133,7 @@ export function buildFunction(): FunctionDefinition { name: 'elasticsearch', contexts: ['core'], description: 'Call Elasticsearch APIs on behalf of the user', + descriptionForUser: 'Call Elasticsearch APIs on behalf of the user', parameters: { type: 'object', properties: { @@ -135,6 +163,7 @@ export function buildFunctionServiceSummary(): FunctionDefinition { contexts: ['core'], description: 'Gets a summary of a single service, including: the language, service version, deployments, infrastructure, alerting, etc. ', + descriptionForUser: 'Get a summary for a single service.', parameters: { type: 'object', }, diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/create_initialized_object.test.ts b/x-pack/plugins/observability_ai_assistant/public/utils/create_initialized_object.test.ts new file mode 100644 index 00000000000000..5dcaa01af1ed05 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/utils/create_initialized_object.test.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createInitializedObject } from './create_initialized_object'; + +describe('createInitializedObject', () => { + it('should return an object with properties of type "string" set to a default value of ""', () => { + expect( + createInitializedObject({ + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + required: ['foo'], + }) + ).toStrictEqual({ foo: '' }); + }); + + it('should return an object with properties of type "number" set to a default value of 1', () => { + expect( + createInitializedObject({ + type: 'object', + properties: { + foo: { + type: 'number', + }, + }, + required: ['foo'], + }) + ).toStrictEqual({ foo: 1 }); + }); + + it('should return an object with properties of type "array" set to a default value of []', () => { + expect( + createInitializedObject({ + type: 'object', + properties: { + foo: { + type: 'array', + }, + }, + required: ['foo'], + }) + ).toStrictEqual({ foo: [] }); + }); + + it('should return an object with default values for properties that are required', () => { + expect( + createInitializedObject({ + type: 'object', + properties: { + requiredProperty: { + type: 'string', + }, + notRequiredProperty: { + type: 'string', + }, + }, + required: ['requiredProperty'], + }) + ).toStrictEqual({ requiredProperty: '' }); + }); + + it('should return an object with nested fields if they are present in the schema', () => { + expect( + createInitializedObject({ + type: 'object', + properties: { + foo: { + type: 'object', + properties: { + bar: { + type: 'object', + properties: { + baz: { + type: 'string', + }, + }, + required: ['baz'], + }, + }, + required: ['bar'], + }, + }, + }) + ).toStrictEqual({ foo: { bar: { baz: '' } } }); + + expect( + createInitializedObject({ + type: 'object', + properties: { + foo: { + type: 'object', + properties: { + bar: { + type: 'string', + }, + }, + }, + }, + }) + ).toStrictEqual({ foo: {} }); + }); + + it('should handle a real life example', () => { + expect( + createInitializedObject({ + type: 'object', + properties: { + method: { + type: 'string', + description: 'The HTTP method of the Elasticsearch endpoint', + enum: ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'] as const, + }, + path: { + type: 'string', + description: 'The path of the Elasticsearch endpoint, including query parameters', + }, + notRequired: { + type: 'string', + description: 'This property is not required.', + }, + }, + required: ['method', 'path'] as const, + }) + ).toStrictEqual({ method: '', path: '' }); + }); +}); diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/create_initialized_object.ts b/x-pack/plugins/observability_ai_assistant/public/utils/create_initialized_object.ts new file mode 100644 index 00000000000000..42f314766dca90 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/utils/create_initialized_object.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FunctionDefinition } from '../../common/types'; + +type Params = FunctionDefinition['options']['parameters']; + +export function createInitializedObject(parameters: Params) { + const emptyObject: Record = {}; + + function traverseProperties({ properties, required }: Params) { + for (const propName in properties) { + if (properties.hasOwnProperty(propName)) { + const prop = properties[propName] as Params; + + if (prop.type === 'object') { + emptyObject[propName] = createInitializedObject(prop); + } else if (required?.includes(propName)) { + if (prop.type === 'array') { + emptyObject[propName] = []; + } + + if (prop.type === 'number') { + emptyObject[propName] = 1; + } + + if (prop.type === 'string') { + emptyObject[propName] = ''; + } + } + } + } + } + + traverseProperties(parameters); + + return emptyObject; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/get_role_translation.ts b/x-pack/plugins/observability_ai_assistant/public/utils/get_role_translation.ts new file mode 100644 index 00000000000000..390c0cfd0321fe --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/utils/get_role_translation.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { MessageRole } from '../../common'; + +export function getRoleTranslation(role: MessageRole) { + if (role === MessageRole.User) { + return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.user.label', { + defaultMessage: 'You', + }); + } + + if (role === MessageRole.System) { + return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.system.label', { + defaultMessage: 'System', + }); + } + + return i18n.translate( + 'xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.label', + { + defaultMessage: 'Elastic Assistant', + } + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.ts b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.ts deleted file mode 100644 index 3c2ba7da420289..00000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.ts +++ /dev/null @@ -1,79 +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 { v4 } from 'uuid'; -import { i18n } from '@kbn/i18n'; -import type { AuthenticatedUser } from '@kbn/security-plugin/common'; -import dedent from 'dedent'; -import { type Message, MessageRole } from '../../common'; -import type { ChatTimelineItem } from '../components/chat/chat_timeline'; - -export function getTimelineItemsfromConversation({ - currentUser, - messages, - hasConnector, -}: { - currentUser?: Pick; - messages: Message[]; - hasConnector: boolean; -}): ChatTimelineItem[] { - return [ - { - id: v4(), - role: MessageRole.User, - title: i18n.translate('xpack.observabilityAiAssistant.conversationStartTitle', { - defaultMessage: 'started a conversation', - }), - canEdit: false, - canGiveFeedback: false, - canRegenerate: false, - loading: false, - currentUser, - }, - ...messages.map((message) => { - const hasFunction = !!message.message.function_call?.name; - const isSystemPrompt = message.message.role === MessageRole.System; - - let title: string; - let content: string | undefined; - - if (hasFunction) { - title = i18n.translate('xpack.observabilityAiAssistant.suggestedFunctionEvent', { - defaultMessage: 'suggested a function', - }); - content = dedent(`I have requested your system performs the function _${ - message.message.function_call?.name - }_ with the payload - \`\`\` - ${JSON.stringify(JSON.parse(message.message.function_call?.arguments || ''), null, 4)} - \`\`\` - and return its results for me to look at.`); - } else if (isSystemPrompt) { - title = i18n.translate('xpack.observabilityAiAssistant.addedSystemPromptEvent', { - defaultMessage: 'added a prompt', - }); - content = ''; - } else { - title = ''; - content = message.message.content; - } - - const props = { - id: v4(), - role: message.message.role, - canEdit: hasConnector && (message.message.role === MessageRole.User || hasFunction), - canRegenerate: hasConnector && message.message.role === MessageRole.Assistant, - canGiveFeedback: message.message.role === MessageRole.Assistant, - loading: false, - title, - content, - currentUser, - }; - - return props; - }), - ]; -} 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 new file mode 100644 index 00000000000000..b016c5c54de80d --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx @@ -0,0 +1,222 @@ +/* + * 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 { v4 } from 'uuid'; +import { isEmpty, omitBy } from 'lodash'; +import { useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import { Message, MessageRole } from '../../common'; +import type { ChatTimelineItem } from '../components/chat/chat_timeline'; +import { RenderFunction } from '../components/render_function'; +import type { ObservabilityAIAssistantChatService } from '../types'; + +function convertMessageToMarkdownCodeBlock(message: Message['message']) { + let value: object; + + if (!message.name) { + const name = message.function_call?.name; + const args = message.function_call?.arguments + ? JSON.parse(message.function_call.arguments) + : undefined; + + value = { + name, + args, + }; + } else { + const content = message.content ? JSON.parse(message.content) : undefined; + const data = message.data ? JSON.parse(message.data) : undefined; + value = omitBy( + { + content, + data, + }, + isEmpty + ); + } + + return `\`\`\`\n${JSON.stringify(value, null, 2)}\n\`\`\``; +} + +function FunctionName({ name: functionName }: { name: string }) { + const { euiTheme } = useEuiTheme(); + + return {functionName}; +} + +export function getTimelineItemsfromConversation({ + currentUser, + messages, + hasConnector, + chatService, +}: { + currentUser?: Pick; + messages: Message[]; + hasConnector: boolean; + chatService: ObservabilityAIAssistantChatService; +}): ChatTimelineItem[] { + return [ + { + id: v4(), + actions: { canCopy: false, canEdit: false, canGiveFeedback: false, canRegenerate: false }, + display: { collapsed: false, hide: false }, + currentUser, + loading: false, + role: MessageRole.User, + title: i18n.translate('xpack.observabilityAiAssistant.conversationStartTitle', { + defaultMessage: 'started a conversation', + }), + }, + ...messages.map((message, index) => { + const id = v4(); + + let title: React.ReactNode = ''; + let content: string | undefined; + let element: React.ReactNode | undefined; + + const prevFunctionCall = + message.message.name && messages[index - 1] && messages[index - 1].message.function_call + ? messages[index - 1].message.function_call + : undefined; + + const role = message.message.function_call?.trigger || message.message.role; + + const actions = { + canCopy: false, + canEdit: false, + canGiveFeedback: false, + canRegenerate: false, + }; + + const display = { + collapsed: false, + hide: false, + }; + + switch (role) { + case MessageRole.System: + display.hide = true; + break; + + case MessageRole.User: + actions.canCopy = true; + actions.canGiveFeedback = false; + actions.canRegenerate = false; + + display.hide = false; + + // User executed a function: + if (message.message.name && prevFunctionCall) { + const parsedContent = JSON.parse(message.message.content ?? 'null'); + const isError = !!(parsedContent && 'error' in parsedContent); + + title = !isError ? ( + , + }} + /> + ) : ( + , + }} + /> + ); + + element = + !isError && chatService.hasRenderFunction(message.message.name) ? ( + + ) : undefined; + + content = !element ? convertMessageToMarkdownCodeBlock(message.message) : undefined; + + actions.canEdit = false; + display.collapsed = !isError && !element; + } else if (message.message.function_call) { + // User suggested a function + title = ( + , + }} + /> + ); + + content = convertMessageToMarkdownCodeBlock(message.message); + + actions.canEdit = hasConnector; + display.collapsed = true; + } else { + // is a prompt by the user + title = ''; + content = message.message.content; + + actions.canEdit = hasConnector; + display.collapsed = false; + } + + break; + + case MessageRole.Assistant: + actions.canRegenerate = hasConnector; + actions.canCopy = true; + actions.canGiveFeedback = true; + display.hide = false; + + // is a function suggestion by the assistant + if (message.message.function_call?.name) { + title = ( + , + }} + /> + ); + content = convertMessageToMarkdownCodeBlock(message.message); + + display.collapsed = true; + actions.canEdit = true; + } else { + // is an assistant response + title = ''; + content = message.message.content; + display.collapsed = false; + actions.canEdit = false; + } + break; + } + + return { + id, + role, + title, + content, + element, + actions, + display, + currentUser, + function_call: message.message.function_call, + loading: false, + }; + }), + ]; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx b/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx index 3673bd98794823..860bec95f69f59 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx @@ -12,12 +12,34 @@ import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { ObservabilityAIAssistantProvider } from '../context/observability_ai_assistant_provider'; import { ObservabilityAIAssistantAPIClient } from '../api'; import type { Message } from '../../common'; -import type { ObservabilityAIAssistantService, PendingMessage } from '../types'; +import type { + ObservabilityAIAssistantChatService, + ObservabilityAIAssistantService, + PendingMessage, +} from '../types'; import { buildFunctionElasticsearch, buildFunctionServiceSummary } from './builders'; +import { ObservabilityAIAssistantChatServiceProvider } from '../context/observability_ai_assistant_chat_service_provider'; + +const chatService: ObservabilityAIAssistantChatService = { + chat: (options: { messages: Message[]; connectorId: string }) => new Observable(), + getContexts: () => [], + getFunctions: () => [buildFunctionElasticsearch(), buildFunctionServiceSummary()], + executeFunction: async ( + name: string, + args: string | undefined, + signal: AbortSignal + ): Promise<{ content?: Serializable; data?: Serializable }> => ({}), + renderFunction: (name: string, args: string | undefined, response: {}) => ( +
Hello! {name}
+ ), + hasRenderFunction: () => true, +}; const service: ObservabilityAIAssistantService = { isEnabled: () => true, - chat: (options: { messages: Message[]; connectorId: string }) => new Observable(), + start: async () => { + return chatService; + }, callApi: {} as ObservabilityAIAssistantAPIClient, getCurrentUser: async (): Promise => ({ username: 'user', @@ -29,14 +51,6 @@ const service: ObservabilityAIAssistantService = { authentication_type: '', elastic_cloud_user: false, }), - getContexts: () => [], - getFunctions: () => [buildFunctionElasticsearch(), buildFunctionServiceSummary()], - executeFunction: async ( - name: string, - args: string | undefined, - signal: AbortSignal - ): Promise<{ content?: Serializable; data?: Serializable }> => ({}), - renderFunction: (name: string, response: {}) =>
Hello! {name}
, }; export function KibanaReactStorybookDecorator(Story: ComponentType) { @@ -54,7 +68,9 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { }} > - + + + ); 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 30da7d10fed91c..c8cc5f0af45fd1 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 @@ -24,7 +24,6 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ name: t.string, description: t.string, parameters: t.any, - contexts: t.array(t.string), }) ), }), diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts index e9b4426171f635..2d0360d0e4716b 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts @@ -109,10 +109,36 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({ }, }); +const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({ + endpoint: 'GET /internal/observability_ai_assistant/functions/kb_status', + options: { + tags: ['access:ai_assistant'], + }, + handler: async ( + resources + ): Promise<{ + ready: boolean; + error?: any; + deployment_state?: string; + allocation_state?: string; + }> => { + const client = await resources.service.getClient({ request: resources.request }); + + if (!client) { + throw notImplemented(); + } + + return await client.getKnowledgeBaseStatus(); + }, +}); + const setupKnowledgeBaseRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/functions/setup_kb', options: { tags: ['access:ai_assistant'], + timeout: { + idleSocket: 20 * 60 * 1000, // 20 minutes + }, }, handler: async (resources): Promise => { const client = await resources.service.getClient({ request: resources.request }); @@ -130,4 +156,5 @@ export const functionRoutes = { ...functionRecallRoute, ...functionSummariseRoute, ...setupKnowledgeBaseRoute, + ...getKnowledgeBaseStatus, }; diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/types.ts b/x-pack/plugins/observability_ai_assistant/server/routes/types.ts index bf7250eb80d303..62667726bb947b 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/types.ts @@ -31,6 +31,9 @@ export interface ObservabilityAIAssistantRouteHandlerResources { export interface ObservabilityAIAssistantRouteCreateOptions { options: { + timeout?: { + idleSocket?: number; + }; tags: Array<'access:ai_assistant'>; }; } 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 1218cc29d409e8..e55144a089b8ae 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 @@ -18,9 +18,10 @@ import type { ChatCompletionRequestMessage, CreateChatCompletionRequest, } from 'openai'; +import pRetry from 'p-retry'; import { v4 } from 'uuid'; import { - KnowledgeBaseEntry, + type KnowledgeBaseEntry, MessageRole, type Conversation, type ConversationCreateRequest, @@ -144,7 +145,7 @@ export class ObservabilityAIAssistantClient implements IObservabilityAIAssistant }: { messages: Message[]; connectorId: string; - functions: Array; + functions: Array>; }): Promise => { const messagesForOpenAI: ChatCompletionRequestMessage[] = compact( messages @@ -164,9 +165,7 @@ export class ObservabilityAIAssistantClient implements IObservabilityAIAssistant }) ); - const functionsForOpenAI: ChatCompletionFunctions[] = functions.map((fn) => - omit(fn, 'contexts') - ); + const functionsForOpenAI: ChatCompletionFunctions[] = functions; const request: Omit & { model?: string } = { messages: messagesForOpenAI, @@ -320,42 +319,96 @@ export class ObservabilityAIAssistantClient implements IObservabilityAIAssistant } }; - setupKnowledgeBase = async () => { - // if this fails, it's fine to propagate the error to the user - await this.dependencies.esClient.ml.putTrainedModel({ - model_id: ELSER_MODEL_ID, - input: { - field_names: ['text_field'], - }, - }); - + getKnowledgeBaseStatus = async () => { try { - await this.dependencies.esClient.ml.startTrainedModelDeployment({ + const modelStats = await this.dependencies.esClient.ml.getTrainedModelsStats({ model_id: ELSER_MODEL_ID, }); + const elserModelStats = modelStats.trained_model_stats[0]; + const deploymentState = elserModelStats.deployment_stats?.state; + const allocationState = elserModelStats.deployment_stats?.allocation_status.state; + return { + ready: deploymentState === 'started' && allocationState === 'fully_allocated', + deployment_state: deploymentState, + allocation_state: allocationState, + }; + } catch (error) { + return { + error: error instanceof errors.ResponseError ? error.body.error : String(error), + ready: false, + }; + } + }; - const modelStats = await this.dependencies.esClient.ml.getTrainedModelsStats({ + setupKnowledgeBase = async () => { + // if this fails, it's fine to propagate the error to the user + + const installModel = async () => { + this.dependencies.logger.info('Installing ELSER model'); + await this.dependencies.esClient.ml.putTrainedModel( + { + model_id: ELSER_MODEL_ID, + input: { + field_names: ['text_field'], + }, + // @ts-expect-error + wait_for_completion: true, + }, + { requestTimeout: '20m' } + ); + this.dependencies.logger.info('Finished installing ELSER model'); + }; + + try { + const getResponse = await this.dependencies.esClient.ml.getTrainedModels({ model_id: ELSER_MODEL_ID, + include: 'definition_status', }); - const elserModelStats = modelStats.trained_model_stats[0]; - - if (elserModelStats?.deployment_stats?.state !== 'started') { - throwKnowledgeBaseNotReady({ - message: `Deployment has not started`, - deployment_stats: elserModelStats.deployment_stats, - }); + if (!getResponse.trained_model_configs[0]?.fully_defined) { + this.dependencies.logger.info('Model is not fully defined'); + await installModel(); } - return; } catch (error) { if ( - (error instanceof errors.ResponseError && - error.body.error.type === 'resource_not_found_exception') || - error.body.error.type === 'status_exception' + error instanceof errors.ResponseError && + error.body.error.type === 'resource_not_found_exception' ) { - throwKnowledgeBaseNotReady(error.body); + await installModel(); + } else { + throw error; + } + } + + try { + await this.dependencies.esClient.ml.startTrainedModelDeployment({ + model_id: ELSER_MODEL_ID, + wait_for: 'fully_allocated', + }); + } catch (error) { + if (error instanceof errors.ResponseError && error.body.error.type === 'status_exception') { + await pRetry( + async () => { + const response = await this.dependencies.esClient.ml.getTrainedModelsStats({ + model_id: ELSER_MODEL_ID, + }); + + if ( + response.trained_model_stats[0]?.deployment_stats?.allocation_status.state === + 'fully_allocated' + ) { + return Promise.resolve(); + } + + this.dependencies.logger.debug('Model is not allocated yet'); + + return Promise.reject(new Error('Not Ready')); + }, + { factor: 1, minTimeout: 10000, maxRetryTime: 20 * 60 * 1000 } + ); + } else { + throw error; } - throw error; } }; } diff --git a/x-pack/plugins/observability_ai_assistant/server/service/types.ts b/x-pack/plugins/observability_ai_assistant/server/service/types.ts index 265f0c695d446d..cc6c4c744f9696 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/types.ts @@ -20,7 +20,7 @@ export interface IObservabilityAIAssistantClient { chat: (options: { messages: Message[]; connectorId: string; - functions: Array; + functions: Array>; }) => Promise; get: (conversationId: string) => Promise; find: (options?: { query?: string }) => Promise<{ conversations: Conversation[] }>; @@ -29,6 +29,12 @@ export interface IObservabilityAIAssistantClient { delete: (conversationId: string) => Promise; recall: (query: string) => Promise<{ entries: KnowledgeBaseEntry[] }>; summarise: (options: { entry: Omit }) => Promise; + getKnowledgeBaseStatus: () => Promise<{ + ready: boolean; + error?: any; + deployment_state?: string; + allocation_state?: string; + }>; setupKnowledgeBase: () => Promise; } diff --git a/x-pack/plugins/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_ai_assistant/tsconfig.json index 152f4a37234654..5c28ab53aae4ff 100644 --- a/x-pack/plugins/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_ai_assistant/tsconfig.json @@ -35,7 +35,9 @@ "@kbn/io-ts-utils", "@kbn/std", "@kbn/alerting-plugin", - "@kbn/features-plugin" + "@kbn/features-plugin", + "@kbn/react-kibana-context-theme", + "@kbn/i18n-react" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx index e0dcb88064585b..a29286f84336ae 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx @@ -10,6 +10,7 @@ import { EuiButton, EuiButtonEmpty, EuiButtonIcon, + EuiCallOut, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -27,7 +28,12 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { isEmpty } from 'lodash'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; +import { + IntegrationError, + IntegrationOptions, + useCreateIntegration, +} from '../../../../hooks/use_create_integration'; import { useWizard } from '.'; import { OptionalFormRow } from '../../../shared/optional_form_row'; import { @@ -45,6 +51,13 @@ export function ConfigureLogs() { const { goToStep, goBack, getState, setState } = useWizard(); const wizardState = getState(); + const [integrationName, setIntegrationName] = useState( + wizardState.integrationName + ); + const [integrationNameTouched, setIntegrationNameTouched] = useState(false); + const [integrationError, setIntegrationError] = useState< + IntegrationError | undefined + >(); const [datasetName, setDatasetName] = useState(wizardState.datasetName); const [serviceName, setServiceName] = useState(wizardState.serviceName); const [logFilePaths, setLogFilePaths] = useState(wizardState.logFilePaths); @@ -52,20 +65,65 @@ export function ConfigureLogs() { const [customConfigurations, setCustomConfigurations] = useState( wizardState.customConfigurations ); - const logFilePathNotConfigured = logFilePaths.every((filepath) => !filepath); - function onContinue() { + const onIntegrationCreationSuccess = useCallback( + (integration: IntegrationOptions) => { + setState((state) => ({ + ...state, + lastCreatedIntegration: integration, + })); + goToStep('installElasticAgent'); + }, + [goToStep, setState] + ); + + const onIntegrationCreationFailure = useCallback( + (error: IntegrationError) => { + setIntegrationError(error); + }, + [setIntegrationError] + ); + + const { createIntegration, createIntegrationRequest } = useCreateIntegration({ + onIntegrationCreationSuccess, + onIntegrationCreationFailure, + initialLastCreatedIntegration: wizardState.lastCreatedIntegration, + }); + + const isCreatingIntegration = createIntegrationRequest.state === 'pending'; + const hasFailedCreatingIntegration = + createIntegrationRequest.state === 'rejected'; + + const onContinue = useCallback(() => { setState((state) => ({ ...state, datasetName, + integrationName, serviceName, logFilePaths: logFilePaths.filter((filepath) => !!filepath), namespace, customConfigurations, })); - goToStep('installElasticAgent'); - } + createIntegration({ + integrationName, + datasets: [ + { + name: datasetName, + type: 'logs' as const, + }, + ], + }); + }, [ + createIntegration, + customConfigurations, + datasetName, + integrationName, + logFilePaths, + namespace, + serviceName, + setState, + ]); function addLogFilePath() { setLogFilePaths((prev) => [...prev, '']); @@ -85,17 +143,30 @@ export function ConfigureLogs() { ); if (index === 0) { + setIntegrationName(getFilename(filepath)); setDatasetName(getFilename(filepath)); } } - const isDatasetNameInvalid = datasetNameTouched && isEmpty(datasetName); + const hasNamingCollision = + integrationError && integrationError.type === 'NamingCollision'; + + const isIntegrationNameInvalid = + (integrationNameTouched && + (isEmpty(integrationName) || !isLowerCase(integrationName))) || + hasNamingCollision; - const datasetNameError = i18n.translate( - 'xpack.observability_onboarding.configureLogs.dataset.error', - { defaultMessage: 'A dataset name is required.' } + const integrationNameError = getIntegrationNameError( + integrationName, + integrationNameTouched, + integrationError ); + const isDatasetNameInvalid = + datasetNameTouched && (isEmpty(datasetName) || !isLowerCase(datasetName)); + + const datasetNameError = getDatasetNameError(datasetName, datasetNameTouched); + return ( - {i18n.translate('xpack.observability_onboarding.steps.continue', { - defaultMessage: 'Continue', - })} + {isCreatingIntegration + ? i18n.translate( + 'xpack.observability_onboarding.steps.loading', + { + defaultMessage: 'Creating integration...', + } + ) + : i18n.translate( + 'xpack.observability_onboarding.steps.continue', + { + defaultMessage: 'Continue', + } + )} , ]} /> @@ -124,8 +206,7 @@ export function ConfigureLogs() { {i18n.translate( 'xpack.observability_onboarding.configureLogs.description', { - defaultMessage: - 'Fill the paths to the log files on your hosts.', + defaultMessage: 'Configure inputs', } )}

@@ -195,61 +276,6 @@ export function ConfigureLogs() { - - - {i18n.translate( - 'xpack.observability_onboarding.configureLogs.dataset.name', - { - defaultMessage: 'Dataset name', - } - )} - - - - - - } - helpText={i18n.translate( - 'xpack.observability_onboarding.configureLogs.dataset.helper', - { - defaultMessage: - "All lowercase, max 100 chars, special characters will be replaced with '_'.", - } - )} - isInvalid={isDatasetNameInvalid} - error={datasetNameError} - > - - setDatasetName(replaceSpecialChars(event.target.value)) - } - isInvalid={isDatasetNameInvalid} - onInput={() => setDatasetNameTouched(true)} - /> - - + + +

+ {i18n.translate( + 'xpack.observability_onboarding.configureLogs.configureIntegrationDescription', + { + defaultMessage: 'Configure integration', + } + )} +

+
+ + + + + {i18n.translate( + 'xpack.observability_onboarding.configureLogs.integration.name', + { + defaultMessage: 'Integration name', + } + )} + + + + + + } + helpText={i18n.translate( + 'xpack.observability_onboarding.configureLogs.integration.helper', + { + defaultMessage: + "All lowercase, max 100 chars, special characters will be replaced with '_'.", + } + )} + isInvalid={isIntegrationNameInvalid} + error={integrationNameError} + > + + setIntegrationName(replaceSpecialChars(event.target.value)) + } + isInvalid={isIntegrationNameInvalid} + onInput={() => setIntegrationNameTouched(true)} + /> + + + + {i18n.translate( + 'xpack.observability_onboarding.configureLogs.dataset.name', + { + defaultMessage: 'Dataset name', + } + )} + + + + + + } + helpText={i18n.translate( + 'xpack.observability_onboarding.configureLogs.dataset.helper', + { + defaultMessage: + "All lowercase, max 100 chars, special characters will be replaced with '_'.", + } + )} + isInvalid={isDatasetNameInvalid} + error={datasetNameError} + > + + setDatasetName(replaceSpecialChars(event.target.value)) + } + isInvalid={isDatasetNameInvalid} + onInput={() => setDatasetNameTouched(true)} + /> + + + {hasFailedCreatingIntegration && integrationError && ( + <> + + {getIntegrationErrorCallout(integrationError)} + + )}
); } + +const getIntegrationErrorCallout = (integrationError: IntegrationError) => { + const title = i18n.translate( + 'xpack.observability_onboarding.configureLogs.integrationCreation.error.title', + { defaultMessage: 'Sorry, there was an error' } + ); + + switch (integrationError.type) { + case 'AuthorizationError': + const authorizationDescription = i18n.translate( + 'xpack.observability_onboarding.configureLogs.integrationCreation.error.authorization.description', + { + defaultMessage: + 'This user does not have permissions to create an integration.', + } + ); + return ( + +

{authorizationDescription}

+
+ ); + case 'UnknownError': + return ( + +

{integrationError.message}

+
+ ); + } +}; + +const isLowerCase = (str: string) => str.toLowerCase() === str; + +const getIntegrationNameError = ( + integrationName: string, + touched: boolean, + integrationError?: IntegrationError +) => { + if (touched && isEmpty(integrationName)) { + return i18n.translate( + 'xpack.observability_onboarding.configureLogs.integration.emptyError', + { defaultMessage: 'An integration name is required.' } + ); + } + if (touched && !isLowerCase(integrationName)) { + return i18n.translate( + 'xpack.observability_onboarding.configureLogs.integration.lowercaseError', + { defaultMessage: 'An integration name should be lowercase.' } + ); + } + if (integrationError && integrationError.type === 'NamingCollision') { + return integrationError.message; + } +}; + +const getDatasetNameError = (datasetName: string, touched: boolean) => { + if (touched && isEmpty(datasetName)) { + return i18n.translate( + 'xpack.observability_onboarding.configureLogs.dataset.emptyError', + { defaultMessage: 'A dataset name is required.' } + ); + } + if (touched && !isLowerCase(datasetName)) { + return i18n.translate( + 'xpack.observability_onboarding.configureLogs.dataset.lowercaseError', + { defaultMessage: 'A dataset name should be lowercase.' } + ); + } +}; diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/index.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/index.tsx index ce4245dd003545..fecd9c4de83841 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/index.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/index.tsx @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { IntegrationOptions } from '../../../../hooks/use_create_integration'; import { createWizardContext, Step, @@ -16,6 +17,8 @@ import { InstallElasticAgent } from './install_elastic_agent'; import { SelectLogs } from './select_logs'; interface WizardState { + integrationName: string; + lastCreatedIntegration?: IntegrationOptions; datasetName: string; serviceName: string; logFilePaths: string[]; @@ -37,6 +40,7 @@ interface WizardState { } const initialState: WizardState = { + integrationName: '', datasetName: '', serviceName: '', logFilePaths: [''], diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/install_elastic_agent.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/install_elastic_agent.tsx index 187724f68bbb82..9d57f94cdde103 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/install_elastic_agent.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/install_elastic_agent.tsx @@ -8,8 +8,10 @@ import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiFlexGroup, EuiFlexItem, + EuiHorizontalRule, EuiSpacer, EuiText, } from '@elastic/eui'; @@ -36,6 +38,7 @@ import { import { ApiKeyBanner } from './api_key_banner'; import { BackButton } from './back_button'; import { getDiscoverNavigationParams } from '../../utils'; +import { TroubleshootingLink } from '../../../shared/troubleshooting_link'; export function InstallElasticAgent() { const { @@ -267,6 +270,24 @@ export function InstallElasticAgent() {

+ {wizardState.integrationName && ( + <> + + + + )} {apiKeyEncoded && onboardingId ? ( + + ); } diff --git a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx index d1744793bbd313..470ce4517bed44 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx @@ -9,6 +9,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, + EuiHorizontalRule, EuiSpacer, EuiText, } from '@elastic/eui'; @@ -36,6 +37,8 @@ import { } from '../../shared/step_panel'; import { ApiKeyBanner } from '../custom_logs/wizard/api_key_banner'; import { getDiscoverNavigationParams } from '../utils'; +import { SystemIntegrationBanner } from './system_integration_banner'; +import { TroubleshootingLink } from '../../shared/troubleshooting_link'; export function InstallElasticAgent() { const { @@ -224,6 +227,8 @@ export function InstallElasticAgent() {

+ + {apiKeyEncoded && onboardingId ? ( + + ); } diff --git a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx new file mode 100644 index 00000000000000..d567a13f3d3a4a --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/system_integration_banner.tsx @@ -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 { + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useCallback, useEffect, useState } from 'react'; +import type { MouseEvent } from 'react'; +import { + SystemIntegrationError, + useInstallSystemIntegration, +} from '../../../hooks/use_install_system_integration'; +import { useKibanaNavigation } from '../../../hooks/use_kibana_navigation'; +import { PopoverTooltip } from '../../shared/popover_tooltip'; + +export function SystemIntegrationBanner() { + const { navigateToAppUrl } = useKibanaNavigation(); + const [integrationVersion, setIntegrationVersion] = useState(); + const [error, setError] = useState(); + + const onIntegrationCreationSuccess = useCallback( + ({ version }: { version?: string }) => { + setIntegrationVersion(version); + }, + [] + ); + + const onIntegrationCreationFailure = useCallback( + (e: SystemIntegrationError) => { + setError(e); + }, + [] + ); + + const { performRequest, requestState } = useInstallSystemIntegration({ + onIntegrationCreationSuccess, + onIntegrationCreationFailure, + }); + + useEffect(() => { + performRequest(); + }, [performRequest]); + + const isInstallingIntegration = requestState.state === 'pending'; + const hasFailedInstallingIntegration = requestState.state === 'rejected'; + const hasInstalledIntegration = requestState.state === 'resolved'; + + if (isInstallingIntegration) { + return ( + + + + + + {i18n.translate( + 'xpack.observability_onboarding.systemIntegration.installing', + { + defaultMessage: 'Installing system integration', + } + )} + + + } + color="primary" + /> + ); + } + if (hasFailedInstallingIntegration) { + return ( + + + {error?.message} + + + ); + } + if (hasInstalledIntegration) { + return ( + + + + + {i18n.translate( + 'xpack.observability_onboarding.systemIntegration.installed.tooltip.description', + { + defaultMessage: + 'Integrations streamline connecting your data to the Elastic Stack.', + } + )} + + + { + event.preventDefault(); + navigateToAppUrl( + `/integrations/detail/system-${integrationVersion}` + ); + }} + > + {i18n.translate( + 'xpack.observability_onboarding.systemIntegration.installed.tooltip.link.label', + { + defaultMessage: 'Learn more', + } + )} + + ), + }} + /> + + + + ), + }} + /> + } + color="success" + iconType="check" + /> + + ); + } + return null; +} diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx new file mode 100644 index 00000000000000..66165edc8e1333 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx @@ -0,0 +1,50 @@ +/* + * 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 { EuiButtonIcon, EuiPopover, EuiPopoverTitle } from '@elastic/eui'; +import React, { useState } from 'react'; + +interface PopoverTooltipProps { + ariaLabel?: string; + iconType?: string; + title?: string; + children: React.ReactNode; +} + +export function PopoverTooltip({ + ariaLabel, + iconType = 'iInCircle', + title, + children, +}: PopoverTooltipProps) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + setIsPopoverOpen(false)} + style={{ margin: '-5px 0 0 -5px' }} + button={ + ) => { + setIsPopoverOpen(!isPopoverOpen); + event.stopPropagation(); + }} + size="xs" + color="primary" + iconType={iconType} + /> + } + > + {title && {title}} + {children} + + ); +} diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/troubleshooting_link.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/troubleshooting_link.tsx new file mode 100644 index 00000000000000..5b6a1588d643cc --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/shared/troubleshooting_link.tsx @@ -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 React from 'react'; +import { EuiButtonEmpty, EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function TroubleshootingLink() { + return ( + + + {i18n.translate( + 'xpack.observability_onboarding.installElasticAgent.troubleshooting', + { defaultMessage: 'Troubleshooting' } + )} + + + ); +} diff --git a/x-pack/plugins/observability_onboarding/public/hooks/use_create_integration.ts b/x-pack/plugins/observability_onboarding/public/hooks/use_create_integration.ts new file mode 100644 index 00000000000000..15d383cb4aa42f --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/hooks/use_create_integration.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState } from 'react'; +import deepEqual from 'react-fast-compare'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useTrackedPromise } from '@kbn/use-tracked-promise'; +import { i18n } from '@kbn/i18n'; + +export interface IntegrationOptions { + integrationName: string; + datasets: Array<{ + name: string; + type: 'logs'; + }>; +} + +// Errors +const GENERIC_ERROR_MESSAGE = i18n.translate( + 'xpack.observability_onboarding.useCreateIntegration.integrationError.genericError', + { + defaultMessage: 'Unable to create an integration', + } +); + +type ErrorType = 'NamingCollision' | 'AuthorizationError' | 'UnknownError'; +export interface IntegrationError { + type: ErrorType; + message: string; +} + +export const useCreateIntegration = ({ + onIntegrationCreationSuccess, + onIntegrationCreationFailure, + initialLastCreatedIntegration, + deletePreviousIntegration = true, +}: { + integrationOptions?: IntegrationOptions; + onIntegrationCreationSuccess: (integration: IntegrationOptions) => void; + onIntegrationCreationFailure: (error: IntegrationError) => void; + initialLastCreatedIntegration?: IntegrationOptions; + deletePreviousIntegration?: boolean; +}) => { + const { + services: { http }, + } = useKibana(); + const [lastCreatedIntegration, setLastCreatedIntegration] = useState< + IntegrationOptions | undefined + >(initialLastCreatedIntegration); + + const [createIntegrationRequest, callCreateIntegration] = useTrackedPromise( + { + cancelPreviousOn: 'creation', + createPromise: async (integrationOptions) => { + if (lastCreatedIntegration && deletePreviousIntegration) { + await http?.delete( + `/api/fleet/epm/packages/${lastCreatedIntegration.integrationName}/1.0.0`, + {} + ); + } + await http?.post('/api/fleet/epm/custom_integrations', { + body: JSON.stringify(integrationOptions), + }); + + return integrationOptions; + }, + onResolve: (integrationOptions: IntegrationOptions) => { + setLastCreatedIntegration(integrationOptions); + onIntegrationCreationSuccess(integrationOptions!); + }, + onReject: (requestError: any) => { + if (requestError?.body?.statusCode === 409) { + onIntegrationCreationFailure({ + type: 'NamingCollision' as const, + message: requestError.body.message, + }); + } else if (requestError?.body?.statusCode === 403) { + onIntegrationCreationFailure({ + type: 'AuthorizationError' as const, + message: requestError?.body?.message, + }); + } else { + onIntegrationCreationFailure({ + type: 'UnknownError' as const, + message: requestError?.body?.message ?? GENERIC_ERROR_MESSAGE, + }); + } + }, + }, + [ + lastCreatedIntegration, + deletePreviousIntegration, + onIntegrationCreationSuccess, + onIntegrationCreationFailure, + setLastCreatedIntegration, + ] + ); + + const createIntegration = useCallback( + (integrationOptions: IntegrationOptions) => { + // Bypass creating the integration again + if (deepEqual(integrationOptions, lastCreatedIntegration)) { + onIntegrationCreationSuccess(integrationOptions); + } else { + callCreateIntegration(integrationOptions); + } + }, + [ + callCreateIntegration, + lastCreatedIntegration, + onIntegrationCreationSuccess, + ] + ); + + return { + createIntegration, + createIntegrationRequest, + }; +}; diff --git a/x-pack/plugins/observability_onboarding/public/hooks/use_install_system_integration.ts b/x-pack/plugins/observability_onboarding/public/hooks/use_install_system_integration.ts new file mode 100644 index 00000000000000..5473ebd09c2409 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/hooks/use_install_system_integration.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useTrackedPromise } from '@kbn/use-tracked-promise'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from './use_kibana'; + +// Errors +const UNAUTHORIZED_ERROR = i18n.translate( + 'xpack.observability_onboarding.installSystemIntegration.error.unauthorized', + { + defaultMessage: + 'Required kibana privilege {requiredKibanaPrivileges} is missing, please add the required privilege to the role of the authenticated user.', + values: { + requiredKibanaPrivileges: "['Fleet', 'Integrations']", + }, + } +); + +type ErrorType = 'AuthorizationError' | 'UnknownError'; +export interface SystemIntegrationError { + type: ErrorType; + message: string; +} + +type IntegrationInstallStatus = + | 'installed' + | 'installing' + | 'install_failed' + | 'not_installed'; + +export const useInstallSystemIntegration = ({ + onIntegrationCreationSuccess, + onIntegrationCreationFailure, +}: { + onIntegrationCreationSuccess: ({ version }: { version?: string }) => void; + onIntegrationCreationFailure: (error: SystemIntegrationError) => void; +}) => { + const { + services: { http }, + } = useKibana(); + const [requestState, callPerformRequest] = useTrackedPromise( + { + cancelPreviousOn: 'creation', + createPromise: async () => { + const { item: systemIntegration } = await http.get<{ + item: { version: string; status: IntegrationInstallStatus }; + }>('/api/fleet/epm/packages/system'); + + if (systemIntegration.status !== 'installed') { + await http.post('/api/fleet/epm/packages/system'); + } + + return { + version: systemIntegration.version, + }; + }, + onResolve: ({ version }: { version?: string }) => { + onIntegrationCreationSuccess({ version }); + }, + onReject: (requestError: any) => { + if (requestError?.body?.statusCode === 403) { + onIntegrationCreationFailure({ + type: 'AuthorizationError' as const, + message: UNAUTHORIZED_ERROR, + }); + } else { + onIntegrationCreationFailure({ + type: 'UnknownError' as const, + message: requestError?.body?.message, + }); + } + }, + }, + [onIntegrationCreationSuccess, onIntegrationCreationFailure] + ); + + const performRequest = useCallback(() => { + callPerformRequest(); + }, [callPerformRequest]); + + return { + performRequest, + requestState, + }; +}; diff --git a/x-pack/plugins/observability_onboarding/public/hooks/use_kibana.ts b/x-pack/plugins/observability_onboarding/public/hooks/use_kibana.ts new file mode 100644 index 00000000000000..3102d3903b85fc --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/hooks/use_kibana.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from '@kbn/core/public'; +import { + context as KibanaContext, + KibanaContextProvider, + useKibana, +} from '@kbn/kibana-react-plugin/public'; + +export type Services = CoreStart; + +const useTypedKibana = () => useKibana(); + +export { KibanaContextProvider, useTypedKibana as useKibana, KibanaContext }; diff --git a/x-pack/plugins/observability_onboarding/tsconfig.json b/x-pack/plugins/observability_onboarding/tsconfig.json index 6bb24fde8c5884..2119563923cb9a 100644 --- a/x-pack/plugins/observability_onboarding/tsconfig.json +++ b/x-pack/plugins/observability_onboarding/tsconfig.json @@ -32,6 +32,7 @@ "@kbn/std", "@kbn/data-views-plugin", "@kbn/es-query", + "@kbn/use-tracked-promise", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/profiling/common/index.ts b/x-pack/plugins/profiling/common/index.ts index 5eb235bcf29be5..2713ed3f98a132 100644 --- a/x-pack/plugins/profiling/common/index.ts +++ b/x-pack/plugins/profiling/common/index.ts @@ -29,6 +29,9 @@ export function getRoutePaths() { Flamechart: `${BASE_ROUTE_PATH}/flamechart`, HasSetupESResources: `${BASE_ROUTE_PATH}/setup/es_resources`, SetupDataCollectionInstructions: `${BASE_ROUTE_PATH}/setup/instructions`, + StorageExplorerSummary: `${BASE_ROUTE_PATH}/storage_explorer/summary`, + StorageExplorerHostStorageDetails: `${BASE_ROUTE_PATH}/storage_explorer/host_storage_details`, + StorageExplorerIndicesStorageDetails: `${BASE_ROUTE_PATH}/storage_explorer/indices_storage_details`, }; } diff --git a/x-pack/plugins/profiling/common/storage_explorer.ts b/x-pack/plugins/profiling/common/storage_explorer.ts new file mode 100644 index 00000000000000..984619af5ea988 --- /dev/null +++ b/x-pack/plugins/profiling/common/storage_explorer.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as t from 'io-ts'; + +export enum IndexLifecyclePhaseSelectOption { + All = 'all', + Hot = 'hot', + Warm = 'warm', + Cold = 'cold', + Frozen = 'frozen', +} + +export const indexLifecyclePhaseRt = t.type({ + indexLifecyclePhase: t.union([ + t.literal(IndexLifecyclePhaseSelectOption.All), + t.literal(IndexLifecyclePhaseSelectOption.Hot), + t.literal(IndexLifecyclePhaseSelectOption.Warm), + t.literal(IndexLifecyclePhaseSelectOption.Cold), + t.literal(IndexLifecyclePhaseSelectOption.Frozen), + ]), +}); + +export const indexLifeCyclePhaseToDataTier = { + [IndexLifecyclePhaseSelectOption.Hot]: 'data_hot', + [IndexLifecyclePhaseSelectOption.Warm]: 'data_warm', + [IndexLifecyclePhaseSelectOption.Cold]: 'data_cold', + [IndexLifecyclePhaseSelectOption.Frozen]: 'data_frozen', +}; + +export interface StorageExplorerSummaryAPIResponse { + totalProfilingSizeBytes: number; + totalSymbolsSizeBytes: number; + diskSpaceUsedPct: number; + totalNumberOfDistinctProbabilisticValues: number; + totalNumberOfHosts: number; + dailyDataGenerationBytes: number; +} + +export interface StorageExplorerHostDetailsTimeseries { + hostId: string; + hostName: string; + timeseries: Array<{ + x: number; + y?: number | null; + }>; +} + +export interface StorageExplorerHostDetails { + hostId: string; + hostName: string; + projectId: string; + probabilisticValues: Array<{ value: number; date: number | null }>; + totalEventsSize: number; + totalMetricsSize: number; + totalSize: number; +} + +export interface StorageHostDetailsAPIResponse { + hostDetailsTimeseries: StorageExplorerHostDetailsTimeseries[]; + hostDetails: StorageExplorerHostDetails[]; +} + +export type StorageGroupedIndexNames = + | 'events' + | 'stackframes' + | 'stacktraces' + | 'executables' + | 'metrics'; + +export interface StorageDetailsGroupedByIndex { + indexName: StorageGroupedIndexNames; + docCount: number; + sizeInBytes: number; +} + +export interface StorageDetailsPerIndex { + indexName: string; + docCount?: number; + primaryShardsCount?: number; + replicaShardsCount?: number; + sizeInBytes?: number; + dataStream?: string; + lifecyclePhase?: string; +} + +export interface IndicesStorageDetailsAPIResponse { + storageDetailsGroupedByIndex: StorageDetailsGroupedByIndex[]; + storageDetailsPerIndex: StorageDetailsPerIndex[]; +} diff --git a/x-pack/plugins/profiling/e2e/cypress/e2e/empty_state/home.cy.ts b/x-pack/plugins/profiling/e2e/cypress/e2e/empty_state/home.cy.ts index 628d921f30690d..26f2347a623405 100644 --- a/x-pack/plugins/profiling/e2e/cypress/e2e/empty_state/home.cy.ts +++ b/x-pack/plugins/profiling/e2e/cypress/e2e/empty_state/home.cy.ts @@ -16,7 +16,7 @@ describe('Home page with empty state', () => { }).as('getEsResources'); cy.visitKibana('/app/profiling'); cy.wait('@getEsResources'); - cy.contains('Universal Profiling (now in Beta)'); + cy.contains('Universal Profiling'); cy.contains('Set up Universal Profiling'); }); diff --git a/x-pack/plugins/profiling/public/components/check_setup.tsx b/x-pack/plugins/profiling/public/components/check_setup.tsx index 5f4841f9d2b8a4..cdd7e0bcd3cccf 100644 --- a/x-pack/plugins/profiling/public/components/check_setup.tsx +++ b/x-pack/plugins/profiling/public/components/check_setup.tsx @@ -89,7 +89,7 @@ export function CheckSetup({ children }: { children: React.ReactElement }) { docsLink: `${docLinks.ELASTIC_WEBSITE_URL}/guide/en/observability/${docLinks.DOC_LINK_VERSION}/profiling-get-started.html`, logo: 'logoObservability', pageTitle: i18n.translate('xpack.profiling.noDataConfig.pageTitle', { - defaultMessage: 'Universal Profiling (now in Beta)', + defaultMessage: 'Universal Profiling', }), action: { elasticAgent: { @@ -133,19 +133,6 @@ export function CheckSetup({ children }: { children: React.ReactElement }) { }} /> -
  • - {i18n.translate('xpack.profiling.noDataConfig.action.legalBetaTerms', { - defaultMessage: `By using this feature, you acknowledge that you have read and agree to `, - })} - - {i18n.translate('xpack.profiling.noDataConfig.betaTerms.linkLabel', { - defaultMessage: 'Elastic Beta Release Terms', - })} - -
  • @@ -170,7 +157,9 @@ export function CheckSetup({ children }: { children: React.ReactElement }) { notifications.toasts.addError(err, { title: i18n.translate( 'xpack.profiling.checkSetup.setupFailureToastTitle', - { defaultMessage: 'Failed to complete setup' } + { + defaultMessage: 'Failed to complete setup', + } ), toastMessage: message, }); diff --git a/x-pack/plugins/profiling/public/components/label_with_hint/index.tsx b/x-pack/plugins/profiling/public/components/label_with_hint/index.tsx index 1c3dd7d2c5be3e..4e4c21b1c88503 100644 --- a/x-pack/plugins/profiling/public/components/label_with_hint/index.tsx +++ b/x-pack/plugins/profiling/public/components/label_with_hint/index.tsx @@ -25,7 +25,7 @@ interface Props { export function LabelWithHint({ label, hint, iconSize, labelSize, labelStyle }: Props) { return ( - + {label} diff --git a/x-pack/plugins/profiling/public/components/profiling_app_page_template/index.tsx b/x-pack/plugins/profiling/public/components/profiling_app_page_template/index.tsx index f7c0d95e4b8f14..1a7949eff11c83 100644 --- a/x-pack/plugins/profiling/public/components/profiling_app_page_template/index.tsx +++ b/x-pack/plugins/profiling/public/components/profiling_app_page_template/index.tsx @@ -25,20 +25,22 @@ export const PROFILING_FEEDBACK_LINK = 'https://ela.st/profiling-feedback'; export function ProfilingAppPageTemplate({ children, - tabs, + tabs = [], hideSearchBar = false, noDataConfig, restrictWidth = false, pageTitle = i18n.translate('xpack.profiling.appPageTemplate.pageTitle', { defaultMessage: 'Universal Profiling', }), + showBetaBadge = false, }: { children: React.ReactElement; - tabs: EuiPageHeaderContentProps['tabs']; + tabs?: EuiPageHeaderContentProps['tabs']; hideSearchBar?: boolean; noDataConfig?: NoDataPageProps; restrictWidth?: boolean; pageTitle?: React.ReactNode; + showBetaBadge?: boolean; }) { const { start: { observabilityShared }, @@ -73,15 +75,17 @@ export function ProfilingAppPageTemplate({

    {pageTitle}

    - - - + {showBetaBadge && ( + + + + )}
    ), tabs, diff --git a/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx b/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx index fafcc1b36d9d8a..be823c583fa21a 100644 --- a/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx +++ b/x-pack/plugins/profiling/public/components/profiling_header_action_menu.tsx @@ -5,16 +5,47 @@ * 2.0. */ import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiHeaderLinks, EuiIcon } from '@elastic/eui'; -import React from 'react'; import { i18n } from '@kbn/i18n'; +import qs from 'query-string'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import url from 'url'; import { ObservabilityAIAssistantActionMenuItem } from '@kbn/observability-ai-assistant-plugin/public'; import { useProfilingRouter } from '../hooks/use_profiling_router'; import { NoDataTabs } from '../views/no_data_view'; export function ProfilingHeaderActionMenu() { const router = useProfilingRouter(); + const history = useHistory(); + return ( + { + const query = qs.parse(window.location.search); + const storageExplorerURL = url.format({ + pathname: '/storage-explorer', + query: { + kuery: query.kuery, + rangeFrom: query.rangeFrom, + rangeTo: query.rangeTo, + }, + }); + history.push(storageExplorerURL); + }} + > + + + + + + {i18n.translate('xpack.profiling.headerActionMenu.storageExplorer', { + defaultMessage: 'Storage Explorer', + })} + + + = { barsPadding: 0, histogramPadding: 0, }, + partition: { + fillLabel: { + textColor: 'white', + }, + emptySizeRatio: 0.3, + sectorLineWidth: 0, + }, }; export function useProfilingChartsTheme() { diff --git a/x-pack/plugins/profiling/public/plugin.tsx b/x-pack/plugins/profiling/public/plugin.tsx index c8f457931013d5..a59f991df58d8d 100644 --- a/x-pack/plugins/profiling/public/plugin.tsx +++ b/x-pack/plugins/profiling/public/plugin.tsx @@ -62,7 +62,6 @@ export class ProfilingPlugin implements Plugin { label: i18n.translate('xpack.profiling.navigation.sectionLabel', { defaultMessage: 'Universal Profiling', }), - isBetaFeature: true, entries: links.map((link) => { return { app: 'profiling', diff --git a/x-pack/plugins/profiling/public/routing/index.tsx b/x-pack/plugins/profiling/public/routing/index.tsx index b27ef5f5511998..3bbd6c482c9ae0 100644 --- a/x-pack/plugins/profiling/public/routing/index.tsx +++ b/x-pack/plugins/profiling/public/routing/index.tsx @@ -11,6 +11,10 @@ import * as t from 'io-ts'; import React from 'react'; import { TopNFunctionSortField, topNFunctionSortFieldRt } from '../../common/functions'; import { StackTracesDisplayOption, TopNType } from '../../common/stack_traces'; +import { + indexLifecyclePhaseRt, + IndexLifecyclePhaseSelectOption, +} from '../../common/storage_explorer'; import { ComparisonMode, NormalizationMode } from '../components/normalization_menu'; import { RedirectTo } from '../components/redirect_to'; import { FlameGraphsView } from '../views/flamegraphs'; @@ -21,6 +25,7 @@ import { DifferentialTopNFunctionsView } from '../views/functions/differential_t import { TopNFunctionsView } from '../views/functions/topn'; import { NoDataTabs, NoDataView } from '../views/no_data_view'; import { StackTracesView } from '../views/stack_traces_view'; +import { StorageExplorerView } from '../views/storage_explorer'; import { RouteBreadcrumb } from './route_breadcrumb'; const routes = { @@ -246,6 +251,26 @@ const routes = { }, }, }, + '/storage-explorer': { + element: ( + + + + ), + params: t.type({ + query: indexLifecyclePhaseRt, + }), + defaults: { + query: { + indexLifecyclePhase: IndexLifecyclePhaseSelectOption.All, + }, + }, + }, '/': { element: , }, diff --git a/x-pack/plugins/profiling/public/services.ts b/x-pack/plugins/profiling/public/services.ts index edd8922ddd3b0a..a477d522b3417c 100644 --- a/x-pack/plugins/profiling/public/services.ts +++ b/x-pack/plugins/profiling/public/services.ts @@ -8,6 +8,12 @@ import { HttpFetchQuery } from '@kbn/core/public'; import { getRoutePaths } from '../common'; import { BaseFlameGraph, createFlameGraph, ElasticFlameGraph } from '../common/flamegraph'; import { TopNFunctions } from '../common/functions'; +import type { + IndexLifecyclePhaseSelectOption, + IndicesStorageDetailsAPIResponse, + StorageExplorerSummaryAPIResponse, + StorageHostDetailsAPIResponse, +} from '../common/storage_explorer'; import { TopNResponse } from '../common/topn'; import type { SetupDataCollectionInstructions } from '../server/lib/setup/get_setup_instructions'; import { AutoAbortedHttpService } from './hooks/use_auto_aborted_http_client'; @@ -41,6 +47,24 @@ export interface Services { setupDataCollectionInstructions: (params: { http: AutoAbortedHttpService; }) => Promise; + fetchStorageExplorerSummary: (params: { + http: AutoAbortedHttpService; + timeFrom: number; + timeTo: number; + kuery: string; + indexLifecyclePhase: IndexLifecyclePhaseSelectOption; + }) => Promise; + fetchStorageExplorerHostStorageDetails: (params: { + http: AutoAbortedHttpService; + timeFrom: number; + timeTo: number; + kuery: string; + indexLifecyclePhase: IndexLifecyclePhaseSelectOption; + }) => Promise; + fetchStorageExplorerIndicesStorageDetails: (params: { + http: AutoAbortedHttpService; + indexLifecyclePhase: IndexLifecyclePhaseSelectOption; + }) => Promise; } export function getServices(): Services { @@ -93,5 +117,45 @@ export function getServices(): Services { )) as SetupDataCollectionInstructions; return instructions; }, + fetchStorageExplorerSummary: async ({ http, timeFrom, timeTo, kuery, indexLifecyclePhase }) => { + const query: HttpFetchQuery = { + timeFrom, + timeTo, + kuery, + indexLifecyclePhase, + }; + const summary = (await http.get(paths.StorageExplorerSummary, { + query, + })) as StorageExplorerSummaryAPIResponse; + return summary; + }, + fetchStorageExplorerHostStorageDetails: async ({ + http, + timeFrom, + timeTo, + kuery, + indexLifecyclePhase, + }) => { + const query: HttpFetchQuery = { + timeFrom, + timeTo, + kuery, + indexLifecyclePhase, + }; + const eventsMetricsSizeTimeseries = (await http.get(paths.StorageExplorerHostStorageDetails, { + query, + })) as StorageHostDetailsAPIResponse; + return eventsMetricsSizeTimeseries; + }, + fetchStorageExplorerIndicesStorageDetails: async ({ http, indexLifecyclePhase }) => { + const query: HttpFetchQuery = { + indexLifecyclePhase, + }; + const eventsMetricsSizeTimeseries = (await http.get( + paths.StorageExplorerIndicesStorageDetails, + { query } + )) as IndicesStorageDetailsAPIResponse; + return eventsMetricsSizeTimeseries; + }, }; } diff --git a/x-pack/plugins/profiling/public/views/no_data_view/index.tsx b/x-pack/plugins/profiling/public/views/no_data_view/index.tsx index e982e4d53e3088..c50f50d3170ed7 100644 --- a/x-pack/plugins/profiling/public/views/no_data_view/index.tsx +++ b/x-pack/plugins/profiling/public/views/no_data_view/index.tsx @@ -330,7 +330,7 @@ docker.elastic.co/observability/profiling-agent:${hostAgentVersion} /root/pf-hos iconType="gear" fill href={`${core.http.basePath.prepend( - `/app/integrations/detail/profiler_agent-${data?.profilerAgent.version}/overview?prerelease=true` + `/app/integrations/detail/profiler_agent-${data?.profilerAgent.version}/overview` )}`} > {i18n.translate('xpack.profiling.tabs.elasticAgentIntegrarion.step2.button', { diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/grouped_index_details.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/grouped_index_details.tsx new file mode 100644 index 00000000000000..2a438c38e3ca08 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/grouped_index_details.tsx @@ -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 { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { asDynamicBytes, asInteger } from '@kbn/observability-plugin/common'; +import React from 'react'; +import { NOT_AVAILABLE_LABEL } from '../../../../common'; +import type { + StorageDetailsGroupedByIndex, + StorageGroupedIndexNames, +} from '../../../../common/storage_explorer'; +import { LabelWithHint } from '../../../components/label_with_hint'; +import { getGroupedIndexLabel } from './utils'; + +interface Props { + data?: StorageDetailsGroupedByIndex[]; +} + +const hintMap: Partial> = { + events: i18n.translate('xpack.profiling.storageExplorer.dataBreakdown.events.hint', { + defaultMessage: + 'Universal Profiling samples linearly correlate with the probabilistic profiling value. The lower the probabilistic profiling value, the fewer samples are collected.', + }), +}; + +export function GroupedIndexDetails({ data = [] }: Props) { + const orderedIndexNames = [ + 'stackframes', + 'stacktraces', + 'executables', + 'metrics', + 'events', + ] as StorageGroupedIndexNames[]; + return ( + + {orderedIndexNames.map((indexName) => { + const stats = data.find((item) => item.indexName === indexName); + + return ( + + + + ); + })} + + ); +} + +function IndexSizeItem({ + indexName, + docCount, + sizeInBytes, + hint, +}: { + indexName: StorageGroupedIndexNames; + docCount?: number; + sizeInBytes?: number; + hint?: string; +}) { + const theme = useEuiTheme(); + + const indexLabel = getGroupedIndexLabel(indexName); + + return ( + <> + + + {hint ? ( + + ) : ( + + {indexLabel} + + )} + + + + {i18n.translate('xpack.profiling.storageExplorer.dataBreakdown.size', { + defaultMessage: 'Size', + })} + + + + + + {docCount ? ( + asInteger(docCount) + ) : ( + + {NOT_AVAILABLE_LABEL} + + )} + + + {sizeInBytes ? ( + asDynamicBytes(sizeInBytes) + ) : ( + + {NOT_AVAILABLE_LABEL} + + )} + + + + ); +} diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/grouped_index_details_chart.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/grouped_index_details_chart.tsx new file mode 100644 index 00000000000000..15d91ed4729f41 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/grouped_index_details_chart.tsx @@ -0,0 +1,84 @@ +/* + * 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 { Chart, Datum, Partition, Position, Settings } from '@elastic/charts'; +import { euiPaletteColorBlind, EuiText, useEuiTheme } from '@elastic/eui'; +import { asDynamicBytes } from '@kbn/observability-plugin/common'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import type { StorageDetailsGroupedByIndex } from '../../../../common/storage_explorer'; +import { useProfilingChartsTheme } from '../../../hooks/use_profiling_charts_theme'; +import { getGroupedIndexLabel } from './utils'; + +interface Props { + data?: StorageDetailsGroupedByIndex[]; +} + +export function GroupedIndexDetailsChart({ data = [] }: Props) { + const theme = useEuiTheme(); + const { chartsBaseTheme, chartsTheme } = useProfilingChartsTheme(); + const groupedPalette = euiPaletteColorBlind(); + + const sunburstData = data.map((item) => { + const { indexName, ...values } = item; + return { key: getGroupedIndexLabel(item.indexName), ...values }; + }); + + return ( +
    + {sunburstData.length ? ( + + + Number(d.sizeInBytes)} + valueGetter="percent" + valueFormatter={(value: number) => asDynamicBytes(value)} + layers={[ + { + groupByRollup: (d: Datum) => d.key, + shape: { + fillColor: (_, sortIndex) => groupedPalette[sortIndex], + }, + }, + ]} + /> + + ) : ( +
    + + {i18n.translate('xpack.profiling.storageExplorer.dataBreakdown.noDataToDisplay', { + defaultMessage: 'No data to display', + })} + +
    + )} +
    + ); +} diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/index.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/index.tsx new file mode 100644 index 00000000000000..8567db7ac0c6cf --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/index.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { AsyncComponent } from '../../../components/async_component'; +import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies'; +import { useTimeRangeAsync } from '../../../hooks/use_time_range_async'; +import { GroupedIndexDetailsChart } from './grouped_index_details_chart'; +import { GroupedIndexDetails } from './grouped_index_details'; +import { StorageDetailsTable } from './storage_details_table'; +import { useProfilingParams } from '../../../hooks/use_profiling_params'; + +export function DataBreakdown() { + const theme = useEuiTheme(); + const { query } = useProfilingParams('/storage-explorer'); + const { indexLifecyclePhase } = query; + const { + services: { fetchStorageExplorerIndicesStorageDetails }, + } = useProfilingDependencies(); + + const indicesStorageDetails = useTimeRangeAsync( + ({ http }) => { + return fetchStorageExplorerIndicesStorageDetails({ http, indexLifecyclePhase }); + }, + [fetchStorageExplorerIndicesStorageDetails, indexLifecyclePhase] + ); + + return ( + <> + + + {i18n.translate('xpack.profiling.storageExplorer.dataBreakdown.title', { + defaultMessage: 'Data breakdown', + })} + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/storage_details_table.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/storage_details_table.tsx new file mode 100644 index 00000000000000..16627d7c9fbaa5 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/storage_details_table.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 { + CriteriaWithPagination, + EuiBasicTableColumn, + EuiInMemoryTable, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo, useState } from 'react'; +import { asDynamicBytes, asInteger } from '@kbn/observability-plugin/common'; +import { StorageDetailsPerIndex } from '../../../../common/storage_explorer'; +import { NOT_AVAILABLE_LABEL } from '../../../../common'; + +interface Props { + data?: StorageDetailsPerIndex[]; +} + +const sorting = { + sort: { + field: 'sizeInBytes', + direction: 'desc' as const, + }, +}; + +export function StorageDetailsTable({ data = [] }: Props) { + const [pagination, setPagination] = useState({ pageIndex: 0 }); + + function onTableChange({ page: { index } }: CriteriaWithPagination) { + setPagination({ pageIndex: index }); + } + + const columns: Array> = useMemo( + () => [ + { + field: 'indexName', + name: i18n.translate( + 'xpack.profiling.storageExplorer.dataBreakdown.storageDetailsTable.index', + { defaultMessage: 'Index' } + ), + sortable: true, + }, + { + field: 'primaryShardsCount', + width: '150px', + name: i18n.translate( + 'xpack.profiling.storageExplorer.dataBreakdown.storageDetailsTable.primaries', + { defaultMessage: 'Primaries' } + ), + render: (_, { primaryShardsCount }) => primaryShardsCount ?? NOT_AVAILABLE_LABEL, + sortable: true, + }, + { + field: 'replicaShardsCount', + width: '150px', + name: i18n.translate( + 'xpack.profiling.storageExplorer.dataBreakdown.storageDetailsTable.replicas', + { defaultMessage: 'Replicas' } + ), + render: (_, { replicaShardsCount }) => replicaShardsCount ?? NOT_AVAILABLE_LABEL, + sortable: true, + }, + { + field: 'docCount', + width: '150px', + name: i18n.translate( + 'xpack.profiling.storageExplorer.dataBreakdown.storageDetailsTable.docCount', + { defaultMessage: 'Doc count' } + ), + sortable: true, + render: (_, { docCount }) => (docCount ? asInteger(docCount) : NOT_AVAILABLE_LABEL), + }, + { + field: 'sizeInBytes', + width: '150px', + name: i18n.translate( + 'xpack.profiling.storageExplorer.dataBreakdown.storageDetailsTable.storageSize', + { defaultMessage: 'Storage size' } + ), + sortable: true, + render: (_, { sizeInBytes }) => + sizeInBytes ? asDynamicBytes(sizeInBytes) : NOT_AVAILABLE_LABEL, + }, + { + field: 'dataStream', + name: i18n.translate( + 'xpack.profiling.storageExplorer.dataBreakdown.storageDetailsTable.dataStream', + { defaultMessage: 'Data stream' } + ), + sortable: true, + render: (_, { dataStream }) => dataStream ?? NOT_AVAILABLE_LABEL, + }, + { + field: 'lifecyclePhase', + width: '150px', + name: i18n.translate( + 'xpack.profiling.storageExplorer.dataBreakdown.storageDetailsTable.lifecyclePhase', + { defaultMessage: 'Lifecycle phase' } + ), + sortable: true, + render: (_, { lifecyclePhase }) => lifecyclePhase ?? NOT_AVAILABLE_LABEL, + }, + ], + [] + ); + return ( + <> + + + {i18n.translate( + 'xpack.profiling.storageExplorer.dataBreakdown.storageDetailsTable.title', + { defaultMessage: 'Indices breakdown' } + )} + + + + + ); +} diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/utils.ts b/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/utils.ts new file mode 100644 index 00000000000000..1c7311471a7701 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/data_breakdown/utils.ts @@ -0,0 +1,34 @@ +/* + * 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 { StorageGroupedIndexNames } from '../../../../common/storage_explorer'; + +export function getGroupedIndexLabel(label: StorageGroupedIndexNames) { + switch (label) { + case 'events': + return i18n.translate('xpack.profiling.storageExplorer.dataBreakdown.chart.samples', { + defaultMessage: 'Samples', + }); + case 'executables': + return i18n.translate('xpack.profiling.storageExplorer.dataBreakdown.chart.executables', { + defaultMessage: 'Executables', + }); + case 'metrics': + return i18n.translate('xpack.profiling.storageExplorer.dataBreakdown.chart.metrics', { + defaultMessage: 'Metrics', + }); + case 'stackframes': + return i18n.translate('xpack.profiling.storageExplorer.dataBreakdown.chart.stackframes', { + defaultMessage: 'Stackframes', + }); + case 'stacktraces': + return i18n.translate('xpack.profiling.storageExplorer.dataBreakdown.chart.stacktraces', { + defaultMessage: 'Stacktraces', + }); + } +} diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/distinct_probabilistic_values_warning.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/distinct_probabilistic_values_warning.tsx new file mode 100644 index 00000000000000..1427eed54de41f --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/distinct_probabilistic_values_warning.tsx @@ -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 { EuiButton, EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { useProfilingDependencies } from '../../components/contexts/profiling_dependencies/use_profiling_dependencies'; + +interface Props { + totalNumberOfDistinctProbabilisticValues: number; +} + +export function DistinctProbabilisticValuesWarning({ + totalNumberOfDistinctProbabilisticValues, +}: Props) { + const { docLinks } = useProfilingDependencies().start.core; + + return ( + + + {i18n.translate( + 'xpack.profiling.storageExplorer.distinctProbabilisticProfilingValues.description', + { + defaultMessage: + 'We recommend using a consistent probabilistic value for each project for more efficient storage, cost management, and to maintain good statistical accuracy.', + } + )} + + + + {i18n.translate( + 'xpack.profiling.storageExplorer.distinctProbabilisticProfilingValues.button', + { defaultMessage: 'Learn how' } + )} + + + ); +} diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/host_breakdown/host_breakdown_chart.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/host_breakdown/host_breakdown_chart.tsx new file mode 100644 index 00000000000000..5388585f536880 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/host_breakdown/host_breakdown_chart.tsx @@ -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 { + AreaSeries, + Axis, + Chart, + niceTimeFormatter, + Position, + ScaleType, + Settings, +} from '@elastic/charts'; +import { asDynamicBytes } from '@kbn/observability-plugin/common'; +import React, { useMemo } from 'react'; +import type { StorageExplorerHostDetailsTimeseries } from '../../../../common/storage_explorer'; +import { useKibanaTimeZoneSetting } from '../../../hooks/use_kibana_timezone_setting'; +import { useProfilingChartsTheme } from '../../../hooks/use_profiling_charts_theme'; + +interface Props { + data?: StorageExplorerHostDetailsTimeseries[]; +} +export function HostBreakdownChart({ data = [] }: Props) { + const { chartsBaseTheme, chartsTheme } = useProfilingChartsTheme(); + const timeZone = useKibanaTimeZoneSetting(); + + const hostBreakdownTimeseries = useMemo(() => { + return ( + data.map(({ hostId, hostName, timeseries }) => { + return { + data: timeseries ?? [], + type: 'area', + title: `${hostName} [${hostId}]`, + }; + }) ?? [] + ); + }, [data]); + + const xValues = hostBreakdownTimeseries.flatMap(({ data: timeseriesData }) => + timeseriesData.map(({ x }) => x) + ); + + const min = Math.min(...xValues); + const max = Math.max(...xValues); + const xFormatter = niceTimeFormatter([min, max]); + + return ( + + + + + {hostBreakdownTimeseries.map((serie) => ( + + ))} + + ); +} diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/host_breakdown/hosts_table.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/host_breakdown/hosts_table.tsx new file mode 100644 index 00000000000000..e72dead84f9091 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/host_breakdown/hosts_table.tsx @@ -0,0 +1,213 @@ +/* + * 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 { + CriteriaWithPagination, + EuiBadge, + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiInMemoryTable, + EuiLink, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { asDynamicBytes, asAbsoluteDateTime } from '@kbn/observability-plugin/common'; +import React, { useMemo, useState } from 'react'; +import { StorageExplorerHostDetails } from '../../../../common/storage_explorer'; +import { LabelWithHint } from '../../../components/label_with_hint'; +import { useProfilingParams } from '../../../hooks/use_profiling_params'; +import { useProfilingRouter } from '../../../hooks/use_profiling_router'; + +interface Props { + data?: StorageExplorerHostDetails[]; + hasDistinctProbabilisticValues: boolean; +} + +const sorting = { + sort: { + field: 'hostName', + direction: 'desc' as const, + }, +}; + +export function HostsTable({ data = [], hasDistinctProbabilisticValues }: Props) { + const { query } = useProfilingParams('/storage-explorer'); + const { rangeFrom, rangeTo } = query; + const profilingRouter = useProfilingRouter(); + const [pagination, setPagination] = useState({ pageIndex: 0 }); + + function onTableChange({ page: { index } }: CriteriaWithPagination) { + setPagination({ pageIndex: index }); + } + + const probabilisticValuesCountPerProjectId = data.reduce>((acc, curr) => { + const projectId = curr.projectId; + const currentCount = acc[projectId] ?? 0; + return { ...acc, [projectId]: currentCount + 1 }; + }, {}); + + const columns: Array> = useMemo( + () => [ + ...(hasDistinctProbabilisticValues + ? [ + { + field: 'distinctProbabilisticWarning', + width: '30', + name: '', + sortable: true, + render: (_, item) => { + if (probabilisticValuesCountPerProjectId[item.projectId] > 1) { + return ( + + + + ); + } + }, + } as EuiBasicTableColumn, + ] + : []), + { + field: 'projectId', + width: '100', + name: i18n.translate('xpack.profiling.storageExplorer.hostsTable.projectId', { + defaultMessage: 'Project ID', + }), + sortable: true, + }, + { + field: 'hostName', + name: ( + + ), + sortable: true, + render: (_, item) => { + return ( + {`${item.hostName} [${item.hostId}]`} + ); + }, + }, + { + field: 'probabilisticValues', + name: i18n.translate('xpack.profiling.storageExplorer.hostsTable.probabilisticValues', { + defaultMessage: 'Probabilistic Profiling values', + }), + sortable: true, + render: (probabilisticValues: StorageExplorerHostDetails['probabilisticValues']) => { + return ( + + {probabilisticValues.map((value, index) => { + return ( + + {value.date ? ( + + 0}> + {value.value} + + + ) : ( + 0}> + {value.value} + + )} + + ); + })} + + ); + }, + }, + { + field: 'totalMetricsSize', + name: i18n.translate('xpack.profiling.storageExplorer.hostsTable.metricsData', { + defaultMessage: 'Metrics data', + }), + sortable: true, + width: '200', + render: (size: StorageExplorerHostDetails['totalMetricsSize']) => asDynamicBytes(size), + }, + { + field: 'totalEventsSize', + name: i18n.translate('xpack.profiling.storageExplorer.hostsTable.samplesData', { + defaultMessage: 'Samples data', + }), + sortable: true, + width: '200', + render: (size: StorageExplorerHostDetails['totalEventsSize']) => asDynamicBytes(size), + }, + { + field: 'totalSize', + name: ( + + ), + sortable: true, + width: '200', + render: (size: StorageExplorerHostDetails['totalSize']) => asDynamicBytes(size), + }, + ], + [ + hasDistinctProbabilisticValues, + probabilisticValuesCountPerProjectId, + profilingRouter, + rangeFrom, + rangeTo, + ] + ); + + return ( + + ); +} diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/host_breakdown/index.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/host_breakdown/index.tsx new file mode 100644 index 00000000000000..a4770c1e2253f1 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/host_breakdown/index.tsx @@ -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 { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { AsyncComponent } from '../../../components/async_component'; +import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies'; +import { useProfilingParams } from '../../../hooks/use_profiling_params'; +import { useTimeRange } from '../../../hooks/use_time_range'; +import { useTimeRangeAsync } from '../../../hooks/use_time_range_async'; +import { HostsTable } from './hosts_table'; +import { HostBreakdownChart } from './host_breakdown_chart'; + +interface Props { + hasDistinctProbabilisticValues: boolean; +} + +export function HostBreakdown({ hasDistinctProbabilisticValues }: Props) { + const { query } = useProfilingParams('/storage-explorer'); + const { rangeFrom, rangeTo, kuery, indexLifecyclePhase } = query; + const timeRange = useTimeRange({ rangeFrom, rangeTo }); + const { + services: { fetchStorageExplorerHostStorageDetails }, + } = useProfilingDependencies(); + + const storageExplorerHostDetailsState = useTimeRangeAsync( + ({ http }) => { + return fetchStorageExplorerHostStorageDetails({ + http, + timeFrom: timeRange.inSeconds.start, + timeTo: timeRange.inSeconds.end, + kuery, + indexLifecyclePhase, + }); + }, + [ + fetchStorageExplorerHostStorageDetails, + timeRange.inSeconds.start, + timeRange.inSeconds.end, + kuery, + indexLifecyclePhase, + ] + ); + + return ( + <> + + + {i18n.translate('xpack.profiling.storageExplorer.hostBreakdown.title', { + defaultMessage: 'Host agent breakdown', + })} + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/index.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/index.tsx new file mode 100644 index 00000000000000..8969a389df01de --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/index.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPanel, + EuiTab, + EuiTabs, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { useProfilingDependencies } from '../../components/contexts/profiling_dependencies/use_profiling_dependencies'; +import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template'; +import { PrimaryProfilingSearchBar } from '../../components/profiling_app_page_template/primary_profiling_search_bar'; +import { AsyncStatus } from '../../hooks/use_async'; +import { useProfilingParams } from '../../hooks/use_profiling_params'; +import { useTimeRange } from '../../hooks/use_time_range'; +import { useTimeRangeAsync } from '../../hooks/use_time_range_async'; +import { DataBreakdown } from './data_breakdown'; +import { DistinctProbabilisticValuesWarning } from './distinct_probabilistic_values_warning'; +import { HostBreakdown } from './host_breakdown'; +import { IndexLifecyclePhaseSelect } from './index_lifecycle_phase_select'; +import { Summary } from './summary'; + +export function StorageExplorerView() { + const { query } = useProfilingParams('/storage-explorer'); + const { rangeFrom, rangeTo, kuery, indexLifecyclePhase } = query; + const timeRange = useTimeRange({ rangeFrom, rangeTo }); + + const [selectedTab, setSelectedTab] = useState<'host_breakdown' | 'data_breakdown'>( + 'host_breakdown' + ); + + const { + services: { fetchStorageExplorerSummary }, + } = useProfilingDependencies(); + + const storageExplorerSummaryState = useTimeRangeAsync( + ({ http }) => { + return fetchStorageExplorerSummary({ + http, + timeFrom: timeRange.inSeconds.start, + timeTo: timeRange.inSeconds.end, + kuery, + indexLifecyclePhase, + }); + }, + [ + fetchStorageExplorerSummary, + timeRange.inSeconds.start, + timeRange.inSeconds.end, + kuery, + indexLifecyclePhase, + ] + ); + + const totalNumberOfDistinctProbabilisticValues = + storageExplorerSummaryState.data?.totalNumberOfDistinctProbabilisticValues || 0; + const hasDistinctProbabilisticValues = totalNumberOfDistinctProbabilisticValues > 1; + + return ( + + + + + + +
    + +
    +
    +
    + {hasDistinctProbabilisticValues && ( + + + + )} + + + + + + { + setSelectedTab('host_breakdown'); + }} + isSelected={selectedTab === 'host_breakdown'} + > + {i18n.translate('xpack.profiling.storageExplorer.tabs.hostBreakdown', { + defaultMessage: 'Host agent breakdown', + })} + + { + setSelectedTab('data_breakdown'); + }} + isSelected={selectedTab === 'data_breakdown'} + > + {i18n.translate('xpack.profiling.storageExplorer.tabs.dataBreakdown', { + defaultMessage: 'Data breakdown', + })} + + + + {selectedTab === 'host_breakdown' ? ( + + + + ) : null} + {selectedTab === 'data_breakdown' ? ( + + + + ) : null} + + + ); +} diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/index_lifecycle_phase_select.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/index_lifecycle_phase_select.tsx new file mode 100644 index 00000000000000..05fd8868ed14a7 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/index_lifecycle_phase_select.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSuperSelect, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { IndexLifecyclePhaseSelectOption } from '../../../common/storage_explorer'; +import { useProfilingParams } from '../../hooks/use_profiling_params'; +import { useProfilingRouter } from '../../hooks/use_profiling_router'; + +// import * as urlHelpers from '../../shared/links/url_helpers'; + +export function IndexLifecyclePhaseSelect() { + const profilingRouter = useProfilingRouter(); + const { query } = useProfilingParams('/storage-explorer'); + const { indexLifecyclePhase } = query; + + const options = [ + { + value: IndexLifecyclePhaseSelectOption.All, + label: i18n.translate('xpack.profiling.storageExplorer.indexLifecyclePhase.all.label', { + defaultMessage: 'All', + }), + description: i18n.translate( + 'xpack.profiling.storageExplorer.indexLifecyclePhase.all.description', + { + defaultMessage: 'Search data in all lifecycle phases.', + } + ), + }, + { + value: IndexLifecyclePhaseSelectOption.Hot, + label: i18n.translate('xpack.profiling.storageExplorer.indexLifecyclePhase.hot.label', { + defaultMessage: 'Hot', + }), + description: i18n.translate( + 'xpack.profiling.storageExplorer.indexLifecyclePhase.hot.description', + { + defaultMessage: 'Holds your most-recent, most-frequently-searched data.', + } + ), + }, + { + value: IndexLifecyclePhaseSelectOption.Warm, + label: i18n.translate('xpack.profiling.storageExplorer.indexLifecyclePhase.warm.label', { + defaultMessage: 'Warm', + }), + description: i18n.translate( + 'xpack.profiling.storageExplorer.indexLifecyclePhase.warm.description', + { + defaultMessage: + 'Holds data from recent weeks. Updates are still allowed, but likely infrequent.', + } + ), + }, + { + value: IndexLifecyclePhaseSelectOption.Cold, + label: i18n.translate('xpack.profiling.storageExplorer.indexLifecyclePhase.cold.label', { + defaultMessage: 'Cold', + }), + description: i18n.translate( + 'xpack.profiling.storageExplorer.indexLifecyclePhase.cold.description', + { + defaultMessage: + 'While still searchable, this tier is typically optimized for lower storage costs rather than search speed.', + } + ), + }, + { + value: IndexLifecyclePhaseSelectOption.Frozen, + label: i18n.translate('xpack.profiling.storageExplorer.indexLifecyclePhase.frozen.label', { + defaultMessage: 'Frozen', + }), + description: i18n.translate( + 'xpack.profiling.storageExplorer.indexLifecyclePhase.frozen.description', + { + defaultMessage: 'Holds data that are no longer being queried, or being queried rarely.', + } + ), + }, + ].map(({ value, label, description }) => ({ + value, + inputDisplay: label, + dropdownDisplay: ( + <> + {label} + +

    {description}

    +
    + + ), + })); + + return ( + { + profilingRouter.push('/storage-explorer', { + path: {}, + query: { ...query, indexLifecyclePhase: value }, + }); + }} + hasDividers + style={{ minWidth: 200 }} + /> + ); +} diff --git a/x-pack/plugins/profiling/public/views/storage_explorer/summary.tsx b/x-pack/plugins/profiling/public/views/storage_explorer/summary.tsx new file mode 100644 index 00000000000000..196e7f68c38009 --- /dev/null +++ b/x-pack/plugins/profiling/public/views/storage_explorer/summary.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel, EuiStat, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { asDynamicBytes } from '@kbn/observability-plugin/common'; +import React from 'react'; +import { StackTracesDisplayOption, TopNType } from '../../../common/stack_traces'; +import { StorageExplorerSummaryAPIResponse } from '../../../common/storage_explorer'; +import { useProfilingDependencies } from '../../components/contexts/profiling_dependencies/use_profiling_dependencies'; +import { LabelWithHint } from '../../components/label_with_hint'; +import { useProfilingParams } from '../../hooks/use_profiling_params'; +import { useProfilingRouter } from '../../hooks/use_profiling_router'; +import { asPercentage } from '../../utils/formatters/as_percentage'; + +interface Props { + data?: StorageExplorerSummaryAPIResponse; + isLoading: boolean; +} + +interface SummaryInfo { + title: string; + value?: string | number; + hint?: string; +} + +export function Summary({ data, isLoading }: Props) { + const { query } = useProfilingParams('/storage-explorer'); + const { rangeFrom, rangeTo, kuery } = query; + const profilingRouter = useProfilingRouter(); + const { + start: { core }, + } = useProfilingDependencies(); + + const summaryInfo: SummaryInfo[] = [ + { + title: i18n.translate('xpack.profiling.storageExplorer.summary.totalData', { + defaultMessage: 'Total data', + }), + value: data?.totalProfilingSizeBytes + ? asDynamicBytes(data?.totalProfilingSizeBytes) + : undefined, + hint: i18n.translate('xpack.profiling.storageExplorer.summary.totalData.hint', { + defaultMessage: + 'Total storage size of all Universal Profiling indices including replicas, ignoring the filter settings.', + }), + }, + { + title: i18n.translate('xpack.profiling.storageExplorer.summary.dailyDataGeneration', { + defaultMessage: 'Daily data generation', + }), + value: data?.dailyDataGenerationBytes + ? asDynamicBytes(data?.dailyDataGenerationBytes) + : undefined, + }, + { + title: i18n.translate('xpack.profiling.storageExplorer.summary.totalDebugSymbolsSize', { + defaultMessage: 'Total debug symbols size', + }), + value: data?.totalSymbolsSizeBytes ? asDynamicBytes(data?.totalSymbolsSizeBytes) : undefined, + hint: i18n.translate('xpack.profiling.storageExplorer.summary.totalDebugSymbolsSize.hint', { + defaultMessage: 'The total sum of private and public debug symbols.', + }), + }, + { + title: i18n.translate('xpack.profiling.storageExplorer.summary.discSpaceUsed', { + defaultMessage: 'Disk space used', + }), + value: data?.diskSpaceUsedPct ? asPercentage(data?.diskSpaceUsedPct) : undefined, + hint: i18n.translate('xpack.profiling.storageExplorer.summary.discSpaceUsed.hint', { + defaultMessage: + 'The percentage of the storage capacity that is currently used by all of the Universal Profiling indices compared to the maximum storage capacity currently configured for Elasticsearch.', + }), + }, + { + title: i18n.translate('xpack.profiling.storageExplorer.summary.numberOfHosts', { + defaultMessage: 'Number of host agents', + }), + value: data?.totalNumberOfHosts, + hint: i18n.translate('xpack.profiling.storageExplorer.summary.numberOfHosts.hint', { + defaultMessage: + 'Total number of Universal Profiling host agents reporting into the deployment.', + }), + }, + ]; + return ( + + + {summaryInfo.map((item, idx) => { + return ( + + + ) : ( + {item.title} + ) + } + titleSize="s" + title={item.value} + isLoading={isLoading} + /> + + ); + })} + + + + + {i18n.translate('xpack.profiling.storageExplorer.summary.universalProfilingLink', { + defaultMessage: 'Go to Universal Profiling', + })} + + + + + {i18n.translate('xpack.profiling.storageExplorer.summary.indexManagement', { + defaultMessage: 'Go to Index Management', + })} + + + + + + + ); +} diff --git a/x-pack/plugins/profiling/server/routes/index.ts b/x-pack/plugins/profiling/server/routes/index.ts index 7940001e264673..a552670dac79d7 100644 --- a/x-pack/plugins/profiling/server/routes/index.ts +++ b/x-pack/plugins/profiling/server/routes/index.ts @@ -19,6 +19,7 @@ import { ProfilingESClient } from '../utils/create_profiling_es_client'; import { registerFlameChartSearchRoute } from './flamechart'; import { registerTopNFunctionsSearchRoute } from './functions'; import { registerSetupRoute } from './setup'; +import { registerStorageExplorerRoute } from './storage_explorer/route'; import { registerTraceEventsTopNContainersSearchRoute, registerTraceEventsTopNDeploymentsSearchRoute, @@ -56,4 +57,5 @@ export function registerRoutes(params: RouteRegisterParameters) { // Setup of Profiling resources, automates the configuration of Universal Profiling // and will show instructions on how to add data registerSetupRoute(params); + registerStorageExplorerRoute(params); } diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/get_daily_data_generation.size.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/get_daily_data_generation.size.ts new file mode 100644 index 00000000000000..afab5be7329c97 --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/get_daily_data_generation.size.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { IndicesStatsIndicesStats } from '@elastic/elasticsearch/lib/api/types'; +import { kqlQuery } from '@kbn/observability-plugin/server'; +import { ProfilingESClient } from '../../utils/create_profiling_es_client'; + +export function getEstimatedSizeForDocumentsInIndex({ + allIndicesStats, + indexName, + numberOfDocs, +}: { + allIndicesStats: Record; + indexName: string; + numberOfDocs: number; +}) { + const indexStats = allIndicesStats[indexName]; + const indexTotalSize = indexStats?.total?.store?.size_in_bytes ?? 0; + const indexTotalDocCount = indexStats?.total?.docs?.count; + + const estimatedSize = indexTotalDocCount + ? (numberOfDocs / indexTotalDocCount) * indexTotalSize + : 0; + + return estimatedSize; +} + +export async function getDailyDataGenerationSize({ + client, + timeFrom, + timeTo, + allIndicesStats, + kuery, +}: { + client: ProfilingESClient; + timeFrom: number; + timeTo: number; + allIndicesStats?: Record; + kuery: string; +}) { + const response = await client.search('profiling_indices_size', { + index: [ + 'profiling-events-*', + 'profiling-stacktraces', + 'profiling-hosts', + 'profiling-metrics', + ].join(), + body: { + query: { + bool: { + filter: { + ...kqlQuery(kuery), + range: { + '@timestamp': { + gte: String(timeFrom), + lt: String(timeTo), + format: 'epoch_second', + }, + }, + }, + }, + }, + aggs: { + indices: { + terms: { + field: '_index', + }, + aggs: { + number_of_documents: { + value_count: { + field: '_index', + }, + }, + }, + }, + }, + }, + }); + + const estimatedIncrementalSize = allIndicesStats + ? response.aggregations?.indices.buckets.reduce((prev, curr) => { + return ( + prev + + getEstimatedSizeForDocumentsInIndex({ + allIndicesStats, + indexName: curr.key as string, + numberOfDocs: curr.number_of_documents.value, + }) + ); + }, 0) ?? 0 + : 0; + + const durationAsDays = (timeTo - timeFrom) / 60 / 60 / 24; + + return { dailyDataGenerationBytes: estimatedIncrementalSize / durationAsDays }; +} diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_breakdown_size_timeseries.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_breakdown_size_timeseries.ts new file mode 100644 index 00000000000000..12adbda75c22fe --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_breakdown_size_timeseries.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kqlQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ProfilingESField } from '../../../common/elasticsearch'; +import { computeBucketWidthFromTimeRangeAndBucketCount } from '../../../common/histogram'; +import { + IndexLifecyclePhaseSelectOption, + indexLifeCyclePhaseToDataTier, + StorageExplorerHostDetailsTimeseries, +} from '../../../common/storage_explorer'; +import { ProfilingESClient } from '../../utils/create_profiling_es_client'; +import { getEstimatedSizeForDocumentsInIndex } from './get_daily_data_generation.size'; +import { allIndices, getIndicesStats } from './get_indices_stats'; +import { getProfilingHostsDetailsById } from './get_profiling_hosts_details_by_id'; + +export async function getHostBreakdownSizeTimeseries({ + client, + timeFrom, + timeTo, + kuery, + indexLifecyclePhase, +}: { + client: ProfilingESClient; + timeFrom: number; + timeTo: number; + kuery: string; + indexLifecyclePhase: IndexLifecyclePhaseSelectOption; +}): Promise { + const bucketWidth = computeBucketWidthFromTimeRangeAndBucketCount(timeFrom, timeTo, 50); + + const [{ indices: allIndicesStats }, response] = await Promise.all([ + getIndicesStats({ client: client.getEsClient(), indices: allIndices }), + client.search('profiling_events_metrics_size', { + index: ['profiling-events-*', 'profiling-metrics'], + body: { + query: { + bool: { + filter: [ + ...kqlQuery(kuery), + { + range: { + [ProfilingESField.Timestamp]: { + gte: String(timeFrom), + lt: String(timeTo), + format: 'epoch_second', + }, + }, + }, + ...(indexLifecyclePhase !== IndexLifecyclePhaseSelectOption.All + ? termQuery('_tier', indexLifeCyclePhaseToDataTier[indexLifecyclePhase]) + : []), + ], + }, + }, + aggs: { + hosts: { + terms: { + field: ProfilingESField.HostID, + }, + aggs: { + storageTimeseries: { + date_histogram: { + field: ProfilingESField.Timestamp, + fixed_interval: `${bucketWidth}s`, + }, + aggs: { + indices: { + terms: { + field: '_index', + size: 500, + }, + }, + }, + }, + }, + }, + }, + }, + }), + ]); + + const hostIds = response.aggregations?.hosts.buckets.map((bucket) => bucket.key as string); + const hostsDetailsMap = hostIds + ? await getProfilingHostsDetailsById({ client, timeFrom, timeTo, kuery, hostIds }) + : {}; + + return ( + response.aggregations?.hosts.buckets.map((bucket) => { + const hostId = bucket.key as string; + const hostDetails = hostsDetailsMap[hostId]; + const timeseries = bucket.storageTimeseries.buckets.map((dateHistogramBucket) => { + const estimatedSize = allIndicesStats + ? dateHistogramBucket.indices.buckets.reduce((prev, curr) => { + return ( + prev + + getEstimatedSizeForDocumentsInIndex({ + allIndicesStats, + indexName: curr.key as string, + numberOfDocs: curr.doc_count, + }) + ); + }, 0) + : 0; + + return { + x: dateHistogramBucket.key, + y: estimatedSize, + }; + }); + + return { + hostId, + hostName: hostDetails.hostName, + timeseries, + }; + }) || [] + ); +} diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_details.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_details.ts new file mode 100644 index 00000000000000..73880c9ad484d9 --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_details.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 { kqlQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ProfilingESField } from '../../../common/elasticsearch'; +import { + IndexLifecyclePhaseSelectOption, + indexLifeCyclePhaseToDataTier, + StorageExplorerHostDetails, +} from '../../../common/storage_explorer'; +import { ProfilingESClient } from '../../utils/create_profiling_es_client'; +import { getEstimatedSizeForDocumentsInIndex } from './get_daily_data_generation.size'; +import { allIndices, getIndicesStats } from './get_indices_stats'; +import { getProfilingHostsDetailsById } from './get_profiling_hosts_details_by_id'; + +const perIndexInitialSize = { events: 0, metrics: 0 }; + +export async function getHostDetails({ + client, + timeFrom, + timeTo, + kuery, + indexLifecyclePhase, +}: { + client: ProfilingESClient; + timeFrom: number; + timeTo: number; + kuery: string; + indexLifecyclePhase: IndexLifecyclePhaseSelectOption; +}): Promise { + const [{ indices: allIndicesStats }, response] = await Promise.all([ + getIndicesStats({ client: client.getEsClient(), indices: allIndices }), + client.search('profiling_events_metrics_details', { + index: ['profiling-events-*', 'profiling-metrics'], + body: { + query: { + bool: { + filter: [ + ...kqlQuery(kuery), + { + range: { + [ProfilingESField.Timestamp]: { + gte: String(timeFrom), + lt: String(timeTo), + format: 'epoch_second', + }, + }, + }, + ...(indexLifecyclePhase !== IndexLifecyclePhaseSelectOption.All + ? termQuery('_tier', indexLifeCyclePhaseToDataTier[indexLifecyclePhase]) + : []), + ], + }, + }, + aggs: { + hosts: { + terms: { + field: ProfilingESField.HostID, + }, + aggs: { + projectIds: { + terms: { + field: 'profiling.project.id', + }, + aggs: { + indices: { + terms: { + field: '_index', + size: 500, + }, + }, + }, + }, + }, + }, + }, + }, + }), + ]); + + const hostIds = response.aggregations?.hosts.buckets.map((bucket) => bucket.key as string); + const hostsDetailsMap = hostIds + ? await getProfilingHostsDetailsById({ client, timeFrom, timeTo, kuery, hostIds }) + : {}; + + return ( + response.aggregations?.hosts.buckets.flatMap((bucket) => { + const hostId = bucket.key as string; + const hostDetails = hostsDetailsMap[hostId]; + + return bucket.projectIds.buckets.map((projectBucket): StorageExplorerHostDetails => { + const totalPerIndex = allIndicesStats + ? projectBucket.indices.buckets.reduce((acc, indexBucket) => { + const indexName = indexBucket.key as string; + const estimatedSize = getEstimatedSizeForDocumentsInIndex({ + allIndicesStats, + indexName, + numberOfDocs: indexBucket.doc_count, + }); + return { + ...acc, + ...(indexName.indexOf('metrics') > 0 + ? { metrics: acc.metrics + estimatedSize } + : { events: acc.events + estimatedSize }), + }; + }, perIndexInitialSize) + : perIndexInitialSize; + const projectId = projectBucket.key as string; + const currentProjectProbabilisticValues = + hostDetails?.probabilisticValuesPerProject?.[projectId]; + + return { + hostId, + hostName: hostDetails?.hostName, + probabilisticValues: currentProjectProbabilisticValues?.probabilisticValues || [], + projectId, + totalEventsSize: totalPerIndex.events, + totalMetricsSize: totalPerIndex.metrics, + totalSize: totalPerIndex.events + totalPerIndex.metrics, + }; + }); + }) || [] + ); +} diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_distinct_probabilistic_count.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_distinct_probabilistic_count.ts new file mode 100644 index 00000000000000..ba8931491d6785 --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/get_host_distinct_probabilistic_count.ts @@ -0,0 +1,107 @@ +/* + * 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 { kqlQuery, termQuery } from '@kbn/observability-plugin/server'; +import { + IndexLifecyclePhaseSelectOption, + indexLifeCyclePhaseToDataTier, +} from '../../../common/storage_explorer'; +import { ProfilingESClient } from '../../utils/create_profiling_es_client'; + +export async function getHostAndDistinctProbabilisticCount({ + client, + timeFrom, + timeTo, + kuery, + indexLifecyclePhase, +}: { + client: ProfilingESClient; + timeFrom: number; + timeTo: number; + kuery: string; + indexLifecyclePhase: IndexLifecyclePhaseSelectOption; +}) { + const response = await client.search('profiling_probabilistic_cardinality', { + index: 'profiling-hosts', + body: { + query: { + bool: { + filter: [ + ...kqlQuery(kuery), + { + range: { + '@timestamp': { + gte: String(timeFrom), + lt: String(timeTo), + format: 'epoch_second', + }, + }, + }, + ...(indexLifecyclePhase !== IndexLifecyclePhaseSelectOption.All + ? termQuery('_tier', indexLifeCyclePhaseToDataTier[indexLifecyclePhase]) + : []), + ], + }, + }, + aggs: { + hostsAndProjectIds: { + multi_terms: { + terms: [{ field: 'host.id' }, { field: 'profiling.project.id' }], + }, + aggs: { + activeProbabilisticValue: { + top_metrics: { + metrics: { + field: 'profiling.agent.config.probabilistic_threshold', + }, + sort: { + '@timestamp': 'desc', + }, + }, + }, + }, + }, + hostCount: { + cardinality: { + field: 'host.id', + }, + }, + }, + }, + }); + + const activeProbabilisticValuesPerProjectId: Record> = {}; + response.aggregations?.hostsAndProjectIds.buckets.forEach((bucket) => { + const projectId = bucket.key[1] as string; + const activeProbabilisticValue = bucket.activeProbabilisticValue.top[0]?.metrics?.[ + 'profiling.agent.config.probabilistic_threshold' + ] as string | undefined; + if (activeProbabilisticValue) { + const currentMap = activeProbabilisticValuesPerProjectId[projectId]; + if (currentMap) { + currentMap.add(activeProbabilisticValue); + } else { + const activeProbabilisticSet = new Set(); + activeProbabilisticSet.add(activeProbabilisticValue); + activeProbabilisticValuesPerProjectId[projectId] = activeProbabilisticSet; + } + } + }); + + let totalNumberOfDistinctProbabilisticValues = 0; + Object.keys(activeProbabilisticValuesPerProjectId).forEach((projectId) => { + const activeProbabilisticValues = activeProbabilisticValuesPerProjectId[projectId]; + if (activeProbabilisticValues.size > 1) { + totalNumberOfDistinctProbabilisticValues += activeProbabilisticValues.size; + } + }); + + return { + totalNumberOfDistinctProbabilisticValues, + totalNumberOfHosts: response.aggregations?.hostCount.value || 0, + }; +} diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/get_indices_stats.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/get_indices_stats.ts new file mode 100644 index 00000000000000..eb10cd30ec40a2 --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/get_indices_stats.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 { ElasticsearchClient } from '@kbn/core/server'; + +export const symbolsIndices = [ + 'profiling-symbols-global', + 'profiling-symbols-private', + 'profiling-executables', + 'profiling-stackframes', + 'profiling-returnpads-private', +]; + +export const stacktracesIndices = [ + 'profiling-events-*', + 'profiling-metrics', + 'profiling-stacktraces', + 'profiling-executables', + 'profiling-stackframes', +]; + +export const allIndices = [ + 'profiling-events-*', + 'profiling-metrics', + 'profiling-stacktraces', + 'profiling-sq-executables', + 'profiling-sq-leafframes', + 'profiling-hosts', + 'profiling-symbols-global', + 'profiling-symbols-private', + 'profiling-executables', + 'profiling-stackframes', + 'profiling-returnpads-private', +]; + +export function getIndicesStats({ + client, + indices, +}: { + client: ElasticsearchClient; + indices: string[]; +}) { + return client.indices.stats({ index: indices.join(), expand_wildcards: 'all' }); +} + +export function getIndicesInfo({ + client, + indices, +}: { + client: ElasticsearchClient; + indices: string[]; +}) { + return client.indices.get({ + index: indices.join(), + filter_path: [ + '*.settings.index.number_of_shards', + '*.settings.index.number_of_replicas', + '*.data_stream', + ], + features: ['settings'], + expand_wildcards: 'all', + }); +} + +export async function getIndicesLifecycleStatus({ + client, + indices, +}: { + client: ElasticsearchClient; + indices: string[]; +}) { + const ilmLifecycle = await client.ilm.explainLifecycle({ + index: indices.join(), + filter_path: 'indices.*.phase', + }); + return ilmLifecycle.indices; +} + +export function getNodesStats({ client }: { client: ElasticsearchClient }) { + return client.nodes.stats({ metric: 'fs', filter_path: 'nodes.*.fs.total.total_in_bytes' }); +} diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/get_profiling_hosts_details_by_id.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/get_profiling_hosts_details_by_id.ts new file mode 100644 index 00000000000000..6e3216dc0fd434 --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/get_profiling_hosts_details_by_id.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { kqlQuery } from '@kbn/observability-plugin/server'; +import { keyBy } from 'lodash'; +import { ProfilingESField } from '../../../common/elasticsearch'; +import { ProfilingESClient } from '../../utils/create_profiling_es_client'; + +interface HostDetails { + hostId: string; + hostName: string; + probabilisticValuesPerProject: Record< + string, + { projectId: string; probabilisticValues: Array<{ value: number; date: number | null }> } + >; +} + +export async function getProfilingHostsDetailsById({ + client, + timeFrom, + timeTo, + kuery, + hostIds, +}: { + client: ProfilingESClient; + timeFrom: number; + timeTo: number; + kuery: string; + hostIds: string[]; +}): Promise> { + const resp = await client.search('get_host_ids_names', { + index: 'profiling-hosts', + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [ProfilingESField.HostID]: hostIds } }, + { + range: { + [ProfilingESField.Timestamp]: { + gte: String(timeFrom), + lt: String(timeTo), + format: 'epoch_second', + }, + }, + }, + ...kqlQuery(kuery), + ], + }, + }, + aggs: { + hostIds: { + terms: { + field: ProfilingESField.HostID, + }, + aggs: { + hostNames: { + top_metrics: { + metrics: { field: 'profiling.host.name' }, + sort: '_score', + }, + }, + projectIds: { + terms: { + field: 'profiling.project.id', + }, + aggs: { + probabilisticValues: { + terms: { + field: 'profiling.agent.config.probabilistic_threshold', + size: 5, + order: { agentFirstStartDate: 'desc' }, + }, + aggs: { + agentFirstStartDate: { + min: { + field: 'profiling.agent.start_time', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + const hostsDetails = + resp.aggregations?.hostIds.buckets.map((bucket): HostDetails => { + const hostId = bucket.key as string; + const hostName = bucket.hostNames.top[0].metrics['profiling.host.name'] as string; + + const probabilisticValuesPerProject = bucket.projectIds.buckets.map((projectIdBucket) => { + const projectId = projectIdBucket.key as string; + const probabilisticValues = projectIdBucket.probabilisticValues.buckets.map( + (probValuesBucket) => { + return { + value: probValuesBucket.key as number, + date: probValuesBucket.agentFirstStartDate.value, + }; + } + ); + return { projectId, probabilisticValues }; + }); + + return { + hostId, + hostName, + probabilisticValuesPerProject: keyBy(probabilisticValuesPerProject, 'projectId'), + }; + }) || []; + + return keyBy(hostsDetails, 'hostId'); +} diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/get_storage_details_grouped_by_index.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/get_storage_details_grouped_by_index.ts new file mode 100644 index 00000000000000..2a4c741a9eaa4f --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/get_storage_details_grouped_by_index.ts @@ -0,0 +1,95 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; +import { groupBy, sumBy } from 'lodash'; +import { + IndexLifecyclePhaseSelectOption, + StorageGroupedIndexNames, +} from '../../../common/storage_explorer'; +import { + getIndicesLifecycleStatus, + getIndicesStats, + stacktracesIndices, +} from './get_indices_stats'; + +function getGroupedIndexName(indexName: string): StorageGroupedIndexNames | undefined { + if (indexName.indexOf('events') > 0) { + return 'events'; + } + + if (indexName.indexOf('stackframes') > 0) { + return 'stackframes'; + } + + if (indexName.indexOf('stacktraces') > 0) { + return 'stacktraces'; + } + + if (indexName.indexOf('executables') > 0) { + return 'executables'; + } + + if (indexName.indexOf('metrics') > 0) { + return 'metrics'; + } +} + +export async function getStorageDetailsGroupedByIndex({ + client, + indexLifecyclePhase, +}: { + client: ElasticsearchClient; + indexLifecyclePhase: IndexLifecyclePhaseSelectOption; +}) { + const [indicesStats, indicesLifecycleStatus] = await Promise.all([ + getIndicesStats({ client, indices: stacktracesIndices }), + getIndicesLifecycleStatus({ client, indices: stacktracesIndices }), + ]); + const indices = indicesStats.indices || {}; + + const groupedIndexStatsMap = groupBy( + Object.keys(indices) + .filter((indexName) => { + const indexLifecycleStatus = indicesLifecycleStatus[indexName]; + const currentIndexLifecyclePhase = + indexLifecycleStatus && 'phase' in indexLifecycleStatus + ? indexLifecycleStatus.phase + : undefined; + if ( + indexLifecyclePhase !== IndexLifecyclePhaseSelectOption.All && + currentIndexLifecyclePhase && + currentIndexLifecyclePhase !== indexLifecyclePhase + ) { + return false; + } + return true; + }) + .map((indexName) => { + const indexStats = indices[indexName]; + const indexDocCount = indexStats.total?.docs?.count || 0; + const indexSizeInBytes = indexStats.total?.store?.size_in_bytes || 0; + + return { + indexName: getGroupedIndexName(indexName), + docCount: indexDocCount, + sizeInBytes: indexSizeInBytes, + }; + }), + 'indexName' + ); + + return Object.keys(groupedIndexStatsMap).map((indexName) => { + const values = groupedIndexStatsMap[indexName]; + const docCount = sumBy(values, 'docCount'); + const sizeInBytes = sumBy(values, 'sizeInBytes'); + return { + indexName, + docCount, + sizeInBytes, + }; + }); +} diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/get_storage_details_per_index.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/get_storage_details_per_index.ts new file mode 100644 index 00000000000000..ca26ec0afc81ee --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/get_storage_details_per_index.ts @@ -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 { ElasticsearchClient } from '@kbn/core/server'; +import { + IndexLifecyclePhaseSelectOption, + StorageDetailsPerIndex, +} from '../../../common/storage_explorer'; +import { + getIndicesLifecycleStatus, + getIndicesStats, + getIndicesInfo, + stacktracesIndices, +} from './get_indices_stats'; + +export async function getStorageDetailsPerIndex({ + client, + indexLifecyclePhase, +}: { + client: ElasticsearchClient; + indexLifecyclePhase: IndexLifecyclePhaseSelectOption; +}): Promise { + const [indicesStats, indicesInfo, indicesLifecycleStatus] = await Promise.all([ + getIndicesStats({ client, indices: stacktracesIndices }), + getIndicesInfo({ client, indices: stacktracesIndices }), + getIndicesLifecycleStatus({ client, indices: stacktracesIndices }), + ]); + + const indices = indicesStats.indices || {}; + + return Object.keys(indices) + .map((indexName) => { + const stats = indices[indexName]; + const indexInfo = indicesInfo[indexName]; + const indexLifecycle = indicesLifecycleStatus[indexName]; + + return { + indexName, + docCount: stats.total?.docs?.count ?? 0, + primaryShardsCount: indexInfo.settings?.index?.number_of_shards as number | undefined, + replicaShardsCount: indexInfo.settings?.index?.number_of_replicas as number | undefined, + sizeInBytes: stats.total?.store?.size_in_bytes ?? 0, + dataStream: indexInfo?.data_stream, + lifecyclePhase: + indexLifecycle && 'phase' in indexLifecycle ? indexLifecycle.phase : undefined, + }; + }) + .filter((item) => { + if ( + indexLifecyclePhase !== IndexLifecyclePhaseSelectOption.All && + item.lifecyclePhase && + item.lifecyclePhase !== indexLifecyclePhase + ) { + return false; + } + return true; + }); +} diff --git a/x-pack/plugins/profiling/server/routes/storage_explorer/route.ts b/x-pack/plugins/profiling/server/routes/storage_explorer/route.ts new file mode 100644 index 00000000000000..2447bfea610112 --- /dev/null +++ b/x-pack/plugins/profiling/server/routes/storage_explorer/route.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 { schema } from '@kbn/config-schema'; +import { sumBy, values } from 'lodash'; +import { RouteRegisterParameters } from '..'; +import { getRoutePaths } from '../../../common'; +import { + IndexLifecyclePhaseSelectOption, + StorageExplorerSummaryAPIResponse, +} from '../../../common/storage_explorer'; +import { getClient } from '../compat'; +import { getDailyDataGenerationSize } from './get_daily_data_generation.size'; +import { getHostBreakdownSizeTimeseries } from './get_host_breakdown_size_timeseries'; +import { getHostDetails } from './get_host_details'; +import { getHostAndDistinctProbabilisticCount } from './get_host_distinct_probabilistic_count'; +import { allIndices, getIndicesStats, getNodesStats, symbolsIndices } from './get_indices_stats'; +import { getStorageDetailsGroupedByIndex } from './get_storage_details_grouped_by_index'; +import { getStorageDetailsPerIndex } from './get_storage_details_per_index'; + +export function registerStorageExplorerRoute({ + router, + services: { createProfilingEsClient }, +}: RouteRegisterParameters) { + const paths = getRoutePaths(); + router.get( + { + path: paths.StorageExplorerSummary, + options: { tags: ['access:profiling'] }, + validate: { + query: schema.object({ + indexLifecyclePhase: schema.oneOf([ + schema.literal(IndexLifecyclePhaseSelectOption.All), + schema.literal(IndexLifecyclePhaseSelectOption.Hot), + schema.literal(IndexLifecyclePhaseSelectOption.Warm), + schema.literal(IndexLifecyclePhaseSelectOption.Cold), + schema.literal(IndexLifecyclePhaseSelectOption.Frozen), + ]), + timeFrom: schema.number(), + timeTo: schema.number(), + kuery: schema.string(), + }), + }, + }, + async (context, request, response) => { + const { timeFrom, timeTo, kuery, indexLifecyclePhase } = request.query; + const client = await getClient(context); + const profilingClient = createProfilingEsClient({ request, esClient: client }); + const profilingEsClient = profilingClient.getEsClient(); + + const [ + totalIndicesStats, + totalSymbolsIndicesStats, + nodeStats, + hostAndDistinctProbabilisticCount, + ] = await Promise.all([ + getIndicesStats({ + client: profilingEsClient, + indices: allIndices, + }), + getIndicesStats({ + client: profilingEsClient, + indices: symbolsIndices, + }), + getNodesStats({ client: profilingEsClient }), + getHostAndDistinctProbabilisticCount({ + client: profilingClient, + timeFrom, + timeTo, + kuery, + indexLifecyclePhase, + }), + ]); + + const { dailyDataGenerationBytes } = await getDailyDataGenerationSize({ + client: profilingClient, + timeFrom, + timeTo, + allIndicesStats: totalIndicesStats.indices, + kuery, + }); + + const { nodes: diskSpacePerNode } = nodeStats; + const { totalNumberOfDistinctProbabilisticValues, totalNumberOfHosts } = + hostAndDistinctProbabilisticCount; + const totalProfilingSizeBytes = totalIndicesStats._all.total?.store?.size_in_bytes ?? 0; + const totalSymbolsSizeBytes = totalSymbolsIndicesStats._all.total?.store?.size_in_bytes ?? 0; + + const totalDiskSpace = sumBy( + values(diskSpacePerNode), + (node) => node?.fs?.total?.total_in_bytes ?? 0 + ); + + const summary: StorageExplorerSummaryAPIResponse = { + totalProfilingSizeBytes, + totalSymbolsSizeBytes, + diskSpaceUsedPct: totalProfilingSizeBytes / totalDiskSpace, + totalNumberOfDistinctProbabilisticValues, + totalNumberOfHosts, + dailyDataGenerationBytes, + }; + + return response.ok({ + body: summary, + }); + } + ); + + router.get( + { + path: paths.StorageExplorerHostStorageDetails, + options: { tags: ['access:profiling'] }, + validate: { + query: schema.object({ + indexLifecyclePhase: schema.oneOf([ + schema.literal(IndexLifecyclePhaseSelectOption.All), + schema.literal(IndexLifecyclePhaseSelectOption.Hot), + schema.literal(IndexLifecyclePhaseSelectOption.Warm), + schema.literal(IndexLifecyclePhaseSelectOption.Cold), + schema.literal(IndexLifecyclePhaseSelectOption.Frozen), + ]), + timeFrom: schema.number(), + timeTo: schema.number(), + kuery: schema.string(), + }), + }, + }, + async (context, request, response) => { + const client = await getClient(context); + const profilingClient = createProfilingEsClient({ request, esClient: client }); + + const { timeFrom, timeTo, kuery, indexLifecyclePhase } = request.query; + const [hostDetailsTimeseries, hostDetails] = await Promise.all([ + getHostBreakdownSizeTimeseries({ + client: profilingClient, + timeFrom, + timeTo, + kuery, + indexLifecyclePhase, + }), + getHostDetails({ + client: profilingClient, + timeFrom, + timeTo, + kuery, + indexLifecyclePhase, + }), + ]); + return response.ok({ body: { hostDetailsTimeseries, hostDetails } }); + } + ); + + router.get( + { + path: paths.StorageExplorerIndicesStorageDetails, + options: { tags: ['access:profiling'] }, + validate: { + query: schema.object({ + indexLifecyclePhase: schema.oneOf([ + schema.literal(IndexLifecyclePhaseSelectOption.All), + schema.literal(IndexLifecyclePhaseSelectOption.Hot), + schema.literal(IndexLifecyclePhaseSelectOption.Warm), + schema.literal(IndexLifecyclePhaseSelectOption.Cold), + schema.literal(IndexLifecyclePhaseSelectOption.Frozen), + ]), + }), + }, + }, + async (context, request, response) => { + const client = await getClient(context); + const profilingClient = createProfilingEsClient({ request, esClient: client }); + const profilingEsClient = profilingClient.getEsClient(); + const { indexLifecyclePhase } = request.query; + + const [storageDetailsGroupedByIndex, storageDetailsPerIndex] = await Promise.all([ + getStorageDetailsGroupedByIndex({ + client: profilingEsClient, + indexLifecyclePhase, + }), + getStorageDetailsPerIndex({ client: profilingEsClient, indexLifecyclePhase }), + ]); + + return response.ok({ body: { storageDetailsGroupedByIndex, storageDetailsPerIndex } }); + } + ); +} diff --git a/x-pack/plugins/remote_clusters/README.md b/x-pack/plugins/remote_clusters/README.md index 1119c98ffe84ac..6440661b69be8b 100644 --- a/x-pack/plugins/remote_clusters/README.md +++ b/x-pack/plugins/remote_clusters/README.md @@ -1,5 +1,13 @@ # Remote Clusters +## Setting up a remote cluster + +* Run `yarn es snapshot --license=trial` and log into kibana +* Create a local copy of the ES snapshot with `cp -R .es/8.10.0 .es/8.10.0-2` +* Start your local copy of the ES snapshot .es/8.10.0-2/bin/elasticsearch -E cluster.name=europe -E transport.port=9400 +* Create a remote cluster using `127.0.0.1:9400` as seed node and proceed with the wizard +* Verify that your newly created remote cluster is shown as connected in the remote clusters list + ## About This plugin helps users manage their [remote clusters](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-remote-clusters.html), which enable cross-cluster search and cross-cluster replication. \ No newline at end of file diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts index 75a1656b0daed6..650d8b50625ab9 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/add/remote_clusters_add.test.ts @@ -179,6 +179,63 @@ describe('Create Remote cluster', () => { }); }); + describe('Setup Trust', () => { + beforeEach(async () => { + await act(async () => { + ({ actions, component } = await setup(httpSetup, { + canUseAPIKeyTrustModel: true, + })); + }); + + component.update(); + + actions.nameInput.setValue('remote_cluster_test'); + actions.seedsInput.setValue('192.168.1.1:3000'); + + await actions.saveButton.click(); + }); + + test('should contain two cards for setting up trust', () => { + // Cards exist + expect(actions.setupTrust.apiCardExist()).toBe(true); + expect(actions.setupTrust.certCardExist()).toBe(true); + // Each card has its doc link + expect(actions.setupTrust.apiCardDocsExist()).toBe(true); + expect(actions.setupTrust.certCardDocsExist()).toBe(true); + }); + + test('on submit should open confirm modal', async () => { + await actions.setupTrust.setupTrustConfirmClick(); + + expect(actions.setupTrust.isSubmitInConfirmDisabled()).toBe(true); + await actions.setupTrust.toggleConfirmSwitch(); + expect(actions.setupTrust.isSubmitInConfirmDisabled()).toBe(false); + }); + + test('back button goes to first step', async () => { + await actions.setupTrust.backToFirstStepClick(); + expect(actions.isOnFirstStep()).toBe(true); + }); + + test('shows only cert based config if API key trust model is not available', async () => { + await act(async () => { + ({ actions, component } = await setup(httpSetup, { + canUseAPIKeyTrustModel: false, + })); + }); + + component.update(); + + actions.nameInput.setValue('remote_cluster_test'); + actions.seedsInput.setValue('192.168.1.1:3000'); + + await actions.saveButton.click(); + + expect(actions.setupTrust.apiCardExist()).toBe(false); + expect(actions.setupTrust.certCardExist()).toBe(true); + }); + }); + describe('on prem', () => { beforeEach(async () => { await act(async () => { diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_actions.ts b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_actions.ts index 3a2d4be3e060dd..f17a790c65041a 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_actions.ts +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_actions.ts @@ -49,10 +49,21 @@ export interface RemoteClustersActions { getLabel: () => string; exists: () => boolean; }; + isOnFirstStep: () => boolean; saveButton: { click: () => void; isDisabled: () => boolean; }; + setupTrust: { + isSubmitInConfirmDisabled: () => boolean; + toggleConfirmSwitch: () => void; + setupTrustConfirmClick: () => void; + backToFirstStepClick: () => void; + apiCardExist: () => boolean; + certCardExist: () => boolean; + apiCardDocsExist: () => boolean; + certCardDocsExist: () => boolean; + }; getErrorMessages: () => string[]; globalErrorExists: () => boolean; } @@ -148,7 +159,7 @@ export const createRemoteClustersActions = (testBed: TestBed): RemoteClustersAct }; }; - const createSaveButtonActions = () => { + const createSetupTrustActions = () => { const click = () => { act(() => { find('remoteClusterFormSaveButton').simulate('click'); @@ -157,7 +168,56 @@ export const createRemoteClustersActions = (testBed: TestBed): RemoteClustersAct component.update(); }; const isDisabled = () => find('remoteClusterFormSaveButton').props().disabled; - return { saveButton: { click, isDisabled } }; + + const setupTrustConfirmClick = () => { + act(() => { + find('setupTrustDoneButton').simulate('click'); + }); + + component.update(); + }; + + const backToFirstStepClick = () => { + act(() => { + find('setupTrustBackButton').simulate('click'); + }); + + component.update(); + }; + + const isOnFirstStep = () => exists('remoteClusterFormNameInput'); + + const toggleConfirmSwitch = () => { + act(() => { + const $checkbox = find('remoteClusterTrustCheckbox'); + const isChecked = $checkbox.props().checked; + $checkbox.simulate('change', { target: { checked: !isChecked } }); + }); + + component.update(); + }; + + const isSubmitInConfirmDisabled = () => find('remoteClusterTrustSubmitButton').props().disabled; + + const apiCardExist = () => exists('setupTrustApiKeyCard'); + const certCardExist = () => exists('setupTrustCertCard'); + const apiCardDocsExist = () => exists('setupTrustApiKeyCardDocs'); + const certCardDocsExist = () => exists('setupTrustCertCardDocs'); + + return { + isOnFirstStep, + saveButton: { click, isDisabled }, + setupTrust: { + setupTrustConfirmClick, + isSubmitInConfirmDisabled, + toggleConfirmSwitch, + apiCardExist, + certCardExist, + apiCardDocsExist, + certCardDocsExist, + backToFirstStepClick, + }, + }; }; const createServerNameActions = () => { @@ -192,7 +252,7 @@ export const createRemoteClustersActions = (testBed: TestBed): RemoteClustersAct ...createCloudUrlInputActions(), ...createProxyAddressActions(), ...createServerNameActions(), - ...createSaveButtonActions(), + ...createSetupTrustActions(), getErrorMessages: form.getErrorsMessages, globalErrorExists, }; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.tsx index 578125ba7b064a..b083d1ea10c8b5 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.tsx @@ -35,6 +35,7 @@ export const WithAppDependencies = isCloudEnabled: !!isCloudEnabled, cloudBaseUrl: 'test.com', executionContext: executionContextServiceMock.createStartContract(), + canUseAPIKeyTrustModel: true, ...overrides, }} > diff --git a/x-pack/plugins/remote_clusters/public/application/app_context.tsx b/x-pack/plugins/remote_clusters/public/application/app_context.tsx index 49679657139aa7..76353539becebc 100644 --- a/x-pack/plugins/remote_clusters/public/application/app_context.tsx +++ b/x-pack/plugins/remote_clusters/public/application/app_context.tsx @@ -12,6 +12,7 @@ export interface Context { isCloudEnabled: boolean; cloudBaseUrl: string; executionContext: ExecutionContextStart; + canUseAPIKeyTrustModel: boolean; } export const AppContext = createContext({} as any); diff --git a/x-pack/plugins/remote_clusters/public/application/index.d.ts b/x-pack/plugins/remote_clusters/public/application/index.d.ts index 26c0721e81b602..e9983689586b3e 100644 --- a/x-pack/plugins/remote_clusters/public/application/index.d.ts +++ b/x-pack/plugins/remote_clusters/public/application/index.d.ts @@ -16,6 +16,7 @@ export declare const renderApp: ( isCloudEnabled: boolean; cloudBaseUrl: string; executionContext: ExecutionContextStart; + canUseAPIKeyTrustModel: boolean; }, history: ScopedHistory, theme$: Observable diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/index.js b/x-pack/plugins/remote_clusters/public/application/sections/components/index.js index 816db5773c3fb9..6fe583f0c5d795 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/index.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/index.js @@ -5,6 +5,7 @@ * 2.0. */ +export { RemoteClusterSetupTrust } from './remote_cluster_setup_trust'; export { RemoteClusterForm } from './remote_cluster_form'; export { RemoteClusterPageTitle } from './remote_cluster_page_title'; export { ConfiguredByNodeWarning } from './configured_by_node_warning'; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.tsx index 77d2db417f904f..fadcd41ce376c1 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.tsx +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.tsx @@ -21,11 +21,9 @@ import { EuiFormRow, EuiLink, EuiLoadingLogo, - EuiLoadingSpinner, EuiOverlayMask, EuiSpacer, EuiSwitch, - EuiText, EuiTitle, EuiDelayRender, EuiScreenReaderOnly, @@ -302,87 +300,67 @@ export class RemoteClusterForm extends Component { } renderActions() { - const { isSaving, cancel } = this.props; + const { isSaving, cancel, cluster: isEditMode } = this.props; const { areErrorsVisible, isRequestVisible } = this.state; + const isSaveDisabled = (areErrorsVisible && this.hasErrors()) || isSaving; - if (isSaving) { - return ( - - - - - + return ( + + {cancel && ( - + - + - - ); - } - - let cancelButton; - - if (cancel) { - cancelButton = ( - - - - - - ); - } - - const isSaveDisabled = areErrorsVisible && this.hasErrors(); + )} - return ( - + + + {isRequestVisible ? ( + + ) : ( + + )} + + + - - {cancelButton} - - - - {isRequestVisible ? ( - - ) : ( - - )} - - ); } @@ -523,20 +501,20 @@ export class RemoteClusterForm extends Component { return ( - + - + } color="danger" - iconType="cross" + iconType="error" /> {messagesToBeRendered} + ); }; @@ -549,6 +527,7 @@ export class RemoteClusterForm extends Component { return ( {this.renderSaveErrorFeedback()} + {this.renderErrors()} { {this.renderSkipUnavailable()} - {this.renderErrors()} - {this.renderActions()} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_setup_trust/confirm_modal.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_setup_trust/confirm_modal.tsx new file mode 100644 index 00000000000000..08ef4377338d16 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_setup_trust/confirm_modal.tsx @@ -0,0 +1,104 @@ +/* + * 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, FormEvent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiButton, + EuiText, + EuiSpacer, + EuiButtonEmpty, + EuiForm, + EuiFormRow, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiCheckbox, + useGeneratedHtmlId, +} from '@elastic/eui'; + +interface ModalProps { + closeModal: () => void; + onSubmit: () => void; +} + +export const ConfirmTrustSetupModal = ({ closeModal, onSubmit }: ModalProps) => { + const [hasSetupTrust, setHasSetupTrust] = useState(false); + const modalFormId = useGeneratedHtmlId({ prefix: 'modalForm' }); + const checkBoxId = useGeneratedHtmlId({ prefix: 'checkBoxId' }); + + const onFormSubmit = (e: FormEvent) => { + e.preventDefault(); + closeModal(); + onSubmit(); + }; + + return ( + + + + + + + + + +

    + +

    +
    + + + + + + setHasSetupTrust(!hasSetupTrust)} + data-test-subj="remoteClusterTrustCheckbox" + /> + + +
    + + + + + + + + + + +
    + ); +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_setup_trust/index.ts b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_setup_trust/index.ts new file mode 100644 index 00000000000000..3286c22c7c17cf --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_setup_trust/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 { RemoteClusterSetupTrust } from './remote_cluster_setup_trust'; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_setup_trust/remote_cluster_setup_trust.tsx b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_setup_trust/remote_cluster_setup_trust.tsx new file mode 100644 index 00000000000000..b1531425f1bca8 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_setup_trust/remote_cluster_setup_trust.tsx @@ -0,0 +1,197 @@ +/* + * 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, useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiSpacer, + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; + +import * as docs from '../../../services/documentation'; +import { AppContext } from '../../../app_context'; +import { ConfirmTrustSetupModal } from './confirm_modal'; + +const MIN_ALLOWED_VERSION_API_KEYS_METHOD = '8.10'; +const CARD_MAX_WIDTH = 400; +const i18nTexts = { + apiKeyTitle: i18n.translate( + 'xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.title', + { defaultMessage: 'API keys' } + ), + apiKeyBadge: i18n.translate( + 'xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.badge', + { defaultMessage: 'BETA' } + ), + apiKeyDescription: i18n.translate( + 'xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.description', + { + defaultMessage: + 'Fine-grained access to remote indices. You need an API key provided by the remote cluster administrator.', + } + ), + certTitle: i18n.translate('xpack.remoteClusters.clusterWizard.trustStep.setupWithCert.title', { + defaultMessage: 'Certificates', + }), + certDescription: i18n.translate( + 'xpack.remoteClusters.clusterWizard.trustStep.setupWithCert.description', + { + defaultMessage: + 'Full access to the remote cluster. You need TLS certificates from the remote cluster.', + } + ), +}; + +const docLinks = { + cert: docs.onPremSetupTrustWithCertUrl, + apiKey: docs.onPremSetupTrustWithApiKeyUrl, + cloud: docs.cloudSetupTrustUrl, +}; + +interface Props { + onBack: () => void; + onSubmit: () => void; + isSaving: boolean; +} + +export const RemoteClusterSetupTrust = ({ onBack, onSubmit, isSaving }: Props) => { + const [isModalVisible, setIsModalVisible] = useState(false); + const { canUseAPIKeyTrustModel, isCloudEnabled } = useContext(AppContext); + + return ( +
    + +

    + , + }} + /> +

    +
    + + + + + {canUseAPIKeyTrustModel && ( + + + +

    {i18nTexts.apiKeyDescription}

    +
    + + + + + + +

    + +

    +
    +
    +
    + )} + + + + + {i18nTexts.certTitle} + + } + paddingSize="l" + data-test-subj="setupTrustCertCard" + > + +

    {i18nTexts.certDescription}

    +
    + + + + +
    +
    +
    + + + + + + + + + + + + + + + + + setIsModalVisible(true)} + > + + + + + + + {isModalVisible && ( + setIsModalVisible(false)} onSubmit={onSubmit} /> + )} + +
    + ); +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js index 7d56902ab10fd2..50b38af10d9480 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js @@ -8,11 +8,13 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiPageSection, EuiPageBody } from '@elastic/eui'; import { extractQueryParams } from '../../../shared_imports'; import { getRouter, redirect } from '../../services'; import { setBreadcrumbs } from '../../services/breadcrumb'; -import { RemoteClusterPageTitle, RemoteClusterForm } from '../components'; +import { RemoteClusterPageTitle } from '../components'; +import { RemoteClusterWizard } from './wizard_form'; export class RemoteClusterAdd extends PureComponent { static propTypes = { @@ -35,7 +37,7 @@ export class RemoteClusterAdd extends PureComponent { this.props.addCluster(clusterConfig); }; - cancel = () => { + redirectToList = () => { const { history, route: { @@ -56,29 +58,31 @@ export class RemoteClusterAdd extends PureComponent { const { isAddingCluster, addClusterError } = this.props; return ( - <> - - } - description={ - - } - /> + + + + } + description={ + + } + /> - - + + + ); } } diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/wizard_form.tsx b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/wizard_form.tsx new file mode 100644 index 00000000000000..267ecddf977733 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/wizard_form.tsx @@ -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 React, { useState, useMemo, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiStepsHorizontal, EuiStepStatus, EuiSpacer, EuiPageSection } from '@elastic/eui'; + +import { RemoteClusterSetupTrust, RemoteClusterForm } from '../components'; +import { Cluster } from '../../../../common/lib/cluster_serialization'; + +const CONFIGURE_CONNECTION = 1; +const SETUP_TRUST = 2; + +interface Props { + saveRemoteClusterConfig: (config: Cluster) => void; + onCancel: () => void; + addClusterError: { message: string } | undefined; + isSaving: boolean; +} + +export const RemoteClusterWizard = ({ + saveRemoteClusterConfig, + onCancel, + isSaving, + addClusterError, +}: Props) => { + const [formState, setFormState] = useState(); + const [currentStep, setCurrentStep] = useState(CONFIGURE_CONNECTION); + + // If there was an error saving the cluster, we need + // to send the user back to the first step. + useEffect(() => { + if (addClusterError) { + setCurrentStep(CONFIGURE_CONNECTION); + } + }, [addClusterError, setCurrentStep]); + + const stepDefinitions = useMemo( + () => [ + { + step: CONFIGURE_CONNECTION, + title: i18n.translate('xpack.remoteClusters.clusterWizard.addConnectionInfoLabel', { + defaultMessage: 'Add connection information', + }), + status: (currentStep === CONFIGURE_CONNECTION ? 'current' : 'complete') as EuiStepStatus, + onClick: () => setCurrentStep(CONFIGURE_CONNECTION), + }, + { + step: SETUP_TRUST, + title: i18n.translate('xpack.remoteClusters.clusterWizard.setupTrustLabel', { + defaultMessage: 'Establish trust', + }), + status: (currentStep === SETUP_TRUST ? 'current' : 'incomplete') as EuiStepStatus, + disabled: !formState, + onClick: () => setCurrentStep(SETUP_TRUST), + }, + ], + [currentStep, formState, setCurrentStep] + ); + + // Upon finalizing configuring the connection, we need to temporarily store the + // cluster configuration so that we can persist it when the user completes the + // trust step. + const completeConfigStep = (clusterConfig: Cluster) => { + setFormState(clusterConfig); + setCurrentStep(SETUP_TRUST); + }; + + const completeTrustStep = () => { + saveRemoteClusterConfig(formState as Cluster); + }; + + return ( + + + + + {/* + Instead of unmounting the Form, we toggle its visibility not to lose the form + state when moving to the next step. + */} +
    + +
    + + {currentStep === SETUP_TRUST && ( + setCurrentStep(CONFIGURE_CONNECTION)} + onSubmit={completeTrustStep} + isSaving={isSaving} + /> + )} +
    + ); +}; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js index 5a938e7b4bd29c..02ae9743806834 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js @@ -12,8 +12,9 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiCallOut, - EuiEmptyPrompt, - EuiPageContent_Deprecated as EuiPageContent, + EuiPageTemplate, + EuiPageSection, + EuiPageBody, EuiSpacer, } from '@elastic/eui'; @@ -95,22 +96,23 @@ export class RemoteClusterEdit extends Component { if (isLoading) { return ( - + - + ); } if (!cluster) { return ( - - + } /> - + ); } @@ -150,8 +152,8 @@ export class RemoteClusterEdit extends Component { if (isConfiguredByNode) { return ( - - + @@ -179,50 +181,52 @@ export class RemoteClusterEdit extends Component { } /> - + ); } return ( - <> - - } - /> + + + + } + /> - {hasDeprecatedProxySetting ? ( - <> - + + } + color="warning" + iconType="help" + > - } - color="warning" - iconType="help" - > - - - - - ) : null} - - - + + + + ) : null} + + + + ); } } diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js index 73dc09898ba2c6..5cdbfe596135f9 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js @@ -9,28 +9,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconTip, EuiText } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { SNIFF_MODE, PROXY_MODE } from '../../../../../../common/constants'; export function ConnectionStatus({ isConnected, mode }) { - let icon; - let message; - - if (isConnected) { - icon = ; - - message = i18n.translate('xpack.remoteClusters.connectedStatus.connectedAriaLabel', { - defaultMessage: 'Connected', - }); - } else { - icon = ; - - message = i18n.translate('xpack.remoteClusters.connectedStatus.notConnectedAriaLabel', { - defaultMessage: 'Not connected', - }); - } - const seedNodeTooltip = i18n.translate( 'xpack.remoteClusters.connectedStatus.notConnectedToolTip', { @@ -41,15 +24,18 @@ export function ConnectionStatus({ isConnected, mode }) { return ( - - {icon} - - - - - - {message} - + + {isConnected + ? i18n.translate('xpack.remoteClusters.connectedStatus.connectedAriaLabel', { + defaultMessage: 'Connected', + }) + : i18n.translate('xpack.remoteClusters.connectedStatus.notConnectedAriaLabel', { + defaultMessage: 'Not connected', + })} + {!isConnected && mode === SNIFF_MODE && ( diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js index 782054caa82e3a..06948e25a05838 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js @@ -12,12 +12,15 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiButtonEmpty, - EuiEmptyPrompt, EuiLoadingLogo, EuiOverlayMask, - EuiPageContent_Deprecated as EuiPageContent, EuiSpacer, EuiPageHeader, + EuiPageSection, + EuiPageBody, + EuiPageTemplate, + EuiTitle, + EuiLink, } from '@elastic/eui'; import { remoteClustersUrl } from '../../services/documentation'; @@ -90,9 +93,10 @@ export class RemoteClusterList extends Component { renderNoPermission() { return ( - - + } /> - + ); } @@ -120,9 +124,10 @@ export class RemoteClusterList extends Component { const { statusCode, error: errorString } = error.body; return ( - - + } /> - + ); } renderEmpty() { return ( - - + } + footer={ + <> + + + + + {' '} + + + + + } /> - + ); } renderLoading() { return ( - @@ -196,7 +220,7 @@ export class RemoteClusterList extends Component { defaultMessage="Loading remote clusters…" /> - + ); } @@ -204,35 +228,37 @@ export class RemoteClusterList extends Component { const { clusters } = this.props; return ( - <> - - } - rightSideItems={[ - + + + - , - ]} - /> + } + rightSideItems={[ + + + , + ]} + /> - + - - - + + + + ); } @@ -256,7 +282,6 @@ export class RemoteClusterList extends Component { } else { content = this.renderList(); } - return ( <> {content} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js index 7d83fe51a35235..9d743df2410f10 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js @@ -235,7 +235,7 @@ export class RemoteClusterTable extends Component { name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.addressesColumnTitle', { defaultMessage: 'Addresses', }), - dataTestSubj: 'remoteClustersAddress', + 'data-test-subj': 'remoteClustersAddress', truncateText: true, render: (mode, { seeds, proxyAddress }) => { const clusterAddressString = mode === PROXY_MODE ? proxyAddress : seeds.join(', '); @@ -259,6 +259,7 @@ export class RemoteClusterTable extends Component { ), sortable: true, width: '160px', + align: 'right', render: (mode, { connectedNodesCount, connectedSocketsCount }) => { const remoteNodesCount = mode === PROXY_MODE ? connectedSocketsCount : connectedNodesCount; diff --git a/x-pack/plugins/remote_clusters/public/application/services/documentation.ts b/x-pack/plugins/remote_clusters/public/application/services/documentation.ts index df6f570c14ccae..1436acaa980f8f 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/documentation.ts +++ b/x-pack/plugins/remote_clusters/public/application/services/documentation.ts @@ -12,6 +12,9 @@ export let remoteClustersUrl: string; export let transportPortUrl: string; export let proxyModeUrl: string; export let proxySettingsUrl: string; +export let onPremSetupTrustWithCertUrl: string; +export let onPremSetupTrustWithApiKeyUrl: string; +export let cloudSetupTrustUrl: string; export function init({ links }: DocLinksStart): void { skippingDisconnectedClustersUrl = links.ccs.skippingDisconnectedClusters; @@ -19,4 +22,7 @@ export function init({ links }: DocLinksStart): void { transportPortUrl = links.elasticsearch.transportSettings; proxyModeUrl = links.elasticsearch.remoteClustersProxy; proxySettingsUrl = links.elasticsearch.remoteClusersProxySettings; + onPremSetupTrustWithCertUrl = links.elasticsearch.remoteClustersOnPremSetupTrustWithCert; + onPremSetupTrustWithApiKeyUrl = links.elasticsearch.remoteClustersOnPremSetupTrustWithApiKey; + cloudSetupTrustUrl = links.elasticsearch.remoteClustersCloudSetupTrust; } diff --git a/x-pack/plugins/remote_clusters/public/plugin.ts b/x-pack/plugins/remote_clusters/public/plugin.ts index aad03252191e2a..fa285fccfa4499 100644 --- a/x-pack/plugins/remote_clusters/public/plugin.ts +++ b/x-pack/plugins/remote_clusters/public/plugin.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Plugin, CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import { Subscription } from 'rxjs'; import { PLUGIN } from '../common/constants'; import { init as initBreadcrumbs } from './application/services/breadcrumb'; @@ -27,6 +28,9 @@ export class RemoteClustersUIPlugin { constructor(private readonly initializerContext: PluginInitializerContext) {} + private canUseApiKeyTrustModel: boolean = false; + private licensingSubscription?: Subscription; + setup( { notifications: { toasts }, http, getStartServices }: CoreSetup, { management, usageCollection, cloud, share }: Dependencies @@ -70,7 +74,12 @@ export class RemoteClustersUIPlugin const unmountAppCallback = await renderApp( element, i18nContext, - { isCloudEnabled, cloudBaseUrl, executionContext }, + { + isCloudEnabled, + cloudBaseUrl, + executionContext, + canUseAPIKeyTrustModel: this.canUseApiKeyTrustModel, + }, history, theme$ ); @@ -94,7 +103,7 @@ export class RemoteClustersUIPlugin }; } - start({ application }: CoreStart) { + start({ application }: CoreStart, { licensing }: Dependencies) { const { ui: { enabled: isRemoteClustersUiEnabled }, } = this.initializerContext.config.get(); @@ -102,7 +111,13 @@ export class RemoteClustersUIPlugin if (isRemoteClustersUiEnabled) { initRedirect(application.navigateToApp); } + + this.licensingSubscription = licensing.license$.subscribe((next) => { + this.canUseApiKeyTrustModel = next.hasAtLeast('enterprise'); + }); } - stop() {} + stop() { + this.licensingSubscription?.unsubscribe(); + } } diff --git a/x-pack/plugins/remote_clusters/public/types.ts b/x-pack/plugins/remote_clusters/public/types.ts index 30fe29e41ff64f..5bf03ecdde3696 100644 --- a/x-pack/plugins/remote_clusters/public/types.ts +++ b/x-pack/plugins/remote_clusters/public/types.ts @@ -11,12 +11,14 @@ import { RegisterManagementAppArgs } from '@kbn/management-plugin/public'; import { SharePluginSetup } from '@kbn/share-plugin/public'; import { I18nStart } from '@kbn/core/public'; import { CloudSetup } from '@kbn/cloud-plugin/public'; +import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; export interface Dependencies { management: ManagementSetup; usageCollection: UsageCollectionSetup; cloud: CloudSetup; share: SharePluginSetup; + licensing: LicensingPluginStart; } export interface ClientConfigType { diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index 0ce9d8e3ea2025..16781a17962c95 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -132,7 +132,7 @@ interface SingleSearchAfterAndAudit { aggs?: Record | undefined; index?: string; _source?: string[] | undefined; - track_total_hits?: boolean | undefined; + track_total_hits?: boolean | number; size?: number | undefined; operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get; sort?: estypes.SortOptions[] | undefined; @@ -966,7 +966,7 @@ export class AlertsClient { search_after?: Array; size?: number; sort?: estypes.SortOptions[]; - track_total_hits?: boolean; + track_total_hits?: boolean | number; _source?: string[]; }) { try { diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx index c09eb20328c49f..933eddbaae29bb 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx @@ -57,7 +57,12 @@ describe('SecurityNavControl', () => { it('should render an avatar when user profile has loaded', async () => { const wrapper = shallow( - + ); expect(useUserProfileMock).toHaveBeenCalledTimes(1); @@ -106,7 +111,12 @@ describe('SecurityNavControl', () => { }); const wrapper = shallow( - + ); expect(useUserProfileMock).toHaveBeenCalledTimes(1); @@ -134,7 +144,12 @@ describe('SecurityNavControl', () => { it('should open popover when avatar is clicked', async () => { const wrapper = shallow( - + ); act(() => { @@ -154,7 +169,12 @@ describe('SecurityNavControl', () => { }); const wrapper = shallow( - + ); act(() => { @@ -186,6 +206,7 @@ describe('SecurityNavControl', () => { }, ]) } + buildFlavour={'traditional'} /> ); @@ -290,6 +311,7 @@ describe('SecurityNavControl', () => { }, ]) } + buildFlavour={'traditional'} /> ); @@ -352,6 +374,73 @@ describe('SecurityNavControl', () => { `); }); + it('should render `Close project` link when in Serverless', async () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiContextMenu).prop('panels')).toMatchInlineSnapshot(` + Array [ + Object { + "content": , + "name": , + "onClick": [Function], + }, + Object { + "content": undefined, + "data-test-subj": "userMenuLink__link1", + "href": "path-to-link-1", + "icon": , + "name": "link1", + }, + Object { + "data-test-subj": "logoutLink", + "href": "", + "icon": , + "name": , + }, + ] + } + />, + "id": 0, + "title": "full name", + }, + ] + `); + }); + it('should render anonymous user', async () => { useUserProfileMock.mockReturnValue({ loading: false, @@ -367,7 +456,12 @@ describe('SecurityNavControl', () => { }); const wrapper = shallow( - + ); expect(wrapper.find(EuiContextMenu).prop('panels')).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx index 13bcb3bcb43419..b2f05f9c6d568f 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx @@ -20,6 +20,7 @@ import React, { Fragment, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import type { Observable } from 'rxjs'; +import type { BuildFlavor } from '@kbn/config/src/types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { UserAvatar, type UserProfileAvatarData } from '@kbn/user-profile-components'; @@ -72,12 +73,14 @@ interface SecurityNavControlProps { editProfileUrl: string; logoutUrl: string; userMenuLinks$: Observable; + buildFlavour: BuildFlavor; } export const SecurityNavControl: FunctionComponent = ({ editProfileUrl, logoutUrl, userMenuLinks$, + buildFlavour, }) => { const userMenuLinks = useObservable(userMenuLinks$, []); const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -157,6 +160,11 @@ export const SecurityNavControl: FunctionComponent = ({ id="xpack.security.navControlComponent.loginLinkText" defaultMessage="Log in" /> + ) : buildFlavour === 'serverless' ? ( + ) : ( { const license$ = new BehaviorSubject(validLicense); const coreStart = coreMock.createStart(); - const navControlService = new SecurityNavControlService(); + const navControlService = new SecurityNavControlService('traditional'); navControlService.setup({ securityLicense: new SecurityLicenseService().setup({ license$ }).license, logoutUrl: '/some/logout/url', @@ -128,7 +128,7 @@ describe('SecurityNavControlService', () => { const license$ = new BehaviorSubject({} as ILicense); const coreStart = coreMock.createStart(); - const navControlService = new SecurityNavControlService(); + const navControlService = new SecurityNavControlService('traditional'); navControlService.setup({ securityLicense: new SecurityLicenseService().setup({ license$ }).license, logoutUrl: '/some/logout/url', @@ -148,7 +148,7 @@ describe('SecurityNavControlService', () => { const license$ = new BehaviorSubject(validLicense); const coreStart = coreMock.createStart(); - const navControlService = new SecurityNavControlService(); + const navControlService = new SecurityNavControlService('traditional'); navControlService.setup({ securityLicense: new SecurityLicenseService().setup({ license$ }).license, logoutUrl: '/some/logout/url', @@ -165,7 +165,7 @@ describe('SecurityNavControlService', () => { const license$ = new BehaviorSubject(validLicense); const coreStart = coreMock.createStart(); - const navControlService = new SecurityNavControlService(); + const navControlService = new SecurityNavControlService('traditional'); navControlService.setup({ securityLicense: new SecurityLicenseService().setup({ license$ }).license, logoutUrl: '/some/logout/url', @@ -187,7 +187,7 @@ describe('SecurityNavControlService', () => { const license$ = new BehaviorSubject(validLicense); const coreStart = coreMock.createStart(); - const navControlService = new SecurityNavControlService(); + const navControlService = new SecurityNavControlService('traditional'); navControlService.setup({ securityLicense: new SecurityLicenseService().setup({ license$ }).license, logoutUrl: '/some/logout/url', @@ -210,7 +210,7 @@ describe('SecurityNavControlService', () => { const coreSetup = coreMock.createSetup(); const license$ = new BehaviorSubject({} as ILicense); - navControlService = new SecurityNavControlService(); + navControlService = new SecurityNavControlService('traditional'); navControlService.setup({ securityLicense: new SecurityLicenseService().setup({ license$ }).license, logoutUrl: '/some/logout/url', diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx index de87a5ea166259..0bcc3a58263fb7 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx @@ -13,6 +13,7 @@ import type { Observable, Subscription } from 'rxjs'; import { BehaviorSubject, ReplaySubject } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; +import type { BuildFlavor } from '@kbn/config/src/types'; import type { CoreStart, CoreTheme } from '@kbn/core/public'; import { I18nProvider } from '@kbn/i18n-react'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; @@ -60,6 +61,8 @@ export class SecurityNavControlService { private readonly stop$ = new ReplaySubject(1); private userMenuLinks$ = new BehaviorSubject([]); + constructor(private readonly buildFlavor: BuildFlavor) {} + public setup({ securityLicense, logoutUrl, securityApiClients }: SetupDeps) { this.securityLicense = securityLicense; this.logoutUrl = logoutUrl; @@ -133,6 +136,7 @@ export class SecurityNavControlService { editProfileUrl={core.http.basePath.prepend('/security/account')} logoutUrl={this.logoutUrl} userMenuLinks$={this.userMenuLinks$} + buildFlavour={this.buildFlavor} /> , element diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index 2c9c4025129e4c..eb5b2723f9eabf 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -68,7 +68,7 @@ export class SecurityPlugin private readonly config: ConfigType; private sessionTimeout?: SessionTimeout; private readonly authenticationService = new AuthenticationService(); - private readonly navControlService = new SecurityNavControlService(); + private readonly navControlService; private readonly securityLicenseService = new SecurityLicenseService(); private readonly managementService = new ManagementService(); private readonly securityCheckupService: SecurityCheckupService; @@ -80,6 +80,9 @@ export class SecurityPlugin constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); this.securityCheckupService = new SecurityCheckupService(this.config, localStorage); + this.navControlService = new SecurityNavControlService( + initializerContext.env.packageInfo.buildFlavor + ); } public setup( diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts index 7f79b8c7d54ba3..4e81f0e4a5f138 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts @@ -78,6 +78,7 @@ describe('AuthenticationService', () => { applicationName: 'kibana-.kibana'; kibanaFeatures: []; isElasticCloudDeployment: jest.Mock; + customLogoutURL?: string; }; beforeEach(() => { logger = loggingSystemMock.createLogger(); @@ -121,6 +122,7 @@ describe('AuthenticationService', () => { applicationName: 'kibana-.kibana', kibanaFeatures: [], isElasticCloudDeployment: jest.fn().mockReturnValue(false), + customLogoutURL: 'https://some-logout-origin/logout', }; (mockStartAuthenticationParams.http.basePath.get as jest.Mock).mockImplementation( () => mockStartAuthenticationParams.http.basePath.serverBasePath diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index 171b7ae4212b37..a26ac8943ee78a 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -57,6 +57,7 @@ interface AuthenticationServiceStartParams { applicationName: string; kibanaFeatures: KibanaFeature[]; isElasticCloudDeployment: () => boolean; + customLogoutURL?: string; } export interface InternalAuthenticationServiceStart extends AuthenticationServiceStart { @@ -328,6 +329,7 @@ export class AuthenticationService { applicationName, kibanaFeatures, isElasticCloudDeployment, + customLogoutURL, }: AuthenticationServiceStartParams): InternalAuthenticationServiceStart { const apiKeys = new APIKeys({ clusterClient, @@ -368,6 +370,7 @@ export class AuthenticationService { license: this.license, session, isElasticCloudDeployment, + customLogoutURL, }); return { diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index f0e1479362fdf4..2de2fdbf4df2ad 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -61,11 +61,13 @@ function getMockOptions({ http = {}, selector, accessAgreementMessage, + customLogoutURL, }: { providers?: Record | string[]; http?: Partial; selector?: AuthenticatorOptions['config']['authc']['selector']; accessAgreementMessage?: string; + customLogoutURL?: string; } = {}) { const auditService = auditServiceMock.create(); auditLogger = auditLoggerMock.create(); @@ -95,6 +97,7 @@ function getMockOptions({ featureUsageService: securityFeatureUsageServiceMock.createStartContract(), userProfileService: userProfileServiceMock.createStart(), isElasticCloudDeployment: jest.fn().mockReturnValue(false), + customLogoutURL, }; } @@ -249,6 +252,33 @@ describe('Authenticator', () => { '/mock-server-basepath/security/logged_out?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED' ); }); + + it('points to a custom URL if `customLogoutURL` is specified', () => { + const authenticationProviderMock = + jest.requireMock(`./providers/saml`).SAMLAuthenticationProvider; + authenticationProviderMock.mockClear(); + new Authenticator( + getMockOptions({ + selector: { enabled: false }, + providers: { saml: { saml1: { order: 0, realm: 'realm' } } }, + customLogoutURL: 'https://some-logout-origin/logout', + }) + ); + const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut; + + expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe( + 'https://some-logout-origin/logout' + ); + + // We don't forward any Kibana specific query string parameters to the external logout URL. + expect( + getLoggedOutURL( + httpServerMock.createKibanaRequest({ + query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' }, + }) + ) + ).toBe('https://some-logout-origin/logout'); + }); }); describe('HTTP authentication provider', () => { diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 3d1ffebabb5133..24329c0e7575f2 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -97,6 +97,7 @@ export interface AuthenticatorOptions { session: PublicMethodsOf; getServerBaseURL: () => string; isElasticCloudDeployment: () => boolean; + customLogoutURL?: string; } /** @internal */ @@ -1013,6 +1014,10 @@ export class Authenticator { * provider in the chain (default) is assumed. */ private getLoggedOutURL(request: KibanaRequest, providerType?: string) { + if (this.options.customLogoutURL) { + return this.options.customLogoutURL; + } + // The app that handles logout needs to know the reason of the logout and the URL we may need to // redirect user to once they log in again (e.g. when session expires). const searchParams = new URLSearchParams(); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index a022fdd418e6e4..4f36c0bf508d05 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -408,6 +408,12 @@ export class SecurityPlugin this.userProfileStart = this.userProfileService.start({ clusterClient, session }); this.userSettingServiceStart = this.userSettingService.start(this.userProfileStart); + // In serverless, we want to redirect users to the list of projects instead of standard "Logged Out" page. + const customLogoutURL = + this.initializerContext.env.packageInfo.buildFlavor === 'serverless' + ? cloud?.projectsUrl + : undefined; + const config = this.getConfig(); this.authenticationStart = this.authenticationService.start({ audit: this.auditSetup!, @@ -421,6 +427,7 @@ export class SecurityPlugin applicationName: this.authorizationSetup!.applicationName, kibanaFeatures: features.getKibanaFeatures(), isElasticCloudDeployment: () => cloud?.isCloudEnabled === true, + customLogoutURL, }); this.authorizationService.start({ diff --git a/x-pack/plugins/security_solution/common/api/endpoint/metadata/list_metadata_route.ts b/x-pack/plugins/security_solution/common/api/endpoint/metadata/list_metadata_route.ts index 44b946faf86239..2a0ea7c0fbac11 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/metadata/list_metadata_route.ts +++ b/x-pack/plugins/security_solution/common/api/endpoint/metadata/list_metadata_route.ts @@ -8,7 +8,7 @@ import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; import { ENDPOINT_DEFAULT_PAGE, ENDPOINT_DEFAULT_PAGE_SIZE } from '../../../endpoint/constants'; -import { HostStatus } from '../../../endpoint/types'; +import { HostStatus, EndpointSortableField } from '../../../endpoint/types'; export const GetMetadataListRequestSchema = { query: schema.object( @@ -16,6 +16,20 @@ export const GetMetadataListRequestSchema = { page: schema.number({ defaultValue: ENDPOINT_DEFAULT_PAGE, min: 0 }), pageSize: schema.number({ defaultValue: ENDPOINT_DEFAULT_PAGE_SIZE, min: 1, max: 10000 }), kuery: schema.maybe(schema.string()), + sortField: schema.maybe( + schema.oneOf([ + schema.literal(EndpointSortableField.ENROLLED_AT.toString()), + schema.literal(EndpointSortableField.HOSTNAME.toString()), + schema.literal(EndpointSortableField.HOST_STATUS.toString()), + schema.literal(EndpointSortableField.POLICY_NAME.toString()), + schema.literal(EndpointSortableField.POLICY_STATUS.toString()), + schema.literal(EndpointSortableField.HOST_OS_NAME.toString()), + schema.literal(EndpointSortableField.HOST_IP.toString()), + schema.literal(EndpointSortableField.AGENT_VERSION.toString()), + schema.literal(EndpointSortableField.LAST_SEEN.toString()), + ]) + ), + sortDirection: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])), hostStatuses: schema.maybe( schema.arrayOf( schema.oneOf([ diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 60f6e597306b1c..3dd1f8b1881787 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -20,6 +20,7 @@ export { SecurityPageName } from '@kbn/security-solution-navigation'; */ export const APP_ID = 'securitySolution' as const; export const APP_UI_ID = 'securitySolutionUI' as const; +export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; export const CASES_FEATURE_ID = 'securitySolutionCases' as const; export const SERVER_APP_ID = 'siem' as const; export const APP_NAME = 'Security' as const; @@ -163,6 +164,9 @@ export const DEFAULT_INDEX_PATTERN = [...INCLUDE_INDEX_PATTERN, ...EXCLUDE_ELAST /** This Kibana Advanced Setting enables the `Security news` feed widget */ export const ENABLE_NEWS_FEED_SETTING = 'securitySolution:enableNewsFeed' as const; +/** This Kibana Advanced Setting allows users to enable/disable the Expandable Flyout */ +export const ENABLE_EXPANDABLE_FLYOUT_SETTING = 'securitySolution:enableExpandableFlyout' as const; + /** This Kibana Advanced Setting enables the warnings for CCS read permissions */ export const ENABLE_CCS_READ_WARNING_SETTING = 'securitySolution:enableCcsWarning' as const; @@ -210,7 +214,6 @@ export const UPDATE_OR_CREATE_LEGACY_ACTIONS = '/internal/api/detection/legacy/n /** * Exceptions management routes */ - export const SHARED_EXCEPTION_LIST_URL = `/api${EXCEPTIONS_PATH}/shared` as const; /** @@ -322,12 +325,12 @@ export const ALERTS_AS_DATA_FIND_URL = `${ALERTS_AS_DATA_URL}/find` as const; */ export const UNAUTHENTICATED_USER = 'Unauthenticated' as const; -/* +/** Licensing requirements */ export const MINIMUM_ML_LICENSE = 'platinum' as const; -/* +/** Machine Learning constants */ export const ML_GROUP_ID = 'security' as const; diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 25380c79cf6ed6..19c77f230eea53 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -7,6 +7,7 @@ /** endpoint data streams that are used for host isolation */ import { getFileDataIndexName, getFileMetadataIndexName } from '@kbn/fleet-plugin/common'; +import { EndpointSortableField } from './types'; /** for index patterns `.logs-endpoint.actions-* and .logs-endpoint.action.responses-*`*/ export const ENDPOINT_ACTIONS_DS = '.logs-endpoint.actions'; @@ -99,6 +100,8 @@ export const failedFleetActionErrorCode = '424'; export const ENDPOINT_DEFAULT_PAGE = 0; export const ENDPOINT_DEFAULT_PAGE_SIZE = 10; +export const ENDPOINT_DEFAULT_SORT_FIELD = EndpointSortableField.ENROLLED_AT; +export const ENDPOINT_DEFAULT_SORT_DIRECTION = 'desc'; export const ENDPOINT_ERROR_CODES: Record = { ES_CONNECTION_ERROR: -272, diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts index 1b01d477f1995e..fe3fd8c2ebd6a9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts @@ -6,14 +6,19 @@ */ import type { PolicyConfig } from '../types'; -import { ProtectionModes } from '../types'; +import { PolicyOperatingSystem, ProtectionModes } from '../types'; import { policyFactory } from './policy_config'; -import { disableProtections } from './policy_config_helpers'; +import { + disableProtections, + isPolicySetToEventCollectionOnly, + ensureOnlyEventCollectionIsAllowed, +} from './policy_config_helpers'; +import { set } from 'lodash'; describe('Policy Config helpers', () => { describe('disableProtections', () => { it('disables all the protections in the default policy', () => { - expect(disableProtections(policyFactory())).toEqual(eventsOnlyPolicy); + expect(disableProtections(policyFactory())).toEqual(eventsOnlyPolicy()); }); it('does not enable supported fields', () => { @@ -51,20 +56,20 @@ describe('Policy Config helpers', () => { }; const expectedPolicyWithoutSupportedProtections: PolicyConfig = { - ...eventsOnlyPolicy, + ...eventsOnlyPolicy(), windows: { - ...eventsOnlyPolicy.windows, + ...eventsOnlyPolicy().windows, memory_protection: notSupported, behavior_protection: notSupportedBehaviorProtection, ransomware: notSupported, }, mac: { - ...eventsOnlyPolicy.mac, + ...eventsOnlyPolicy().mac, memory_protection: notSupported, behavior_protection: notSupportedBehaviorProtection, }, linux: { - ...eventsOnlyPolicy.linux, + ...eventsOnlyPolicy().linux, memory_protection: notSupported, behavior_protection: notSupportedBehaviorProtection, }, @@ -104,10 +109,10 @@ describe('Policy Config helpers', () => { }; const expectedPolicy: PolicyConfig = { - ...eventsOnlyPolicy, - windows: { ...eventsOnlyPolicy.windows, events: { ...windowsEvents } }, - mac: { ...eventsOnlyPolicy.mac, events: { ...macEvents } }, - linux: { ...eventsOnlyPolicy.linux, events: { ...linuxEvents } }, + ...eventsOnlyPolicy(), + windows: { ...eventsOnlyPolicy().windows, events: { ...windowsEvents } }, + mac: { ...eventsOnlyPolicy().mac, events: { ...macEvents } }, + linux: { ...eventsOnlyPolicy().linux, events: { ...linuxEvents } }, }; const inputPolicy = { @@ -120,11 +125,73 @@ describe('Policy Config helpers', () => { expect(disableProtections(inputPolicy)).toEqual(expectedPolicy); }); }); + + describe('setPolicyToEventCollectionOnly()', () => { + it('should set the policy to event collection only', () => { + expect(ensureOnlyEventCollectionIsAllowed(policyFactory())).toEqual(eventsOnlyPolicy()); + }); + }); + + describe('isPolicySetToEventCollectionOnly', () => { + let policy: PolicyConfig; + + beforeEach(() => { + policy = ensureOnlyEventCollectionIsAllowed(policyFactory()); + }); + + it.each([ + { + keyPath: `${PolicyOperatingSystem.windows}.malware.mode`, + keyValue: ProtectionModes.prevent, + expectedResult: false, + }, + { + keyPath: `${PolicyOperatingSystem.mac}.malware.mode`, + keyValue: ProtectionModes.off, + expectedResult: true, + }, + { + keyPath: `${PolicyOperatingSystem.windows}.ransomware.mode`, + keyValue: ProtectionModes.prevent, + expectedResult: false, + }, + { + keyPath: `${PolicyOperatingSystem.linux}.memory_protection.mode`, + keyValue: ProtectionModes.off, + expectedResult: true, + }, + { + keyPath: `${PolicyOperatingSystem.mac}.behavior_protection.mode`, + keyValue: ProtectionModes.detect, + expectedResult: false, + }, + { + keyPath: `${PolicyOperatingSystem.windows}.attack_surface_reduction.credential_hardening.enabled`, + keyValue: true, + expectedResult: false, + }, + { + keyPath: `${PolicyOperatingSystem.windows}.antivirus_registration.enabled`, + keyValue: true, + expectedResult: false, + }, + ])( + 'should return `$expectedResult` if `$keyPath` is set to `$keyValue`', + ({ keyPath, keyValue, expectedResult }) => { + set(policy, keyPath, keyValue); + + expect(isPolicySetToEventCollectionOnly(policy)).toEqual({ + isOnlyCollectingEvents: expectedResult, + message: expectedResult ? undefined : `property [${keyPath}] is set to [${keyValue}]`, + }); + } + ); + }); }); // This constant makes sure that if the type `PolicyConfig` is ever modified, // the logic for disabling protections is also modified due to type check. -export const eventsOnlyPolicy: PolicyConfig = { +export const eventsOnlyPolicy = (): PolicyConfig => ({ meta: { license: '', cloud: false, license_uid: '', cluster_name: '', cluster_uuid: '' }, windows: { events: { @@ -187,4 +254,4 @@ export const eventsOnlyPolicy: PolicyConfig = { capture_env_vars: 'LD_PRELOAD,LD_LIBRARY_PATH', }, }, -}; +}); diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts index 4bdca10547bc29..cb460e2f75f49d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts @@ -5,8 +5,63 @@ * 2.0. */ +import { get, set } from 'lodash'; import type { PolicyConfig } from '../types'; -import { ProtectionModes } from '../types'; +import { PolicyOperatingSystem, ProtectionModes } from '../types'; + +interface PolicyProtectionReference { + keyPath: string; + osList: PolicyOperatingSystem[]; + enableValue: unknown; + disableValue: unknown; +} + +const getPolicyProtectionsReference = (): PolicyProtectionReference[] => { + const allOsValues = [ + PolicyOperatingSystem.mac, + PolicyOperatingSystem.linux, + PolicyOperatingSystem.windows, + ]; + + return [ + { + keyPath: 'malware.mode', + osList: [...allOsValues], + disableValue: ProtectionModes.off, + enableValue: ProtectionModes.prevent, + }, + { + keyPath: 'ransomware.mode', + osList: [PolicyOperatingSystem.windows], + disableValue: ProtectionModes.off, + enableValue: ProtectionModes.prevent, + }, + { + keyPath: 'memory_protection.mode', + osList: [...allOsValues], + disableValue: ProtectionModes.off, + enableValue: ProtectionModes.prevent, + }, + { + keyPath: 'behavior_protection.mode', + osList: [...allOsValues], + disableValue: ProtectionModes.off, + enableValue: ProtectionModes.prevent, + }, + { + keyPath: 'attack_surface_reduction.credential_hardening.enabled', + osList: [PolicyOperatingSystem.windows], + disableValue: false, + enableValue: true, + }, + { + keyPath: 'antivirus_registration.enabled', + osList: [PolicyOperatingSystem.windows], + disableValue: false, + enableValue: true, + }, + ]; +}; /** * Returns a copy of the passed `PolicyConfig` with all protections set to disabled. @@ -106,3 +161,46 @@ const getDisabledWindowsSpecificPopups = (policy: PolicyConfig) => ({ enabled: false, }, }); + +/** + * Returns the provided with only event collection turned enabled + * @param policy + */ +export const ensureOnlyEventCollectionIsAllowed = (policy: PolicyConfig): PolicyConfig => { + const updatedPolicy = disableProtections(policy); + + set(updatedPolicy, 'windows.antivirus_registration.enabled', false); + + return updatedPolicy; +}; + +/** + * Checks to see if the provided policy is set to Event Collection only + */ +export const isPolicySetToEventCollectionOnly = ( + policy: PolicyConfig +): { isOnlyCollectingEvents: boolean; message?: string } => { + const protectionsRef = getPolicyProtectionsReference(); + let message: string | undefined; + + const hasEnabledProtection = protectionsRef.some(({ keyPath, osList, disableValue }) => { + const hasOsPropertyEnabled = osList.some((osValue) => { + const fullKeyPathForOs = `${osValue}.${keyPath}`; + const currentValue = get(policy, fullKeyPathForOs); + const isEnabled = currentValue !== disableValue; + + if (isEnabled) { + message = `property [${fullKeyPathForOs}] is set to [${currentValue}]`; + } + + return isEnabled; + }); + + return hasOsPropertyEnabled; + }); + + return { + isOnlyCollectingEvents: !hasEnabledProtection, + message, + }; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index eaafc268780701..5702f14f2a37a4 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -1325,26 +1325,39 @@ export interface ListPageRouteState { backButtonLabel?: string; } -/** - * REST API standard base response for list types - */ -interface BaseListResponse { - data: D[]; - page: number; - pageSize: number; - total: number; -} - export interface AdditionalOnSwitchChangeParams { value: boolean; policyConfigData: UIPolicyConfig; protectionOsList: ImmutableArray>; } +/** Allowed fields for sorting in the EndpointList table. + * These are the column fields in the EndpointList table, based on the + * returned `HostInfoInterface` data type (and not on the internal data structure). + */ +export enum EndpointSortableField { + ENROLLED_AT = 'enrolled_at', + HOSTNAME = 'metadata.host.hostname', + HOST_STATUS = 'host_status', + POLICY_NAME = 'metadata.Endpoint.policy.applied.name', + POLICY_STATUS = 'metadata.Endpoint.policy.applied.status', + HOST_OS_NAME = 'metadata.host.os.name', + HOST_IP = 'metadata.host.ip', + AGENT_VERSION = 'metadata.agent.version', + LAST_SEEN = 'last_checkin', +} + /** * Returned by the server via GET /api/endpoint/metadata */ -export type MetadataListResponse = BaseListResponse; +export interface MetadataListResponse { + data: HostInfo[]; + page: number; + pageSize: number; + total: number; + sortField: EndpointSortableField; + sortDirection: 'asc' | 'desc'; +} export type { EndpointPrivileges } from './authz'; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/utility_types.ts b/x-pack/plugins/security_solution/common/endpoint/types/utility_types.ts new file mode 100644 index 00000000000000..92880d93221914 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types/utility_types.ts @@ -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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export type PromiseResolvedValue> = T extends Promise + ? Value + : never; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index b2e1cae53d97e9..449092e86130af 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -76,10 +76,6 @@ export const allowedExperimentalValues = Object.freeze({ */ alertsPageChartsEnabled: true, alertTypeEnabled: false, - /** - * Enables the new security flyout over the current alert details flyout - */ - securityFlyoutEnabled: false, /* * Enables new Set of filters on the Alerts page. diff --git a/x-pack/plugins/security_solution/common/types/app_features.ts b/x-pack/plugins/security_solution/common/types/app_features.ts index 80734f9f239928..a8c65aeadfc8aa 100644 --- a/x-pack/plugins/security_solution/common/types/app_features.ts +++ b/x-pack/plugins/security_solution/common/types/app_features.ts @@ -55,6 +55,13 @@ export enum AppFeatureSecurityKey { osqueryAutomatedResponseActions = 'osquery_automated_response_actions', } +export enum AppFeatureAssistantKey { + /** + * Enables Elastic AI Assistant + */ + assistant = 'assistant', +} + export enum AppFeatureCasesKey { /** * Enables Cases Connectors @@ -63,9 +70,13 @@ export enum AppFeatureCasesKey { } // Merges the two enums. -export type AppFeatureKey = AppFeatureSecurityKey | AppFeatureCasesKey; +export type AppFeatureKey = AppFeatureSecurityKey | AppFeatureCasesKey | AppFeatureAssistantKey; export type AppFeatureKeys = AppFeatureKey[]; // We need to merge the value and the type and export both to replicate how enum works. -export const AppFeatureKey = { ...AppFeatureSecurityKey, ...AppFeatureCasesKey }; +export const AppFeatureKey = { + ...AppFeatureSecurityKey, + ...AppFeatureCasesKey, + ...AppFeatureAssistantKey, +}; export const ALL_APP_FEATURE_KEYS = Object.freeze(Object.values(AppFeatureKey)); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/cti_enrichments.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/cti_enrichments.cy.ts index fc424eb192e052..0a626f2fff8d47 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/cti_enrichments.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/cti_enrichments.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { disableExpandableFlyout } from '../../tasks/api_calls/kibana_advanced_settings'; import { getNewThreatIndicatorRule, indicatorRuleMatchingDoc } from '../../objects/rule'; import { cleanKibana } from '../../tasks/common'; import { login, visitWithoutDateRange } from '../../tasks/login'; @@ -34,6 +35,7 @@ describe('CTI Enrichment', () => { cy.task('esArchiverLoad', 'suspicious_source_event'); login(); createRule({ ...getNewThreatIndicatorRule(), rule_id: 'rule_testing', enabled: true }); + disableExpandableFlyout(); }); after(() => { diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/enrichments.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/enrichments.cy.ts index 2eacd9ece48653..95481b23174f1c 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/enrichments.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/enrichments.cy.ts @@ -24,6 +24,7 @@ import { scrollAlertTableColumnIntoView, closeAlertFlyout, } from '../../tasks/alerts'; +import { disableExpandableFlyout } from '../../tasks/api_calls/kibana_advanced_settings'; import { login, visit } from '../../tasks/login'; @@ -41,6 +42,7 @@ describe('Enrichment', () => { describe('Custom query rule', () => { beforeEach(() => { + disableExpandableFlyout(); cy.task('esArchiverLoad', 'risk_hosts'); deleteAlertsAndRules(); createRule(getNewRule({ rule_id: 'rule1' })); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts index 4957d6edc3371b..f148e973300dd4 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts @@ -60,8 +60,7 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', () }); it('should install package from Fleet in the background', () => { - /* Assert that the package in installed from Fleet by checking that - /* the installSource is "registry", as opposed to "bundle" */ + /* Assert that the package in installed from Fleet */ cy.wait('@installPackageBulk', { timeout: 60000, }).then(({ response: bulkResponse }) => { @@ -70,7 +69,6 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', () const packages = bulkResponse?.body.items.map( ({ name, result }: BulkInstallPackageInfo) => ({ name, - installSource: result.installSource, }) ); @@ -86,17 +84,14 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', () cy.wrap(response?.body) .should('have.property', 'items') .should('have.length.greaterThan', 0); - cy.wrap(response?.body) - .should('have.property', '_meta') - .should('have.property', 'install_source') - .should('eql', 'registry'); }); } else { // Normal flow, install via the Fleet bulk install API expect(packages.length).to.have.greaterThan(0); - expect(packages).to.deep.include.members([ - { name: 'security_detection_engine', installSource: 'registry' }, - ]); + // At least one of the packages installed should be the security_detection_engine package + expect(packages).to.satisfy((pckgs: BulkInstallPackageInfo[]) => + pckgs.some((pkg) => pkg.name === 'security_detection_engine') + ); } }); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts index 08bddea26288a1..b3939ff3c7b272 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts @@ -35,7 +35,6 @@ import { duplicateFirstRule, importRules } from '../../../../../tasks/alerts_det import { goToActionsStepTab } from '../../../../../tasks/create_new_rule'; import { goToRuleEditSettings } from '../../../../../tasks/rule_details'; import { actionFormSelector } from '../../../../../screens/common/rule_actions'; -import { RULE_INDICES } from '../../../../../screens/create_new_rule'; import { addEmailConnectorAndRuleAction } from '../../../../../tasks/common/rule_actions'; import { saveEditedRule } from '../../../../../tasks/edit_rule'; import { DISABLED_SNOOZE_BADGE } from '../../../../../screens/rule_snoozing'; @@ -169,8 +168,7 @@ describe('rule snoozing', () => { }); }); - // SKIPPED: https://github.com/elastic/kibana/issues/159349 - describe.skip('Rule editing page / actions tab', () => { + describe('Rule editing page / actions tab', () => { beforeEach(() => { deleteConnectors(); }); @@ -178,8 +176,6 @@ describe('rule snoozing', () => { it('adds an action to a snoozed rule', () => { createSnoozedRule(getNewRule({ name: 'Snoozed rule' })).then(({ body: rule }) => { visitWithoutDateRange(ruleEditUrl(rule.id)); - // Wait for rule data being loaded - cy.get(RULE_INDICES).should('be.visible'); goToActionsStepTab(); addEmailConnectorAndRuleAction('abc@example.com', 'Test action'); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_management/rules_table/rules_table_auto_refresh.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_management/rules_table/rules_table_auto_refresh.cy.ts index 7304972a657908..bd3363572915d8 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_management/rules_table/rules_table_auto_refresh.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_management/rules_table/rules_table_auto_refresh.cy.ts @@ -8,20 +8,19 @@ import { RULE_CHECKBOX, REFRESH_RULES_STATUS, - REFRESH_SETTINGS_SWITCH, - REFRESH_SETTINGS_SELECTION_NOTE, + RULES_TABLE_AUTOREFRESH_INDICATOR, + RULES_MANAGEMENT_TABLE, } from '../../../../screens/alerts_detection_rules'; import { - checkAutoRefresh, - waitForRulesTableToBeLoaded, selectAllRules, - openRefreshSettingsPopover, clearAllRuleSelection, selectNumberOfRules, mockGlobalClock, disableAutoRefresh, - checkAutoRefreshIsDisabled, - checkAutoRefreshIsEnabled, + expectAutoRefreshIsDisabled, + expectAutoRefreshIsEnabled, + expectAutoRefreshIsDeactivated, + expectNumberOfRules, } from '../../../../tasks/alerts_detection_rules'; import { login, visit, visitWithoutDateRange } from '../../../../tasks/login'; @@ -29,16 +28,16 @@ import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../../urls/navigation'; import { createRule } from '../../../../tasks/api_calls/rules'; import { cleanKibana } from '../../../../tasks/common'; import { getNewRule } from '../../../../objects/rule'; -import { setRowsPerPageTo } from '../../../../tasks/table_pagination'; const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; +const NUM_OF_TEST_RULES = 6; -// TODO: See https://github.com/elastic/kibana/issues/154694 -describe.skip('Rules table: auto-refresh', () => { +describe('Rules table: auto-refresh', () => { before(() => { cleanKibana(); login(); - for (let i = 1; i < 7; i += 1) { + + for (let i = 1; i <= NUM_OF_TEST_RULES; ++i) { createRule(getNewRule({ name: `Test rule ${i}`, rule_id: `${i}` })); } }); @@ -48,31 +47,31 @@ describe.skip('Rules table: auto-refresh', () => { }); it('Auto refreshes rules', () => { + mockGlobalClock(); visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - waitForRulesTableToBeLoaded(); - - // ensure rules have rendered. As there is no user interaction in this test, - // rules were not rendered before test completes - cy.get(RULE_CHECKBOX).should('have.length', 6); + expectNumberOfRules(RULES_MANAGEMENT_TABLE, NUM_OF_TEST_RULES); // // mock 1 minute passing to make sure refresh is conducted - mockGlobalClock(); - checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'be.visible'); + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist'); + cy.tick(DEFAULT_RULE_REFRESH_INTERVAL_VALUE); + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('be.visible'); cy.contains(REFRESH_RULES_STATUS, 'Updated now'); }); it('should prevent table from rules refetch if any rule selected', () => { + mockGlobalClock(); visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - waitForRulesTableToBeLoaded(); + expectNumberOfRules(RULES_MANAGEMENT_TABLE, NUM_OF_TEST_RULES); selectNumberOfRules(1); // mock 1 minute passing to make sure refresh is not conducted - mockGlobalClock(); - checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'not.exist'); + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist'); + cy.tick(DEFAULT_RULE_REFRESH_INTERVAL_VALUE); + cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist'); // ensure rule is still selected cy.get(RULE_CHECKBOX).first().should('be.checked'); @@ -82,50 +81,37 @@ describe.skip('Rules table: auto-refresh', () => { it('should disable auto refresh when any rule selected and enable it after rules unselected', () => { visit(DETECTIONS_RULE_MANAGEMENT_URL); - waitForRulesTableToBeLoaded(); - setRowsPerPageTo(5); + + expectNumberOfRules(RULES_MANAGEMENT_TABLE, NUM_OF_TEST_RULES); // check refresh settings if it's enabled before selecting - openRefreshSettingsPopover(); - checkAutoRefreshIsEnabled(); + expectAutoRefreshIsEnabled(); selectAllRules(); - // auto refresh should be disabled after rules selected - openRefreshSettingsPopover(); - checkAutoRefreshIsDisabled(); - - // if any rule selected, refresh switch should be disabled and help note to users should displayed - cy.get(REFRESH_SETTINGS_SWITCH).should('be.disabled'); - cy.contains( - REFRESH_SETTINGS_SELECTION_NOTE, - 'Note: Refresh is disabled while there is an active selection.' - ); + // auto refresh should be deactivated (which means disabled without an ability to enable it) after rules selected + expectAutoRefreshIsDeactivated(); clearAllRuleSelection(); - // after all rules unselected, auto refresh should renew - openRefreshSettingsPopover(); - checkAutoRefreshIsEnabled(); + // after all rules unselected, auto refresh should be reset to its previous state + expectAutoRefreshIsEnabled(); }); it('should not enable auto refresh after rules were unselected if auto refresh was disabled', () => { visit(DETECTIONS_RULE_MANAGEMENT_URL); - waitForRulesTableToBeLoaded(); - setRowsPerPageTo(5); - openRefreshSettingsPopover(); + expectNumberOfRules(RULES_MANAGEMENT_TABLE, NUM_OF_TEST_RULES); + disableAutoRefresh(); selectAllRules(); - openRefreshSettingsPopover(); - checkAutoRefreshIsDisabled(); + expectAutoRefreshIsDeactivated(); clearAllRuleSelection(); // after all rules unselected, auto refresh should still be disabled - openRefreshSettingsPopover(); - checkAutoRefreshIsDisabled(); + expectAutoRefreshIsDisabled(); }); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/read_only_view.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/read_only_view.cy.ts index 5cbb1da916440e..6464b782ae6753 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/read_only_view.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/read_only_view.cy.ts @@ -13,7 +13,7 @@ import { login, visitWithoutDateRange } from '../../../tasks/login'; import { goToExceptionsTab, goToAlertsTab } from '../../../tasks/rule_details'; import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; -import { deleteAlertsAndRules } from '../../../tasks/common'; +import { cleanKibana, deleteAlertsAndRules } from '../../../tasks/common'; import { NO_EXCEPTIONS_EXIST_PROMPT, EXCEPTION_ITEM_VIEWER_CONTAINER, @@ -31,7 +31,7 @@ describe('Exceptions viewer read only', () => { const exceptionList = getExceptionList(); before(() => { - cy.task('esArchiverResetKibana'); + cleanKibana(); // create rule with exceptions createExceptionList(exceptionList, exceptionList.list_id).then((response) => { createRule( @@ -56,6 +56,7 @@ describe('Exceptions viewer read only', () => { login(ROLES.reader); visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL, ROLES.reader); goToRuleDetails(); + cy.url().should('contain', 'app/security/rules/id'); goToExceptionsTab(); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/explore/guided_onboarding/tour.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/explore/guided_onboarding/tour.cy.ts index 33799309fd3a61..1c180857c00b93 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/explore/guided_onboarding/tour.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/explore/guided_onboarding/tour.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { disableExpandableFlyout } from '../../../tasks/api_calls/kibana_advanced_settings'; import { navigateFromHeaderTo } from '../../../tasks/security_header'; import { ALERTS, TIMELINES } from '../../../screens/security_header'; import { closeAlertFlyout, expandFirstAlert } from '../../../tasks/alerts'; @@ -36,6 +37,7 @@ describe('Guided onboarding tour', () => { }); beforeEach(() => { login(); + disableExpandableFlyout(); startAlertsCasesTour(); visit(ALERTS_URL); waitForAlertsToPopulate(); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/alerts_details.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/alerts_details.cy.ts index 7c2ec7a31e12a5..7f5e0cde93b10f 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/alerts_details.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/alerts_details.cy.ts @@ -6,6 +6,7 @@ */ import type { DataTableModel } from '@kbn/securitysolution-data-table'; +import { disableExpandableFlyout } from '../../../tasks/api_calls/kibana_advanced_settings'; import { ALERT_FLYOUT, CELL_TEXT, @@ -39,6 +40,7 @@ describe('Alert details flyout', () => { before(() => { cleanKibana(); login(); + disableExpandableFlyout(); createRule(getNewRule()); visitWithoutDateRange(ALERTS_URL); waitForAlertsToPopulate(); @@ -64,6 +66,7 @@ describe('Alert details flyout', () => { beforeEach(() => { login(); + disableExpandableFlyout(); visitWithoutDateRange(ALERTS_URL); waitForAlertsToPopulate(); expandFirstAlert(); @@ -128,6 +131,7 @@ describe('Alert details flyout', () => { beforeEach(() => { login(); + disableExpandableFlyout(); visit(ALERTS_URL); waitForAlertsToPopulate(); expandFirstAlert(); @@ -173,6 +177,7 @@ describe('Alert details flyout', () => { beforeEach(() => { login(); + disableExpandableFlyout(); visit(ALERTS_URL); waitForAlertsToPopulate(); expandFirstAlert(); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_analyzer_graph_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_analyzer_graph_tab.cy.ts index 43531feb67d732..573286b921d560 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_analyzer_graph_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_analyzer_graph_tab.cy.ts @@ -24,34 +24,30 @@ import { getNewRule } from '../../../../objects/rule'; import { ALERTS_URL } from '../../../../urls/navigation'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( - 'Alert details expandable flyout left panel analyzer graph', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - beforeEach(() => { - cleanKibana(); - login(); - createRule(getNewRule()); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); - expandDocumentDetailsExpandableFlyoutLeftSection(); - openGraphAnalyzerTab(); - }); +describe('Alert details expandable flyout left panel analyzer graph', () => { + beforeEach(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + expandDocumentDetailsExpandableFlyoutLeftSection(); + openGraphAnalyzerTab(); + }); - it('should display analyzer graph and node list under visualize', () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB) - .should('be.visible') - .and('have.text', 'Visualize'); + it('should display analyzer graph and node list under visualize', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB) + .should('be.visible') + .and('have.text', 'Visualize'); - cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_BUTTON_GROUP).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_BUTTON_GROUP).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON) - .should('be.visible') - .and('have.text', 'Analyzer Graph'); + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON) + .should('be.visible') + .and('have.text', 'Analyzer Graph'); - cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_CONTENT).should('be.visible'); - cy.get(ANALYZER_NODE).first().should('be.visible'); - }); - } -); + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_CONTENT).should('be.visible'); + cy.get(ANALYZER_NODE).first().should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_correlations_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_correlations_tab.cy.ts index da6dccc082c7f2..0b02939f5ca4fc 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_correlations_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_correlations_tab.cy.ts @@ -34,60 +34,54 @@ import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; import { login, visit } from '../../../../tasks/login'; import { ALERTS_URL } from '../../../../urls/navigation'; -describe( - 'Expandable flyout left panel correlations', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - beforeEach(() => { - cleanKibana(); - login(); - createRule(getNewRule()); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); - expandDocumentDetailsExpandableFlyoutLeftSection(); - createNewCaseFromExpandableFlyout(); - openInsightsTab(); - openCorrelationsTab(); - }); +describe('Expandable flyout left panel correlations', () => { + beforeEach(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + expandDocumentDetailsExpandableFlyoutLeftSection(); + createNewCaseFromExpandableFlyout(); + openInsightsTab(); + openCorrelationsTab(); + }); - it('should render correlations details correctly', () => { - cy.log('link the alert to a new case'); + it('should render correlations details correctly', () => { + cy.log('link the alert to a new case'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB).scrollIntoView(); - cy.log('should render the Insights header'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB) - .should('be.visible') - .and('have.text', 'Insights'); + cy.log('should render the Insights header'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB).should('be.visible').and('have.text', 'Insights'); - cy.log('should render the inner tab switch'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_BUTTON_GROUP).should('be.visible'); + cy.log('should render the inner tab switch'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_BUTTON_GROUP).should('be.visible'); - cy.log('should render correlations tab activator / button'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_CORRELATIONS_BUTTON) - .should('be.visible') - .and('have.text', 'Correlations'); + cy.log('should render correlations tab activator / button'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_CORRELATIONS_BUTTON) + .should('be.visible') + .and('have.text', 'Correlations'); - cy.log('should render all the correlations sections'); + cy.log('should render all the correlations sections'); - cy.get(CORRELATIONS_ANCESTRY_SECTION) - .should('be.visible') - .and('have.text', '1 alert related by ancestry'); + cy.get(CORRELATIONS_ANCESTRY_SECTION) + .should('be.visible') + .and('have.text', '1 alert related by ancestry'); - cy.get(CORRELATIONS_SOURCE_SECTION) - .should('be.visible') - .and('have.text', '0 alerts related by source event'); + cy.get(CORRELATIONS_SOURCE_SECTION) + .should('be.visible') + .and('have.text', '0 alerts related by source event'); - cy.get(CORRELATIONS_SESSION_SECTION) - .should('be.visible') - .and('have.text', '1 alert related by session'); + cy.get(CORRELATIONS_SESSION_SECTION) + .should('be.visible') + .and('have.text', '1 alert related by session'); - cy.get(CORRELATIONS_CASES_SECTION).should('be.visible').and('have.text', '1 related case'); + cy.get(CORRELATIONS_CASES_SECTION).should('be.visible').and('have.text', '1 related case'); - expandCorrelationsSection(CORRELATIONS_ANCESTRY_SECTION); + expandCorrelationsSection(CORRELATIONS_ANCESTRY_SECTION); - cy.get(CORRELATIONS_ANCESTRY_TABLE).should('be.visible'); - }); - } -); + cy.get(CORRELATIONS_ANCESTRY_TABLE).should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_entities_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_entities_tab.cy.ts index f48611f12af09b..a680f783af336d 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_entities_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_entities_tab.cy.ts @@ -25,38 +25,32 @@ import { getNewRule } from '../../../../objects/rule'; import { ALERTS_URL } from '../../../../urls/navigation'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( - 'Alert details expandable flyout left panel entities', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - beforeEach(() => { - cleanKibana(); - login(); - createRule(getNewRule()); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); - expandDocumentDetailsExpandableFlyoutLeftSection(); - openInsightsTab(); - openEntitiesTab(); - }); +describe('Alert details expandable flyout left panel entities', () => { + beforeEach(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + expandDocumentDetailsExpandableFlyoutLeftSection(); + openInsightsTab(); + openEntitiesTab(); + }); - it('should display analyzer graph and node list under Insights Entities', () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB) - .should('be.visible') - .and('have.text', 'Insights'); + it('should display analyzer graph and node list under Insights Entities', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB).should('be.visible').and('have.text', 'Insights'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_BUTTON_GROUP).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_BUTTON_GROUP).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_BUTTON) - .should('be.visible') - .and('have.text', 'Entities'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_BUTTON) + .should('be.visible') + .and('have.text', 'Entities'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS).should('be.visible'); - }); - } -); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS).should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_investigation_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_investigation_tab.cy.ts index 62d4932a017b8f..833d591344f571 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_investigation_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_investigation_tab.cy.ts @@ -19,27 +19,23 @@ import { getNewRule } from '../../../../objects/rule'; import { ALERTS_URL } from '../../../../urls/navigation'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( - 'Alert details expandable flyout left panel investigation', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - beforeEach(() => { - cleanKibana(); - login(); - createRule(getNewRule()); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); - expandDocumentDetailsExpandableFlyoutLeftSection(); - openInvestigationTab(); - }); +describe('Alert details expandable flyout left panel investigation', () => { + beforeEach(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + expandDocumentDetailsExpandableFlyoutLeftSection(); + openInvestigationTab(); + }); - it('should display investigation guide', () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_INVESTIGATION_TAB) - .should('be.visible') - .and('have.text', 'Investigation'); + it('should display investigation guide', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_INVESTIGATION_TAB) + .should('be.visible') + .and('have.text', 'Investigation'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INVESTIGATION_TAB_CONTENT).should('be.visible'); - }); - } -); + cy.get(DOCUMENT_DETAILS_FLYOUT_INVESTIGATION_TAB_CONTENT).should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts index 3cfe58f22893cb..ab06e9fea187e6 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts @@ -30,56 +30,50 @@ import { getNewRule } from '../../../../objects/rule'; import { ALERTS_URL } from '../../../../urls/navigation'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( - 'Alert details expandable flyout left panel prevalence', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - beforeEach(() => { - cleanKibana(); - login(); - createRule(getNewRule()); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); - expandDocumentDetailsExpandableFlyoutLeftSection(); - openInsightsTab(); - openPrevalenceTab(); - }); +describe('Alert details expandable flyout left panel prevalence', () => { + beforeEach(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + expandDocumentDetailsExpandableFlyoutLeftSection(); + openInsightsTab(); + openPrevalenceTab(); + }); - it('should display prevalence tab', () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB) - .should('be.visible') - .and('have.text', 'Insights'); + it('should display prevalence tab', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB).should('be.visible').and('have.text', 'Insights'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_BUTTON_GROUP).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_BUTTON_GROUP).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_BUTTON) - .should('be.visible') - .and('have.text', 'Prevalence'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_BUTTON) + .should('be.visible') + .and('have.text', 'Prevalence'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_TYPE_CELL) - .should('contain.text', 'host.name') - .and('contain.text', 'user.name'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_NAME_CELL) - .should('contain.text', 'siem-kibana') - .and('contain.text', 'test'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_ALERT_COUNT_CELL).should( - 'contain.text', - 2 - ); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_DOC_COUNT_CELL).should( - 'contain.text', - 0 - ); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_PREVALENCE_CELL).should( - 'contain.text', - 100 - ); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_USER_PREVALENCE_CELL).should( - 'contain.text', - 100 - ); - }); - } -); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_TYPE_CELL) + .should('contain.text', 'host.name') + .and('contain.text', 'user.name'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_NAME_CELL) + .should('contain.text', 'siem-kibana') + .and('contain.text', 'test'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_ALERT_COUNT_CELL).should( + 'contain.text', + 2 + ); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_DOC_COUNT_CELL).should( + 'contain.text', + 0 + ); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_PREVALENCE_CELL).should( + 'contain.text', + 100 + ); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_USER_PREVALENCE_CELL).should( + 'contain.text', + 100 + ); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_response_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_response_tab.cy.ts index 19b9eac037a4ce..19ed92dbff60fa 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_response_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_response_tab.cy.ts @@ -16,23 +16,19 @@ import { getNewRule } from '../../../../objects/rule'; import { ALERTS_URL } from '../../../../urls/navigation'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( - 'Alert details expandable flyout left panel investigation', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - beforeEach(() => { - cleanKibana(); - login(); - createRule(getNewRule()); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); - expandDocumentDetailsExpandableFlyoutLeftSection(); - openResponseTab(); - }); +describe('Alert details expandable flyout left panel investigation', () => { + beforeEach(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + expandDocumentDetailsExpandableFlyoutLeftSection(); + openResponseTab(); + }); - it('should display empty response message', () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_RESPONSE_EMPTY).should('be.visible'); - }); - } -); + it('should display empty response message', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_RESPONSE_EMPTY).should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_session_view_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_session_view_tab.cy.ts index 762b5cf307b4f9..afc3ac1b0b9183 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_session_view_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_session_view_tab.cy.ts @@ -22,36 +22,32 @@ import { getNewRule } from '../../../../objects/rule'; import { ALERTS_URL } from '../../../../urls/navigation'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( - 'Alert details expandable flyout left panel session view', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - beforeEach(() => { - cleanKibana(); - login(); - createRule(getNewRule()); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); - expandDocumentDetailsExpandableFlyoutLeftSection(); - }); +describe('Alert details expandable flyout left panel session view', () => { + beforeEach(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + expandDocumentDetailsExpandableFlyoutLeftSection(); + }); - it('should display session view under visualize', () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB) - .should('be.visible') - .and('have.text', 'Visualize'); + it('should display session view under visualize', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB) + .should('be.visible') + .and('have.text', 'Visualize'); - cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_BUTTON_GROUP).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_BUTTON_GROUP).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_BUTTON) - .should('be.visible') - .and('have.text', 'Session View'); + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_BUTTON) + .should('be.visible') + .and('have.text', 'Session View'); - // TODO ideally we would have a test for the session view component instead - cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_ERROR) - .should('be.visible') - .and('contain.text', 'Unable to display session view') - .and('contain.text', 'There was an error displaying session view'); - }); - } -); + // TODO ideally we would have a test for the session view component instead + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_ERROR) + .should('be.visible') + .and('contain.text', 'Unable to display session view') + .and('contain.text', 'There was an error displaying session view'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts index d79c59ed71440a..c5cd1688361797 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_threat_intelligence_tab.cy.ts @@ -22,34 +22,28 @@ import { } from '../../../../screens/expandable_flyout/alert_details_left_panel'; import { DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_THREAT_INTELLIGENCE_BUTTON } from '../../../../screens/expandable_flyout/alert_details_left_panel_threat_intelligence_tab'; -describe( - 'Expandable flyout left panel threat intelligence', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - beforeEach(() => { - cleanKibana(); - login(); - createRule(getNewRule()); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); - expandDocumentDetailsExpandableFlyoutLeftSection(); - openInsightsTab(); - openThreatIntelligenceTab(); - }); +describe('Expandable flyout left panel threat intelligence', () => { + beforeEach(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + expandDocumentDetailsExpandableFlyoutLeftSection(); + openInsightsTab(); + openThreatIntelligenceTab(); + }); - it('should serialize its state to url', () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB) - .should('be.visible') - .and('have.text', 'Insights'); + it('should serialize its state to url', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB).should('be.visible').and('have.text', 'Insights'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_BUTTON_GROUP).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_BUTTON_GROUP).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_THREAT_INTELLIGENCE_BUTTON) - .should('be.visible') - .and('have.text', 'Threat Intelligence'); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_THREAT_INTELLIGENCE_BUTTON) + .should('be.visible') + .and('have.text', 'Threat Intelligence'); - cy.get(INDICATOR_MATCH_ENRICHMENT_SECTION).should('be.visible'); - }); - } -); + cy.get(INDICATOR_MATCH_ENRICHMENT_SECTION).should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts index 116162983f0b21..cc48e4568d908a 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_rule_preview.cy.ts @@ -34,68 +34,63 @@ import { getNewRule } from '../../../../objects/rule'; import { ALERTS_URL } from '../../../../urls/navigation'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( - 'Alert details expandable flyout rule preview panel', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - const rule = getNewRule(); +describe('Alert details expandable flyout rule preview panel', () => { + const rule = getNewRule(); - beforeEach(() => { - cleanKibana(); - login(); - createRule(rule); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); - clickRuleSummaryButton(); - }); + beforeEach(() => { + cleanKibana(); + login(); + createRule(rule); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + clickRuleSummaryButton(); + }); + + describe('rule preview', () => { + it('should display rule preview and its sub sections', () => { + cy.log('rule preview panel'); - describe('rule preview', () => { - it('should display rule preview and its sub sections', () => { - cy.log('rule preview panel'); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SECTION).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_HEADER).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_BODY).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SECTION).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_HEADER).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_BODY).should('be.visible'); + cy.log('title'); - cy.log('title'); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_TITLE).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_TITLE).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_CREATED_BY).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_UPDATED_BY).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_TITLE).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_TITLE).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_CREATED_BY).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_UPDATED_BY).should('be.visible'); - cy.log('about'); + cy.log('about'); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_HEADER) - .should('be.visible') - .and('contain.text', 'About'); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_CONTENT).should('be.visible'); - toggleRulePreviewAboutSection(); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_HEADER) + .should('be.visible') + .and('contain.text', 'About'); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_ABOUT_SECTION_CONTENT).should('be.visible'); + toggleRulePreviewAboutSection(); - cy.log('definition'); + cy.log('definition'); - toggleRulePreviewDefinitionSection(); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_HEADER) - .should('be.visible') - .and('contain.text', 'Definition'); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_CONTENT).should( - 'be.visible' - ); - toggleRulePreviewDefinitionSection(); + toggleRulePreviewDefinitionSection(); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_HEADER) + .should('be.visible') + .and('contain.text', 'Definition'); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_DEFINITION_SECTION_CONTENT).should('be.visible'); + toggleRulePreviewDefinitionSection(); - cy.log('schedule'); + cy.log('schedule'); - toggleRulePreviewScheduleSection(); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_HEADER) - .should('be.visible') - .and('contain.text', 'Schedule'); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_CONTENT).should('be.visible'); - toggleRulePreviewScheduleSection(); + toggleRulePreviewScheduleSection(); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_HEADER) + .should('be.visible') + .and('contain.text', 'Schedule'); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_SCHEDULE_SECTION_CONTENT).should('be.visible'); + toggleRulePreviewScheduleSection(); - cy.log('footer'); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER).should('be.visible'); - }); + cy.log('footer'); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER).should('be.visible'); }); - } -); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts index 11fc62c7f68f72..a166a72148fd30 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel.cy.ts @@ -65,173 +65,167 @@ import { getNewRule } from '../../../../objects/rule'; import { ALERTS_URL } from '../../../../urls/navigation'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( - 'Alert details expandable flyout right panel', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - const rule = getNewRule(); - - beforeEach(() => { - cleanKibana(); - login(); - createRule(rule); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - }); - - it('should display header and footer basics', () => { - expandFirstAlertExpandableFlyout(); - - cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); - - cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_CHAT_BUTTON).should('be.visible'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_STATUS).should('be.visible'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE_VALUE) - .should('be.visible') - .and('have.text', rule.risk_score); - - cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY_VALUE) - .should('be.visible') - .and('have.text', upperFirst(rule.severity)); - - cy.log('Verify all 3 tabs are visible'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB) - .should('be.visible') - .and('have.text', 'Overview'); - cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB).should('be.visible').and('have.text', 'Table'); - cy.get(DOCUMENT_DETAILS_FLYOUT_JSON_TAB).should('be.visible').and('have.text', 'JSON'); - - cy.log('Verify the expand/collapse button is visible and functionality works'); - - expandDocumentDetailsExpandableFlyoutLeftSection(); - cy.get(DOCUMENT_DETAILS_FLYOUT_COLLAPSE_DETAILS_BUTTON) - .should('be.visible') - .and('have.text', 'Collapse details'); - - collapseDocumentDetailsExpandableFlyoutLeftSection(); - cy.get(DOCUMENT_DETAILS_FLYOUT_EXPAND_DETAILS_BUTTON) - .should('be.visible') - .and('have.text', 'Expand details'); - - cy.log('Verify the take action button is visible on all tabs'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_TAKE_ACTION_BUTTON).should('be.visible'); - - openTableTab(); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_TAKE_ACTION_BUTTON).should('be.visible'); - - openJsonTab(); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_TAKE_ACTION_BUTTON).should('be.visible'); - }); - - // TODO this will change when add to existing case is improved - // https://github.com/elastic/security-team/issues/6298 - it('should add to existing case', () => { - navigateToCasesPage(); - createNewCaseFromCases(); - - cy.get(CASE_DETAILS_PAGE_TITLE).should('be.visible').and('have.text', 'case'); - navigateToAlertsPage(); - expandFirstAlertExpandableFlyout(); - openTakeActionButtonAndSelectItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_TO_EXISTING_CASE); - - cy.get(EXISTING_CASE_SELECT_BUTTON).should('be.visible').contains('Select').click(); - cy.get(VIEW_CASE_TOASTER_LINK).should('be.visible').and('contain.text', 'View case'); - }); - - // TODO this will change when add to new case is improved - // https://github.com/elastic/security-team/issues/6298 - it('should add to new case', () => { - expandFirstAlertExpandableFlyout(); - openTakeActionButtonAndSelectItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_TO_NEW_CASE); - - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_TO_NEW_CASE_NAME_INPUT).type('case'); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_TO_NEW_CASE_DESCRIPTION_INPUT).type( - 'case description' - ); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_TO_NEW_CASE_CREATE_BUTTON).click(); - - cy.get(VIEW_CASE_TOASTER_LINK).should('be.visible').and('contain.text', 'View case'); - }); - - it('should mark as acknowledged', () => { - cy.get(ALERT_CHECKBOX).should('have.length', 2); - - expandFirstAlertExpandableFlyout(); - openTakeActionButtonAndSelectItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_MARK_AS_ACKNOWLEDGED); +describe('Alert details expandable flyout right panel', () => { + const rule = getNewRule(); - // TODO figure out how to verify the toasts pops up - // cy.get(KIBANA_TOAST) - // .should('be.visible') - // .and('have.text', 'Successfully marked 1 alert as acknowledged.'); - cy.get(ALERT_CHECKBOX).should('have.length', 1); - }); + beforeEach(() => { + cleanKibana(); + login(); + createRule(rule); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); - it('should mark as closed', () => { - cy.get(ALERT_CHECKBOX).should('have.length', 2); + it('should display header and footer basics', () => { + expandFirstAlertExpandableFlyout(); - expandFirstAlertExpandableFlyout(); - openTakeActionButtonAndSelectItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_MARK_AS_CLOSED); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); + + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_CHAT_BUTTON).should('be.visible'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_STATUS).should('be.visible'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE_VALUE) + .should('be.visible') + .and('have.text', rule.risk_score); - // TODO figure out how to verify the toasts pops up - // cy.get(KIBANA_TOAST).should('be.visible').and('have.text', 'Successfully closed 1 alert.'); - cy.get(ALERT_CHECKBOX).should('have.length', 1); - }); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY_VALUE) + .should('be.visible') + .and('have.text', upperFirst(rule.severity)); - // these actions are now grouped together as we're not really testing their functionality but just the existence of the option in the dropdown - it('should test other action within take action dropdown', () => { - expandFirstAlertExpandableFlyout(); + cy.log('Verify all 3 tabs are visible'); - cy.log('should add endpoint exception'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB).should('be.visible').and('have.text', 'Overview'); + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB).should('be.visible').and('have.text', 'Table'); + cy.get(DOCUMENT_DETAILS_FLYOUT_JSON_TAB).should('be.visible').and('have.text', 'JSON'); + + cy.log('Verify the expand/collapse button is visible and functionality works'); + + expandDocumentDetailsExpandableFlyoutLeftSection(); + cy.get(DOCUMENT_DETAILS_FLYOUT_COLLAPSE_DETAILS_BUTTON) + .should('be.visible') + .and('have.text', 'Collapse details'); - // TODO figure out why this option is disabled in Cypress but not running the app locally - // https://github.com/elastic/security-team/issues/6300 - openTakeActionButton(); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_ENDPOINT_EXCEPTION).should('be.disabled'); + collapseDocumentDetailsExpandableFlyoutLeftSection(); + cy.get(DOCUMENT_DETAILS_FLYOUT_EXPAND_DETAILS_BUTTON) + .should('be.visible') + .and('have.text', 'Expand details'); + + cy.log('Verify the take action button is visible on all tabs'); - cy.log('should add rule exception'); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_TAKE_ACTION_BUTTON).should('be.visible'); - // TODO this isn't fully testing the add rule exception yet - // https://github.com/elastic/security-team/issues/6301 - selectTakeActionItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_RULE_EXCEPTION); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_RULE_EXCEPTION_FLYOUT_HEADER).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_RULE_EXCEPTION_FLYOUT_CANCEL_BUTTON) - .should('be.visible') - .click(); + openTableTab(); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_TAKE_ACTION_BUTTON).should('be.visible'); - // cy.log('should isolate host'); + openJsonTab(); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_TAKE_ACTION_BUTTON).should('be.visible'); + }); + + // TODO this will change when add to existing case is improved + // https://github.com/elastic/security-team/issues/6298 + it('should add to existing case', () => { + navigateToCasesPage(); + createNewCaseFromCases(); + + cy.get(CASE_DETAILS_PAGE_TITLE).should('be.visible').and('have.text', 'case'); + navigateToAlertsPage(); + expandFirstAlertExpandableFlyout(); + openTakeActionButtonAndSelectItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_TO_EXISTING_CASE); + + cy.get(EXISTING_CASE_SELECT_BUTTON).should('be.visible').contains('Select').click(); + cy.get(VIEW_CASE_TOASTER_LINK).should('be.visible').and('contain.text', 'View case'); + }); + + // TODO this will change when add to new case is improved + // https://github.com/elastic/security-team/issues/6298 + it('should add to new case', () => { + expandFirstAlertExpandableFlyout(); + openTakeActionButtonAndSelectItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_TO_NEW_CASE); + + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_TO_NEW_CASE_NAME_INPUT).type('case'); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_TO_NEW_CASE_DESCRIPTION_INPUT).type( + 'case description' + ); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_TO_NEW_CASE_CREATE_BUTTON).click(); + + cy.get(VIEW_CASE_TOASTER_LINK).should('be.visible').and('contain.text', 'View case'); + }); + + it('should mark as acknowledged', () => { + cy.get(ALERT_CHECKBOX).should('have.length', 2); + + expandFirstAlertExpandableFlyout(); + openTakeActionButtonAndSelectItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_MARK_AS_ACKNOWLEDGED); + + // TODO figure out how to verify the toasts pops up + // cy.get(KIBANA_TOAST) + // .should('be.visible') + // .and('have.text', 'Successfully marked 1 alert as acknowledged.'); + cy.get(ALERT_CHECKBOX).should('have.length', 1); + }); - // TODO figure out why isolate host isn't showing up in the dropdown - // https://github.com/elastic/security-team/issues/6302 - // openTakeActionButton(); - // cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ISOLATE_HOST).should('be.visible'); + it('should mark as closed', () => { + cy.get(ALERT_CHECKBOX).should('have.length', 2); - cy.log('should respond'); + expandFirstAlertExpandableFlyout(); + openTakeActionButtonAndSelectItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_MARK_AS_CLOSED); - // TODO this will change when respond is improved - // https://github.com/elastic/security-team/issues/6303 - openTakeActionButton(); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_RESPOND).should('be.disabled'); - - cy.log('should investigate in timeline'); - - selectTakeActionItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_INVESTIGATE_IN_TIMELINE); - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_INVESTIGATE_IN_TIMELINE_SECTION) - .first() - .within(() => - cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_INVESTIGATE_IN_TIMELINE_ENTRY).should('be.visible') - ); - }); - } -); + // TODO figure out how to verify the toasts pops up + // cy.get(KIBANA_TOAST).should('be.visible').and('have.text', 'Successfully closed 1 alert.'); + cy.get(ALERT_CHECKBOX).should('have.length', 1); + }); + + // these actions are now grouped together as we're not really testing their functionality but just the existence of the option in the dropdown + it('should test other action within take action dropdown', () => { + expandFirstAlertExpandableFlyout(); + + cy.log('should add endpoint exception'); + + // TODO figure out why this option is disabled in Cypress but not running the app locally + // https://github.com/elastic/security-team/issues/6300 + openTakeActionButton(); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_ENDPOINT_EXCEPTION).should('be.disabled'); + + cy.log('should add rule exception'); + + // TODO this isn't fully testing the add rule exception yet + // https://github.com/elastic/security-team/issues/6301 + selectTakeActionItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_RULE_EXCEPTION); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_RULE_EXCEPTION_FLYOUT_HEADER).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ADD_RULE_EXCEPTION_FLYOUT_CANCEL_BUTTON) + .should('be.visible') + .click(); + + // cy.log('should isolate host'); + + // TODO figure out why isolate host isn't showing up in the dropdown + // https://github.com/elastic/security-team/issues/6302 + // openTakeActionButton(); + // cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_ISOLATE_HOST).should('be.visible'); + + cy.log('should respond'); + + // TODO this will change when respond is improved + // https://github.com/elastic/security-team/issues/6303 + openTakeActionButton(); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_RESPOND).should('be.disabled'); + + cy.log('should investigate in timeline'); + + selectTakeActionItem(DOCUMENT_DETAILS_FLYOUT_FOOTER_INVESTIGATE_IN_TIMELINE); + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_INVESTIGATE_IN_TIMELINE_SECTION) + .first() + .within(() => + cy.get(DOCUMENT_DETAILS_FLYOUT_FOOTER_INVESTIGATE_IN_TIMELINE_ENTRY).should('be.visible') + ); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_json_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_json_tab.cy.ts index 7bd86e509ac185..5a1c9703ae83db 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_json_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_json_tab.cy.ts @@ -16,25 +16,21 @@ import { getNewRule } from '../../../../objects/rule'; import { ALERTS_URL } from '../../../../urls/navigation'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( - 'Alert details expandable flyout right panel json tab', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - beforeEach(() => { - cleanKibana(); - login(); - createRule(getNewRule()); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); - openJsonTab(); - }); +describe('Alert details expandable flyout right panel json tab', () => { + beforeEach(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + openJsonTab(); + }); - it('should display the json component', () => { - // the json component is rendered within a dom element with overflow, so Cypress isn't finding it - // this next line is a hack that vertically scrolls down to ensure Cypress finds it - scrollWithinDocumentDetailsExpandableFlyoutRightSection(0, 7000); - cy.get(DOCUMENT_DETAILS_FLYOUT_JSON_TAB_CONTENT).should('be.visible'); - }); - } -); + it('should display the json component', () => { + // the json component is rendered within a dom element with overflow, so Cypress isn't finding it + // this next line is a hack that vertically scrolls down to ensure Cypress finds it + scrollWithinDocumentDetailsExpandableFlyoutRightSection(0, 7000); + cy.get(DOCUMENT_DETAILS_FLYOUT_JSON_TAB_CONTENT).should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts index 9dc5dccbddcc3d..ef8884f560dce7 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts @@ -14,7 +14,7 @@ import { import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_CONTENT, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_HEADER, - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_DETAILS, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON, @@ -25,8 +25,6 @@ import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_CONTENT, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_HEADER, - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_CONTENT, - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_HEADER, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_CONTENT, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_HEADER, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VALUES, @@ -40,17 +38,15 @@ import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE, - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_RESPONSE_SECTION_EMPTY_RESPONSE, } from '../../../../screens/expandable_flyout/alert_details_right_panel_overview_tab'; import { - clickCorrelationsViewAllButton, - clickEntitiesViewAllButton, + navigateToCorrelationsDetails, clickInvestigationGuideButton, - clickPrevalenceViewAllButton, - clickThreatIntelligenceViewAllButton, + navigateToPrevalenceDetails, toggleOverviewTabAboutSection, toggleOverviewTabInsightsSection, toggleOverviewTabInvestigationSection, @@ -69,292 +65,287 @@ import { DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS, } from '../../../../screens/expandable_flyout/alert_details_left_panel_entities_tab'; -describe( - 'Alert details expandable flyout right panel overview tab', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - const rule = getNewRule(); - - beforeEach(() => { - cleanKibana(); - login(); - createRule(rule); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); +describe('Alert details expandable flyout right panel overview tab', () => { + const rule = getNewRule(); + + beforeEach(() => { + cleanKibana(); + login(); + createRule(rule); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + }); + + describe('about section', () => { + it('should display about section', () => { + cy.log('header and content'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_HEADER) + .should('be.visible') + .and('have.text', 'About'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_CONTENT).should('be.visible'); + + cy.log('description'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE) + .should('be.visible') + .and('contain.text', 'Rule description'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE) + .should('be.visible') + .within(() => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON) + .should('be.visible') + .and('have.text', 'Rule summary'); + }); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_DETAILS) + .should('be.visible') + .and('have.text', rule.description); + + cy.log('reason'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE) + .should('be.visible') + .and('have.text', 'Alert reason'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS) + .should('be.visible') + .and('contain.text', rule.name); + + cy.log('mitre attack'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE) + .should('be.visible') + // @ts-ignore + .and('contain.text', rule.threat[0].framework); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_DETAILS) + .should('be.visible') + // @ts-ignore + .and('contain.text', rule.threat[0].technique[0].name) + // @ts-ignore + .and('contain.text', rule.threat[0].tactic.name); }); + }); - describe('about section', () => { - it('should display about section', () => { - cy.log('header and content'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_HEADER) - .should('be.visible') - .and('have.text', 'About'); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_CONTENT).should('be.visible'); - - cy.log('description'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE) - .should('be.visible') - .and('contain.text', 'Rule description'); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE) - .should('be.visible') - .within(() => { - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON) - .should('be.visible') - .and('have.text', 'Rule summary'); - }); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_DETAILS) - .should('be.visible') - .and('have.text', rule.description); - - cy.log('reason'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE) - .should('be.visible') - .and('have.text', 'Alert reason'); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS) - .should('be.visible') - .and('contain.text', rule.name); - - cy.log('mitre attack'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE) - .should('be.visible') - // @ts-ignore - .and('contain.text', rule.threat[0].framework); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_DETAILS) - .should('be.visible') - // @ts-ignore - .and('contain.text', rule.threat[0].technique[0].name) - // @ts-ignore - .and('contain.text', rule.threat[0].tactic.name); - }); + describe('visualizations section', () => { + it('should display analyzer and session previews', () => { + toggleOverviewTabAboutSection(); + toggleOverviewTabVisualizationsSection(); + + cy.log('analyzer graph preview'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT).should('be.visible'); + + cy.log('session view preview'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT).should('be.visible'); }); + }); + + describe('investigation section', () => { + it('should display investigation section', () => { + toggleOverviewTabAboutSection(); + + cy.log('header and content'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_SECTION_HEADER) + .should('be.visible') + .and('have.text', 'Investigation'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_SECTION_CONTENT).should( + 'be.visible' + ); + + cy.log('investigation guide button'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_GUIDE_BUTTON) + .should('be.visible') + .and('have.text', 'Investigation guide'); + + cy.log('should navigate to left Investigation tab'); + + clickInvestigationGuideButton(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INVESTIGATION_TAB_CONTENT).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INVESTIGATION_TAB_CONTENT).should('be.visible'); - describe('visualizations section', () => { - it('should display analyzer and session previews', () => { - toggleOverviewTabAboutSection(); - toggleOverviewTabVisualizationsSection(); + cy.log('highlighted fields'); - cy.log('analyzer graph preview'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_HEADER_TITLE) + .should('be.visible') + .and('have.text', 'Highlighted fields'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_DETAILS).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL) + .should('be.visible') + .and('contain.text', 'host.name'); + const hostNameCell = + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL('siem-kibana'); + cy.get(hostNameCell).should('be.visible').and('have.text', 'siem-kibana'); - cy.log('session view preview'); + cy.get(hostNameCell).click(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS).should('be.visible'); + + collapseDocumentDetailsExpandableFlyoutLeftSection(); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL) + .should('be.visible') + .and('contain.text', 'user.name'); + const userNameCell = + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL('test'); + cy.get(userNameCell).should('be.visible').and('have.text', 'test'); + + cy.get(userNameCell).click(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS).should('be.visible'); + }); + }); + + describe('insights section', () => { + it('should display entities section', () => { + toggleOverviewTabAboutSection(); + toggleOverviewTabInvestigationSection(); + toggleOverviewTabInsightsSection(); + + cy.log('header and content'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_HEADER).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_HEADER) + .should('be.visible') + .and('have.text', 'Entities'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_CONTENT).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_HEADER).should('be.visible'); + + cy.log('should navigate to left panel Entities tab'); + + // TODO: skipping this section as Cypress can't seem to find the element (though it's in the DOM) + // navigateToEntitiesDetails(); + // cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); + }); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW).should('be.visible'); - }); + it('should display threat intelligence section', () => { + toggleOverviewTabAboutSection(); + toggleOverviewTabInvestigationSection(); + toggleOverviewTabInsightsSection(); + + cy.log('header and content'); + + cy.get( + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER + ).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER) + .should('be.visible') + .and('have.text', 'Threat Intelligence'); + cy.get( + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT + ).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT) + .should('be.visible') + .within(() => { + // threat match detected + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) + .eq(0) + .should('be.visible') + .and('have.text', '0 threat match detected'); // TODO work on getting proper IoC data to get proper data here + + // field with threat enrichement + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) + .eq(1) + .should('be.visible') + .and('have.text', '0 field enriched with threat intelligence'); // TODO work on getting proper IoC data to get proper data here + }); + + cy.log('should navigate to left panel Threat Intelligence tab'); + + // TODO: skipping this section as Cypress can't seem to find the element (though it's in the DOM) + // navigateToThreatIntelligenceDetails(); + // cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); // TODO update when we can navigate to Threat Intelligence sub tab directly }); - describe('investigation section', () => { - it('should display investigation section', () => { - toggleOverviewTabAboutSection(); - toggleOverviewTabInvestigationSection(); - - cy.log('header and content'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_SECTION_HEADER) - .should('be.visible') - .and('have.text', 'Investigation'); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_SECTION_CONTENT).should( - 'be.visible' - ); - - cy.log('investigation guide button'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_GUIDE_BUTTON) - .should('be.visible') - .and('have.text', 'Investigation guide'); - - cy.log('should navigate to left Investigation tab'); - - clickInvestigationGuideButton(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INVESTIGATION_TAB_CONTENT).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INVESTIGATION_TAB_CONTENT).should('be.visible'); - - cy.log('highlighted fields'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_HEADER_TITLE) - .should('be.visible') - .and('have.text', 'Highlighted fields'); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_DETAILS).should( - 'be.visible' - ); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL) - .should('be.visible') - .and('contain.text', 'host.name'); - const hostNameCell = - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL('siem-kibana'); - cy.get(hostNameCell).should('be.visible').and('have.text', 'siem-kibana'); - - cy.get(hostNameCell).click(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS).should('be.visible'); - - collapseDocumentDetailsExpandableFlyoutLeftSection(); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL) - .should('be.visible') - .and('contain.text', 'user.name'); - const userNameCell = - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL('test'); - cy.get(userNameCell).should('be.visible').and('have.text', 'test'); - - cy.get(userNameCell).click(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS).should('be.visible'); - }); + // TODO: skipping this due to flakiness + it.skip('should display correlations section', () => { + cy.log('link the alert to a new case'); + + createNewCaseFromExpandableFlyout(); + + toggleOverviewTabAboutSection(); + toggleOverviewTabInvestigationSection(); + toggleOverviewTabInsightsSection(); + + cy.log('header and content'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_HEADER).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_HEADER) + .should('be.visible') + .and('have.text', 'Correlations'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_CONTENT).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_CONTENT) + .should('be.visible') + .within(() => { + // TODO the order in which these appear is not deterministic currently, hence this can cause flakiness + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES) + .eq(0) + .should('be.visible') + .and('have.text', '1 alert related by ancestry'); + // cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES) + // .eq(2) + // .should('be.visible') + // .and('have.text', '1 alert related by the same source event'); // TODO work on getting proper data to display some same source data here + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES) + .eq(2) + .should('be.visible') + .and('have.text', '1 alert related by session'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES) + .eq(1) + .should('be.visible') + .and('have.text', '1 related case'); + }); + + cy.log('should navigate to left panel Correlations tab'); + + navigateToCorrelationsDetails(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); // TODO update when we can navigate to Correlations sub tab directly }); - describe('insights section', () => { - it('should display entities section', () => { - toggleOverviewTabAboutSection(); - toggleOverviewTabInsightsSection(); - - cy.log('header and content'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_HEADER).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_HEADER) - .should('be.visible') - .and('have.text', 'Entities'); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_CONTENT).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_HEADER).should( - 'be.visible' - ); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_CONTENT).should( - 'be.visible' - ); - - cy.log('should navigate to left panel Entities tab'); - - clickEntitiesViewAllButton(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); - }); - - it('should display threat intelligence section', () => { - toggleOverviewTabAboutSection(); - toggleOverviewTabInsightsSection(); - - cy.log('header and content'); - - cy.get( - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER - ).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER) - .should('be.visible') - .and('have.text', 'Threat Intelligence'); - cy.get( - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT - ).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT) - .should('be.visible') - .within(() => { - // threat match detected - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) - .eq(0) - .should('be.visible') - .and('have.text', '0 threat match detected'); // TODO work on getting proper IoC data to get proper data here - - // field with threat enrichement - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) - .eq(1) - .should('be.visible') - .and('have.text', '0 field enriched with threat intelligence'); // TODO work on getting proper IoC data to get proper data here - }); - - cy.log('should navigate to left panel Threat Intelligence tab'); - - clickThreatIntelligenceViewAllButton(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); // TODO update when we can navigate to Threat Intelligence sub tab directly - }); - - // TODO: skipping this due to flakiness - it.skip('should display correlations section', () => { - cy.log('link the alert to a new case'); - - createNewCaseFromExpandableFlyout(); - - toggleOverviewTabAboutSection(); - toggleOverviewTabInsightsSection(); - - cy.log('header and content'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_HEADER).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_HEADER) - .should('be.visible') - .and('have.text', 'Correlations'); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_CONTENT).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_CONTENT) - .should('be.visible') - .within(() => { - // TODO the order in which these appear is not deterministic currently, hence this can cause flakiness - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES) - .eq(0) - .should('be.visible') - .and('have.text', '1 alert related by ancestry'); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES) - .eq(1) - .should('be.visible') - .and('have.text', '1 related case'); - // cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES) - // .eq(2) - // .should('be.visible') - // .and('have.text', '1 alert related by the same source event'); // TODO work on getting proper data to display some same source data here - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES) - .eq(2) - .should('be.visible') - .and('have.text', '1 alert related by session'); - }); - - cy.log('should navigate to left panel Correlations tab'); - - clickCorrelationsViewAllButton(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); // TODO update when we can navigate to Correlations sub tab directly - }); - - // TODO work on getting proper data to make the prevalence section work here - // we need to generate enough data to have at least one field with prevalence - it.skip('should display prevalence section', () => { - toggleOverviewTabAboutSection(); - toggleOverviewTabInsightsSection(); - - cy.log('header and content'); - - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_HEADER).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_HEADER) - .should('be.visible') - .and('have.text', 'Prevalence'); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_CONTENT).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_CONTENT) - .should('be.visible') - .within(() => { - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VALUES) - .should('be.visible') - .and('have.text', 'is uncommon'); - }); - - cy.log('should navigate to left panel Prevalence tab'); - - clickPrevalenceViewAllButton(); - cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); // TODO update when we can navigate to Prevalence sub tab directly - }); + // TODO work on getting proper data to make the prevalence section work here + // we need to generate enough data to have at least one field with prevalence + it.skip('should display prevalence section', () => { + toggleOverviewTabAboutSection(); + toggleOverviewTabInvestigationSection(); + toggleOverviewTabInsightsSection(); + + cy.log('header and content'); + + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_HEADER).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_HEADER) + .should('be.visible') + .and('have.text', 'Prevalence'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_CONTENT).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_CONTENT) + .should('be.visible') + .within(() => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VALUES) + .should('be.visible') + .and('have.text', 'is uncommon'); + }); + + cy.log('should navigate to left panel Prevalence tab'); + + navigateToPrevalenceDetails(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); // TODO update when we can navigate to Prevalence sub tab directly }); + }); - describe('response section', () => { - it('should display empty message', () => { - toggleOverviewTabAboutSection(); - toggleOverviewTabResponseSection(); + describe('response section', () => { + it('should display empty message', () => { + toggleOverviewTabAboutSection(); + toggleOverviewTabInvestigationSection(); + toggleOverviewTabResponseSection(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_RESPONSE_SECTION_EMPTY_RESPONSE).should( - 'be.visible' - ); - }); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_RESPONSE_SECTION_EMPTY_RESPONSE).should( + 'be.visible' + ); }); - } -); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_table_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_table_tab.cy.ts index 9e30ba52b3cdd7..ff89e16a02b03a 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_table_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_table_tab.cy.ts @@ -31,52 +31,48 @@ import { getNewRule } from '../../../../objects/rule'; import { ALERTS_URL } from '../../../../urls/navigation'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( - 'Alert details expandable flyout right panel table tab', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - beforeEach(() => { - cleanKibana(); - login(); - createRule(getNewRule()); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlertExpandableFlyout(); - openTableTab(); - }); +describe('Alert details expandable flyout right panel table tab', () => { + beforeEach(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + openTableTab(); + }); - it('should display and filter the table', () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_ROW).should('be.visible'); - cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ID_ROW).should('be.visible'); - filterTableTabTable('timestamp'); - cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_ROW).should('be.visible'); - clearFilterTableTabTable(); - }); + it('should display and filter the table', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_ROW).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ID_ROW).should('be.visible'); + filterTableTabTable('timestamp'); + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_ROW).should('be.visible'); + clearFilterTableTabTable(); + }); - it('should test cell actions', () => { - cy.log('cell actions filter in'); + it('should test cell actions', () => { + cy.log('cell actions filter in'); - filterInTableTabTable(); - cy.get(FILTER_BADGE).first().should('contain.text', '@timestamp:'); - removeKqlFilter(); + filterInTableTabTable(); + cy.get(FILTER_BADGE).first().should('contain.text', '@timestamp:'); + removeKqlFilter(); - cy.log('cell actions filter out'); + cy.log('cell actions filter out'); - filterOutTableTabTable(); - cy.get(FILTER_BADGE).first().should('contain.text', 'NOT @timestamp:'); - removeKqlFilter(); + filterOutTableTabTable(); + cy.get(FILTER_BADGE).first().should('contain.text', 'NOT @timestamp:'); + removeKqlFilter(); - cy.log('cell actions add to timeline'); + cy.log('cell actions add to timeline'); - addToTimelineTableTabTable(); - openActiveTimeline(); - cy.get(PROVIDER_BADGE).first().should('contain.text', '@timestamp'); - closeTimeline(); + addToTimelineTableTabTable(); + openActiveTimeline(); + cy.get(PROVIDER_BADGE).first().should('contain.text', '@timestamp'); + closeTimeline(); - cy.log('cell actions copy to clipboard'); + cy.log('cell actions copy to clipboard'); - copyToClipboardTableTabTable(); - cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_COPY_TO_CLIPBOARD).should('be.visible'); - }); - } -); + copyToClipboardTableTabTable(); + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_COPY_TO_CLIPBOARD).should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_url_sync.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_url_sync.cy.ts index fa268bfdfa341c..e926e93e633019 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_url_sync.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_url_sync.cy.ts @@ -15,43 +15,39 @@ import { closeFlyout } from '../../../../tasks/expandable_flyout/alert_details_r import { expandFirstAlertExpandableFlyout } from '../../../../tasks/expandable_flyout/common'; import { DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE } from '../../../../screens/expandable_flyout/alert_details_right_panel'; -describe( - 'Expandable flyout state sync', - { env: { ftrConfig: { enableExperimental: ['securityFlyoutEnabled'] } } }, - () => { - const rule = getNewRule(); +describe('Expandable flyout state sync', () => { + const rule = getNewRule(); - beforeEach(() => { - cleanKibana(); - login(); - createRule(rule); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - }); + beforeEach(() => { + cleanKibana(); + login(); + createRule(rule); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); - it('should test flyout url sync', () => { - cy.url().should('not.include', 'eventFlyout'); + it('should test flyout url sync', () => { + cy.url().should('not.include', 'eventFlyout'); - expandFirstAlertExpandableFlyout(); + expandFirstAlertExpandableFlyout(); - cy.log('should serialize its state to url'); + cy.log('should serialize its state to url'); - cy.url().should('include', 'eventFlyout'); - cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); + cy.url().should('include', 'eventFlyout'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); - cy.log('should reopen the flyout after browser refresh'); + cy.log('should reopen the flyout after browser refresh'); - cy.reload(); - waitForAlertsToPopulate(); + cy.reload(); + waitForAlertsToPopulate(); - cy.url().should('include', 'eventFlyout'); - cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); + cy.url().should('include', 'eventFlyout'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); - cy.log('should clear the url state when flyout is closed'); + cy.log('should clear the url state when flyout is closed'); - closeFlyout(); + closeFlyout(); - cy.url().should('not.include', 'eventFlyout'); - }); - } -); + cy.url().should('not.include', 'eventFlyout'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts index 5a8c382bdf4afd..5ef959899178a7 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/investigate_in_timeline.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { disableExpandableFlyout } from '../../../tasks/api_calls/kibana_advanced_settings'; import { getNewRule } from '../../../objects/rule'; import { PROVIDER_BADGE, QUERY_TAB_BUTTON, TIMELINE_TITLE } from '../../../screens/timeline'; import { FILTER_BADGE } from '../../../screens/alerts'; @@ -53,6 +54,7 @@ describe('Investigate in timeline', () => { describe('From alerts details flyout', () => { beforeEach(() => { login(); + disableExpandableFlyout(); visit(ALERTS_URL); waitForAlertsToPopulate(); expandFirstAlert(); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 40d16dd088af8e..ab5a2697d4be42 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -158,7 +158,7 @@ export const RULES_SELECTED_TAG = '.euiSelectableListItem[aria-checked="true"]'; export const SELECTED_RULES_NUMBER_LABEL = '[data-test-subj="selectedRules"]'; -export const REFRESH_SETTINGS_POPOVER = '[data-test-subj="refreshSettings-popover"]'; +export const AUTO_REFRESH_POPOVER_TRIGGER_BUTTON = '[data-test-subj="autoRefreshButton"]'; export const REFRESH_RULES_TABLE_BUTTON = '[data-test-subj="refreshRulesAction-linkIcon"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts index 21fef179980d59..6bc2f1400e0ae5 100644 --- a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts +++ b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts @@ -9,30 +9,15 @@ import { getDataTestSubjectSelector } from '../../helpers/common'; import { ABOUT_SECTION_CONTENT_TEST_ID, ABOUT_SECTION_HEADER_TEST_ID, - ANALYZER_TREE_TEST_ID, DESCRIPTION_DETAILS_TEST_ID, DESCRIPTION_TITLE_TEST_ID, RULE_SUMMARY_BUTTON_TEST_ID, - ENTITIES_CONTENT_TEST_ID, - ENTITIES_HEADER_TEST_ID, - ENTITIES_VIEW_ALL_BUTTON_TEST_ID, - ENTITY_PANEL_CONTENT_TEST_ID, - ENTITY_PANEL_HEADER_TEST_ID, HIGHLIGHTED_FIELDS_DETAILS_TEST_ID, HIGHLIGHTED_FIELDS_TITLE_TEST_ID, INSIGHTS_CORRELATIONS_CONTENT_TEST_ID, - INSIGHTS_CORRELATIONS_TITLE_TEST_ID, - INSIGHTS_CORRELATIONS_VALUE_TEST_ID, - INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID, INSIGHTS_HEADER_TEST_ID, INSIGHTS_PREVALENCE_CONTENT_TEST_ID, - INSIGHTS_PREVALENCE_TITLE_TEST_ID, - INSIGHTS_PREVALENCE_VALUE_TEST_ID, - INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON_TEST_ID, INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID, - INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID, - INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID, - INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID, INVESTIGATION_GUIDE_BUTTON_TEST_ID, INVESTIGATION_SECTION_CONTENT_TEST_ID, INVESTIGATION_SECTION_HEADER_TEST_ID, @@ -40,11 +25,20 @@ import { MITRE_ATTACK_TITLE_TEST_ID, REASON_DETAILS_TEST_ID, REASON_TITLE_TEST_ID, - SESSION_PREVIEW_TEST_ID, VISUALIZATIONS_SECTION_HEADER_TEST_ID, HIGHLIGHTED_FIELDS_CELL_TEST_ID, RESPONSE_SECTION_HEADER_TEST_ID, RESPONSE_EMPTY_TEST_ID, + INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID, + INSIGHTS_ENTITIES_CONTENT_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID, + INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID, + INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID, + INSIGHTS_CORRELATIONS_VALUE_TEST_ID, + ANALYZER_PREVIEW_CONTENT_TEST_ID, + SESSION_PREVIEW_CONTENT_TEST_ID, + INSIGHTS_PREVALENCE_VALUE_TEST_ID, } from '../../../public/flyout/right/components/test_ids'; /* About section */ @@ -94,50 +88,49 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_GUIDE_BUTTON = export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_SECTION_HEADER = getDataTestSubjectSelector(INSIGHTS_HEADER_TEST_ID); + +/* Insights Entities */ + export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_HEADER = - getDataTestSubjectSelector(ENTITIES_HEADER_TEST_ID); + getDataTestSubjectSelector(INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_CONTENT = - getDataTestSubjectSelector(ENTITIES_CONTENT_TEST_ID); -export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_VIEW_ALL_ENTITIES_BUTTON = - getDataTestSubjectSelector(ENTITIES_VIEW_ALL_BUTTON_TEST_ID); -export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_HEADER = - getDataTestSubjectSelector(ENTITY_PANEL_HEADER_TEST_ID); -export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_CONTENT = - getDataTestSubjectSelector(ENTITY_PANEL_CONTENT_TEST_ID); + getDataTestSubjectSelector(INSIGHTS_ENTITIES_CONTENT_TEST_ID); + +/* Insights Threat Intelligence */ + export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER = - getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID); + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT = getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES = getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID); -export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON = - getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID); + +/* Insights Correlations */ + export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_HEADER = - getDataTestSubjectSelector(INSIGHTS_CORRELATIONS_TITLE_TEST_ID); + getDataTestSubjectSelector(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_CONTENT = getDataTestSubjectSelector(INSIGHTS_CORRELATIONS_CONTENT_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES = getDataTestSubjectSelector(INSIGHTS_CORRELATIONS_VALUE_TEST_ID); -export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON = - getDataTestSubjectSelector(INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID); + +/* Insights Prevalence */ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_HEADER = - getDataTestSubjectSelector(INSIGHTS_PREVALENCE_TITLE_TEST_ID); + getDataTestSubjectSelector(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_CONTENT = getDataTestSubjectSelector(INSIGHTS_PREVALENCE_CONTENT_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VALUES = getDataTestSubjectSelector(INSIGHTS_PREVALENCE_VALUE_TEST_ID); -export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON = - getDataTestSubjectSelector(INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON_TEST_ID); /* Visualization section */ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_VISUALIZATIONS_SECTION_HEADER = getDataTestSubjectSelector(VISUALIZATIONS_SECTION_HEADER_TEST_ID); -export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE = - getDataTestSubjectSelector(ANALYZER_TREE_TEST_ID); -export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW = - getDataTestSubjectSelector(SESSION_PREVIEW_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT = + getDataTestSubjectSelector(ANALYZER_PREVIEW_CONTENT_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT = + getDataTestSubjectSelector(SESSION_PREVIEW_CONTENT_TEST_ID); /* Response section */ diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index d73a67cd6c271d..221c3d6a615016 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -12,7 +12,6 @@ import { DELETE_RULE_ACTION_BTN, RULES_SELECTED_TAG, RULES_TABLE_INITIAL_LOADING_INDICATOR, - RULES_TABLE_AUTOREFRESH_INDICATOR, RULE_CHECKBOX, RULE_NAME, RULE_SWITCH, @@ -37,7 +36,6 @@ import { RULES_TAGS_POPOVER_WRAPPER, INTEGRATIONS_POPOVER, SELECTED_RULES_NUMBER_LABEL, - REFRESH_SETTINGS_POPOVER, REFRESH_SETTINGS_SWITCH, ELASTIC_RULES_BTN, TOASTER_ERROR_BTN, @@ -57,6 +55,7 @@ import { TOASTER_CLOSE_ICON, ADD_ELASTIC_RULES_EMPTY_PROMPT_BTN, CONFIRM_DELETE_RULE_BTN, + AUTO_REFRESH_POPOVER_TRIGGER_BUTTON, } from '../screens/alerts_detection_rules'; import type { RULES_MONITORING_TABLE } from '../screens/alerts_detection_rules'; import { EUI_CHECKBOX } from '../screens/common/controls'; @@ -305,12 +304,6 @@ export const waitForRuleToUpdate = () => { cy.get(RULE_SWITCH_LOADER, { timeout: 300000 }).should('not.exist'); }; -export const checkAutoRefresh = (ms: number, condition: string) => { - cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should('not.exist'); - cy.tick(ms); - cy.get(RULES_TABLE_AUTOREFRESH_INDICATOR).should(condition); -}; - export const importRules = (rulesFile: string) => { cy.get(RULE_IMPORT_MODAL).click(); cy.get(INPUT_FILE).click({ force: true }); @@ -459,22 +452,45 @@ export const testMultipleSelectedRulesLabel = (rulesCount: number) => { cy.get(SELECTED_RULES_NUMBER_LABEL).should('have.text', `Selected ${rulesCount} rules`); }; -export const openRefreshSettingsPopover = () => { - cy.get(REFRESH_SETTINGS_POPOVER).click(); +const openRefreshSettingsPopover = () => { + cy.get(REFRESH_SETTINGS_SWITCH).should('not.exist'); + cy.get(AUTO_REFRESH_POPOVER_TRIGGER_BUTTON).click(); cy.get(REFRESH_SETTINGS_SWITCH).should('be.visible'); }; -export const checkAutoRefreshIsDisabled = () => { +const closeRefreshSettingsPopover = () => { + cy.get(REFRESH_SETTINGS_SWITCH).should('be.visible'); + cy.get(AUTO_REFRESH_POPOVER_TRIGGER_BUTTON).click(); + cy.get(REFRESH_SETTINGS_SWITCH).should('not.exist'); +}; + +export const expectAutoRefreshIsDisabled = () => { + cy.get(AUTO_REFRESH_POPOVER_TRIGGER_BUTTON).should('be.enabled'); + cy.get(AUTO_REFRESH_POPOVER_TRIGGER_BUTTON).should('have.text', 'Off'); + openRefreshSettingsPopover(); cy.get(REFRESH_SETTINGS_SWITCH).should('have.attr', 'aria-checked', 'false'); + closeRefreshSettingsPopover(); }; -export const checkAutoRefreshIsEnabled = () => { +export const expectAutoRefreshIsEnabled = () => { + cy.get(AUTO_REFRESH_POPOVER_TRIGGER_BUTTON).should('be.enabled'); + cy.get(AUTO_REFRESH_POPOVER_TRIGGER_BUTTON).should('have.text', 'On'); + openRefreshSettingsPopover(); cy.get(REFRESH_SETTINGS_SWITCH).should('have.attr', 'aria-checked', 'true'); + closeRefreshSettingsPopover(); +}; + +// Expects the auto refresh to be deactivated which means it's disabled without an ability to enable it +// so it's even impossible to open the popover +export const expectAutoRefreshIsDeactivated = () => { + cy.get(AUTO_REFRESH_POPOVER_TRIGGER_BUTTON).should('be.disabled'); + cy.get(AUTO_REFRESH_POPOVER_TRIGGER_BUTTON).should('have.text', 'Off'); }; export const disableAutoRefresh = () => { + openRefreshSettingsPopover(); cy.get(REFRESH_SETTINGS_SWITCH).click(); - checkAutoRefreshIsDisabled(); + expectAutoRefreshIsDisabled(); }; export const mockGlobalClock = () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/kibana_advanced_settings.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/kibana_advanced_settings.ts index 0cadb50d72fe16..6256539beca1d8 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/kibana_advanced_settings.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/kibana_advanced_settings.ts @@ -27,3 +27,8 @@ export const enableRelatedIntegrations = () => { export const disableRelatedIntegrations = () => { kibanaSettings(relatedIntegrationsBody(false)); }; + +export const disableExpandableFlyout = () => { + const body = { changes: { 'securitySolution:enableExpandableFlyout': false } }; + kibanaSettings(body); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts b/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts index f1f4f2a74d429e..90b040845812bb 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts @@ -5,15 +5,17 @@ * 2.0. */ +import { + INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID, + INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID, + INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID, +} from '../../../public/flyout/right/components/test_ids'; import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_HEADER, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_VISUALIZATIONS_SECTION_HEADER, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_SECTION_HEADER, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_SECTION_HEADER, - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_VIEW_ALL_ENTITIES_BUTTON, - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON, - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON, - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_GUIDE_BUTTON, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON, @@ -53,47 +55,35 @@ export const toggleOverviewTabInsightsSection = () => { }; /** - * Click on the view all button under the right section, Insights, Entities + * Click on the header in the right section, Insights, Entities */ -export const clickEntitiesViewAllButton = () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_VIEW_ALL_ENTITIES_BUTTON).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_VIEW_ALL_ENTITIES_BUTTON) - .should('be.visible') - .click(); +export const navigateToEntitiesDetails = () => { + cy.get(INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID).scrollIntoView(); + cy.get(INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID).should('be.visible').click(); }; /** - * Click on the view all button under the right section, Insights, Threat Intelligence + * Click on the header in the right section, Insights, Threat Intelligence */ -export const clickThreatIntelligenceViewAllButton = () => { - cy.get( - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON - ).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON) - .should('be.visible') - .click(); +export const navigateToThreatIntelligenceDetails = () => { + cy.get(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID).scrollIntoView(); + cy.get(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID).should('be.visible').click(); }; /** - * Click on the view all button under the right section, Insights, Correlations + * Click on the header in the right section, Insights, Correlations */ -export const clickCorrelationsViewAllButton = () => { - cy.get( - DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON - ).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON) - .should('be.visible') - .click(); +export const navigateToCorrelationsDetails = () => { + cy.get(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID).scrollIntoView(); + cy.get(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID).should('be.visible').click(); }; /** * Click on the view all button under the right section, Insights, Prevalence */ -export const clickPrevalenceViewAllButton = () => { - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON).scrollIntoView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON) - .should('be.visible') - .click(); +export const navigateToPrevalenceDetails = () => { + cy.get(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID).scrollIntoView(); + cy.get(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID).should('be.visible').click(); }; /* Visualizations section */ diff --git a/x-pack/plugins/security_solution/cypress/tasks/privileges.ts b/x-pack/plugins/security_solution/cypress/tasks/privileges.ts index bd55745200b4f1..b0489b14e8a8e1 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/privileges.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/privileges.ts @@ -63,6 +63,7 @@ export const secAll: Role = { { feature: { siem: ['all'], + securitySolutionAssistant: ['all'], securitySolutionCases: ['all'], actions: ['all'], actionsSimulators: ['all'], @@ -94,6 +95,7 @@ export const secReadCasesAll: Role = { { feature: { siem: ['read'], + securitySolutionAssistant: ['all'], securitySolutionCases: ['all'], actions: ['all'], actionsSimulators: ['all'], @@ -125,6 +127,7 @@ export const secAllCasesOnlyReadDelete: Role = { { feature: { siem: ['all'], + securitySolutionAssistant: ['all'], securitySolutionCases: ['cases_read', 'cases_delete'], actions: ['all'], actionsSimulators: ['all'], @@ -156,6 +159,7 @@ export const secAllCasesNoDelete: Role = { { feature: { siem: ['all'], + securitySolutionAssistant: ['all'], securitySolutionCases: ['minimal_all'], actions: ['all'], actionsSimulators: ['all'], diff --git a/x-pack/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx b/x-pack/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx index fb58fb5509ba2d..b2206157661b95 100644 --- a/x-pack/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx +++ b/x-pack/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx @@ -6,6 +6,8 @@ */ import { useLicense } from '../../common/hooks/use_license'; +import { useKibana } from '../../common/lib/kibana'; +import { ASSISTANT_FEATURE_ID } from '../../../common/constants'; export interface UseAssistantAvailability { // True when user is Enterprise. When false, the Assistant is disabled and unavailable @@ -16,11 +18,11 @@ export interface UseAssistantAvailability { export const useAssistantAvailability = (): UseAssistantAvailability => { const isEnterprise = useLicense().isEnterprise(); + const capabilities = useKibana().services.application.capabilities; + const isAssistantEnabled = capabilities[ASSISTANT_FEATURE_ID]?.['ai-assistant'] === true; + return { isAssistantEnabled: isEnterprise, - // TODO: RBAC check (https://github.com/elastic/security-team/issues/6932) - // Leaving as a placeholder for RBAC as the same behavior will be required - // When false, the Assistant is hidden and unavailable - hasAssistantPrivilege: true, + hasAssistantPrivilege: isAssistantEnabled, }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx index cd226d4347af60..2e9493dc6ff428 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx @@ -10,6 +10,8 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; import { dataTableActions } from '@kbn/securitysolution-data-table'; +import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; +import { ENABLE_EXPANDABLE_FLYOUT_SETTING } from '../../../../../common/constants'; import { RightPanelKey } from '../../../../flyout/right'; import type { SetEventsDeleted, @@ -21,7 +23,6 @@ import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/ import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/types/timeline'; -import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; type Props = EuiDataGridCellValueElementProps & { columnHeaders: ColumnHeaderOptions[]; @@ -70,7 +71,7 @@ const RowActionComponent = ({ const { openFlyout } = useExpandableFlyoutContext(); const dispatch = useDispatch(); - const isSecurityFlyoutEnabled = useIsExperimentalFeatureEnabled('securityFlyoutEnabled'); + const [isSecurityFlyoutEnabled] = useUiSetting$(ENABLE_EXPANDABLE_FLYOUT_SETTING); const columnValues = useMemo( () => diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts index 25c29fdb9fa2db..c8c675f0f40a5f 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts @@ -12,6 +12,7 @@ import { SecurityPageName } from '../../../../../common/constants'; import type { LinkInfo, LinkItem } from '../../../links'; import { useBreadcrumbsNav } from './use_breadcrumbs_nav'; import type { BreadcrumbsNav } from '../../../breadcrumbs'; +import * as kibanaLib from '../../../lib/kibana'; jest.mock('../../../lib/kibana'); @@ -136,6 +137,16 @@ describe('useBreadcrumbsNav', () => { }); it('should create breadcrumbs onClick handler', () => { + const reportBreadcrumbClickedMock = jest.fn(); + + (kibanaLib.useKibana as jest.Mock).mockImplementation(() => ({ + services: { + telemetry: { + reportBreadcrumbClicked: reportBreadcrumbClickedMock, + }, + }, + })); + renderHook(useBreadcrumbsNav); const event = { preventDefault: jest.fn() } as unknown as React.MouseEvent< HTMLElement, @@ -146,5 +157,6 @@ describe('useBreadcrumbsNav', () => { expect(event.preventDefault).toHaveBeenCalled(); expect(mockDispatch).toHaveBeenCalled(); + expect(reportBreadcrumbClickedMock).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts index 9eeae743bffaa0..5a467464670a5b 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { SyntheticEvent } from 'react'; import { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import type { ChromeBreadcrumb } from '@kbn/core/public'; -import { METRIC_TYPE } from '@kbn/analytics'; import type { Dispatch } from 'redux'; import { SecurityPageName } from '../../../../app/types'; import type { RouteSpyState } from '../../../utils/route/types'; @@ -16,8 +16,8 @@ import { timelineActions } from '../../../../timelines/store/timeline'; import { TimelineId } from '../../../../../common/types/timeline'; import type { GetSecuritySolutionUrl } from '../../link_to'; import { useGetSecuritySolutionUrl } from '../../link_to'; -import { TELEMETRY_EVENT, track } from '../../../lib/telemetry'; -import { useNavigateTo, type NavigateTo } from '../../../lib/kibana'; +import type { TelemetryClientStart } from '../../../lib/telemetry'; +import { useKibana, useNavigateTo, type NavigateTo } from '../../../lib/kibana'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; import { updateBreadcrumbsNav } from '../../../breadcrumbs'; import { getAncestorLinksInfo } from '../../../links'; @@ -26,6 +26,7 @@ import { getTrailingBreadcrumbs } from './trailing_breadcrumbs'; export const useBreadcrumbsNav = () => { const dispatch = useDispatch(); + const { telemetry } = useKibana().services; const [routeProps] = useRouteSpy(); const { navigateTo } = useNavigateTo(); const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); @@ -40,10 +41,10 @@ export const useBreadcrumbsNav = () => { const trailingBreadcrumbs = getTrailingBreadcrumbs(routeProps, getSecuritySolutionUrl); updateBreadcrumbsNav({ - leading: addOnClicksHandlers(leadingBreadcrumbs, dispatch, navigateTo), - trailing: addOnClicksHandlers(trailingBreadcrumbs, dispatch, navigateTo), + leading: addOnClicksHandlers(leadingBreadcrumbs, dispatch, navigateTo, telemetry), + trailing: addOnClicksHandlers(trailingBreadcrumbs, dispatch, navigateTo, telemetry), }); - }, [routeProps, getSecuritySolutionUrl, dispatch, navigateTo]); + }, [routeProps, getSecuritySolutionUrl, dispatch, navigateTo, telemetry]); }; const getLeadingBreadcrumbs = ( @@ -66,22 +67,36 @@ const getLeadingBreadcrumbs = ( const addOnClicksHandlers = ( breadcrumbs: ChromeBreadcrumb[], dispatch: Dispatch, - navigateTo: NavigateTo + navigateTo: NavigateTo, + telemetry: TelemetryClientStart ): ChromeBreadcrumb[] => breadcrumbs.map((breadcrumb) => ({ ...breadcrumb, ...(breadcrumb.href && !breadcrumb.onClick && { - onClick: createOnClickHandler(breadcrumb.href, dispatch, navigateTo), + onClick: createOnClickHandler( + breadcrumb.href, + dispatch, + navigateTo, + telemetry, + breadcrumb.text + ), }), })); const createOnClickHandler = - (href: string, dispatch: Dispatch, navigateTo: NavigateTo): ChromeBreadcrumb['onClick'] => - (ev) => { + ( + href: string, + dispatch: Dispatch, + navigateTo: NavigateTo, + telemetry: TelemetryClientStart, + title: React.ReactNode + ) => + (ev: SyntheticEvent) => { ev.preventDefault(); - const trackedPath = href.split('?')[0]; - track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.BREADCRUMB}${trackedPath}`); + if (typeof title === 'string') { + telemetry.reportBreadcrumbClicked({ title }); + } dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); navigateTo({ url: href }); }; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts index 73ea8a4dcdde44..624c845315f27e 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts @@ -34,21 +34,21 @@ export enum TELEMETRY_EVENT { // Landing page - dashboard DASHBOARD = 'navigate_to_dashboard', CREATE_DASHBOARD = 'create_dashboard', - - // Breadcrumbs - BREADCRUMB = 'breadcrumb_', } export enum TelemetryEventTypes { AlertsGroupingChanged = 'Alerts Grouping Changed', AlertsGroupingToggled = 'Alerts Grouping Toggled', AlertsGroupingTakeAction = 'Alerts Grouping Take Action', + BreadcrumbClicked = 'Breadcrumb Clicked', EntityDetailsClicked = 'Entity Details Clicked', EntityAlertsClicked = 'Entity Alerts Clicked', EntityRiskFiltered = 'Entity Risk Filtered', MLJobUpdate = 'ML Job Update', CellActionClicked = 'Cell Action Clicked', AnomaliesCountClicked = 'Anomalies Count Clicked', + DataQualityIndexChecked = 'Data Quality Index Checked', + DataQualityCheckAllCompleted = 'Data Quality Check All Completed', } export enum ML_JOB_TELEMETRY_STATUS { diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/index.ts new file mode 100644 index 00000000000000..3eb9cf66392d47 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/index.ts @@ -0,0 +1,90 @@ +/* + * 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 { TelemetryEvent } from '../../types'; +import { TelemetryEventTypes } from '../../constants'; + +export const alertsGroupingToggledEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.AlertsGroupingToggled, + schema: { + isOpen: { + type: 'boolean', + _meta: { + description: 'on or off', + optional: false, + }, + }, + tableId: { + type: 'text', + _meta: { + description: 'Table ID', + optional: false, + }, + }, + groupNumber: { + type: 'integer', + _meta: { + description: 'Group number', + optional: false, + }, + }, + }, +}; + +export const alertsGroupingChangedEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.AlertsGroupingChanged, + schema: { + tableId: { + type: 'keyword', + _meta: { + description: 'Table ID', + optional: false, + }, + }, + groupByField: { + type: 'keyword', + _meta: { + description: 'Selected field', + optional: false, + }, + }, + }, +}; + +export const alertsGroupingTakeActionEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.AlertsGroupingTakeAction, + schema: { + tableId: { + type: 'keyword', + _meta: { + description: 'Table ID', + optional: false, + }, + }, + groupNumber: { + type: 'integer', + _meta: { + description: 'Group number', + optional: false, + }, + }, + status: { + type: 'keyword', + _meta: { + description: 'Alert status', + optional: false, + }, + }, + groupByField: { + type: 'keyword', + _meta: { + description: 'Selected field', + optional: false, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/types.ts new file mode 100644 index 00000000000000..cc654e532f88d7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/alerts_grouping/types.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 type { RootSchema } from '@kbn/analytics-client'; +import type { TelemetryEventTypes } from '../../constants'; + +export interface ReportAlertsGroupingChangedParams { + tableId: string; + groupByField: string; +} + +export interface ReportAlertsGroupingToggledParams { + isOpen: boolean; + tableId: string; + groupNumber: number; +} + +export interface ReportAlertsTakeActionParams { + tableId: string; + groupNumber: number; + status: 'open' | 'closed' | 'acknowledged'; + groupByField: string; +} + +export type ReportAlertsGroupingTelemetryEventParams = + | ReportAlertsGroupingChangedParams + | ReportAlertsGroupingToggledParams + | ReportAlertsTakeActionParams; + +export type AlertsGroupingTelemetryEvent = + | { + eventType: TelemetryEventTypes.AlertsGroupingToggled; + schema: RootSchema; + } + | { + eventType: TelemetryEventTypes.AlertsGroupingChanged; + schema: RootSchema; + } + | { + eventType: TelemetryEventTypes.AlertsGroupingTakeAction; + schema: RootSchema; + }; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/index.ts new file mode 100644 index 00000000000000..cdd0643e3dc11c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/index.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 { TelemetryEventTypes } from '../../constants'; +import type { + DataQualityTelemetryCheckAllCompletedEvent, + DataQualityTelemetryIndexCheckedEvent, +} from '../../types'; + +export const dataQualityIndexCheckedEvent: DataQualityTelemetryIndexCheckedEvent = { + eventType: TelemetryEventTypes.DataQualityIndexChecked, + schema: { + batchId: { + type: 'keyword', + _meta: { + description: 'batch id', + optional: false, + }, + }, + indexId: { + type: 'keyword', + _meta: { + description: 'Index uuid', + optional: false, + }, + }, + numberOfIndices: { + type: 'integer', + _meta: { + description: 'Number of indices', + optional: true, + }, + }, + numberOfIndicesChecked: { + type: 'integer', + _meta: { + description: 'Number of indices checked', + optional: true, + }, + }, + timeConsumedMs: { + type: 'integer', + _meta: { + description: 'Time consumed in milliseconds', + optional: true, + }, + }, + ecsVersion: { + type: 'keyword', + _meta: { + description: 'ECS version', + optional: true, + }, + }, + errorCount: { + type: 'integer', + _meta: { + description: 'Error count', + optional: true, + }, + }, + numberOfIncompatibleFields: { + type: 'integer', + _meta: { + description: 'Number of incompatible fields', + optional: true, + }, + }, + numberOfDocuments: { + type: 'integer', + _meta: { + description: 'Number of documents', + optional: true, + }, + }, + sizeInBytes: { + type: 'integer', + _meta: { + description: 'Size in bytes', + optional: true, + }, + }, + isCheckAll: { + type: 'boolean', + _meta: { + description: 'Is triggered by check all button', + optional: true, + }, + }, + unallowedMappingFields: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: 'Unallowed mapping fields', + }, + }, + }, + unallowedValueFields: { + type: 'array', + items: { + type: 'keyword', + _meta: { + description: 'Unallowed value fields', + }, + }, + }, + ilmPhase: { + type: 'keyword', + _meta: { + description: 'ILM phase', + optional: true, + }, + }, + }, +}; + +export const dataQualityCheckAllClickedEvent: DataQualityTelemetryCheckAllCompletedEvent = { + eventType: TelemetryEventTypes.DataQualityCheckAllCompleted, + schema: { + batchId: { + type: 'keyword', + _meta: { + description: 'batch id', + optional: false, + }, + }, + numberOfIndices: { + type: 'integer', + _meta: { + description: 'Number of indices', + optional: true, + }, + }, + numberOfIndicesChecked: { + type: 'integer', + _meta: { + description: 'Number of indices checked', + optional: true, + }, + }, + timeConsumedMs: { + type: 'integer', + _meta: { + description: 'Time consumed in milliseconds', + optional: true, + }, + }, + ecsVersion: { + type: 'keyword', + _meta: { + description: 'ECS version', + optional: true, + }, + }, + numberOfIncompatibleFields: { + type: 'integer', + _meta: { + description: 'Number of incompatible fields', + optional: true, + }, + }, + numberOfDocuments: { + type: 'integer', + _meta: { + description: 'Number of documents', + optional: true, + }, + }, + sizeInBytes: { + type: 'integer', + _meta: { + description: 'Size in bytes', + optional: true, + }, + }, + isCheckAll: { + type: 'boolean', + _meta: { + description: 'Is triggered by check all button', + optional: true, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/types.ts new file mode 100644 index 00000000000000..8c7b01e15f65e1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/data_quality/types.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RootSchema } from '@kbn/analytics-client'; +import type { TelemetryEventTypes } from '../../constants'; + +export type ReportDataQualityIndexCheckedParams = ReportDataQualityCheckAllCompletedParams & { + errorCount?: number; + indexId: string; + ilmPhase?: string; + unallowedMappingFields?: string[]; + unallowedValueFields?: string[]; +}; + +export interface ReportDataQualityCheckAllCompletedParams { + batchId: string; + ecsVersion?: string; + isCheckAll?: boolean; + numberOfDocuments?: number; + numberOfIncompatibleFields?: number; + numberOfIndices?: number; + numberOfIndicesChecked?: number; + sizeInBytes?: number; + timeConsumedMs?: number; +} + +export interface DataQualityTelemetryIndexCheckedEvent { + eventType: TelemetryEventTypes.DataQualityIndexChecked; + schema: RootSchema; +} + +export interface DataQualityTelemetryCheckAllCompletedEvent { + eventType: TelemetryEventTypes.DataQualityCheckAllCompleted; + schema: RootSchema; +} + +export type DataQualityTelemetryEvents = + | DataQualityTelemetryIndexCheckedEvent + | DataQualityTelemetryCheckAllCompletedEvent; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/index.ts new file mode 100644 index 00000000000000..73cec55dabfc11 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/index.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 { TelemetryEvent } from '../../types'; +import { TelemetryEventTypes } from '../../constants'; + +export const entityClickedEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.EntityDetailsClicked, + schema: { + entity: { + type: 'keyword', + _meta: { + description: 'Entity name (host|user)', + optional: false, + }, + }, + }, +}; + +export const entityAlertsClickedEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.EntityAlertsClicked, + schema: { + entity: { + type: 'keyword', + _meta: { + description: 'Entity name (host|user)', + optional: false, + }, + }, + }, +}; + +export const entityRiskFilteredEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.EntityRiskFiltered, + schema: { + entity: { + type: 'keyword', + _meta: { + description: 'Entity name (host|user)', + optional: false, + }, + }, + selectedSeverity: { + type: 'keyword', + _meta: { + description: 'Selected severity (Unknown|Low|Moderate|High|Critical)', + optional: false, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/types.ts new file mode 100644 index 00000000000000..dd0aeb4ce33848 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/entity_analytics/types.ts @@ -0,0 +1,39 @@ +/* + * 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 { RootSchema } from '@kbn/analytics-client'; +import type { RiskSeverity } from '../../../../../../common/search_strategy'; +import type { TelemetryEventTypes } from '../../constants'; + +interface EntityParam { + entity: 'host' | 'user'; +} + +export type ReportEntityDetailsClickedParams = EntityParam; +export type ReportEntityAlertsClickedParams = EntityParam; +export interface ReportEntityRiskFilteredParams extends EntityParam { + selectedSeverity: RiskSeverity; +} + +export type ReportEntityAnalyticsTelemetryEventParams = + | ReportEntityDetailsClickedParams + | ReportEntityAlertsClickedParams + | ReportEntityRiskFilteredParams; + +export type EntityAnalyticsTelemetryEvent = + | { + eventType: TelemetryEventTypes.EntityDetailsClicked; + schema: RootSchema; + } + | { + eventType: TelemetryEventTypes.EntityAlertsClicked; + schema: RootSchema; + } + | { + eventType: TelemetryEventTypes.EntityRiskFiltered; + schema: RootSchema; + }; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts new file mode 100644 index 00000000000000..7088597e2d15f7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts @@ -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 type { TelemetryEvent } from '../types'; +import { TelemetryEventTypes } from '../constants'; +import { + alertsGroupingChangedEvent, + alertsGroupingTakeActionEvent, + alertsGroupingToggledEvent, +} from './alerts_grouping'; +import { + entityAlertsClickedEvent, + entityClickedEvent, + entityRiskFilteredEvent, +} from './entity_analytics'; +import { dataQualityIndexCheckedEvent, dataQualityCheckAllClickedEvent } from './data_quality'; + +const mlJobUpdateEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.MLJobUpdate, + schema: { + jobId: { + type: 'keyword', + _meta: { + description: 'Job id', + optional: false, + }, + }, + isElasticJob: { + type: 'boolean', + _meta: { + description: 'If true the job is one of the pre-configure security solution modules', + optional: false, + }, + }, + moduleId: { + type: 'keyword', + _meta: { + description: 'Module id', + optional: true, + }, + }, + status: { + type: 'keyword', + _meta: { + description: 'It describes what has changed in the job.', + optional: false, + }, + }, + errorMessage: { + type: 'text', + _meta: { + description: 'Error message', + optional: true, + }, + }, + }, +}; + +const cellActionClickedEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.CellActionClicked, + schema: { + fieldName: { + type: 'keyword', + _meta: { + description: 'Field Name', + optional: false, + }, + }, + actionId: { + type: 'keyword', + _meta: { + description: 'Action id', + optional: false, + }, + }, + displayName: { + type: 'keyword', + _meta: { + description: 'User friendly action name', + optional: false, + }, + }, + metadata: { + type: 'pass_through', + _meta: { + description: 'Action metadata', + optional: true, + }, + }, + }, +}; + +const anomaliesCountClickedEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.AnomaliesCountClicked, + schema: { + jobId: { + type: 'keyword', + _meta: { + description: 'Job id', + optional: false, + }, + }, + count: { + type: 'integer', + _meta: { + description: 'Number of anomalies', + optional: false, + }, + }, + }, +}; + +const breadCrumbClickedEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.BreadcrumbClicked, + schema: { + title: { + type: 'keyword', + _meta: { + description: 'Breadcrumb title', + optional: false, + }, + }, + }, +}; + +export const telemetryEvents = [ + alertsGroupingToggledEvent, + alertsGroupingChangedEvent, + alertsGroupingTakeActionEvent, + entityClickedEvent, + entityAlertsClickedEvent, + entityRiskFilteredEvent, + mlJobUpdateEvent, + cellActionClickedEvent, + anomaliesCountClickedEvent, + dataQualityIndexCheckedEvent, + dataQualityCheckAllClickedEvent, + breadCrumbClickedEvent, +]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts index 9cd22c469160da..397ab4846a5e43 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts @@ -17,4 +17,7 @@ export const createTelemetryClientMock = (): jest.Mocked = reportMLJobUpdate: jest.fn(), reportCellActionClicked: jest.fn(), reportAnomaliesCountClicked: jest.fn(), + reportDataQualityIndexChecked: jest.fn(), + reportDataQualityCheckAllCompleted: jest.fn(), + reportBreadcrumbClicked: jest.fn(), }); diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts index 109d873ece3aae..30ae55bc1722e6 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts @@ -17,6 +17,9 @@ import type { ReportMLJobUpdateParams, ReportCellActionClickedParams, ReportAnomaliesCountClickedParams, + ReportDataQualityIndexCheckedParams, + ReportDataQualityCheckAllCompletedParams, + ReportBreadcrumbClickedParams, } from './types'; import { TelemetryEventTypes } from './constants'; @@ -27,42 +30,16 @@ import { TelemetryEventTypes } from './constants'; export class TelemetryClient implements TelemetryClientStart { constructor(private analytics: AnalyticsServiceSetup) {} - public reportAlertsGroupingChanged = ({ - tableId, - groupByField, - }: ReportAlertsGroupingChangedParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingChanged, { - tableId, - groupByField, - }); + public reportAlertsGroupingChanged = (params: ReportAlertsGroupingChangedParams) => { + this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingChanged, params); }; - public reportAlertsGroupingToggled = ({ - isOpen, - tableId, - groupNumber, - groupName, - }: ReportAlertsGroupingToggledParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingToggled, { - isOpen, - tableId, - groupNumber, - groupName, - }); + public reportAlertsGroupingToggled = (params: ReportAlertsGroupingToggledParams) => { + this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingToggled, params); }; - public reportAlertsGroupingTakeAction = ({ - tableId, - groupNumber, - status, - groupByField, - }: ReportAlertsTakeActionParams) => { - this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingTakeAction, { - tableId, - groupNumber, - status, - groupByField, - }); + public reportAlertsGroupingTakeAction = (params: ReportAlertsTakeActionParams) => { + this.analytics.reportEvent(TelemetryEventTypes.AlertsGroupingTakeAction, params); }; public reportEntityDetailsClicked = ({ entity }: ReportEntityDetailsClickedParams) => { @@ -98,4 +75,20 @@ export class TelemetryClient implements TelemetryClientStart { public reportAnomaliesCountClicked = (params: ReportAnomaliesCountClickedParams) => { this.analytics.reportEvent(TelemetryEventTypes.AnomaliesCountClicked, params); }; + + public reportDataQualityIndexChecked = (params: ReportDataQualityIndexCheckedParams) => { + this.analytics.reportEvent(TelemetryEventTypes.DataQualityIndexChecked, params); + }; + + public reportDataQualityCheckAllCompleted = ( + params: ReportDataQualityCheckAllCompletedParams + ) => { + this.analytics.reportEvent(TelemetryEventTypes.DataQualityCheckAllCompleted, params); + }; + + public reportBreadcrumbClicked = ({ title }: ReportBreadcrumbClickedParams) => { + this.analytics.reportEvent(TelemetryEventTypes.BreadcrumbClicked, { + title, + }); + }; } diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_events.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_events.ts deleted file mode 100644 index dff3def5b38137..00000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_events.ts +++ /dev/null @@ -1,249 +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 { TelemetryEvent } from './types'; -import { TelemetryEventTypes } from './constants'; - -const alertsGroupingToggledEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AlertsGroupingToggled, - schema: { - isOpen: { - type: 'boolean', - _meta: { - description: 'on or off', - optional: false, - }, - }, - tableId: { - type: 'text', - _meta: { - description: 'Table ID', - optional: false, - }, - }, - groupNumber: { - type: 'integer', - _meta: { - description: 'Group number', - optional: false, - }, - }, - groupName: { - type: 'keyword', - _meta: { - description: 'Group value', - optional: true, - }, - }, - }, -}; - -const alertsGroupingChangedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AlertsGroupingChanged, - schema: { - tableId: { - type: 'keyword', - _meta: { - description: 'Table ID', - optional: false, - }, - }, - groupByField: { - type: 'keyword', - _meta: { - description: 'Selected field', - optional: false, - }, - }, - }, -}; - -const alertsGroupingTakeActionEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AlertsGroupingTakeAction, - schema: { - tableId: { - type: 'keyword', - _meta: { - description: 'Table ID', - optional: false, - }, - }, - groupNumber: { - type: 'integer', - _meta: { - description: 'Group number', - optional: false, - }, - }, - status: { - type: 'keyword', - _meta: { - description: 'Alert status', - optional: false, - }, - }, - groupByField: { - type: 'keyword', - _meta: { - description: 'Selected field', - optional: false, - }, - }, - }, -}; - -const entityClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.EntityDetailsClicked, - schema: { - entity: { - type: 'keyword', - _meta: { - description: 'Entity name (host|user)', - optional: false, - }, - }, - }, -}; - -const entityAlertsClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.EntityAlertsClicked, - schema: { - entity: { - type: 'keyword', - _meta: { - description: 'Entity name (host|user)', - optional: false, - }, - }, - }, -}; - -const entityRiskFilteredEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.EntityRiskFiltered, - schema: { - entity: { - type: 'keyword', - _meta: { - description: 'Entity name (host|user)', - optional: false, - }, - }, - selectedSeverity: { - type: 'keyword', - _meta: { - description: 'Selected severity (Unknown|Low|Moderate|High|Critical)', - optional: false, - }, - }, - }, -}; - -const mlJobUpdateEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.MLJobUpdate, - schema: { - jobId: { - type: 'keyword', - _meta: { - description: 'Job id', - optional: false, - }, - }, - isElasticJob: { - type: 'boolean', - _meta: { - description: 'If true the job is one of the pre-configure security solution modules', - optional: false, - }, - }, - moduleId: { - type: 'keyword', - _meta: { - description: 'Module id', - optional: true, - }, - }, - status: { - type: 'keyword', - _meta: { - description: 'It describes what has changed in the job.', - optional: false, - }, - }, - errorMessage: { - type: 'text', - _meta: { - description: 'Error message', - optional: true, - }, - }, - }, -}; - -const cellActionClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.CellActionClicked, - schema: { - fieldName: { - type: 'keyword', - _meta: { - description: 'Field Name', - optional: false, - }, - }, - actionId: { - type: 'keyword', - _meta: { - description: 'Action id', - optional: false, - }, - }, - displayName: { - type: 'keyword', - _meta: { - description: 'User friendly action name', - optional: false, - }, - }, - metadata: { - type: 'pass_through', - _meta: { - description: 'Action metadata', - optional: true, - }, - }, - }, -}; - -const anomaliesCountClickedEvent: TelemetryEvent = { - eventType: TelemetryEventTypes.AnomaliesCountClicked, - schema: { - jobId: { - type: 'keyword', - _meta: { - description: 'Job id', - optional: false, - }, - }, - count: { - type: 'integer', - _meta: { - description: 'Number of anomalies', - optional: false, - }, - }, - }, -}; - -export const telemetryEvents = [ - alertsGroupingToggledEvent, - alertsGroupingChangedEvent, - alertsGroupingTakeActionEvent, - entityClickedEvent, - entityAlertsClickedEvent, - entityRiskFilteredEvent, - mlJobUpdateEvent, - cellActionClickedEvent, - anomaliesCountClickedEvent, -]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts index 557bbc96015dbd..04c268dec1371f 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.test.ts @@ -5,7 +5,7 @@ * 2.0. */ import { coreMock } from '@kbn/core/server/mocks'; -import { telemetryEvents } from './telemetry_events'; +import { telemetryEvents } from './events/telemetry_events'; import { TelemetryService } from './telemetry_service'; import { TelemetryEventTypes } from './constants'; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts index 77bb3036fd9abe..d4c100d5fe407c 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_service.ts @@ -12,7 +12,7 @@ import type { TelemetryClientStart, TelemetryEventParams, } from './types'; -import { telemetryEvents } from './telemetry_events'; +import { telemetryEvents } from './events/telemetry_events'; import { TelemetryClient } from './telemetry_client'; /** diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts index d0116dc50f074e..272b4a66d9a630 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts @@ -7,43 +7,40 @@ import type { RootSchema } from '@kbn/analytics-client'; import type { AnalyticsServiceSetup } from '@kbn/core/public'; -import type { RiskSeverity } from '../../../../common/search_strategy'; import type { SecurityMetadata } from '../../../actions/types'; import type { ML_JOB_TELEMETRY_STATUS, TelemetryEventTypes } from './constants'; +import type { + AlertsGroupingTelemetryEvent, + ReportAlertsGroupingChangedParams, + ReportAlertsGroupingTelemetryEventParams, + ReportAlertsGroupingToggledParams, + ReportAlertsTakeActionParams, +} from './events/alerts_grouping/types'; +import type { + ReportDataQualityCheckAllCompletedParams, + ReportDataQualityIndexCheckedParams, + DataQualityTelemetryEvents, +} from './events/data_quality/types'; +import type { + EntityAnalyticsTelemetryEvent, + ReportEntityAlertsClickedParams, + ReportEntityAnalyticsTelemetryEventParams, + ReportEntityDetailsClickedParams, + ReportEntityRiskFilteredParams, +} from './events/entity_analytics/types'; + +export * from './events/alerts_grouping/types'; +export * from './events/data_quality/types'; +export type { + ReportEntityAlertsClickedParams, + ReportEntityDetailsClickedParams, + ReportEntityRiskFilteredParams, +} from './events/entity_analytics/types'; export interface TelemetryServiceSetupParams { analytics: AnalyticsServiceSetup; } -export interface ReportAlertsGroupingChangedParams { - tableId: string; - groupByField: string; -} - -export interface ReportAlertsGroupingToggledParams { - isOpen: boolean; - tableId: string; - groupNumber: number; - groupName?: string | undefined; -} - -export interface ReportAlertsTakeActionParams { - tableId: string; - groupNumber: number; - status: 'open' | 'closed' | 'acknowledged'; - groupByField: string; -} - -interface EntityParam { - entity: 'host' | 'user'; -} - -export type ReportEntityDetailsClickedParams = EntityParam; -export type ReportEntityAlertsClickedParams = EntityParam; -export interface ReportEntityRiskFilteredParams extends EntityParam { - selectedSeverity: RiskSeverity; -} - export interface ReportMLJobUpdateParams { jobId: string; isElasticJob: boolean; @@ -64,17 +61,19 @@ export interface ReportAnomaliesCountClickedParams { count: number; } +export interface ReportBreadcrumbClickedParams { + title: string; +} + export type TelemetryEventParams = - | ReportAlertsGroupingChangedParams - | ReportAlertsGroupingToggledParams - | ReportAlertsTakeActionParams - | ReportEntityDetailsClickedParams - | ReportEntityAlertsClickedParams - | ReportEntityRiskFilteredParams + | ReportAlertsGroupingTelemetryEventParams + | ReportEntityAnalyticsTelemetryEventParams | ReportMLJobUpdateParams | ReportCellActionClickedParams - | ReportCellActionClickedParams - | ReportAnomaliesCountClickedParams; + | ReportAnomaliesCountClickedParams + | ReportDataQualityIndexCheckedParams + | ReportDataQualityCheckAllCompletedParams + | ReportBreadcrumbClickedParams; export interface TelemetryClientStart { reportAlertsGroupingChanged(params: ReportAlertsGroupingChangedParams): void; @@ -89,33 +88,15 @@ export interface TelemetryClientStart { reportCellActionClicked(params: ReportCellActionClickedParams): void; reportAnomaliesCountClicked(params: ReportAnomaliesCountClickedParams): void; + reportDataQualityIndexChecked(params: ReportDataQualityIndexCheckedParams): void; + reportDataQualityCheckAllCompleted(params: ReportDataQualityCheckAllCompletedParams): void; + reportBreadcrumbClicked(params: ReportBreadcrumbClickedParams): void; } export type TelemetryEvent = - | { - eventType: TelemetryEventTypes.AlertsGroupingToggled; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AlertsGroupingChanged; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.AlertsGroupingTakeAction; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.EntityDetailsClicked; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.EntityAlertsClicked; - schema: RootSchema; - } - | { - eventType: TelemetryEventTypes.EntityRiskFiltered; - schema: RootSchema; - } + | AlertsGroupingTelemetryEvent + | EntityAnalyticsTelemetryEvent + | DataQualityTelemetryEvents | { eventType: TelemetryEventTypes.MLJobUpdate; schema: RootSchema; @@ -127,4 +108,8 @@ export type TelemetryEvent = | { eventType: TelemetryEventTypes.AnomaliesCountClicked; schema: RootSchema; + } + | { + eventType: TelemetryEventTypes.BreadcrumbClicked; + schema: RootSchema; }; diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index ef8c68f9a25bb5..44d9300a6d65e0 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -35,7 +35,7 @@ import { import type { FieldHook } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; -import { CASES_FEATURE_ID } from '../../../common/constants'; +import { ASSISTANT_FEATURE_ID, CASES_FEATURE_ID } from '../../../common/constants'; import { UserPrivilegesProvider } from '../components/user_privileges/user_privileges_context'; const state: State = mockGlobalState; @@ -125,6 +125,7 @@ const TestProvidersWithPrivilegesComponent: React.FC = ({ { siem: { show: true, crud: true }, [CASES_FEATURE_ID]: { read_cases: true, crud_cases: false }, + [ASSISTANT_FEATURE_ID]: { 'ai-assistant': true }, } as unknown as Capabilities } > diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx index 33e93c748f270e..ed3f06c54e50e3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx @@ -113,6 +113,9 @@ jest.mock('../../../../common/lib/kibana', () => { save: true, show: true, }, + siem: { + 'ai-assistant': true, + }, }, }, data: { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index d62114881adadc..27ce67f23a9783 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -104,10 +104,15 @@ export const createRule = async ({ rule, signal }: CreateRulesProps): Promise An updated rule + * + * In fact this function should return Promise but it'd require massive refactoring. + * It should be addressed as a part of OpenAPI schema adoption. + * * @throws An error if response is not OK */ -export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { +export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { method: 'PUT', body: JSON.stringify(rule), signal, @@ -198,6 +203,11 @@ export const fetchRules = async ({ * @param id Rule ID's (not rule_id) * @param signal to cancel request * + * @returns Promise + * + * In fact this function should return Promise but it'd require massive refactoring. + * It should be addressed as a part of OpenAPI schema adoption. + * * @throws An error if response is not OK */ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rule_by_id_query.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rule_by_id_query.ts index 4c8ee37e3e211f..197b97effa1f8f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rule_by_id_query.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rule_by_id_query.ts @@ -59,3 +59,27 @@ export const useInvalidateFetchRuleByIdQuery = () => { }); }, [queryClient]); }; + +/** + * We should use this hook to update the rules cache when modifying a rule. + * Use it with the new rule data after operations like rule edit. + * + * @returns A rules cache update callback + */ +export const useUpdateRuleByIdCache = () => { + const queryClient = useQueryClient(); + /** + * Use this method to update rules data cached by react-query. + * It is useful when we receive new rules back from a mutation query (bulk edit, etc.); + * we can merge those rules with the existing cache to avoid an extra roundtrip to re-fetch updated rules. + */ + return useCallback( + (updatedRuleResponse: Rule) => { + queryClient.setQueryData['data']>( + [...FIND_ONE_RULE_QUERY_KEY, updatedRuleResponse.id], + transformInput(updatedRuleResponse) + ); + }, + [queryClient] + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts index 53103287046bed..932c50ff2a46f0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts @@ -6,42 +6,43 @@ */ import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; -import type { - RuleResponse, - RuleUpdateProps, -} from '../../../../../common/api/detection_engine/model/rule_schema'; +import type { RuleUpdateProps } from '../../../../../common/api/detection_engine/model/rule_schema'; import { transformOutput } from '../../../../detections/containers/detection_engine/rules/transforms'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { updateRule } from '../api'; import { useInvalidateFindRulesQuery } from './use_find_rules_query'; -import { useInvalidateFetchRuleByIdQuery } from './use_fetch_rule_by_id_query'; +import { useUpdateRuleByIdCache } from './use_fetch_rule_by_id_query'; import { useInvalidateFetchRuleManagementFiltersQuery } from './use_fetch_rule_management_filters_query'; import { useInvalidateFetchCoverageOverviewQuery } from './use_fetch_coverage_overview'; +import type { Rule } from '../../logic/types'; export const UPDATE_RULE_MUTATION_KEY = ['PUT', DETECTION_ENGINE_RULES_URL]; export const useUpdateRuleMutation = ( - options?: UseMutationOptions + options?: UseMutationOptions ) => { const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); - const invalidateFetchRuleByIdQuery = useInvalidateFetchRuleByIdQuery(); const invalidateFetchCoverageOverviewQuery = useInvalidateFetchCoverageOverviewQuery(); + const updateRuleCache = useUpdateRuleByIdCache(); - return useMutation( + return useMutation( (rule: RuleUpdateProps) => updateRule({ rule: transformOutput(rule) }), { ...options, mutationKey: UPDATE_RULE_MUTATION_KEY, onSettled: (...args) => { invalidateFindRulesQuery(); - invalidateFetchRuleByIdQuery(); invalidateFetchRuleManagementFilters(); invalidateFetchCoverageOverviewQuery(); - if (options?.onSettled) { - options.onSettled(...args); + const [response] = args; + + if (response) { + updateRuleCache(response); } + + options?.onSettled?.(...args); }, } ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/auto_refresh_button/auto_refresh_button.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/auto_refresh_button/auto_refresh_button.tsx index 83499efd323c83..560be75abee263 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/auto_refresh_button/auto_refresh_button.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/auto_refresh_button/auto_refresh_button.tsx @@ -41,9 +41,14 @@ const AutoRefreshButtonComponent = ({ setIsRefreshOn, }: AutoRefreshButtonProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const closePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); + const togglePopover = useCallback( + () => setIsPopoverOpen((prevState) => !prevState), + [setIsPopoverOpen] + ); const handleAutoRefreshSwitch = useCallback( - (closePopover: () => void) => (e: EuiSwitchEvent) => { + (e: EuiSwitchEvent) => { const refreshOn = e.target.checked; if (refreshOn) { reFetchRules(); @@ -51,18 +56,35 @@ const AutoRefreshButtonComponent = ({ setIsRefreshOn(refreshOn); closePopover(); }, - [reFetchRules, setIsRefreshOn] + [reFetchRules, setIsRefreshOn, closePopover] ); - const handleGetRefreshSettingsPopoverContent = useCallback( - (closePopover: () => void) => ( + return ( + + {isRefreshOn ? 'On' : 'Off'} + + } + > - ), - [isRefreshOn, handleAutoRefreshSwitch, isDisabled] - ); - - return ( - setIsPopoverOpen(false)} - button={ - setIsPopoverOpen(!isPopoverOpen)} - disabled={isDisabled} - css={css` - margin-left: 10px; - `} - > - {isRefreshOn ? 'On' : 'Off'} - - } - > - {handleGetRefreshSettingsPopoverContent(() => setIsPopoverOpen(false))} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.test.tsx index 620f562a7bf5aa..94a96a0958725c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.test.tsx @@ -156,7 +156,7 @@ describe('AlertDetailsRedirect', () => { const [{ search, pathname }] = historyMock.replace.mock.lastCall; - expect(search as string).toMatch(/eventFlyout.*right/); + expect(search as string).toMatch(/eventFlyout.*/); expect(pathname).toEqual(ALERTS_PATH); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx index 0786b4cdf38245..41516d06942ffb 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx @@ -12,12 +12,16 @@ import { Redirect, useLocation, useParams } from 'react-router-dom'; import moment from 'moment'; import { encode } from '@kbn/rison'; import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; +import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import type { FilterItemObj } from '../../../common/components/filter_group/types'; -import { ALERTS_PATH, DEFAULT_ALERTS_INDEX } from '../../../../common/constants'; +import { + ALERTS_PATH, + DEFAULT_ALERTS_INDEX, + ENABLE_EXPANDABLE_FLYOUT_SETTING, +} from '../../../../common/constants'; import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state'; import { inputsSelectors } from '../../../common/store'; import { formatPageFilterSearchParam } from '../../../../common/utils/format_page_filter_search_param'; -import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { resolveFlyoutParams } from './utils'; import { FLYOUT_URL_PARAM } from '../../../flyout/shared/hooks/url/use_sync_flyout_state_with_url'; @@ -70,7 +74,7 @@ export const AlertDetailsRedirect = () => { const currentFlyoutParams = searchParams.get(FLYOUT_URL_PARAM); - const isSecurityFlyoutEnabled = useIsExperimentalFeatureEnabled('securityFlyoutEnabled'); + const [isSecurityFlyoutEnabled] = useUiSetting$(ENABLE_EXPANDABLE_FLYOUT_SETTING); const urlParams = new URLSearchParams({ [URL_PARAM_KEY.appQuery]: kqlAppQuery, diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.tsx index f0dfab8bb1cbd2..85b497716b32d9 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.tsx @@ -21,7 +21,6 @@ import { useLeftPanelContext } from '../context'; import { useRouteSpy } from '../../../common/utils/route/use_route_spy'; import { SecurityPageName } from '../../../../common'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { EntityPanel } from '../../right/components/entity_panel'; import { AlertsTable } from './correlations_details_alerts_table'; import { ERROR_MESSAGE, ERROR_TITLE } from '../../shared/translations'; import { @@ -41,6 +40,7 @@ import { SESSION_ALERTS_HEADING, SOURCE_ALERTS_HEADING, } from './translations'; +import { ExpandablePanel } from '../../shared/components/expandable_panel'; export const CORRELATIONS_TAB_ID = 'correlations-details'; @@ -105,56 +105,64 @@ export const CorrelationsDetails: React.FC = () => { return ( <> - - + - - + - - + - - + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx index 269aff686cf611..731bfeda957126 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx @@ -20,9 +20,9 @@ import { EuiIcon, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; +import { ExpandablePanel } from '../../shared/components/expandable_panel'; import type { RelatedUser } from '../../../../common/search_strategy/security_solution/related_entities/related_users'; import type { RiskSeverity } from '../../../../common/search_strategy'; -import { EntityPanel } from '../../right/components/entity_panel'; import { HostOverview } from '../../../overview/components/host_overview'; import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider'; import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; @@ -210,12 +210,13 @@ export const HostDetails: React.FC = ({ hostName, timestamp })

    {i18n.HOSTS_TITLE}

    - @@ -284,7 +285,7 @@ export const HostDetails: React.FC = ({ hostName, timestamp }) inspectIndex={0} /> - + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts index f2ea803b53a9f4..ea58b280a101e3 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts @@ -55,16 +55,12 @@ export const HOST_DETAILS_INFO_TEST_ID = 'host-overview'; export const HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID = `${PREFIX}HostsDetailsRelatedUsersTable` as const; -export const THREAT_INTELLIGENCE_DETAILS_TEST_ID = `${PREFIX}ThreatIntelligenceDetails` as const; -export const PREVALENCE_DETAILS_TEST_ID = `${PREFIX}PrevalenceDetails` as const; export const CORRELATIONS_DETAILS_TEST_ID = `${PREFIX}CorrelationsDetails` as const; export const THREAT_INTELLIGENCE_DETAILS_ENRICHMENTS_TEST_ID = `threat-match-detected` as const; export const THREAT_INTELLIGENCE_DETAILS_SPINNER_TEST_ID = `${PREFIX}ThreatIntelligenceDetailsLoadingSpinner` as const; -export const INVESTIGATION_TEST_ID = `${PREFIX}Investigation` as const; - export const CORRELATIONS_DETAILS_ERROR_TEST_ID = `${CORRELATIONS_DETAILS_TEST_ID}Error` as const; export const CORRELATIONS_DETAILS_BY_ANCESTRY_TABLE_TEST_ID = diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx index bc0066f2488fd9..ea55f811c341a7 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx @@ -20,9 +20,9 @@ import { EuiToolTip, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; +import { ExpandablePanel } from '../../shared/components/expandable_panel'; import type { RelatedHost } from '../../../../common/search_strategy/security_solution/related_entities/related_hosts'; import type { RiskSeverity } from '../../../../common/search_strategy'; -import { EntityPanel } from '../../right/components/entity_panel'; import { UserOverview } from '../../../overview/components/user_overview'; import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider'; import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; @@ -211,12 +211,16 @@ export const UserDetails: React.FC = ({ userName, timestamp })

    {i18n.USERS_TITLE}

    - @@ -284,7 +288,7 @@ export const UserDetails: React.FC = ({ userName, timestamp }) inspectIndex={0} /> - + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts index 32a26d3f87db92..1c27fd7472fcab 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts @@ -36,6 +36,5 @@ export const RULE_PREVIEW_ACTIONS_HEADER_TEST_ID = RULE_PREVIEW_ACTIONS_TEST_ID export const RULE_PREVIEW_ACTIONS_CONTENT_TEST_ID = RULE_PREVIEW_ACTIONS_TEST_ID + CONTENT_TEST_ID; export const RULE_PREVIEW_LOADING_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewLoadingSpinner'; -export const RULE_PREVIEW_HEADER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewHeader'; export const RULE_PREVIEW_FOOTER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewFooter'; export const RULE_PREVIEW_NAVIGATE_TO_RULE_TEST_ID = 'goToRuleDetails'; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts index 8112b796c1d398..e3f3b1fd095fb4 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts @@ -31,8 +31,3 @@ export const RULE_PREVIEW_ACTIONS_TEXT = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.rulePreviewActionsSectionText', { defaultMessage: 'Actions' } ); - -export const ENABLE_RULE_TEXT = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.rulePreviewEnableRuleText', - { defaultMessage: 'Enable' } -); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx index dc96b74bc52a62..8d691ad870892b 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx @@ -13,7 +13,7 @@ import { mockContextValue } from '../mocks/mock_right_panel_context'; import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context'; import { RightPanelContext } from '../context'; import { AnalyzerPreview } from './analyzer_preview'; -import { ANALYZER_PREVIEW_TEST_ID, ANALYZER_TREE_TEST_ID } from './test_ids'; +import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; import * as mock from '../mocks/mock_analyzer_data'; jest.mock('../../../common/containers/alerts/use_alert_prevalence_from_process_tree', () => ({ @@ -65,7 +65,6 @@ describe('', () => { indices: ['rule-parameters-index'], }); expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument(); - expect(wrapper.getByTestId(ANALYZER_TREE_TEST_ID)).toBeInTheDocument(); }); it('does not show analyzer preview when documentid and index are not present', () => { @@ -88,6 +87,6 @@ describe('', () => { documentId: '', indices: [], }); - expect(queryByTestId(ANALYZER_TREE_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(ANALYZER_PREVIEW_TEST_ID)).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx index 33b1c56e43e8ad..e26ede68bc3979 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx @@ -6,7 +6,6 @@ */ import React, { useEffect, useState } from 'react'; import { find } from 'lodash/fp'; -import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; import { useRightPanelContext } from '../context'; import { useAlertPrevalenceFromProcessTree } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; import type { StatsNode } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; @@ -50,19 +49,19 @@ export const AnalyzerPreview: React.FC = () => { } }, [statsNodes, setCache]); + if (!documentId || !index) { + return null; + } + return ( -
    - {documentId && index && ( - - )} -
    + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx index cf05f61ace9eb4..ce29b9959ff8cd 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx @@ -8,10 +8,12 @@ import React from 'react'; import { render } from '@testing-library/react'; import { - ANALYZER_TREE_TEST_ID, - ANALYZER_TREE_LOADING_TEST_ID, - ANALYZER_TREE_ERROR_TEST_ID, - ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID, + ANALYZER_PREVIEW_TOGGLE_ICON_TEST_ID, + ANALYZER_PREVIEW_TITLE_LINK_TEST_ID, + ANALYZER_PREVIEW_TITLE_ICON_TEST_ID, + ANALYZER_PREVIEW_CONTENT_TEST_ID, + ANALYZER_PREVIEW_TITLE_TEXT_TEST_ID, + ANALYZER_PREVIEW_LOADING_TEST_ID, } from './test_ids'; import { ANALYZER_PREVIEW_TITLE } from './translations'; import * as mock from '../mocks/mock_analyzer_data'; @@ -51,10 +53,19 @@ const renderAnalyzerTree = (children: React.ReactNode) => ); describe('', () => { + it('should render wrapper component', () => { + const { getByTestId, queryByTestId } = renderAnalyzerTree(); + expect(queryByTestId(ANALYZER_PREVIEW_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(ANALYZER_PREVIEW_TITLE_LINK_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ANALYZER_PREVIEW_TITLE_ICON_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ANALYZER_PREVIEW_CONTENT_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(ANALYZER_PREVIEW_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument(); + }); + it('should render the component when data is passed', () => { const { getByTestId, getByText } = renderAnalyzerTree(); expect(getByText(ANALYZER_PREVIEW_TITLE)).toBeInTheDocument(); - expect(getByTestId(ANALYZER_TREE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ANALYZER_PREVIEW_CONTENT_TEST_ID)).toBeInTheDocument(); }); it('should render blank when data is not passed', () => { @@ -62,26 +73,23 @@ describe('', () => { ); expect(queryByText(ANALYZER_PREVIEW_TITLE)).not.toBeInTheDocument(); - expect(queryByTestId(ANALYZER_TREE_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(ANALYZER_PREVIEW_CONTENT_TEST_ID)).not.toBeInTheDocument(); }); it('should render loading spinner when loading is true', () => { const { getByTestId } = renderAnalyzerTree(); - expect(getByTestId(ANALYZER_TREE_LOADING_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ANALYZER_PREVIEW_LOADING_TEST_ID)).toBeInTheDocument(); }); - it('should display error message when error is true', () => { - const { getByTestId, getByText } = renderAnalyzerTree( - - ); - expect(getByText('Unable to display analyzer preview.')).toBeInTheDocument(); - expect(getByTestId(ANALYZER_TREE_ERROR_TEST_ID)).toBeInTheDocument(); + it('should not render when error is true', () => { + const { getByTestId } = renderAnalyzerTree(); + expect(getByTestId(ANALYZER_PREVIEW_CONTENT_TEST_ID)).toBeEmptyDOMElement(); }); it('should navigate to left section Visualize tab when clicking on title', () => { const { getByTestId } = renderAnalyzerTree(); - getByTestId(ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID).click(); + getByTestId(ANALYZER_PREVIEW_TITLE_LINK_TEST_ID).click(); expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ id: LeftPanelKey, path: LeftPanelVisualizeTabPath, diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx index 34b0274dd55af2..87504dc05818f5 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx @@ -5,26 +5,15 @@ * 2.0. */ import React, { useCallback, useMemo } from 'react'; -import { - EuiPanel, - EuiButtonEmpty, - EuiTreeView, - EuiLoadingSpinner, - EuiEmptyPrompt, -} from '@elastic/eui'; +import { EuiTreeView } from '@elastic/eui'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { ExpandablePanel } from '../../shared/components/expandable_panel'; import { useRightPanelContext } from '../context'; import { LeftPanelKey, LeftPanelVisualizeTabPath } from '../../left'; -import { ANALYZER_PREVIEW_TITLE, ANALYZER_PREVIEW_TEXT } from './translations'; -import { - ANALYZER_TREE_TEST_ID, - ANALYZER_TREE_LOADING_TEST_ID, - ANALYZER_TREE_ERROR_TEST_ID, - ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID, -} from './test_ids'; +import { ANALYZER_PREVIEW_TITLE } from './translations'; +import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; import type { StatsNode } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; import { getTreeNodes } from '../utils/analyzer_helpers'; -import { ERROR_TITLE, ERROR_MESSAGE } from '../../shared/translations'; export interface AnalyzerTreeProps { /** @@ -83,42 +72,24 @@ export const AnalyzerTree: React.FC = ({ }); }, [eventId, openLeftPanel, indexName, scopeId]); - if (loading) { - return ; - } - - if (error) { - return ( - {ERROR_TITLE(ANALYZER_PREVIEW_TEXT)}} - body={

    {ERROR_MESSAGE(ANALYZER_PREVIEW_TEXT)}

    } - data-test-subj={ANALYZER_TREE_ERROR_TEST_ID} - /> - ); - } - if (items && items.length !== 0) { return ( - - - - {ANALYZER_PREVIEW_TITLE} - - - - + + + ); } return null; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.test.tsx index dfd81606c10bdc..b5be75716664aa 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.test.tsx @@ -9,16 +9,18 @@ import React from 'react'; import { render } from '@testing-library/react'; import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; import { RightPanelContext } from '../context'; -import { - INSIGHTS_CORRELATIONS_CONTENT_TEST_ID, - INSIGHTS_CORRELATIONS_LOADING_TEST_ID, - INSIGHTS_CORRELATIONS_TITLE_TEST_ID, - INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID, -} from './test_ids'; import { TestProviders } from '../../../common/mock'; import { CorrelationsOverview } from './correlations_overview'; import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left'; import { useCorrelations } from '../../shared/hooks/use_correlations'; +import { + INSIGHTS_CORRELATIONS_CONTENT_TEST_ID, + INSIGHTS_CORRELATIONS_LOADING_TEST_ID, + INSIGHTS_CORRELATIONS_TITLE_ICON_TEST_ID, + INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID, + INSIGHTS_CORRELATIONS_TITLE_TEXT_TEST_ID, + INSIGHTS_CORRELATIONS_TOGGLE_ICON_TEST_ID, +} from './test_ids'; jest.mock('../../shared/hooks/use_correlations'); @@ -38,8 +40,22 @@ const renderCorrelationsOverview = (contextValue: RightPanelContext) => ( ); -describe('', () => { - it('should show component with all rows in summary panel', () => { +describe('', () => { + it('should render wrapper component', () => { + (useCorrelations as jest.Mock).mockReturnValue({ + loading: false, + error: false, + data: [], + }); + + const { getByTestId, queryByTestId } = render(renderCorrelationsOverview(panelContextValue)); + expect(queryByTestId(INSIGHTS_CORRELATIONS_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(INSIGHTS_CORRELATIONS_TITLE_ICON_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(INSIGHTS_CORRELATIONS_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should show component with all rows in expandable panel', () => { (useCorrelations as jest.Mock).mockReturnValue({ loading: false, error: false, @@ -53,7 +69,7 @@ describe('', () => { }); const { getByTestId } = render(renderCorrelationsOverview(panelContextValue)); - expect(getByTestId(INSIGHTS_CORRELATIONS_TITLE_TEST_ID)).toHaveTextContent('Correlations'); + expect(getByTestId(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID)).toHaveTextContent('Correlations'); expect(getByTestId(INSIGHTS_CORRELATIONS_CONTENT_TEST_ID)).toHaveTextContent('1 related case'); expect(getByTestId(INSIGHTS_CORRELATIONS_CONTENT_TEST_ID)).toHaveTextContent( '2 alerts related by ancestry' @@ -64,7 +80,6 @@ describe('', () => { expect(getByTestId(INSIGHTS_CORRELATIONS_CONTENT_TEST_ID)).toHaveTextContent( '4 alerts related by session' ); - expect(getByTestId(INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument(); }); it('should hide row if data is missing', () => { @@ -93,8 +108,8 @@ describe('', () => { dataCount: 0, }); - const { container } = render(renderCorrelationsOverview(panelContextValue)); - expect(container).toBeEmptyDOMElement(); + const { getByTestId } = render(renderCorrelationsOverview(panelContextValue)); + expect(getByTestId(INSIGHTS_CORRELATIONS_CONTENT_TEST_ID)).toBeEmptyDOMElement(); }); it('should render loading if any rows are loading', () => { @@ -136,7 +151,7 @@ describe('', () => { ); - getByTestId(INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID).click(); + getByTestId(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID).click(); expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ id: LeftPanelKey, path: LeftPanelInsightsTabPath, diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.tsx index 3936e80155a0d8..92bf782f4988a3 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.tsx @@ -6,14 +6,14 @@ */ import React, { useCallback, useMemo } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { ExpandablePanel } from '../../shared/components/expandable_panel'; import { InsightsSummaryRow } from './insights_summary_row'; import { useCorrelations } from '../../shared/hooks/use_correlations'; import { INSIGHTS_CORRELATIONS_TEST_ID } from './test_ids'; -import { InsightsSubSection } from './insights_subsection'; import { useRightPanelContext } from '../context'; -import { CORRELATIONS_TEXT, CORRELATIONS_TITLE, VIEW_ALL } from './translations'; +import { CORRELATIONS_TITLE } from './translations'; import { LeftPanelKey, LeftPanelInsightsTabPath } from '../../left'; /** @@ -60,27 +60,19 @@ export const CorrelationsOverview: React.FC = () => { ); return ( - - - - {correlationRows} - - - - {VIEW_ALL(CORRELATIONS_TEXT)} - - + + {correlationRows} + + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx index d059b5180abab3..29bb8068281aeb 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx @@ -9,18 +9,40 @@ import React from 'react'; import { render } from '@testing-library/react'; import { RightPanelContext } from '../context'; import { - ENTITIES_HEADER_TEST_ID, - ENTITIES_USER_CONTENT_TEST_ID, - ENTITIES_HOST_CONTENT_TEST_ID, ENTITIES_HOST_OVERVIEW_TEST_ID, ENTITIES_USER_OVERVIEW_TEST_ID, + INSIGHTS_ENTITIES_TOGGLE_ICON_TEST_ID, + INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID, + INSIGHTS_ENTITIES_TITLE_ICON_TEST_ID, + INSIGHTS_ENTITIES_TITLE_TEXT_TEST_ID, } from './test_ids'; import { EntitiesOverview } from './entities_overview'; import { TestProviders } from '../../../common/mock'; import { mockGetFieldsData } from '../mocks/mock_context'; describe('', () => { - it('should render user and host by default', () => { + it('should render wrapper component', () => { + const contextValue = { + eventId: 'event id', + getFieldsData: mockGetFieldsData, + } as unknown as RightPanelContext; + + const { getByTestId, queryByTestId } = render( + + + + + + ); + + expect(queryByTestId(INSIGHTS_ENTITIES_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID)).toHaveTextContent('Entities'); + expect(getByTestId(INSIGHTS_ENTITIES_TITLE_ICON_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(INSIGHTS_ENTITIES_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render user and host', () => { const contextValue = { eventId: 'event id', getFieldsData: mockGetFieldsData, @@ -33,9 +55,8 @@ describe('', () => { ); - expect(getByTestId(ENTITIES_HEADER_TEST_ID)).toHaveTextContent('Entities'); - expect(getByTestId(ENTITIES_USER_CONTENT_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(ENTITIES_HOST_CONTENT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ENTITIES_HOST_OVERVIEW_TEST_ID)).toBeInTheDocument(); }); it('should only render user when host name is null', () => { @@ -44,7 +65,7 @@ describe('', () => { getFieldsData: (field: string) => (field === 'user.name' ? 'user1' : null), } as unknown as RightPanelContext; - const { queryByTestId, queryByText, getByTestId } = render( + const { queryByTestId, getByTestId } = render( @@ -52,10 +73,8 @@ describe('', () => { ); - expect(getByTestId(ENTITIES_USER_CONTENT_TEST_ID)).toBeInTheDocument(); - expect(queryByTestId(ENTITIES_HOST_CONTENT_TEST_ID)).not.toBeInTheDocument(); - expect(queryByText('user1')).toBeInTheDocument(); - expect(queryByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(ENTITIES_HOST_OVERVIEW_TEST_ID)).not.toBeInTheDocument(); }); it('should only render host when user name is null', () => { @@ -64,7 +83,7 @@ describe('', () => { getFieldsData: (field: string) => (field === 'host.name' ? 'host1' : null), } as unknown as RightPanelContext; - const { queryByTestId, queryByText, getByTestId } = render( + const { queryByTestId, getByTestId } = render( @@ -72,10 +91,8 @@ describe('', () => { ); - expect(getByTestId(ENTITIES_HOST_CONTENT_TEST_ID)).toBeInTheDocument(); - expect(queryByTestId(ENTITIES_USER_CONTENT_TEST_ID)).not.toBeInTheDocument(); - expect(queryByText('host1')).toBeInTheDocument(); - expect(queryByTestId(ENTITIES_HOST_OVERVIEW_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ENTITIES_HOST_OVERVIEW_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).not.toBeInTheDocument(); }); it('should not render if both host name and user name are null/blank', () => { @@ -84,7 +101,7 @@ describe('', () => { getFieldsData: (field: string) => {}, } as unknown as RightPanelContext; - const { queryByTestId } = render( + const { container } = render( @@ -92,9 +109,7 @@ describe('', () => { ); - expect(queryByTestId(ENTITIES_HEADER_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(ENTITIES_HOST_CONTENT_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(ENTITIES_USER_CONTENT_TEST_ID)).not.toBeInTheDocument(); + expect(container).toBeEmptyDOMElement(); }); it('should not render if eventId is null', () => { @@ -103,7 +118,7 @@ describe('', () => { getFieldsData: (field: string) => {}, } as unknown as RightPanelContext; - const { queryByTestId } = render( + const { container } = render( @@ -111,6 +126,6 @@ describe('', () => { ); - expect(queryByTestId(ENTITIES_HEADER_TEST_ID)).not.toBeInTheDocument(); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx index b0a8d5c2faeb2c..3a6c58caeb54f5 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx @@ -6,26 +6,17 @@ */ import React, { useCallback } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiButtonEmpty } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { ExpandablePanel } from '../../shared/components/expandable_panel'; import { useRightPanelContext } from '../context'; -import { - ENTITIES_HEADER_TEST_ID, - ENTITIES_CONTENT_TEST_ID, - ENTITIES_HOST_CONTENT_TEST_ID, - ENTITIES_USER_CONTENT_TEST_ID, - ENTITIES_VIEW_ALL_BUTTON_TEST_ID, -} from './test_ids'; -import { ENTITIES_TITLE, ENTITIES_TEXT, VIEW_ALL } from './translations'; -import { EntityPanel } from './entity_panel'; +import { INSIGHTS_ENTITIES_TEST_ID } from './test_ids'; +import { ENTITIES_TITLE } from './translations'; import { getField } from '../../shared/utils'; import { HostEntityOverview } from './host_entity_overview'; import { UserEntityOverview } from './user_entity_overview'; import { LeftPanelKey, LeftPanelInsightsTabPath } from '../../left'; -const USER_ICON = 'user'; -const HOST_ICON = 'storage'; - /** * Entities section under Insights section, overview tab. It contains a preview of host and user information. */ @@ -53,43 +44,28 @@ export const EntitiesOverview: React.FC = () => { return ( <> - -
    {ENTITIES_TITLE}
    -
    - - - {userName && ( - - + + + {userName && ( + - - - )} - {hostName && ( - - + + )} + + {hostName && ( + - - - )} - - {VIEW_ALL(ENTITIES_TEXT)} - - + + )} +
    + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.stories.tsx deleted file mode 100644 index 183d4c4b643b31..00000000000000 --- a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.stories.tsx +++ /dev/null @@ -1,60 +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 type { Story } from '@storybook/react'; -import { EuiIcon } from '@elastic/eui'; -import { EntityPanel } from './entity_panel'; - -export default { - component: EntityPanel, - title: 'Flyout/EntityPanel', -}; - -const defaultProps = { - title: 'title', - iconType: 'storage', -}; -const headerContent = ; - -const children =

    {'test content'}

    ; - -export const Default: Story = () => { - return {children}; -}; - -export const DefaultWithHeaderContent: Story = () => { - return ( - - {children} - - ); -}; - -export const Expandable: Story = () => { - return ( - - {children} - - ); -}; - -export const ExpandableDefaultOpen: Story = () => { - return ( - - {children} - - ); -}; - -export const EmptyDefault: Story = () => { - return ; -}; - -export const EmptyDefaultExpanded: Story = () => { - return ; -}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.test.tsx deleted file mode 100644 index 5eedc99cf5e615..00000000000000 --- a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.test.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import { EntityPanel } from './entity_panel'; -import { - ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID, - ENTITY_PANEL_HEADER_TEST_ID, - ENTITY_PANEL_HEADER_LEFT_SECTION_TEST_ID, - ENTITY_PANEL_HEADER_RIGHT_SECTION_TEST_ID, - ENTITY_PANEL_CONTENT_TEST_ID, -} from './test_ids'; -import { ThemeProvider } from 'styled-components'; -import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; - -const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); -const ENTITY_PANEL_TEST_ID = 'entityPanel'; -const defaultProps = { - title: 'test', - iconType: 'storage', - 'data-test-subj': ENTITY_PANEL_TEST_ID, -}; -const children =

    {'test content'}

    ; - -describe('', () => { - describe('panel is not expandable by default', () => { - it('should render non-expandable panel by default', () => { - const { getByTestId, queryByTestId } = render( - - {children} - - ); - expect(getByTestId(ENTITY_PANEL_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(ENTITY_PANEL_HEADER_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).toHaveTextContent('test content'); - expect(queryByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID)).not.toBeInTheDocument(); - }); - - it('should only render left section of panel header when headerContent is not passed', () => { - const { getByTestId, queryByTestId } = render( - - {children} - - ); - expect(getByTestId(ENTITY_PANEL_HEADER_LEFT_SECTION_TEST_ID)).toHaveTextContent('test'); - expect(queryByTestId(ENTITY_PANEL_HEADER_RIGHT_SECTION_TEST_ID)).not.toBeInTheDocument(); - }); - - it('should render header properly when headerContent is available', () => { - const { getByTestId } = render( - - {'test header content'}}> - {children} - - - ); - expect(getByTestId(ENTITY_PANEL_HEADER_LEFT_SECTION_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(ENTITY_PANEL_HEADER_RIGHT_SECTION_TEST_ID)).toBeInTheDocument(); - }); - - it('should not render content when content is null', () => { - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID)).not.toBeInTheDocument(); - }); - }); - - describe('panel is expandable', () => { - it('should render panel with toggle and collapsed by default', () => { - const { getByTestId, queryByTestId } = render( - - - {children} - - - ); - expect(getByTestId(ENTITY_PANEL_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(ENTITY_PANEL_HEADER_TEST_ID)).toHaveTextContent('test'); - expect(queryByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).not.toBeInTheDocument(); - }); - - it('click toggle button should expand the panel', () => { - const { getByTestId } = render( - - - {children} - - - ); - - const toggle = getByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID); - expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowRight'); - toggle.click(); - - expect(getByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).toHaveTextContent('test content'); - expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowDown'); - }); - - it('should not render toggle or content when content is null', () => { - const { queryByTestId } = render( - - - - ); - expect(queryByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).not.toBeInTheDocument(); - }); - }); - - describe('panel is expandable and expanded by default', () => { - it('should render header and content', () => { - const { getByTestId } = render( - - - {children} - - - ); - expect(getByTestId(ENTITY_PANEL_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(ENTITY_PANEL_HEADER_TEST_ID)).toHaveTextContent('test'); - expect(getByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).toHaveTextContent('test content'); - expect(getByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID)).toBeInTheDocument(); - }); - - it('click toggle button should collapse the panel', () => { - const { getByTestId, queryByTestId } = render( - - - {children} - - - ); - - const toggle = getByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID); - expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowDown'); - expect(getByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).toBeInTheDocument(); - - toggle.click(); - expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowRight'); - expect(queryByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).not.toBeInTheDocument(); - }); - - it('should not render content when content is null', () => { - const { queryByTestId } = render( - - - - ); - expect(queryByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).not.toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.tsx deleted file mode 100644 index d095bf72e4c398..00000000000000 --- a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.tsx +++ /dev/null @@ -1,160 +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, { useMemo, useState, useCallback } from 'react'; -import { - EuiButtonIcon, - EuiSplitPanel, - EuiText, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiPanel, - EuiIcon, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { - ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID, - ENTITY_PANEL_HEADER_TEST_ID, - ENTITY_PANEL_HEADER_LEFT_SECTION_TEST_ID, - ENTITY_PANEL_HEADER_RIGHT_SECTION_TEST_ID, - ENTITY_PANEL_CONTENT_TEST_ID, -} from './test_ids'; - -const PanelHeaderRightSectionWrapper = styled(EuiFlexItem)` - margin-right: ${({ theme }) => theme.eui.euiSizeM}; -`; - -const IconWrapper = styled(EuiIcon)` - margin: ${({ theme }) => theme.eui.euiSizeS} 0; -`; - -export interface EntityPanelProps { - /** - * String value of the title to be displayed in the header of panel - */ - title: string; - /** - * Icon string for displaying the specified icon in the header - */ - iconType: string; - /** - * Boolean to determine the panel to be collapsable (with toggle) - */ - expandable?: boolean; - /** - * Boolean to allow the component to be expanded or collapsed on first render - */ - expanded?: boolean; - /** - Optional content and actions to be displayed on the right side of header - */ - headerContent?: React.ReactNode; - /** - Data test subject string for testing - */ - ['data-test-subj']?: string; -} - -/** - * Panel component to display user or host information. - */ -export const EntityPanel: React.FC = ({ - title, - iconType, - children, - expandable = false, - expanded = false, - headerContent, - 'data-test-subj': dataTestSub, -}) => { - const [toggleStatus, setToggleStatus] = useState(expanded); - const toggleQuery = useCallback(() => { - setToggleStatus(!toggleStatus); - }, [setToggleStatus, toggleStatus]); - - const toggleIcon = useMemo( - () => ( - - ), - [toggleStatus, toggleQuery] - ); - - const headerLeftSection = useMemo( - () => ( - - - {expandable && children && toggleIcon} - - - - - - {title} - - - - - ), - [title, children, toggleIcon, expandable, iconType] - ); - - const headerRightSection = useMemo( - () => - headerContent && ( - - {headerContent} - - ), - [headerContent] - ); - - const showContent = useMemo(() => { - if (!children) { - return false; - } - return !expandable || (expandable && toggleStatus); - }, [children, expandable, toggleStatus]); - - return ( - - - - {headerLeftSection} - {headerRightSection} - - - {showContent && ( - - {children} - - )} - - ); -}; - -EntityPanel.displayName = 'EntityPanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.test.tsx index 4515e011d3790b..c7cd137808c18f 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.test.tsx @@ -12,9 +12,15 @@ import { useRiskScore } from '../../../explore/containers/risk_score'; import { useHostDetails } from '../../../explore/hosts/containers/hosts/details'; import { ENTITIES_HOST_OVERVIEW_IP_TEST_ID, + ENTITIES_HOST_OVERVIEW_LINK_TEST_ID, ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID, TECHNICAL_PREVIEW_ICON_TEST_ID, } from './test_ids'; +import { RightPanelContext } from '../context'; +import { mockContextValue } from '../mocks/mock_right_panel_context'; +import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left'; const hostName = 'host'; const ip = '10.200.000.000'; @@ -24,6 +30,15 @@ const selectedPatterns = 'alerts'; const hostData = { host: { ip: [ip] } }; const riskLevel = [{ host: { risk: { calculated_level: 'Medium' } } }]; +const panelContextValue = { + ...mockContextValue, + dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, +}; + +const flyoutContextValue = { + openLeftPanel: jest.fn(), +} as unknown as ExpandableFlyoutContext; + const mockUseGlobalTime = jest.fn().mockReturnValue({ from, to }); jest.mock('../../../common/containers/use_global_time', () => { return { @@ -52,7 +67,9 @@ describe('', () => { const { getByTestId } = render( - + + + ); @@ -67,7 +84,9 @@ describe('', () => { const { getByTestId } = render( - + + + ); expect(getByTestId(ENTITIES_HOST_OVERVIEW_IP_TEST_ID)).toHaveTextContent('—'); @@ -82,7 +101,9 @@ describe('', () => { mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: false }); const { getByTestId, queryByTestId } = render( - + + + ); @@ -95,12 +116,40 @@ describe('', () => { mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: false }); const { getByTestId, queryByTestId } = render( - + + + ); expect(getByTestId(ENTITIES_HOST_OVERVIEW_IP_TEST_ID)).toHaveTextContent('—'); expect(queryByTestId(TECHNICAL_PREVIEW_ICON_TEST_ID)).not.toBeInTheDocument(); }); + + it('should navigate to left panel entities tab when clicking on title', () => { + mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]); + mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true }); + + const { getByTestId } = render( + + + + + + + + ); + + getByTestId(ENTITIES_HOST_OVERVIEW_LINK_TEST_ID).click(); + expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ + id: LeftPanelKey, + path: LeftPanelInsightsTabPath, + params: { + id: panelContextValue.eventId, + indexName: panelContextValue.indexName, + scopeId: panelContextValue.scopeId, + }, + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.tsx index 14a72804ead1fa..d32df4f205088e 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.tsx @@ -5,10 +5,20 @@ * 2.0. */ -import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiBetaBadge } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiBetaBadge, + EuiLink, + EuiIcon, + useEuiTheme, + useEuiFontSize, +} from '@elastic/eui'; +import { css } from '@emotion/css'; import { getOr } from 'lodash/fp'; -import styled from 'styled-components'; +import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { useRightPanelContext } from '../context'; import type { DescriptionList } from '../../../../common/utility_types'; import { buildHostNamesFilter, @@ -32,11 +42,11 @@ import { ENTITIES_HOST_OVERVIEW_TEST_ID, ENTITIES_HOST_OVERVIEW_IP_TEST_ID, ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID, + ENTITIES_HOST_OVERVIEW_LINK_TEST_ID, } from './test_ids'; +import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left'; -const StyledEuiBetaBadge = styled(EuiBetaBadge)` - margin-left: ${({ theme }) => theme.eui.euiSizeXS}; -`; +const HOST_ICON = 'storage'; const CONTEXT_ID = `flyout-host-entity-overview`; export interface HostEntityOverviewProps { @@ -50,6 +60,20 @@ export interface HostEntityOverviewProps { * Host preview content for the entities preview in right flyout. It contains ip addresses and risk classification */ export const HostEntityOverview: React.FC = ({ hostName }) => { + const { eventId, indexName, scopeId } = useRightPanelContext(); + const { openLeftPanel } = useExpandableFlyoutContext(); + const goToEntitiesTab = useCallback(() => { + openLeftPanel({ + id: LeftPanelKey, + path: LeftPanelInsightsTabPath, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + }, [eventId, openLeftPanel, indexName, scopeId]); + const { from, to } = useGlobalTime(); const { selectedPatterns } = useSourcererDataView(); @@ -80,6 +104,9 @@ export const HostEntityOverview: React.FC = ({ hostName endDate: to, }); + const { euiTheme } = useEuiTheme(); + const xsFontSize = useEuiFontSize('xs').fontSize; + const [hostRiskLevel] = useMemo(() => { const hostRiskData = hostRisk && hostRisk.length > 0 ? hostRisk[0] : undefined; return [ @@ -87,7 +114,10 @@ export const HostEntityOverview: React.FC = ({ hostName title: ( <> {i18n.HOST_RISK_CLASSIFICATION} - = ({ hostName ), }, ]; - }, [hostRisk]); + }, [euiTheme.size.xs, hostRisk]); const descriptionList: DescriptionList[] = useMemo( () => [ @@ -130,20 +160,43 @@ export const HostEntityOverview: React.FC = ({ hostName ); return ( - + - + + + + + + + {hostName} + + + - {isAuthorized && ( - - )} + + + + + + {isAuthorized && ( + + )} + + ); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx index 7e78e9f121a121..86d0447a5a5906 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; import { CorrelationsOverview } from './correlations_overview'; import { PrevalenceOverview } from './prevalence_overview'; import { ThreatIntelligenceOverview } from './threat_intelligence_overview'; @@ -28,8 +29,11 @@ export const InsightsSection: React.FC = ({ expanded = fal return ( + + + ); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx deleted file mode 100644 index c1fc9dfe8a7f88..00000000000000 --- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import type { Story } from '@storybook/react'; -import { InsightsSubSection } from './insights_subsection'; - -export default { - component: InsightsSubSection, - title: 'Flyout/InsightsSubSection', -}; - -const title = 'Title'; -const children =
    {'hello'}
    ; - -export const Basic: Story = () => { - return {children}; -}; - -export const Loading: Story = () => { - return ( - - {null} - - ); -}; - -export const NoTitle: Story = () => { - return {children}; -}; - -export const NoChildren: Story = () => { - return {null}; -}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx deleted file mode 100644 index 271953c8e81054..00000000000000 --- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import { InsightsSubSection } from './insights_subsection'; - -const title = 'Title'; -const dataTestSubj = 'test'; -const children =
    {'hello'}
    ; - -describe('', () => { - it('should render children component', () => { - const { getByTestId } = render( - - {children} - - ); - - const titleDataTestSubj = `${dataTestSubj}Title`; - const contentDataTestSubj = `${dataTestSubj}Content`; - - expect(getByTestId(titleDataTestSubj)).toHaveTextContent(title); - expect(getByTestId(contentDataTestSubj)).toBeInTheDocument(); - }); - - it('should render loading component', () => { - const { getByTestId } = render( - - {children} - - ); - - const loadingDataTestSubj = `${dataTestSubj}Loading`; - expect(getByTestId(loadingDataTestSubj)).toBeInTheDocument(); - }); - - it('should render null if error', () => { - const { container } = render( - - {children} - - ); - - expect(container).toBeEmptyDOMElement(); - }); - - it('should render null if no title', () => { - const { container } = render({children}); - - expect(container).toBeEmptyDOMElement(); - }); - - it('should render null if no children', () => { - const { container } = render( - - {null} - - ); - - expect(container).toBeEmptyDOMElement(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx deleted file mode 100644 index 5993d2d7555c39..00000000000000 --- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx +++ /dev/null @@ -1,78 +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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer, EuiTitle } from '@elastic/eui'; - -export interface InsightsSubSectionProps { - /** - * Renders a loading spinner if true - */ - loading?: boolean; - /** - * Returns a null component if true - */ - error?: boolean; - /** - * Title at the top of the component - */ - title: string; - /** - * Content of the component - */ - children: React.ReactNode; - /** - * Prefix data-test-subj to use for the elements - */ - ['data-test-subj']?: string; -} - -/** - * Presentational component to handle loading and error in the subsections of the Insights section. - * Should be used for Entities, Threat Intelligence, Prevalence, Correlations and Results - */ -export const InsightsSubSection: React.FC = ({ - loading = false, - error = false, - title, - 'data-test-subj': dataTestSubj, - children, -}) => { - // showing the loading in this component as well as in SummaryPanel because we're hiding the entire section if no data - const loadingDataTestSubj = `${dataTestSubj}Loading`; - if (loading) { - return ( - - - - - - ); - } - - // hide everything - if (error || !title || !children) { - return null; - } - - const titleDataTestSubj = `${dataTestSubj}Title`; - const contentDataTestSubj = `${dataTestSubj}Content`; - - return ( - <> - -
    {title}
    -
    - - - {children} - - - ); -}; - -InsightsSubSection.displayName = 'InsightsSubSection'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/investigation_section.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/investigation_section.tsx index 2c8de1e8aa71f8..b0b021d1bd60cf 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/investigation_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/investigation_section.tsx @@ -23,7 +23,7 @@ export interface DescriptionSectionProps { /** * Most top section of the overview tab. It contains the description, reason and mitre attack information (for a document of type alert). */ -export const InvestigationSection: VFC = ({ expanded = false }) => { +export const InvestigationSection: VFC = ({ expanded = true }) => { return ( + ({ + eventId, + indexName: 'indexName', + browserFields, + dataFormattedForFieldBrowser, + scopeId: 'scopeId', + } as unknown as RightPanelContext); const renderPrevalenceOverview = (contextValue: RightPanelContext) => ( @@ -44,7 +54,7 @@ const renderPrevalenceOverview = (contextValue: RightPanelContext) => ( ); describe('', () => { - it('should render PrevalenceOverviewRows', () => { + it('should render wrapper component', () => { (useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({ loading: false, error: false, @@ -55,35 +65,62 @@ describe('', () => { error: false, count: 10, }); - (usePrevalence as jest.Mock).mockReturnValue({ - empty: false, - prevalenceRows: [ - , - ], + (usePrevalence as jest.Mock).mockReturnValue([]); + + const { getByTestId, queryByTestId } = render( + renderPrevalenceOverview(panelContextValue('eventId', {}, [])) + ); + expect(queryByTestId(INSIGHTS_PREVALENCE_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(INSIGHTS_PREVALENCE_TITLE_ICON_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(INSIGHTS_PREVALENCE_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('should render component', () => { + (useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({ + loading: false, + error: false, + count: 1, }); + (useFetchUniqueByField as jest.Mock).mockReturnValue({ + loading: false, + error: false, + count: 10, + }); + (usePrevalence as jest.Mock).mockReturnValue([ + , + ]); - const titleDataTestSubj = `${INSIGHTS_PREVALENCE_TEST_ID}Title`; - const iconDataTestSubj = 'testIcon'; - const valueDataTestSubj = 'testValue'; + const { getByTestId } = render(renderPrevalenceOverview(panelContextValue('eventId', {}, []))); - const { getByTestId } = render(renderPrevalenceOverview(panelContextValue)); + expect(getByTestId(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID)).toHaveTextContent('Prevalence'); - expect(getByTestId(titleDataTestSubj)).toBeInTheDocument(); + const iconDataTestSubj = 'testIcon'; + const valueDataTestSubj = 'testValue'; expect(getByTestId(iconDataTestSubj)).toBeInTheDocument(); expect(getByTestId(valueDataTestSubj)).toBeInTheDocument(); }); - it('should render null if no rows are rendered', () => { - (usePrevalence as jest.Mock).mockReturnValue({ - empty: true, - prevalenceRows: [], - }); + it('should render null if eventId is null', () => { + (usePrevalence as jest.Mock).mockReturnValue([]); - const { container } = render(renderPrevalenceOverview(panelContextValue)); + const { container } = render(renderPrevalenceOverview(panelContextValue(null, {}, []))); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null if browserFields is null', () => { + (usePrevalence as jest.Mock).mockReturnValue([]); + + const { container } = render(renderPrevalenceOverview(panelContextValue('eventId', null, []))); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null if dataFormattedForFieldBrowser is null', () => { + (usePrevalence as jest.Mock).mockReturnValue([]); + + const { container } = render(renderPrevalenceOverview(panelContextValue('eventId', {}, null))); expect(container).toBeEmptyDOMElement(); }); @@ -99,16 +136,9 @@ describe('', () => { error: false, count: 10, }); - (usePrevalence as jest.Mock).mockReturnValue({ - empty: false, - prevalenceRows: [ - , - ], - }); + (usePrevalence as jest.Mock).mockReturnValue([ + , + ]); const flyoutContextValue = { openLeftPanel: jest.fn(), } as unknown as ExpandableFlyoutContext; @@ -116,21 +146,21 @@ describe('', () => { const { getByTestId } = render( - + ); - getByTestId(`${INSIGHTS_PREVALENCE_TEST_ID}ViewAllButton`).click(); + getByTestId(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID).click(); expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ id: LeftPanelKey, path: LeftPanelInsightsTabPath, params: { - id: panelContextValue.eventId, - indexName: panelContextValue.indexName, - scopeId: panelContextValue.scopeId, + id: 'eventId', + indexName: 'indexName', + scopeId: 'scopeId', }, }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx index 77f5065bad4508..c4a9dfd235949e 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx @@ -7,13 +7,13 @@ import type { FC } from 'react'; import React, { useCallback } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { ExpandablePanel } from '../../shared/components/expandable_panel'; import { usePrevalence } from '../hooks/use_prevalence'; import { INSIGHTS_PREVALENCE_TEST_ID } from './test_ids'; -import { InsightsSubSection } from './insights_subsection'; import { useRightPanelContext } from '../context'; -import { PREVALENCE_TEXT, PREVALENCE_TITLE, VIEW_ALL } from './translations'; +import { PREVALENCE_TITLE } from './translations'; import { LeftPanelKey, LeftPanelInsightsTabPath } from '../../left'; /** @@ -38,34 +38,30 @@ export const PrevalenceOverview: FC = () => { }); }, [eventId, openLeftPanel, indexName, scopeId]); - const { empty, prevalenceRows } = usePrevalence({ + const prevalenceRows = usePrevalence({ eventId, browserFields, dataFormattedForFieldBrowser, scopeId, }); - if (!eventId || !browserFields || !dataFormattedForFieldBrowser || empty) { + if (!eventId || !browserFields || !dataFormattedForFieldBrowser) { return null; } return ( - - - - {prevalenceRows} - - - - {VIEW_ALL(PREVALENCE_TEXT)} - - + + + {prevalenceRows} + + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx index fc1a4ce2b1a3fc..6398a55b50cdda 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx @@ -38,11 +38,7 @@ describe('', () => { }); const { getByTestId, getAllByText, queryByTestId } = render( - {}} - data-test-subj={dataTestSubj} - /> + ); const { name, values } = highlightedField; @@ -64,18 +60,12 @@ describe('', () => { error: false, count: 2, }); - const callbackIfNull = jest.fn(); const { queryAllByAltText } = render( - + ); expect(queryAllByAltText('is uncommon')).toHaveLength(0); - expect(callbackIfNull).toHaveBeenCalled(); }); it('should not display row if error retrieving data', () => { @@ -89,18 +79,12 @@ describe('', () => { error: true, count: 0, }); - const callbackIfNull = jest.fn(); const { queryAllByAltText } = render( - + ); expect(queryAllByAltText('is uncommon')).toHaveLength(0); - expect(callbackIfNull).toHaveBeenCalled(); }); it('should display loading', () => { @@ -116,11 +100,7 @@ describe('', () => { }); const { getByTestId } = render( - {}} - data-test-subj={dataTestSubj} - /> + ); expect(getByTestId(loadingDataTestSubj)).toBeInTheDocument(); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx index d7cef2fe99f8b3..12e44a42220db6 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx @@ -20,10 +20,6 @@ export interface PrevalenceOverviewRowProps { * The highlighted field name and values * */ highlightedField: { name: string; values: string[] }; - /** - * This is a solution to allow the parent component to NOT render if all its row children are null - */ - callbackIfNull: () => void; /** * Prefix data-test-subj because this component will be used in multiple places */ @@ -32,12 +28,10 @@ export interface PrevalenceOverviewRowProps { /** * Retrieves the unique hosts for the field/value pair as well as the total number of unique hosts, - * calculate the prevalence. If the prevalence is higher than 1, use the callback method to let the parent know - * the row will render null. + * calculate the prevalence. If the prevalence is higher than 0.1, the row will render null. */ export const PrevalenceOverviewRow: VFC = ({ highlightedField, - callbackIfNull, 'data-test-subj': dataTestSubj, }) => { const { @@ -67,11 +61,6 @@ export const PrevalenceOverviewRow: VFC = ({ const shouldNotRender = isFinite(prevalence) && (prevalence === 0 || prevalence > PERCENTAGE_THRESHOLD); - // callback to let the parent component aware of which rows are null (so it can hide itself completely if all are null) - if (!loading && (error || shouldNotRender)) { - callbackIfNull(); - } - return ( { jest.resetAllMocks(); }); + it('should render wrapper component', () => { + jest.mocked(useProcessData).mockReturnValue({ + processName: 'process1', + userName: 'user1', + startAt: '2022-01-01T00:00:00.000Z', + ruleName: 'rule1', + ruleId: 'id', + workdir: '/path/to/workdir', + command: 'command1', + }); + + renderSessionPreview(); + + expect(screen.queryByTestId(SESSION_PREVIEW_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument(); + expect(screen.getByTestId(SESSION_PREVIEW_TITLE_LINK_TEST_ID)).toBeInTheDocument(); + expect(screen.getByTestId(SESSION_PREVIEW_TITLE_ICON_TEST_ID)).toBeInTheDocument(); + expect(screen.getByTestId(SESSION_PREVIEW_CONTENT_TEST_ID)).toBeInTheDocument(); + expect(screen.queryByTestId(SESSION_PREVIEW_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument(); + }); + it('renders session preview with all data', () => { jest.mocked(useProcessData).mockReturnValue({ processName: 'process1', @@ -61,7 +87,7 @@ describe('SessionPreview', () => { expect(screen.getByText('started')).toBeInTheDocument(); expect(screen.getByText('process1')).toBeInTheDocument(); expect(screen.getByText('at')).toBeInTheDocument(); - expect(screen.getByText('Jan 1, 2022 @ 00:00:00.000')).toBeInTheDocument(); + expect(screen.getByText('2022-01-01T00:00:00Z')).toBeInTheDocument(); expect(screen.getByText('with rule')).toBeInTheDocument(); expect(screen.getByText('rule1')).toBeInTheDocument(); expect(screen.getByText('by')).toBeInTheDocument(); @@ -102,7 +128,7 @@ describe('SessionPreview', () => { const { getByTestId } = renderSessionPreview(); - getByTestId(SESSION_PREVIEW_VIEW_DETAILS_BUTTON_TEST_ID).click(); + getByTestId(SESSION_PREVIEW_TITLE_LINK_TEST_ID).click(); expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ id: LeftPanelKey, path: LeftPanelVisualizeTabPath, diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx index 0d79d2b51f25b5..b6ba4398f7e6bd 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx @@ -5,15 +5,16 @@ * 2.0. */ -import { EuiButtonEmpty, EuiCode, EuiIcon, EuiPanel, useEuiTheme } from '@elastic/eui'; +import { EuiCode, EuiIcon, useEuiTheme } from '@elastic/eui'; import React, { useMemo, type FC, useCallback } from 'react'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { ExpandablePanel } from '../../shared/components/expandable_panel'; import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants'; import { useRightPanelContext } from '../context'; import { PreferenceFormattedDate } from '../../../common/components/formatted_date'; import { useProcessData } from '../hooks/use_process_data'; -import { SESSION_PREVIEW_TEST_ID, SESSION_PREVIEW_VIEW_DETAILS_BUTTON_TEST_ID } from './test_ids'; +import { SESSION_PREVIEW_TEST_ID } from './test_ids'; import { SESSION_PREVIEW_COMMAND_TEXT, SESSION_PREVIEW_PROCESS_TEXT, @@ -120,29 +121,25 @@ export const SessionPreview: FC = () => { }, [command, workdir]); return ( - - - - {SESSION_PREVIEW_TITLE} - - -
    - - -   - {userName} - - {processNameFragment} - {timeFragment} - {ruleFragment} - {commandFragment} -
    -
    -
    + +
    + + +   + {userName} + + {processNameFragment} + {timeFragment} + {ruleFragment} + {commandFragment} +
    +
    ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx index 92c8b0a3826388..ed56178531de66 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx @@ -14,6 +14,7 @@ import { } from './test_ids'; import { DocumentSeverity } from './severity'; import { mockGetFieldsData } from '../mocks/mock_context'; +import { TestProviders } from '../../../common/mock'; describe('', () => { it('should render severity information', () => { @@ -22,9 +23,11 @@ describe('', () => { } as unknown as RightPanelContext; const { getByTestId } = render( - - - + + + + + ); expect(getByTestId(FLYOUT_HEADER_SEVERITY_TITLE_TEST_ID)).toBeInTheDocument(); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/severity.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/severity.tsx index 8fdffb134ed70c..9059b24a6fd2ba 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/severity.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/severity.tsx @@ -10,6 +10,9 @@ import React, { memo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { ALERT_SEVERITY } from '@kbn/rule-data-utils'; import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; +import { CellActionsMode } from '@kbn/cell-actions'; +import { SecurityCellActions } from '../../../common/components/cell_actions'; +import { SecurityCellActionsTrigger } from '../../../actions/constants'; import { SEVERITY_TITLE } from './translations'; import { useRightPanelContext } from '../context'; import { SeverityBadge } from '../../../detections/components/rules/severity_badge'; @@ -46,7 +49,17 @@ export const DocumentSeverity: FC = memo(() => { - + + +
    ); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/status.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/status.tsx index b71bbfbdcf3af2..dacb68435fcb1c 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/status.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/status.tsx @@ -9,6 +9,8 @@ import type { FC } from 'react'; import React, { useMemo } from 'react'; import { find } from 'lodash/fp'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { CellActionsMode } from '@kbn/cell-actions'; +import { SecurityCellActions } from '../../../common/components/cell_actions'; import type { EnrichedFieldInfo, EnrichedFieldInfoWithValues, @@ -17,6 +19,7 @@ import { SIGNAL_STATUS_FIELD_NAME } from '../../../timelines/components/timeline import { StatusPopoverButton } from '../../../common/components/event_details/overview/status_popover_button'; import { useRightPanelContext } from '../context'; import { getEnrichedFieldInfo } from '../../../common/components/event_details/helpers'; +import { SecurityCellActionsTrigger } from '../../../actions/constants'; /** * Checks if the field info has data to convert EnrichedFieldInfo into EnrichedFieldInfoWithValues @@ -52,13 +55,23 @@ export const DocumentStatus: FC = () => { if (!statusData || !hasData(statusData)) return null; return ( - + + + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts index 3224619575a073..43868275c0399b 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts @@ -5,6 +5,14 @@ * 2.0. */ +import { + EXPANDABLE_PANEL_CONTENT_TEST_ID, + EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID, + EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID, + EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID, + EXPANDABLE_PANEL_LOADING_TEST_ID, + EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID, +} from '../../shared/components/test_ids'; import { RESPONSE_BASE_TEST_ID } from '../../left/components/test_ids'; import { CONTENT_TEST_ID, HEADER_TEST_ID } from './expandable_section'; @@ -64,85 +72,145 @@ export const HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID = export const INVESTIGATION_GUIDE_BUTTON_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInvestigationGuideButton'; -/* Insights section*/ +/* Insights section */ export const INSIGHTS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsights'; -export const INSIGHTS_HEADER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsightsHeader'; -export const ENTITIES_HEADER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesHeader'; -export const ENTITIES_CONTENT_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesContent'; -export const ENTITIES_USER_CONTENT_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutEntitiesUserContent'; -export const ENTITIES_HOST_CONTENT_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutEntitiesHostContent'; -export const ENTITIES_VIEW_ALL_BUTTON_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutEntitiesViewAllButton'; -export const ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutEntityPanelToggleButton'; -export const ENTITY_PANEL_HEADER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntityPanelHeader'; -export const ENTITY_PANEL_HEADER_LEFT_SECTION_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutEntityPanelHeaderLeftSection'; -export const ENTITY_PANEL_HEADER_RIGHT_SECTION_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutEntityPanelHeaderRightSection'; -export const ENTITY_PANEL_CONTENT_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutEntityPanelContent'; +export const INSIGHTS_HEADER_TEST_ID = `${INSIGHTS_TEST_ID}Header`; + +/* Insights Entities */ + +export const INSIGHTS_ENTITIES_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsightsEntities'; +export const INSIGHTS_ENTITIES_TOGGLE_ICON_TEST_ID = + EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(INSIGHTS_ENTITIES_TEST_ID); +export const INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(INSIGHTS_ENTITIES_TEST_ID); +export const INSIGHTS_ENTITIES_TITLE_TEXT_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(INSIGHTS_ENTITIES_TEST_ID); +export const INSIGHTS_ENTITIES_TITLE_ICON_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(INSIGHTS_ENTITIES_TEST_ID); +export const INSIGHTS_ENTITIES_CONTENT_TEST_ID = + EXPANDABLE_PANEL_CONTENT_TEST_ID(INSIGHTS_ENTITIES_TEST_ID); export const TECHNICAL_PREVIEW_ICON_TEST_ID = 'securitySolutionDocumentDetailsFlyoutTechnicalPreviewIcon'; export const ENTITIES_USER_OVERVIEW_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesUserOverview'; -export const ENTITIES_USER_OVERVIEW_IP_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutEntitiesUserOverviewIP'; -export const ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutEntitiesUserOverviewRiskLevel'; +export const ENTITIES_USER_OVERVIEW_LINK_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}Link`; +export const ENTITIES_USER_OVERVIEW_IP_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}IP`; +export const ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}RiskLevel`; export const ENTITIES_HOST_OVERVIEW_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesHostOverview'; -export const ENTITIES_HOST_OVERVIEW_IP_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutEntitiesHostOverviewIP'; -export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID = - 'securitySolutionDocumentDetailsFlyoutEntitiesHostOverviewRiskLevel'; +export const ENTITIES_HOST_OVERVIEW_LINK_TEST_ID = `${ENTITIES_HOST_OVERVIEW_TEST_ID}Link`; +export const ENTITIES_HOST_OVERVIEW_IP_TEST_ID = `${ENTITIES_HOST_OVERVIEW_TEST_ID}IP`; +export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID = `${ENTITIES_HOST_OVERVIEW_TEST_ID}RiskLevel`; /* Insights Threat Intelligence */ export const INSIGHTS_THREAT_INTELLIGENCE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsightsThreatIntelligence'; -export const INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Title`; -export const INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Content`; -export const INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}ViewAllButton`; -export const INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Loading`; +export const INSIGHTS_THREAT_INTELLIGENCE_TOGGLE_ICON_TEST_ID = + EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(INSIGHTS_THREAT_INTELLIGENCE_TEST_ID); +export const INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(INSIGHTS_THREAT_INTELLIGENCE_TEST_ID); +export const INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEXT_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(INSIGHTS_THREAT_INTELLIGENCE_TEST_ID); +export const INSIGHTS_THREAT_INTELLIGENCE_TITLE_ICON_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(INSIGHTS_THREAT_INTELLIGENCE_TEST_ID); +export const INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID = EXPANDABLE_PANEL_LOADING_TEST_ID( + INSIGHTS_THREAT_INTELLIGENCE_TEST_ID +); +export const INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID( + INSIGHTS_THREAT_INTELLIGENCE_TEST_ID +); +export const INSIGHTS_THREAT_INTELLIGENCE_CONTAINER_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Container`; export const INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Value`; /* Insights Correlations */ export const INSIGHTS_CORRELATIONS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsightsCorrelations'; -export const INSIGHTS_CORRELATIONS_TITLE_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}Title`; -export const INSIGHTS_CORRELATIONS_CONTENT_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}Content`; -export const INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}ViewAllButton`; -export const INSIGHTS_CORRELATIONS_LOADING_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}Loading`; +export const INSIGHTS_CORRELATIONS_TOGGLE_ICON_TEST_ID = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID( + INSIGHTS_CORRELATIONS_TEST_ID +); +export const INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID( + INSIGHTS_CORRELATIONS_TEST_ID +); +export const INSIGHTS_CORRELATIONS_TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID( + INSIGHTS_CORRELATIONS_TEST_ID +); +export const INSIGHTS_CORRELATIONS_TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID( + INSIGHTS_CORRELATIONS_TEST_ID +); +export const INSIGHTS_CORRELATIONS_LOADING_TEST_ID = EXPANDABLE_PANEL_LOADING_TEST_ID( + INSIGHTS_CORRELATIONS_TEST_ID +); +export const INSIGHTS_CORRELATIONS_CONTENT_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID( + INSIGHTS_CORRELATIONS_TEST_ID +); export const INSIGHTS_CORRELATIONS_VALUE_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}Value`; /* Insights Prevalence */ export const INSIGHTS_PREVALENCE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsightsPrevalence'; -export const INSIGHTS_PREVALENCE_TITLE_TEST_ID = `${INSIGHTS_PREVALENCE_TEST_ID}Title`; -export const INSIGHTS_PREVALENCE_CONTENT_TEST_ID = `${INSIGHTS_PREVALENCE_TEST_ID}Content`; -export const INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON_TEST_ID = `${INSIGHTS_PREVALENCE_TEST_ID}ViewAllButton`; +export const INSIGHTS_PREVALENCE_TOGGLE_ICON_TEST_ID = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID( + INSIGHTS_PREVALENCE_TEST_ID +); +export const INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID( + INSIGHTS_PREVALENCE_TEST_ID +); +export const INSIGHTS_PREVALENCE_TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID( + INSIGHTS_PREVALENCE_TEST_ID +); +export const INSIGHTS_PREVALENCE_TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID( + INSIGHTS_PREVALENCE_TEST_ID +); +export const INSIGHTS_PREVALENCE_LOADING_TEST_ID = EXPANDABLE_PANEL_LOADING_TEST_ID( + INSIGHTS_PREVALENCE_TEST_ID +); +export const INSIGHTS_PREVALENCE_CONTENT_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID( + INSIGHTS_PREVALENCE_TEST_ID +); export const INSIGHTS_PREVALENCE_VALUE_TEST_ID = `${INSIGHTS_PREVALENCE_TEST_ID}Value`; +export const INSIGHTS_PREVALENCE_ROW_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutInsightsPrevalenceRow'; /* Visualizations section */ export const VISUALIZATIONS_SECTION_TEST_ID = 'securitySolutionDocumentDetailsVisualizationsTitle'; export const VISUALIZATIONS_SECTION_HEADER_TEST_ID = 'securitySolutionDocumentDetailsVisualizationsTitleHeader'; + +/* Visualizations analyzer preview */ + export const ANALYZER_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerPreview'; -export const ANALYZER_TREE_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerTree'; -export const ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID = - 'securitySolutionDocumentDetailsAnalayzerTreeViewDetailsButton'; -export const ANALYZER_TREE_LOADING_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerTreeLoading'; -export const ANALYZER_TREE_ERROR_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerTreeError'; +export const ANALYZER_PREVIEW_TOGGLE_ICON_TEST_ID = + EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID); +export const ANALYZER_PREVIEW_TITLE_LINK_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID); +export const ANALYZER_PREVIEW_TITLE_TEXT_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(ANALYZER_PREVIEW_TEST_ID); +export const ANALYZER_PREVIEW_TITLE_ICON_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID); +export const ANALYZER_PREVIEW_LOADING_TEST_ID = + EXPANDABLE_PANEL_LOADING_TEST_ID(ANALYZER_PREVIEW_TEST_ID); +export const ANALYZER_PREVIEW_CONTENT_TEST_ID = + EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID); + +/* Visualizations session preview */ + export const SESSION_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsSessionPreview'; -export const SESSION_PREVIEW_VIEW_DETAILS_BUTTON_TEST_ID = - 'securitySolutionDocumentDetailsSessionPreviewViewDetailsButton'; +export const SESSION_PREVIEW_TOGGLE_ICON_TEST_ID = + EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID); +export const SESSION_PREVIEW_TITLE_LINK_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(SESSION_PREVIEW_TEST_ID); +export const SESSION_PREVIEW_TITLE_TEXT_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID); +export const SESSION_PREVIEW_TITLE_ICON_TEST_ID = + EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID); +export const SESSION_PREVIEW_LOADING_TEST_ID = + EXPANDABLE_PANEL_LOADING_TEST_ID(SESSION_PREVIEW_TEST_ID); +export const SESSION_PREVIEW_CONTENT_TEST_ID = + EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID); /* Response section */ diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx index ba954d09609136..09628a71fbbba9 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx @@ -9,16 +9,19 @@ import React from 'react'; import { render } from '@testing-library/react'; import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; import { RightPanelContext } from '../context'; -import { - INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID, - INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID, - INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID, - INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID, -} from './test_ids'; import { TestProviders } from '../../../common/mock'; import { ThreatIntelligenceOverview } from './threat_intelligence_overview'; import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left'; import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence'; +import { + INSIGHTS_THREAT_INTELLIGENCE_CONTAINER_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TITLE_ICON_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEXT_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TOGGLE_ICON_TEST_ID, +} from './test_ids'; jest.mock('../hooks/use_fetch_threat_intelligence'); @@ -37,6 +40,21 @@ const renderThreatIntelligenceOverview = (contextValue: RightPanelContext) => ( ); describe('', () => { + it('should render wrapper component', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + }); + + const { getByTestId, queryByTestId } = render( + renderThreatIntelligenceOverview(panelContextValue) + ); + + expect(queryByTestId(INSIGHTS_THREAT_INTELLIGENCE_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_ICON_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument(); + }); + it('should render 1 match detected and 1 field enriched', () => { (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ loading: false, @@ -46,7 +64,7 @@ describe('', () => { const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); - expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID)).toHaveTextContent( + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID)).toHaveTextContent( 'Threat Intelligence' ); expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( @@ -55,7 +73,6 @@ describe('', () => { expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( '1 field enriched with threat intelligence' ); - expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument(); }); it('should render 2 matches detected and 2 fields enriched', () => { @@ -67,7 +84,7 @@ describe('', () => { const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); - expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID)).toHaveTextContent( + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID)).toHaveTextContent( 'Threat Intelligence' ); expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( @@ -76,7 +93,6 @@ describe('', () => { expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( '2 fields enriched with threat intelligence' ); - expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument(); }); it('should render 0 field enriched', () => { @@ -126,9 +142,9 @@ describe('', () => { eventId: null, } as unknown as RightPanelContext; - const { container } = render(renderThreatIntelligenceOverview(contextValue)); + const { getByTestId } = render(renderThreatIntelligenceOverview(contextValue)); - expect(container).toBeEmptyDOMElement(); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTAINER_TEST_ID)).toBeEmptyDOMElement(); }); it('should render null when dataFormattedForFieldBrowser is null', () => { @@ -141,9 +157,9 @@ describe('', () => { dataFormattedForFieldBrowser: null, } as unknown as RightPanelContext; - const { container } = render(renderThreatIntelligenceOverview(contextValue)); + const { getByTestId } = render(renderThreatIntelligenceOverview(contextValue)); - expect(container).toBeEmptyDOMElement(); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTAINER_TEST_ID)).toBeEmptyDOMElement(); }); it('should navigate to left section Insights tab when clicking on button', () => { @@ -166,7 +182,7 @@ describe('', () => { ); - getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID).click(); + getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID).click(); expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ id: LeftPanelKey, path: LeftPanelInsightsTabPath, diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx index 3c9bbc1e356dfb..aa6ced53979e8d 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx @@ -7,17 +7,15 @@ import type { FC } from 'react'; import React, { useCallback } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { ExpandablePanel } from '../../shared/components/expandable_panel'; import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence'; -import { InsightsSubSection } from './insights_subsection'; import { InsightsSummaryRow } from './insights_summary_row'; import { useRightPanelContext } from '../context'; import { INSIGHTS_THREAT_INTELLIGENCE_TEST_ID } from './test_ids'; import { - VIEW_ALL, THREAT_INTELLIGENCE_TITLE, - THREAT_INTELLIGENCE_TEXT, THREAT_MATCH_DETECTED, THREAT_ENRICHMENT, THREAT_MATCHES_DETECTED, @@ -58,41 +56,37 @@ export const ThreatIntelligenceOverview: FC = () => { const error: boolean = !eventId || !dataFormattedForFieldBrowser || threatIntelError; return ( - - - - - - - - - {VIEW_ALL(THREAT_INTELLIGENCE_TEXT)} - - + + +
    + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts index 03b3c8d4b9d803..65dcb49f36f213 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts @@ -31,13 +31,6 @@ export const SEVERITY_TITLE = i18n.translate( } ); -export const STATUS_TITLE = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.statusTitle', - { - defaultMessage: 'Status', - } -); - export const RISK_SCORE_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.riskScoreTitle', { @@ -158,20 +151,6 @@ export const TECHNICAL_PREVIEW_MESSAGE = i18n.translate( } ); -export const ENTITIES_TEXT = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.entitiesText', - { - defaultMessage: 'entities', - } -); - -export const THREAT_INTELLIGENCE_TEXT = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligenceText', - { - defaultMessage: 'fields of threat intelligence', - } -); - export const THREAT_MATCH_DETECTED = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch', { @@ -200,73 +179,6 @@ export const THREAT_ENRICHMENTS = i18n.translate( } ); -export const CORRELATIONS_TEXT = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlationsText', - { - defaultMessage: 'fields of correlation', - } -); - -export const CORRELATIONS_ANCESTRY_ALERT = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.ancestryAlert', - { - defaultMessage: 'alert related by ancestry', - } -); - -export const CORRELATIONS_ANCESTRY_ALERTS = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.ancestryAlerts', - { - defaultMessage: 'alerts related by ancestry', - } -); -export const CORRELATIONS_SAME_SOURCE_EVENT_ALERT = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlert', - { - defaultMessage: 'alert related by the same source event', - } -); - -export const CORRELATIONS_SAME_SOURCE_EVENT_ALERTS = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlerts', - { - defaultMessage: 'alerts related by the same source event', - } -); -export const CORRELATIONS_SAME_SESSION_ALERT = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSessionAlert', - { - defaultMessage: 'alert related by session', - } -); - -export const CORRELATIONS_SAME_SESSION_ALERTS = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSessionAlerts', - { - defaultMessage: 'alerts related by session', - } -); -export const CORRELATIONS_RELATED_CASE = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.relatedCase', - { - defaultMessage: 'related case', - } -); - -export const CORRELATIONS_RELATED_CASES = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.relatedCases', - { - defaultMessage: 'related cases', - } -); - -export const PREVALENCE_TEXT = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceText', - { - defaultMessage: 'fields of prevalence', - } -); - export const PREVALENCE_ROW_UNCOMMON = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceRowText', { @@ -274,11 +186,6 @@ export const PREVALENCE_ROW_UNCOMMON = i18n.translate( } ); -export const VIEW_ALL = (text: string) => - i18n.translate('xpack.securitySolution.flyout.documentDetails.overviewTab.viewAllButton', { - values: { text }, - defaultMessage: 'View all {text}', - }); export const VISUALIZATIONS_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.visualizationsTitle', { defaultMessage: 'Visualizations' } @@ -289,13 +196,6 @@ export const ANALYZER_PREVIEW_TITLE = i18n.translate( { defaultMessage: 'Analyzer preview' } ); -export const ANALYZER_PREVIEW_TEXT = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.analyzerPreviewText', - { - defaultMessage: 'analyzer preview.', - } -); - export const SHARE = i18n.translate('xpack.securitySolution.flyout.documentDetails.share', { defaultMessage: 'Share Alert', }); @@ -349,13 +249,6 @@ export const RESPONSE_TITLE = i18n.translate( } ); -export const RESPONSE_BUTTON = i18n.translate( - 'xpack.securitySolution.flyout.documentDetails.responseSectionButton', - { - defaultMessage: 'Response details', - } -); - export const RESPONSE_EMPTY = i18n.translate('xpack.securitySolution.flyout.response.empty', { defaultMessage: 'There are no response actions defined for this event.', }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.test.tsx index 1d97eb642967db..c2c26a8d288b81 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.test.tsx @@ -12,10 +12,16 @@ import { useRiskScore } from '../../../explore/containers/risk_score'; import { ENTITIES_USER_OVERVIEW_IP_TEST_ID, + ENTITIES_USER_OVERVIEW_LINK_TEST_ID, ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID, TECHNICAL_PREVIEW_ICON_TEST_ID, } from './test_ids'; import { useObservedUserDetails } from '../../../explore/users/containers/users/observed_details'; +import { mockContextValue } from '../mocks/mock_right_panel_context'; +import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { RightPanelContext } from '../context'; +import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left'; const userName = 'user'; const ip = '10.200.000.000'; @@ -25,6 +31,15 @@ const selectedPatterns = 'alerts'; const userData = { host: { ip: [ip] } }; const riskLevel = [{ user: { risk: { calculated_level: 'Medium' } } }]; +const panelContextValue = { + ...mockContextValue, + dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, +}; + +const flyoutContextValue = { + openLeftPanel: jest.fn(), +} as unknown as ExpandableFlyoutContext; + const mockUseGlobalTime = jest.fn().mockReturnValue({ from, to }); jest.mock('../../../common/containers/use_global_time', () => { return { @@ -53,7 +68,9 @@ describe('', () => { const { getByTestId } = render( - + + + ); @@ -68,7 +85,9 @@ describe('', () => { const { getByTestId } = render( - + + + ); expect(getByTestId(ENTITIES_USER_OVERVIEW_IP_TEST_ID)).toHaveTextContent('—'); @@ -83,7 +102,9 @@ describe('', () => { mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: false }); const { getByTestId, queryByTestId } = render( - + + + ); @@ -96,12 +117,40 @@ describe('', () => { mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: false }); const { getByTestId, queryByTestId } = render( - + + + ); expect(getByTestId(ENTITIES_USER_OVERVIEW_IP_TEST_ID)).toHaveTextContent('—'); expect(queryByTestId(TECHNICAL_PREVIEW_ICON_TEST_ID)).not.toBeInTheDocument(); }); + + it('should navigate to left panel entities tab when clicking on title', () => { + mockUseUserDetails.mockReturnValue([false, { userDetails: userData }]); + mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true }); + + const { getByTestId } = render( + + + + + + + + ); + + getByTestId(ENTITIES_USER_OVERVIEW_LINK_TEST_ID).click(); + expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ + id: LeftPanelKey, + path: LeftPanelInsightsTabPath, + params: { + id: panelContextValue.eventId, + indexName: panelContextValue.indexName, + scopeId: panelContextValue.scopeId, + }, + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.tsx index da821902ff190a..61a598ddef771a 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.tsx @@ -5,10 +5,21 @@ * 2.0. */ -import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiBetaBadge } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiBetaBadge, + EuiIcon, + EuiLink, + useEuiTheme, + useEuiFontSize, +} from '@elastic/eui'; +import { css } from '@emotion/css'; import { getOr } from 'lodash/fp'; -import styled from 'styled-components'; +import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left'; +import { useRightPanelContext } from '../context'; import type { DescriptionList } from '../../../../common/utility_types'; import { buildUserNamesFilter, @@ -31,13 +42,11 @@ import { ENTITIES_USER_OVERVIEW_TEST_ID, ENTITIES_USER_OVERVIEW_IP_TEST_ID, ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID, + ENTITIES_USER_OVERVIEW_LINK_TEST_ID, } from './test_ids'; import { useObservedUserDetails } from '../../../explore/users/containers/users/observed_details'; -const StyledEuiBetaBadge = styled(EuiBetaBadge)` - margin-left: ${({ theme }) => theme.eui.euiSizeXS}; -`; - +const USER_ICON = 'user'; const CONTEXT_ID = `flyout-user-entity-overview`; export interface UserEntityOverviewProps { @@ -51,6 +60,20 @@ export interface UserEntityOverviewProps { * User preview content for the entities preview in right flyout. It contains ip addresses and risk classification */ export const UserEntityOverview: React.FC = ({ userName }) => { + const { eventId, indexName, scopeId } = useRightPanelContext(); + const { openLeftPanel } = useExpandableFlyoutContext(); + const goToEntitiesTab = useCallback(() => { + openLeftPanel({ + id: LeftPanelKey, + path: LeftPanelInsightsTabPath, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + }, [eventId, openLeftPanel, indexName, scopeId]); + const { from, to } = useGlobalTime(); const { selectedPatterns } = useSourcererDataView(); @@ -97,6 +120,9 @@ export const UserEntityOverview: React.FC = ({ userName [data] ); + const { euiTheme } = useEuiTheme(); + const xsFontSize = useEuiFontSize('xs').fontSize; + const [userRiskLevel] = useMemo(() => { const userRiskData = userRisk && userRisk.length > 0 ? userRisk[0] : undefined; @@ -105,7 +131,10 @@ export const UserEntityOverview: React.FC = ({ userName title: ( <> {i18n.USER_RISK_CLASSIFICATION} - = ({ userName ), }, ]; - }, [userRisk]); + }, [euiTheme.size.xs, userRisk]); return ( - + - + + + + + + + {userName} + + + - {isAuthorized && ( - - )} + + + + + + {isAuthorized && ( + + )} + + ); diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.test.tsx index dfbdf3f278ef52..aff691037d435e 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ +import type { ReactElement } from 'react'; import { getSummaryRows } from '../../../common/components/event_details/get_alert_summary_rows'; -import type { UsePrevalenceResult } from './use_prevalence'; import { usePrevalence } from './use_prevalence'; import type { RenderHookResult } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react-hooks'; @@ -20,7 +20,7 @@ const dataFormattedForFieldBrowser = mockDataFormattedForFieldBrowser; const scopeId = 'scopeId'; describe('usePrevalence', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it('should return 1 row to render', () => { const mockSummaryRow = { @@ -38,8 +38,7 @@ describe('usePrevalence', () => { usePrevalence({ browserFields, dataFormattedForFieldBrowser, eventId, scopeId }) ); - expect(hookResult.result.current.prevalenceRows.length).toEqual(1); - expect(hookResult.result.current.empty).toEqual(false); + expect(hookResult.result.current.length).toEqual(1); }); it('should return empty true', () => { @@ -49,7 +48,6 @@ describe('usePrevalence', () => { usePrevalence({ browserFields, dataFormattedForFieldBrowser, eventId, scopeId }) ); - expect(hookResult.result.current.prevalenceRows.length).toEqual(0); - expect(hookResult.result.current.empty).toEqual(true); + expect(hookResult.result.current.length).toEqual(0); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx index 82a359d0e70f1e..162bc8bc851aa0 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx @@ -7,10 +7,10 @@ import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import type { ReactElement } from 'react'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { getSummaryRows } from '../../../common/components/event_details/get_alert_summary_rows'; import { PrevalenceOverviewRow } from '../components/prevalence_overview_row'; -import { INSIGHTS_PREVALENCE_TEST_ID } from '../components/test_ids'; +import { INSIGHTS_PREVALENCE_ROW_TEST_ID } from '../components/test_ids'; export interface UsePrevalenceParams { /** @@ -30,16 +30,6 @@ export interface UsePrevalenceParams { */ scopeId: string; } -export interface UsePrevalenceResult { - /** - * Returns all row children to render - */ - prevalenceRows: ReactElement[]; - /** - * Returns true if all row children render null - */ - empty: boolean; -} /** * This hook retrieves the highlighted fields from the {@link getSummaryRows} method, then iterates through them @@ -52,9 +42,7 @@ export const usePrevalence = ({ browserFields, dataFormattedForFieldBrowser, scopeId, -}: UsePrevalenceParams): UsePrevalenceResult => { - const [count, setCount] = useState(0); // TODO this needs to be changed at it causes a re-render when the count is updated - +}: UsePrevalenceParams): ReactElement[] => { // retrieves the highlighted fields const summaryRows = useMemo( () => @@ -68,7 +56,7 @@ export const usePrevalence = ({ [browserFields, dataFormattedForFieldBrowser, eventId, scopeId] ); - const prevalenceRows = useMemo( + return useMemo( () => summaryRows.map((row) => { const highlightedField = { @@ -79,17 +67,11 @@ export const usePrevalence = ({ return ( setCount((prevCount) => prevCount + 1)} - data-test-subj={INSIGHTS_PREVALENCE_TEST_ID} + data-test-subj={INSIGHTS_PREVALENCE_ROW_TEST_ID} key={row.description.data.field} /> ); }), [summaryRows] ); - - return { - prevalenceRows, - empty: count >= summaryRows.length, - }; }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/tabs/overview_tab.tsx b/x-pack/plugins/security_solution/public/flyout/right/tabs/overview_tab.tsx index 5a59c56a2394d1..9c689e4d3fc7be 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/tabs/overview_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/tabs/overview_tab.tsx @@ -22,10 +22,10 @@ export const OverviewTab: FC = memo(() => { <> - - + + diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.stories.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.stories.tsx new file mode 100644 index 00000000000000..25b5243e82d3aa --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.stories.tsx @@ -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 React from 'react'; +import type { Story } from '@storybook/react'; +import { EuiIcon } from '@elastic/eui'; +import { ExpandablePanel } from './expandable_panel'; + +export default { + component: ExpandablePanel, + title: 'Flyout/ExpandablePanel', +}; + +const defaultProps = { + header: { + title: 'title', + iconType: 'storage', + }, +}; +const headerContent = ; + +const children =

    {'test content'}

    ; + +export const Default: Story = () => { + return {children}; +}; + +export const DefaultWithHeaderContent: Story = () => { + const props = { + ...defaultProps, + header: { ...defaultProps.header, headerContent }, + }; + return {children}; +}; + +export const Expandable: Story = () => { + const props = { + ...defaultProps, + expand: { expandable: true }, + }; + return {children}; +}; + +export const ExpandableDefaultOpen: Story = () => { + const props = { + ...defaultProps, + expand: { expandable: true, expandedOnFirstRender: true }, + }; + return {children}; +}; + +export const EmptyDefault: Story = () => { + return ; +}; + +export const EmptyDefaultExpanded: Story = () => { + const props = { + ...defaultProps, + expand: { expandable: true }, + }; + return ; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.test.tsx new file mode 100644 index 00000000000000..7c7f46a11c308a --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.test.tsx @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { + EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID, + EXPANDABLE_PANEL_HEADER_RIGHT_SECTION_TEST_ID, + EXPANDABLE_PANEL_CONTENT_TEST_ID, + EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID, + EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID, +} from './test_ids'; +import { ThemeProvider } from 'styled-components'; +import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; +import { ExpandablePanel } from './expandable_panel'; + +const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); +const TEST_ID = 'test-id'; +const defaultProps = { + header: { + title: 'test title', + iconType: 'storage', + }, + 'data-test-subj': TEST_ID, +}; +const children =

    {'test content'}

    ; + +describe('', () => { + describe('panel is not expandable by default', () => { + it('should render non-expandable panel by default', () => { + const { getByTestId, queryByTestId } = render( + + {children} + + ); + expect(getByTestId(TEST_ID)).toBeInTheDocument(); + expect(getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(TEST_ID))).toBeInTheDocument(); + expect(getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).toHaveTextContent( + 'test content' + ); + expect(queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID))).not.toBeInTheDocument(); + }); + + it('should only render left section of panel header when headerContent is not passed', () => { + const { getByTestId, queryByTestId } = render( + + {children} + + ); + expect(getByTestId(EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID(TEST_ID))).toHaveTextContent( + 'test title' + ); + expect( + queryByTestId(EXPANDABLE_PANEL_HEADER_RIGHT_SECTION_TEST_ID(TEST_ID)) + ).not.toBeInTheDocument(); + }); + + it('should render header properly when headerContent is available', () => { + const props = { + ...defaultProps, + header: { ...defaultProps.header, headerContent: <>{'test header content'} }, + }; + const { getByTestId } = render( + + {children} + + ); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID(TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_RIGHT_SECTION_TEST_ID(TEST_ID)) + ).toBeInTheDocument(); + expect(getByTestId(EXPANDABLE_PANEL_HEADER_RIGHT_SECTION_TEST_ID(TEST_ID))).toHaveTextContent( + 'test header content' + ); + }); + + it('should not render content when content is null', () => { + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).not.toBeInTheDocument(); + expect(queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID))).not.toBeInTheDocument(); + }); + }); + + describe('panel is expandable', () => { + const expandableDefaultProps = { + ...defaultProps, + expand: { expandable: true }, + }; + + it('should render panel with toggle and collapsed by default', () => { + const { getByTestId, queryByTestId } = render( + + {children} + + ); + expect(getByTestId(TEST_ID)).toBeInTheDocument(); + expect(getByTestId(EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID(TEST_ID))).toHaveTextContent( + 'test title' + ); + expect(queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).not.toBeInTheDocument(); + }); + + it('click toggle button should expand the panel', () => { + const { getByTestId } = render( + + {children} + + ); + + const toggle = getByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID)); + expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowRight'); + toggle.click(); + + expect(getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).toHaveTextContent( + 'test content' + ); + expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowDown'); + }); + + it('should not render toggle or content when content is null', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID))).not.toBeInTheDocument(); + expect(queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).not.toBeInTheDocument(); + }); + }); + + describe('panel is expandable and expanded', () => { + const expandedDefaultProps = { + ...defaultProps, + expand: { expandable: true, expandedOnFirstRender: true }, + }; + + it('should render header and content', () => { + const { getByTestId } = render( + + {children} + + ); + expect(getByTestId(TEST_ID)).toBeInTheDocument(); + expect(getByTestId(EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID(TEST_ID))).toHaveTextContent( + 'test title' + ); + expect(getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).toHaveTextContent( + 'test content' + ); + expect(getByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID))).toBeInTheDocument(); + }); + + it('click toggle button should collapse the panel', () => { + const { getByTestId, queryByTestId } = render( + + {children} + + ); + + const toggle = getByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID)); + expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowDown'); + expect(getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).toBeInTheDocument(); + + toggle.click(); + expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowRight'); + expect(queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).not.toBeInTheDocument(); + }); + + it('should not render content when content is null', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID))).not.toBeInTheDocument(); + expect(queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx new file mode 100644 index 00000000000000..f9bf5994dff35f --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx @@ -0,0 +1,194 @@ +/* + * 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, useState, useCallback } from 'react'; +import { + EuiButtonIcon, + EuiSplitPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiIcon, + EuiLink, + EuiTitle, + EuiText, + EuiLoadingSpinner, +} from '@elastic/eui'; +import styled from 'styled-components'; + +const StyledEuiFlexItem = styled(EuiFlexItem)` + margin-right: ${({ theme }) => theme.eui.euiSizeM}; +`; + +const StyledEuiIcon = styled(EuiIcon)` + margin: ${({ theme }) => theme.eui.euiSizeS} 0; +`; + +const StyledEuiLink = styled(EuiLink)` + font-size: 12px; + font-weight: 700; +`; + +export interface ExpandablePanelPanelProps { + header: { + /** + * String value of the title to be displayed in the header of panel + */ + title: string; + /** + * Callback function to be called when the title is clicked + */ + callback?: () => void; + /** + * Icon string for displaying the specified icon in the header + */ + iconType: string; + /** + * Optional content and actions to be displayed on the right side of header + */ + headerContent?: React.ReactNode; + }; + content?: { + /** + * Renders a loading spinner if true + */ + loading?: boolean; + /** + * Returns a null component if true + */ + error?: boolean; + }; + expand?: { + /** + * Boolean to determine the panel to be collapsable (with toggle) + */ + expandable?: boolean; + /** + * Boolean to allow the component to be expanded or collapsed on first render + */ + expandedOnFirstRender?: boolean; + }; + /** + Data test subject string for testing + */ + ['data-test-subj']?: string; +} + +/** + * Wrapper component that is composed of a header section and a content section. + * The header can display an icon, a title (that can be a link), and an optional content section on the right. + * The content section can display a loading spinner, an error message, or any other content. + * The component can be expanded or collapsed by clicking on the chevron icon on the left of the title. + */ +export const ExpandablePanel: React.FC = ({ + header: { title, callback, iconType, headerContent }, + content: { loading, error } = { loading: false, error: false }, + expand: { expandable, expandedOnFirstRender } = { + expandable: false, + expandedOnFirstRender: false, + }, + 'data-test-subj': dataTestSubj, + children, +}) => { + const [toggleStatus, setToggleStatus] = useState(expandedOnFirstRender); + const toggleQuery = useCallback(() => { + setToggleStatus(!toggleStatus); + }, [setToggleStatus, toggleStatus]); + + const toggleIcon = useMemo( + () => ( + + ), + [dataTestSubj, toggleStatus, toggleQuery] + ); + + const headerLeftSection = useMemo( + () => ( + + + {expandable && children && toggleIcon} + + + + + {callback ? ( + + {title} + + ) : ( + + {title} + + )} + + + + ), + [dataTestSubj, expandable, children, toggleIcon, callback, iconType, title] + ); + + const headerRightSection = useMemo( + () => + headerContent && ( + + {headerContent} + + ), + [dataTestSubj, headerContent] + ); + + const showContent = useMemo(() => { + if (!children) { + return false; + } + return !expandable || (expandable && toggleStatus); + }, [children, expandable, toggleStatus]); + + const content = loading ? ( + + + + + + ) : error ? null : ( + children + ); + + return ( + + + + {headerLeftSection} + {headerRightSection} + + + {showContent && ( + + {content} + + )} + + ); +}; + +ExpandablePanel.displayName = 'ExpandablePanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/shared/components/test_ids.ts new file mode 100644 index 00000000000000..1e5ed99958b04c --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/shared/components/test_ids.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/* Insights section*/ + +export const EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID = (dataTestSubj: string) => + `${dataTestSubj}ToggleIcon`; +export const EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID = (dataTestSubj: string) => + `${dataTestSubj}LeftSection`; +export const EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID = (dataTestSubj: string) => + `${dataTestSubj}TitleIcon`; +export const EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID = (dataTestSubj: string) => + `${dataTestSubj}TitleLink`; +export const EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID = (dataTestSubj: string) => + `${dataTestSubj}TitleText`; +export const EXPANDABLE_PANEL_HEADER_RIGHT_SECTION_TEST_ID = (dataTestSubj: string) => + `${dataTestSubj}RightSection`; +export const EXPANDABLE_PANEL_LOADING_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Loading`; +export const EXPANDABLE_PANEL_CONTENT_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Content`; diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/automated_response_actions.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/automated_response_actions.cy.ts index a62e877f018e45..5bb5021a3229c6 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/automated_response_actions.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/automated_response_actions.cy.ts @@ -11,7 +11,7 @@ import { closeAllToasts } from '../../tasks/toasts'; import { toggleRuleOffAndOn, visitRuleAlerts } from '../../tasks/isolate'; import { cleanupRule, loadRule } from '../../tasks/api_fixtures'; import { login } from '../../tasks/login'; -import { loadPage } from '../../tasks/common'; +import { disableExpandableFlyoutAdvancedSettings, loadPage } from '../../tasks/common'; import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet'; import { changeAlertsFilter } from '../../tasks/alerts'; @@ -60,6 +60,7 @@ describe('Automated Response Actions', () => { beforeEach(() => { login(); + disableExpandableFlyoutAdvancedSettings(); }); describe('From alerts', () => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/isolate.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/isolate.cy.ts index ff4adacc9b73f5..5ec6ed11c80a40 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/isolate.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/endpoint/isolate.cy.ts @@ -23,7 +23,7 @@ import { } from '../../tasks/isolate'; import { cleanupCase, cleanupRule, loadCase, loadRule } from '../../tasks/api_fixtures'; import { login } from '../../tasks/login'; -import { loadPage } from '../../tasks/common'; +import { disableExpandableFlyoutAdvancedSettings, loadPage } from '../../tasks/common'; import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet'; import type { CreateAndEnrollEndpointHostResponse } from '../../../../../scripts/endpoint/common/endpoint_host_services'; @@ -72,6 +72,7 @@ describe('Isolate command', () => { beforeEach(() => { login(); + disableExpandableFlyoutAdvancedSettings(); }); describe('From manage', () => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/no_license.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/no_license.cy.ts index 4d830c959399a3..3ef371b1c847bb 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/no_license.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/no_license.cy.ts @@ -6,6 +6,7 @@ */ import { generateRandomStringName } from '@kbn/osquery-plugin/cypress/tasks/integrations'; +import { disableExpandableFlyoutAdvancedSettings } from '../../../tasks/common'; import { APP_ALERTS_PATH } from '../../../../../../common/constants'; import { closeAllToasts } from '../../../tasks/toasts'; import { fillUpNewRule } from '../../../tasks/response_actions'; @@ -39,6 +40,7 @@ describe('No License', { env: { ftrConfig: { license: 'basic' } } }, () => { const [endpointAgentId, endpointHostname] = generateRandomStringName(2); before(() => { login(); + disableExpandableFlyoutAdvancedSettings(); indexEndpointRuleAlerts({ endpointAgentId, endpointHostname, diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/results.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/results.cy.ts index bb3be124418f8e..f0cac7527c19ea 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/results.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/automated_response_actions/results.cy.ts @@ -6,6 +6,7 @@ */ import { generateRandomStringName } from '@kbn/osquery-plugin/cypress/tasks/integrations'; +import { disableExpandableFlyoutAdvancedSettings } from '../../../tasks/common'; import { APP_ALERTS_PATH } from '../../../../../../common/constants'; import { closeAllToasts } from '../../../tasks/toasts'; import { indexEndpointHosts } from '../../../tasks/index_endpoint_hosts'; @@ -52,6 +53,7 @@ describe('Results', () => { describe('see results when has RBAC', () => { before(() => { login(ROLE.endpoint_response_actions_access); + disableExpandableFlyoutAdvancedSettings(); }); it('see endpoint action', () => { @@ -67,6 +69,7 @@ describe('Results', () => { describe('do not see results results when does not have RBAC', () => { before(() => { login(ROLE.endpoint_response_actions_no_access); + disableExpandableFlyoutAdvancedSettings(); }); it('show the permission denied callout', () => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts index 6dd4b6664d6a3f..ce9533d8576771 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { MetadataListResponse } from '../../../../../common/endpoint/types'; +import { EndpointSortableField } from '../../../../../common/endpoint/types'; import { APP_ENDPOINTS_PATH } from '../../../../../common/constants'; import type { ReturnTypeFromChainable } from '../../types'; import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts'; @@ -15,7 +17,7 @@ describe('Endpoints page', () => { let endpointData: ReturnTypeFromChainable; before(() => { - indexEndpointHosts().then((indexEndpoints) => { + indexEndpointHosts({ count: 3 }).then((indexEndpoints) => { endpointData = indexEndpoints; }); }); @@ -36,4 +38,69 @@ describe('Endpoints page', () => { loadPage(APP_ENDPOINTS_PATH); cy.contains('Hosts running Elastic Defend').should('exist'); }); + + describe('Sorting', () => { + it('Sorts by enrollment date descending order by default', () => { + cy.intercept('api/endpoint/metadata*').as('getEndpointMetadataRequest'); + + loadPage(APP_ENDPOINTS_PATH); + + cy.wait('@getEndpointMetadataRequest').then((subject) => { + const body = subject.response?.body as MetadataListResponse; + + expect(body.sortField).to.equal(EndpointSortableField.ENROLLED_AT); + expect(body.sortDirection).to.equal('desc'); + }); + + // no sorting indicator is present on the screen + cy.get('.euiTableSortIcon').should('not.exist'); + }); + + it('User can sort by any field', () => { + loadPage(APP_ENDPOINTS_PATH); + + const fields = Object.values(EndpointSortableField).filter( + // enrolled_at is not present in the table, it's just the default sorting + (value) => value !== EndpointSortableField.ENROLLED_AT + ); + + for (let i = 0; i < fields.length; i++) { + const field = fields[i]; + cy.intercept(`api/endpoint/metadata*${encodeURIComponent(field)}*`).as(`request.${field}`); + + cy.getByTestSubj(`tableHeaderCell_${field}_${i}`).as('header').click(); + validateSortingInResponse(field, 'asc'); + cy.get('@header').should('have.attr', 'aria-sort', 'ascending'); + cy.get('.euiTableSortIcon').should('exist'); + + cy.get('@header').click(); + validateSortingInResponse(field, 'desc'); + cy.get('@header').should('have.attr', 'aria-sort', 'descending'); + cy.get('.euiTableSortIcon').should('exist'); + } + }); + + it('Sorting can be passed via URL', () => { + cy.intercept('api/endpoint/metadata*').as(`request.host_status`); + + loadPage(`${APP_ENDPOINTS_PATH}?sort_field=host_status&sort_direction=desc`); + + validateSortingInResponse('host_status', 'desc'); + cy.get('[data-test-subj^=tableHeaderCell_host_status').should( + 'have.attr', + 'aria-sort', + 'descending' + ); + }); + + const validateSortingInResponse = (field: string, direction: 'asc' | 'desc') => + cy.wait(`@request.${field}`).then((subject) => { + expect(subject.response?.statusCode).to.equal(200); + + const body = subject.response?.body as MetadataListResponse; + expect(body.total).to.equal(3); + expect(body.sortField).to.equal(field); + expect(body.sortDirection).to.equal(direction); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts index f633fd25abdcc3..8e3811c93ee0e3 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts @@ -24,7 +24,7 @@ import type { ReturnTypeFromChainable } from '../../types'; import { addAlertsToCase } from '../../tasks/add_alerts_to_case'; import { APP_ALERTS_PATH, APP_CASES_PATH, APP_PATH } from '../../../../../common/constants'; import { login } from '../../tasks/login'; -import { loadPage } from '../../tasks/common'; +import { disableExpandableFlyoutAdvancedSettings, loadPage } from '../../tasks/common'; import { indexNewCase } from '../../tasks/index_new_case'; import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts'; import { indexEndpointRuleAlerts } from '../../tasks/index_endpoint_rule_alerts'; @@ -95,6 +95,7 @@ describe('Isolate command', () => { let hostname: string; before(() => { + disableExpandableFlyoutAdvancedSettings(); indexEndpointHosts({ withResponseActions: false, isolation: false }).then( (indexEndpoints) => { endpointData = indexEndpoints; diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts index fb879fa5244b05..38866ec5b5d29b 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts @@ -34,3 +34,23 @@ export const request = ({ headers: { ...COMMON_API_HEADERS, ...headers }, ...options, }); + +const API_HEADERS = Object.freeze({ 'kbn-xsrf': 'cypress' }); +export const rootRequest = ( + options: Partial +): Cypress.Chainable> => + cy.request({ + auth: API_AUTH, + headers: API_HEADERS, + ...options, + }); + +export const disableExpandableFlyoutAdvancedSettings = () => { + const body = { changes: { 'securitySolution:enableExpandableFlyout': false } }; + rootRequest({ + method: 'POST', + url: 'internal/kibana/settings', + body, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts index 69baec57326930..260cb90ed172ff 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { + ENDPOINT_DEFAULT_SORT_DIRECTION, + ENDPOINT_DEFAULT_SORT_FIELD, +} from '../../../../../common/endpoint/constants'; import type { Immutable } from '../../../../../common/endpoint/types'; import { DEFAULT_POLL_INTERVAL } from '../../../common/constants'; import { createLoadedResourceState, createUninitialisedResourceState } from '../../../state'; @@ -16,6 +20,8 @@ export const initialEndpointPageState = (): Immutable => { pageSize: 10, pageIndex: 0, total: 0, + sortDirection: ENDPOINT_DEFAULT_SORT_DIRECTION, + sortField: ENDPOINT_DEFAULT_SORT_FIELD, loading: false, error: undefined, location: undefined, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 44165211f7b410..5cefbe2fa55885 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -14,6 +14,10 @@ import type { EndpointAction } from './action'; import { endpointListReducer } from './reducer'; import { DEFAULT_POLL_INTERVAL } from '../../../common/constants'; import { createUninitialisedResourceState } from '../../../state'; +import { + ENDPOINT_DEFAULT_SORT_DIRECTION, + ENDPOINT_DEFAULT_SORT_FIELD, +} from '../../../../../common/endpoint/constants'; describe('EndpointList store concerns', () => { let store: Store; @@ -40,6 +44,8 @@ describe('EndpointList store concerns', () => { hosts: [], pageSize: 10, pageIndex: 0, + sortField: ENDPOINT_DEFAULT_SORT_FIELD, + sortDirection: ENDPOINT_DEFAULT_SORT_DIRECTION, total: 0, loading: false, error: undefined, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 6caf5b221b9963..c3572b38d40eaa 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -353,7 +353,12 @@ async function endpointListMiddleware({ }) { const { getState, dispatch } = store; - const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(getState()); + const { + page_index: pageIndex, + page_size: pageSize, + sort_field: sortField, + sort_direction: sortDirection, + } = uiQueryParams(getState()); let endpointResponse: MetadataListResponse | undefined; try { @@ -365,6 +370,8 @@ async function endpointListMiddleware({ page: pageIndex, pageSize, kuery: decodedQuery.query as string, + sortField, + sortDirection, }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index f6c5c144f529b4..1bc156d0c5a377 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -32,6 +32,8 @@ import type { GetPolicyListResponse } from '../../policy/types'; import { pendingActionsResponseMock } from '../../../../common/lib/endpoint_pending_actions/mocks'; import { ACTION_STATUS_ROUTE, + ENDPOINT_DEFAULT_SORT_DIRECTION, + ENDPOINT_DEFAULT_SORT_FIELD, HOST_METADATA_LIST_ROUTE, METADATA_TRANSFORMS_STATUS_ROUTE, } from '../../../../../common/endpoint/constants'; @@ -67,6 +69,8 @@ export const mockEndpointResultList: (options?: { total, page, pageSize, + sortDirection: ENDPOINT_DEFAULT_SORT_DIRECTION, + sortField: ENDPOINT_DEFAULT_SORT_FIELD, }; return mock; }; @@ -121,6 +125,8 @@ const endpointListApiPathHandlerMocks = ({ total: endpointsResults?.length || 0, page: 0, pageSize: 10, + sortDirection: ENDPOINT_DEFAULT_SORT_DIRECTION, + sortField: ENDPOINT_DEFAULT_SORT_FIELD, }; }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 227ffebea8dec3..de1bb7b834e0f8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -63,13 +63,15 @@ const handleMetadataTransformStatsChanged: CaseReducer { if (action.type === 'serverReturnedEndpointList') { - const { data, total, page, pageSize } = action.payload; + const { data, total, page, pageSize, sortDirection, sortField } = action.payload; return { ...state, hosts: data, total, pageIndex: page, pageSize, + sortField, + sortDirection, loading: false, error: undefined, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 7f6e464536412d..c33407b36515de 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -11,7 +11,11 @@ import { createSelector } from 'reselect'; import { matchPath } from 'react-router-dom'; import { decode } from '@kbn/rison'; import type { Query } from '@kbn/es-query'; -import type { EndpointPendingActions, Immutable } from '../../../../../common/endpoint/types'; +import type { + EndpointPendingActions, + EndpointSortableField, + Immutable, +} from '../../../../../common/endpoint/types'; import type { EndpointIndexUIQueryParams, EndpointState } from '../types'; import { extractListPaginationParams } from '../../../common/routing'; import { @@ -35,6 +39,12 @@ export const pageIndex = (state: Immutable): number => state.page export const pageSize = (state: Immutable): number => state.pageSize; +export const sortField = (state: Immutable): EndpointSortableField => + state.sortField; + +export const sortDirection = (state: Immutable): 'asc' | 'desc' => + state.sortDirection; + export const totalHits = (state: Immutable): number => state.total; export const listLoading = (state: Immutable): boolean => state.loading; @@ -94,6 +104,8 @@ export const uiQueryParams: ( 'selected_endpoint', 'show', 'admin_query', + 'sort_field', + 'sort_direction', ]; const allowedShowValues: Array = [ @@ -117,6 +129,12 @@ export const uiQueryParams: ( if (allowedShowValues.includes(value as EndpointIndexUIQueryParams['show'])) { data[key] = value as EndpointIndexUIQueryParams['show']; } + } else if (key === 'sort_direction') { + if (['asc', 'desc'].includes(value)) { + data[key] = value as EndpointIndexUIQueryParams['sort_direction']; + } + } else if (key === 'sort_field') { + data[key] = value as EndpointSortableField; } else { data[key] = value; } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index f0408b45373334..0528c8a5ad5720 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -10,6 +10,7 @@ import type { GetInfoResponse } from '@kbn/fleet-plugin/common'; import type { AppLocation, EndpointPendingActions, + EndpointSortableField, HostInfo, Immutable, PolicyData, @@ -26,6 +27,10 @@ export interface EndpointState { pageSize: number; /** which page to show */ pageIndex: number; + /** field used for sorting */ + sortField: EndpointSortableField; + /** direction of sorting */ + sortDirection: 'asc' | 'desc'; /** total number of hosts returned */ total: number; /** list page is retrieving data */ @@ -97,6 +102,10 @@ export interface EndpointIndexUIQueryParams { page_size?: string; /** Which page to show */ page_index?: string; + /** Field used for sorting */ + sort_field?: EndpointSortableField; + /** Direction of sorting */ + sort_direction?: 'asc' | 'desc'; /** show the policy response or host details */ show?: 'policy_response' | 'activity_log' | 'details' | 'isolate' | 'unisolate'; /** Query text from search bar*/ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 0bd26b0dddd341..b1e8b4925a2c29 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -7,6 +7,7 @@ import React, { type CSSProperties, useCallback, useMemo } from 'react'; import styled from 'styled-components'; +import type { CriteriaWithPagination } from '@elastic/eui'; import { EuiBasicTable, type EuiBasicTableColumn, @@ -42,9 +43,11 @@ import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_con import type { CreateStructuredSelector } from '../../../../common/store'; import type { HostInfo, + HostInfoInterface, Immutable, PolicyDetailsRouteState, } from '../../../../../common/endpoint/types'; +import { EndpointSortableField } from '../../../../../common/endpoint/types'; import { DEFAULT_POLL_INTERVAL, MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants'; import { HostsEmptyState, PolicyEmptyState } from '../../../components/management_empty_state'; import { FormattedDate } from '../../../../common/components/formatted_date'; @@ -81,6 +84,21 @@ interface GetEndpointListColumnsProps { getAppUrl: ReturnType['getAppUrl']; } +const columnWidths: Record< + Exclude | 'actions', + string +> = { + [EndpointSortableField.HOSTNAME]: '18%', + [EndpointSortableField.HOST_STATUS]: '15%', + [EndpointSortableField.POLICY_NAME]: '20%', + [EndpointSortableField.POLICY_STATUS]: '150px', + [EndpointSortableField.HOST_OS_NAME]: '90px', + [EndpointSortableField.HOST_IP]: '22%', + [EndpointSortableField.AGENT_VERSION]: '10%', + [EndpointSortableField.LAST_SEEN]: '15%', + actions: '65px', +}; + const getEndpointListColumns = ({ canReadPolicyManagement, backToEndpointList, @@ -96,17 +114,18 @@ const getEndpointListColumns = ({ return [ { - field: 'metadata', - width: '15%', + field: EndpointSortableField.HOSTNAME, + width: columnWidths[EndpointSortableField.HOSTNAME], name: i18n.translate('xpack.securitySolution.endpoint.list.hostname', { defaultMessage: 'Endpoint', }), - render: ({ host: { hostname }, agent: { id } }: HostInfo['metadata']) => { + sortable: true, + render: (hostname: HostInfo['metadata']['host']['hostname'], item: HostInfo) => { const toRoutePath = getEndpointDetailsPath( { ...queryParams, name: 'endpointDetails', - selected_endpoint: id, + selected_endpoint: item.metadata.agent.id, }, search ); @@ -124,11 +143,12 @@ const getEndpointListColumns = ({ }, }, { - field: 'host_status', - width: '14%', + field: EndpointSortableField.HOST_STATUS, + width: columnWidths[EndpointSortableField.HOST_STATUS], name: i18n.translate('xpack.securitySolution.endpoint.list.hostStatus', { defaultMessage: 'Agent status', }), + sortable: true, render: (hostStatus: HostInfo['host_status'], endpointInfo) => { return ( { + render: ( + policyName: HostInfo['metadata']['Endpoint']['policy']['applied']['name'], + item: HostInfo + ) => { + const policy = item.metadata.Endpoint.policy.applied; + return ( <> - + {canReadPolicyManagement ? ( - {policy.name} + {policyName} ) : ( - <>{policy.name} + <>{policyName} )} {policy.endpoint_policy_version && ( @@ -186,12 +212,16 @@ const getEndpointListColumns = ({ }, }, { - field: 'metadata.Endpoint.policy.applied', - width: '9%', + field: EndpointSortableField.POLICY_STATUS, + width: columnWidths[EndpointSortableField.POLICY_STATUS], name: i18n.translate('xpack.securitySolution.endpoint.list.policyStatus', { defaultMessage: 'Policy status', }), - render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => { + sortable: true, + render: ( + status: HostInfo['metadata']['Endpoint']['policy']['applied']['status'], + item: HostInfo + ) => { const toRoutePath = getEndpointDetailsPath({ name: 'endpointPolicyResponse', ...queryParams, @@ -199,17 +229,14 @@ const getEndpointListColumns = ({ }); const toRouteUrl = getAppUrl({ path: toRoutePath }); return ( - + { return ( @@ -236,11 +264,12 @@ const getEndpointListColumns = ({ }, }, { - field: 'metadata.host.ip', - width: '12%', + field: EndpointSortableField.HOST_IP, + width: columnWidths[EndpointSortableField.HOST_IP], name: i18n.translate('xpack.securitySolution.endpoint.list.ip', { defaultMessage: 'IP address', }), + sortable: true, render: (ip: string[]) => { return ( @@ -254,11 +283,12 @@ const getEndpointListColumns = ({ }, }, { - field: 'metadata.agent.version', - width: '9%', + field: EndpointSortableField.AGENT_VERSION, + width: columnWidths[EndpointSortableField.AGENT_VERSION], name: i18n.translate('xpack.securitySolution.endpoint.list.endpointVersion', { defaultMessage: 'Version', }), + sortable: true, render: (version: string) => { return ( @@ -270,10 +300,11 @@ const getEndpointListColumns = ({ }, }, { - field: 'metadata.@timestamp', + field: EndpointSortableField.LAST_SEEN, + width: columnWidths[EndpointSortableField.LAST_SEEN], name: lastActiveColumnName, - width: '9%', - render(dateValue: HostInfo['metadata']['@timestamp']) { + sortable: true, + render(dateValue: HostInfo['last_checkin']) { return ( { const history = useHistory(); const { listData, pageIndex, pageSize, + sortField, + sortDirection, totalHits: totalItemCount, listLoading: loading, listError, @@ -369,7 +406,7 @@ export const EndpointList = () => { }, [pageIndex, pageSize, maxPageCount]); const onTableChange = useCallback( - ({ page }: { page: { index: number; size: number } }) => { + ({ page, sort }: CriteriaWithPagination) => { const { index, size } = page; // FIXME: PT: if endpoint details is open, table is not displaying correct number of rows history.push( @@ -378,33 +415,40 @@ export const EndpointList = () => { ...queryParams, page_index: JSON.stringify(index), page_size: JSON.stringify(size), + sort_direction: sort?.direction, + sort_field: sort?.field as EndpointSortableField, }) ); }, [history, queryParams] ); + const stateHandleCreatePolicyClick: CreatePackagePolicyRouteState = useMemo( + () => ({ + onCancelNavigateTo: [ + APP_UI_ID, + { + path: getEndpointListPath({ name: 'endpointList' }), + }, + ], + onCancelUrl: getAppUrl({ path: getEndpointListPath({ name: 'endpointList' }) }), + onSaveNavigateTo: [ + APP_UI_ID, + { + path: getEndpointListPath({ name: 'endpointList' }), + }, + ], + }), + [getAppUrl] + ); + const handleCreatePolicyClick = useNavigateToAppEventHandler( 'fleet', { path: `/integrations/${ endpointPackageVersion ? `/endpoint-${endpointPackageVersion}` : '' }/add-integration`, - state: { - onCancelNavigateTo: [ - APP_UI_ID, - { - path: getEndpointListPath({ name: 'endpointList' }), - }, - ], - onCancelUrl: getAppUrl({ path: getEndpointListPath({ name: 'endpointList' }) }), - onSaveNavigateTo: [ - APP_UI_ID, - { - path: getEndpointListPath({ name: 'endpointList' }), - }, - ], - }, + state: stateHandleCreatePolicyClick, } ); @@ -450,9 +494,7 @@ export const EndpointList = () => { const handleDeployEndpointsClick = useNavigateToAppEventHandler('fleet', { path: `/policies/${selectedPolicyId}?openEnrollmentFlyout=true`, - state: { - onDoneNavigateTo: [APP_UI_ID, { path: getEndpointListPath({ name: 'endpointList' }) }], - }, + state: stateHandleDeployEndpointsClick, }); const handleSelectableOnChange = useCallback<(o: EuiSelectableProps['options']) => void>( @@ -500,18 +542,28 @@ export const EndpointList = () => { ] ); + const sorting = useMemo( + () => ({ + sort: { field: sortField as keyof HostInfoInterface, direction: sortDirection }, + }), + [sortDirection, sortField] + ); + + const mutableListData = useMemo(() => [...listData], [listData]); + const renderTableOrEmptyState = useMemo(() => { if (endpointsExist) { return ( ); } else if (canReadEndpointList && !canAccessFleet) { @@ -554,15 +606,16 @@ export const EndpointList = () => { handleDeployEndpointsClick, handleSelectableOnChange, hasPolicyData, - listData, listError?.message, loading, + mutableListData, onTableChange, paginationSetup, policyItemsLoading, policyItems, selectedPolicyId, setTableRowProps, + sorting, ]); return ( diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx index 248142e7827b37..641a32bae889ba 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx @@ -49,6 +49,10 @@ import { import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; import * as i18n from './translations'; +import type { + ReportDataQualityCheckAllCompletedParams, + ReportDataQualityIndexCheckedParams, +} from '../../common/lib/telemetry'; const LOCAL_STORAGE_KEY = 'dataQualityDashboardLastChecked'; @@ -133,6 +137,9 @@ const DataQualityComponent: React.FC = () => { const httpFetch = KibanaServices.get().http.fetch; const { baseTheme, theme } = useThemes(); const toasts = useToasts(); + const { + services: { telemetry }, + } = useKibana(); const addSuccessToast = useCallback( (toast: { title: string }) => { toasts.addSuccess(toast); @@ -204,6 +211,20 @@ const DataQualityComponent: React.FC = () => { [createCaseFlyout] ); + const reportDataQualityIndexChecked = useCallback( + (params: ReportDataQualityIndexCheckedParams) => { + telemetry.reportDataQualityIndexChecked(params); + }, + [telemetry] + ); + + const reportDataQualityCheckAllCompleted = useCallback( + (params: ReportDataQualityCheckAllCompletedParams) => { + telemetry.reportDataQualityCheckAllCompleted(params); + }, + [telemetry] + ); + if (isSourcererLoading || isSignalIndexNameLoading) { return ; } @@ -235,6 +256,8 @@ const DataQualityComponent: React.FC = () => { defaultBytesFormat={defaultBytesFormat} defaultNumberFormat={defaultNumberFormat} getGroupByFieldsOnClick={getGroupByFieldsOnClick} + reportDataQualityCheckAllCompleted={reportDataQualityCheckAllCompleted} + reportDataQualityIndexChecked={reportDataQualityIndexChecked} httpFetch={httpFetch} ilmPhases={ilmPhases} isAssistantEnabled={hasAssistantPrivilege} diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx index ac425b18bbb7ca..10a536f69c8d08 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx @@ -21,7 +21,11 @@ import { coreMock } from '@kbn/core/public/mocks'; import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context'; import { useTimelineEventsDetails } from '../../../containers/details'; import { allCasesPermissions } from '../../../../cases_test_utils'; -import { DEFAULT_ALERTS_INDEX, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; +import { + DEFAULT_ALERTS_INDEX, + DEFAULT_PREVIEW_INDEX, + ASSISTANT_FEATURE_ID, +} from '../../../../../common/constants'; const ecsData: Ecs = { _id: '1', @@ -138,6 +142,13 @@ describe('event details panel component', () => { (KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock); (useKibana as jest.Mock).mockReturnValue({ services: { + application: { + capabilities: { + [ASSISTANT_FEATURE_ID]: { + 'ai-assistant': true, + }, + }, + }, uiSettings: { get: jest.fn().mockReturnValue([]), }, diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts index b46eac58e24b3b..a69f348c366ebe 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts @@ -10,6 +10,7 @@ import type { KbnClient } from '@kbn/test'; import type { WriteResponseBase } from '@elastic/elasticsearch/lib/api/types'; import { clone, merge } from 'lodash'; import type { DeepPartial } from 'utility-types'; +import { catchAxiosErrorFormatAndThrow } from './format_axios_error'; import type { GetMetadataListRequestQuery } from '../../../common/api/endpoint'; import { resolvePathVariables } from '../../../public/common/utils/resolve_path_variables'; import { @@ -27,13 +28,15 @@ export const fetchEndpointMetadata = async ( agentId: string ): Promise => { return ( - await kbnClient.request({ - method: 'GET', - path: resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: agentId }), - headers: { - 'Elastic-Api-Version': '2023-10-31', - }, - }) + await kbnClient + .request({ + method: 'GET', + path: resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: agentId }), + headers: { + 'Elastic-Api-Version': '2023-10-31', + }, + }) + .catch(catchAxiosErrorFormatAndThrow) ).data; }; @@ -42,18 +45,20 @@ export const fetchEndpointMetadataList = async ( { page = 0, pageSize = 100, ...otherOptions }: Partial = {} ): Promise => { return ( - await kbnClient.request({ - method: 'GET', - path: HOST_METADATA_LIST_ROUTE, - headers: { - 'Elastic-Api-Version': '2023-10-31', - }, - query: { - page, - pageSize, - ...otherOptions, - }, - }) + await kbnClient + .request({ + method: 'GET', + path: HOST_METADATA_LIST_ROUTE, + headers: { + 'Elastic-Api-Version': '2023-10-31', + }, + query: { + page, + pageSize, + ...otherOptions, + }, + }) + .catch(catchAxiosErrorFormatAndThrow) ).data; }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts index ea788063b572c8..d81fa0c2947065 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts @@ -35,6 +35,7 @@ import type { } from '@kbn/fleet-plugin/common/types'; import nodeFetch from 'node-fetch'; import semver from 'semver'; +import { catchAxiosErrorFormatAndThrow } from './format_axios_error'; import { FleetAgentGenerator } from '../../../common/endpoint/data_generators/fleet_agent_generator'; const fleetGenerator = new FleetAgentGenerator(); @@ -106,6 +107,7 @@ export const fetchFleetAgents = async ( path: AGENT_API_ROUTES.LIST_PATTERN, query: options, }) + .catch(catchAxiosErrorFormatAndThrow) .then((response) => response.data); }; @@ -161,6 +163,7 @@ export const fetchFleetServerUrl = async (kbnClient: KbnClient): Promise response.data); // TODO:PT need to also pull in the Proxies and use that instead if defiend for url @@ -195,6 +198,7 @@ export const fetchAgentPolicyEnrollmentKey = async ( path: enrollmentAPIKeyRouteService.getListPath(), query: { kuery: `policy_id: "${agentPolicyId}"` }, }) + .catch(catchAxiosErrorFormatAndThrow) .then((response) => response.data.items[0]); if (!apiKey) { @@ -219,6 +223,7 @@ export const fetchAgentPolicyList = async ( path: agentPolicyRouteService.getListPath(), query: options, }) + .catch(catchAxiosErrorFormatAndThrow) .then((response) => response.data); }; @@ -369,11 +374,13 @@ export const unEnrollFleetAgent = async ( agentId: string, force = false ): Promise => { - const { data } = await kbnClient.request({ - method: 'POST', - path: agentRouteService.getUnenrollPath(agentId), - body: { revoke: force }, - }); + const { data } = await kbnClient + .request({ + method: 'POST', + path: agentRouteService.getUnenrollPath(agentId), + body: { revoke: force }, + }) + .catch(catchAxiosErrorFormatAndThrow); return data; }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts new file mode 100644 index 00000000000000..ccb3dc125f5616 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts @@ -0,0 +1,65 @@ +/* + * 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 { AxiosError } from 'axios'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export class FormattedAxiosError extends Error { + public readonly request: { + method: string; + url: string; + data: unknown; + }; + public readonly response: { + status: number; + statusText: string; + data: any; + }; + + constructor(axiosError: AxiosError) { + super(axiosError.message); + + this.request = { + method: axiosError.config.method ?? '?', + url: axiosError.config.url ?? '?', + data: axiosError.config.data ?? '', + }; + + this.response = { + status: axiosError?.response?.status ?? 0, + statusText: axiosError?.response?.statusText ?? '', + data: axiosError?.response?.data, + }; + + this.name = this.constructor.name; + } + + toJSON() { + return { + message: this.message, + request: this.request, + response: this.response, + }; + } + + toString() { + return JSON.stringify(this.toJSON(), null, 2); + } +} + +/** + * Used with `promise.catch()`, it will format the Axios error to a new error and will re-throw + * @param error + */ +export const catchAxiosErrorFormatAndThrow = (error: Error): never => { + if (error instanceof AxiosError) { + throw new FormattedAxiosError(error); + } + + throw error; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts index acf6f53bc785e9..3b494d3bfe9cb2 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts @@ -12,6 +12,7 @@ import { PACKAGE_POLICY_API_ROUTES, PACKAGE_POLICY_SAVED_OBJECT_TYPE, } from '@kbn/fleet-plugin/common/constants'; +import { catchAxiosErrorFormatAndThrow } from './format_axios_error'; import { indexFleetEndpointPolicy } from '../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { setupFleetForEndpoint } from '../../../common/endpoint/data_loaders/setup_fleet_for_endpoint'; import type { GetPolicyListResponse } from '../../../public/management/pages/policy/types'; @@ -20,14 +21,16 @@ import { getEndpointPackageInfo } from '../../../common/endpoint/utils/package'; const fetchEndpointPolicies = ( kbnClient: KbnClient ): Promise> => { - return kbnClient.request({ - method: 'GET', - path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, - query: { - perPage: 100, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`, - }, - }); + return kbnClient + .request({ + method: 'GET', + path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, + query: { + perPage: 100, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`, + }, + }) + .catch(catchAxiosErrorFormatAndThrow); }; // Setup a list of real endpoint policies and return a method to randomly select one diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts index a2a24aec142b80..a3ad237fc3bcb4 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts @@ -11,6 +11,7 @@ import { KbnClient } from '@kbn/test'; import type { StatusResponse } from '@kbn/core-status-common-internal'; import pRetry from 'p-retry'; import nodeFetch from 'node-fetch'; +import { catchAxiosErrorFormatAndThrow } from './format_axios_error'; import { isLocalhost } from './is_localhost'; import { getLocalhostRealIp } from './localhost_services'; import { createSecuritySuperuser } from './security_user_services'; @@ -189,10 +190,12 @@ export const createKbnClient = ({ */ export const fetchStackVersion = async (kbnClient: KbnClient): Promise => { const status = ( - await kbnClient.request({ - method: 'GET', - path: '/api/status', - }) + await kbnClient + .request({ + method: 'GET', + path: '/api/status', + }) + .catch(catchAxiosErrorFormatAndThrow) ).data; if (!status?.version?.number) { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts index a7058501f125c9..f9d88382d81c74 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts @@ -36,6 +36,8 @@ import type { PostFleetServerHostsResponse, } from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts'; import chalk from 'chalk'; +import type { FormattedAxiosError } from '../common/format_axios_error'; +import { catchAxiosErrorFormatAndThrow } from '../common/format_axios_error'; import { isLocalhost } from '../common/is_localhost'; import { dump } from './utils'; import { fetchFleetServerUrl, waitForHostToEnroll } from '../common/fleet_services'; @@ -243,7 +245,7 @@ export const startFleetServerWithDocker = async ({ containerId = (await execa('docker', dockerArgs)).stdout; - const fleetServerAgent = await waitForHostToEnroll(kbnClient, containerName); + const fleetServerAgent = await waitForHostToEnroll(kbnClient, containerName, 120000); log.verbose(`Fleet server enrolled agent:\n${JSON.stringify(fleetServerAgent, null, 2)}`); @@ -313,11 +315,13 @@ const configureFleetIfNeeded = async () => { log.info(`Updating Fleet Settings for Output [${output.name} (${id})]`); - await kbnClient.request({ - method: 'PUT', - path: outputRoutesService.getUpdatePath(id), - body: update, - }); + await kbnClient + .request({ + method: 'PUT', + path: outputRoutesService.getUpdatePath(id), + body: update, + }) + .catch(catchAxiosErrorFormatAndThrow); } } } @@ -354,6 +358,25 @@ const addFleetServerHostToFleetSettings = async ( path: fleetServerHostsRoutesService.getCreatePath(), body: newFleetHostEntry, }) + .catch(catchAxiosErrorFormatAndThrow) + .catch((error: FormattedAxiosError) => { + if ( + error.response.status === 403 && + ((error.response?.data?.message as string) ?? '').includes('disabled') + ) { + log.error(`Update failed with [403: ${error.response.data.message}]. + +${chalk.red('Are you running this utility against a Serverless project?')} +If so, the following entry should be added to your local +'config/serverless.[project_type].dev.yml' (ex. 'serverless.security.dev.yml'): + +${chalk.bold(chalk.cyan('xpack.fleet.internal.fleetServerStandalone: false'))} + +`); + } + + throw error; + }) .then((response) => response.data); log.verbose(item); diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 50d4ae02eeb9bf..058f8892013a06 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -17,6 +17,7 @@ import type { import type { PluginStartContract as AlertsPluginStartContract } from '@kbn/alerting-plugin/server'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions/types'; +import type { AppFeatures } from '../lib/app_features'; import { getPackagePolicyCreateCallback, getPackagePolicyUpdateCallback, @@ -69,6 +70,7 @@ export interface EndpointAppContextServiceStartContract { actionCreateService: ActionCreateService | undefined; cloud: CloudSetup; esClient: ElasticsearchClient; + appFeatures: AppFeatures; } /** @@ -106,6 +108,7 @@ export class EndpointAppContextService { featureUsageService, endpointMetadataService, esClient, + appFeatures, } = dependencies; registerIngestCallback( @@ -117,7 +120,8 @@ export class EndpointAppContextService { alerting, licenseService, exceptionListsClient, - cloud + cloud, + appFeatures ) ); @@ -134,7 +138,8 @@ export class EndpointAppContextService { featureUsageService, endpointMetadataService, cloud, - esClient + esClient, + appFeatures ) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts new file mode 100644 index 00000000000000..1d39b72670b986 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { createMockEndpointAppContextServiceStartContract } from '../mocks'; +import type { Logger } from '@kbn/logging'; +import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { EndpointInternalFleetServicesInterface } from '../services/fleet'; +import type { AppFeatures } from '../../lib/app_features'; +import { createAppFeaturesMock } from '../../lib/app_features/mocks'; +import { ALL_APP_FEATURE_KEYS } from '../../../common'; +import { turnOffPolicyProtectionsIfNotSupported } from './turn_off_policy_protections'; +import { FleetPackagePolicyGenerator } from '../../../common/endpoint/data_generators/fleet_package_policy_generator'; +import type { PolicyData } from '../../../common/endpoint/types'; +import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; +import type { PromiseResolvedValue } from '../../../common/endpoint/types/utility_types'; +import { ensureOnlyEventCollectionIsAllowed } from '../../../common/endpoint/models/policy_config_helpers'; + +describe('Turn Off Policy Protections Migration', () => { + let esClient: ElasticsearchClient; + let fleetServices: EndpointInternalFleetServicesInterface; + let appFeatures: AppFeatures; + let logger: Logger; + + const callTurnOffPolicyProtections = () => + turnOffPolicyProtectionsIfNotSupported(esClient, fleetServices, appFeatures, logger); + + beforeEach(() => { + const endpointContextStartContract = createMockEndpointAppContextServiceStartContract(); + + ({ esClient, appFeatures, logger } = endpointContextStartContract); + fleetServices = endpointContextStartContract.endpointFleetServicesFactory.asInternalUser(); + }); + + describe('and `endpointPolicyProtections` is enabled', () => { + it('should do nothing', async () => { + await callTurnOffPolicyProtections(); + + expect(fleetServices.packagePolicy.list as jest.Mock).not.toHaveBeenCalled(); + expect(logger.info).toHaveBeenLastCalledWith( + 'App feature [endpoint_policy_protections] is enabled. Nothing to do!' + ); + }); + }); + + describe('and `endpointPolicyProtections` is disabled', () => { + let policyGenerator: FleetPackagePolicyGenerator; + let page1Items: PolicyData[] = []; + let page2Items: PolicyData[] = []; + let bulkUpdateResponse: PromiseResolvedValue>; + + const generatePolicyMock = (withDisabledProtections = false): PolicyData => { + const policy = policyGenerator.generateEndpointPackagePolicy(); + + if (!withDisabledProtections) { + return policy; + } + + policy.inputs[0].config.policy.value = ensureOnlyEventCollectionIsAllowed( + policy.inputs[0].config.policy.value + ); + + return policy; + }; + + beforeEach(() => { + policyGenerator = new FleetPackagePolicyGenerator('seed'); + const packagePolicyListSrv = fleetServices.packagePolicy.list as jest.Mock; + + appFeatures = createAppFeaturesMock( + ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections') + ); + + page1Items = [generatePolicyMock(), generatePolicyMock(true)]; + page2Items = [generatePolicyMock(true), generatePolicyMock()]; + + packagePolicyListSrv + .mockImplementationOnce(async () => { + return { + total: 1500, + page: 1, + perPage: 1000, + items: page1Items, + }; + }) + .mockImplementationOnce(async () => { + return { + total: 1500, + page: 2, + perPage: 1000, + items: page2Items, + }; + }); + + bulkUpdateResponse = { + updatedPolicies: [page1Items[0], page2Items[1]], + failedPolicies: [], + }; + + (fleetServices.packagePolicy.bulkUpdate as jest.Mock).mockImplementation(async () => { + return bulkUpdateResponse; + }); + }); + + it('should update only policies that have protections turn on', async () => { + await callTurnOffPolicyProtections(); + + expect(fleetServices.packagePolicy.list as jest.Mock).toHaveBeenCalledTimes(2); + expect(fleetServices.packagePolicy.bulkUpdate as jest.Mock).toHaveBeenCalledWith( + fleetServices.internalSoClient, + esClient, + [ + expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![0].id }), + expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![1].id }), + ], + { user: { username: 'elastic' } } + ); + expect(logger.info).toHaveBeenCalledWith( + 'Found 2 policies that need updates:\n' + + `Policy [${bulkUpdateResponse.updatedPolicies![0].id}][${ + bulkUpdateResponse.updatedPolicies![0].name + }] updated to disable protections. Trigger: [property [mac.malware.mode] is set to [prevent]]\n` + + `Policy [${bulkUpdateResponse.updatedPolicies![1].id}][${ + bulkUpdateResponse.updatedPolicies![1].name + }] updated to disable protections. Trigger: [property [mac.malware.mode] is set to [prevent]]` + ); + expect(logger.info).toHaveBeenCalledWith('Done. All updates applied successfully'); + }); + + it('should log failures', async () => { + bulkUpdateResponse.failedPolicies.push({ + error: new Error('oh oh'), + packagePolicy: bulkUpdateResponse.updatedPolicies![0], + }); + await callTurnOffPolicyProtections(); + + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Done. 1 out of 2 failed to update:') + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts new file mode 100644 index 00000000000000..c4a63b8ec841cc --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts @@ -0,0 +1,107 @@ +/* + * 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 { Logger, ElasticsearchClient } from '@kbn/core/server'; +import type { UpdatePackagePolicy } from '@kbn/fleet-plugin/common'; +import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import { + isPolicySetToEventCollectionOnly, + ensureOnlyEventCollectionIsAllowed, +} from '../../../common/endpoint/models/policy_config_helpers'; +import type { PolicyData } from '../../../common/endpoint/types'; +import { AppFeatureSecurityKey } from '../../../common/types/app_features'; +import type { EndpointInternalFleetServicesInterface } from '../services/fleet'; +import type { AppFeatures } from '../../lib/app_features'; +import { getPolicyDataForUpdate } from '../../../common/endpoint/service/policy'; + +export const turnOffPolicyProtectionsIfNotSupported = async ( + esClient: ElasticsearchClient, + fleetServices: EndpointInternalFleetServicesInterface, + appFeaturesService: AppFeatures, + logger: Logger +): Promise => { + const log = logger.get('endpoint', 'policyProtections'); + + if (appFeaturesService.isEnabled(AppFeatureSecurityKey.endpointPolicyProtections)) { + log.info( + `App feature [${AppFeatureSecurityKey.endpointPolicyProtections}] is enabled. Nothing to do!` + ); + + return; + } + + log.info( + `App feature [${AppFeatureSecurityKey.endpointPolicyProtections}] is disabled. Checking endpoint integration policies for compliance` + ); + + const { packagePolicy, internalSoClient, endpointPolicyKuery } = fleetServices; + const updates: UpdatePackagePolicy[] = []; + const messages: string[] = []; + const perPage = 1000; + let hasMoreData = true; + let total = 0; + let page = 1; + + do { + const currentPage = page++; + const { items, total: totalPolicies } = await packagePolicy.list(internalSoClient, { + page: currentPage, + kuery: endpointPolicyKuery, + perPage, + }); + + total = totalPolicies; + hasMoreData = currentPage * perPage < total; + + for (const item of items) { + const integrationPolicy = item as PolicyData; + const policySettings = integrationPolicy.inputs[0].config.policy.value; + const { message, isOnlyCollectingEvents } = isPolicySetToEventCollectionOnly(policySettings); + + if (!isOnlyCollectingEvents) { + messages.push( + `Policy [${integrationPolicy.id}][${integrationPolicy.name}] updated to disable protections. Trigger: [${message}]` + ); + + integrationPolicy.inputs[0].config.policy.value = + ensureOnlyEventCollectionIsAllowed(policySettings); + + updates.push({ + ...getPolicyDataForUpdate(integrationPolicy), + id: integrationPolicy.id, + }); + } + } + } while (hasMoreData); + + if (updates.length > 0) { + log.info(`Found ${updates.length} policies that need updates:\n${messages.join('\n')}`); + + const bulkUpdateResponse = await fleetServices.packagePolicy.bulkUpdate( + internalSoClient, + esClient, + updates, + { + user: { username: 'elastic' } as AuthenticatedUser, + } + ); + + log.debug(`Bulk update response:\n${JSON.stringify(bulkUpdateResponse, null, 2)}`); + + if (bulkUpdateResponse.failedPolicies.length > 0) { + log.error( + `Done. ${bulkUpdateResponse.failedPolicies.length} out of ${ + updates.length + } failed to update:\n${JSON.stringify(bulkUpdateResponse.failedPolicies, null, 2)}` + ); + } else { + log.info(`Done. All updates applied successfully`); + } + } else { + log.info(`Done. Checked ${total} policies and no updates needed`); + } +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index f38d96cbf70639..5a3c9ee2297ac9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -71,6 +71,7 @@ import type { EndpointAuthz } from '../../common/endpoint/types/authz'; import { EndpointFleetServicesFactory } from './services/fleet'; import { createLicenseServiceMock } from '../../common/license/mocks'; import { createFeatureUsageServiceMock } from './services/feature_usage/mocks'; +import { createAppFeaturesMock } from '../lib/app_features/mocks'; /** * Creates a mocked EndpointAppContext. @@ -163,6 +164,8 @@ export const createMockEndpointAppContextServiceStartContract = }, savedObjectsStart ); + const experimentalFeatures = config.experimentalFeatures; + const appFeatures = createAppFeaturesMock(undefined, experimentalFeatures, undefined, logger); packagePolicyService.list.mockImplementation(async (_, options) => { return { @@ -207,11 +210,12 @@ export const createMockEndpointAppContextServiceStartContract = cases: casesMock, cloud: cloudMock.createSetup(), featureUsageService: createFeatureUsageServiceMock(), - experimentalFeatures: createMockConfig().experimentalFeatures, + experimentalFeatures, messageSigningService: createMessageSigningServiceMock(), actionCreateService: undefined, createFleetActionsClient: jest.fn((_) => fleetActionsClientMock), esClient: elasticsearchClientMock.createElasticsearchClient(), + appFeatures, }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 123eab09255c6c..9131304d529a3c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -7,7 +7,10 @@ import type { TypeOf } from '@kbn/config-schema'; import type { Logger, RequestHandler } from '@kbn/core/server'; -import type { MetadataListResponse } from '../../../../common/endpoint/types'; +import type { + MetadataListResponse, + EndpointSortableField, +} from '../../../../common/endpoint/types'; import { errorHandler } from '../error_handler'; import type { SecuritySolutionRequestHandlerContext } from '../../../types'; @@ -19,6 +22,8 @@ import type { import { ENDPOINT_DEFAULT_PAGE, ENDPOINT_DEFAULT_PAGE_SIZE, + ENDPOINT_DEFAULT_SORT_DIRECTION, + ENDPOINT_DEFAULT_SORT_FIELD, METADATA_TRANSFORMS_PATTERN, } from '../../../../common/endpoint/constants'; @@ -54,6 +59,9 @@ export function getMetadataListRequestHandler( total, page: request.query.page || ENDPOINT_DEFAULT_PAGE, pageSize: request.query.pageSize || ENDPOINT_DEFAULT_PAGE_SIZE, + sortField: + (request.query.sortField as EndpointSortableField) || ENDPOINT_DEFAULT_SORT_FIELD, + sortDirection: request.query.sortDirection || ENDPOINT_DEFAULT_SORT_DIRECTION, }; return response.ok({ body }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 061c090f2df282..236879a4e41641 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -41,6 +41,8 @@ import type { PackagePolicyClient, } from '@kbn/fleet-plugin/server'; import { + ENDPOINT_DEFAULT_SORT_DIRECTION, + ENDPOINT_DEFAULT_SORT_FIELD, HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, METADATA_TRANSFORMS_STATUS_ROUTE, @@ -226,6 +228,8 @@ describe('test endpoint routes', () => { expect(endpointResultList.total).toEqual(1); expect(endpointResultList.page).toEqual(0); expect(endpointResultList.pageSize).toEqual(10); + expect(endpointResultList.sortField).toEqual(ENDPOINT_DEFAULT_SORT_FIELD); + expect(endpointResultList.sortDirection).toEqual(ENDPOINT_DEFAULT_SORT_DIRECTION); }); it('should get forbidden if no security solution access', async () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index bfdc8022e5ca2a..7efe718b458a9e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -10,6 +10,8 @@ import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constan import { get } from 'lodash'; import { expectedCompleteUnitedIndexQuery } from './query_builders.fixtures'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; +import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { EndpointSortableField } from '../../../../common/endpoint/types'; describe('query builder', () => { describe('MetadataGetQuery', () => { @@ -38,15 +40,19 @@ describe('query builder', () => { }); describe('buildUnitedIndexQuery', () => { - it('correctly builds empty query', async () => { - const soClient = savedObjectsClientMock.create(); + let soClient: jest.Mocked; + + beforeEach(() => { + soClient = savedObjectsClientMock.create(); soClient.find.mockResolvedValue({ saved_objects: [], total: 0, per_page: 0, page: 0, }); + }); + it('correctly builds empty query', async () => { const query = await buildUnitedIndexQuery( soClient, { page: 1, pageSize: 10, hostStatuses: [], kuery: '' }, @@ -91,15 +97,27 @@ describe('query builder', () => { expect(query.body.query).toEqual(expected); }); - it('correctly builds query', async () => { - const soClient = savedObjectsClientMock.create(); - soClient.find.mockResolvedValue({ - saved_objects: [], - total: 0, - per_page: 0, - page: 0, - }); + it('adds `status` runtime field', async () => { + const query = await buildUnitedIndexQuery( + soClient, + { page: 1, pageSize: 10, hostStatuses: [], kuery: '' }, + [] + ); + expect(query.body.runtime_mappings).toHaveProperty('status'); + }); + + it('adds `last_checkin` runtime field', async () => { + const query = await buildUnitedIndexQuery( + soClient, + { page: 1, pageSize: 10, hostStatuses: [], kuery: '' }, + [] + ); + + expect(query.body.runtime_mappings).toHaveProperty('last_checkin'); + }); + + it('correctly builds query', async () => { const query = await buildUnitedIndexQuery( soClient, { @@ -113,5 +131,51 @@ describe('query builder', () => { const expected = expectedCompleteUnitedIndexQuery; expect(query.body.query).toEqual(expected); }); + + describe('sorting', () => { + it('uses default sort field if none passed', async () => { + const query = await buildUnitedIndexQuery(soClient, { + page: 1, + pageSize: 10, + }); + + expect(query.body.sort).toEqual([ + { 'united.agent.enrolled_at': { order: 'desc', unmapped_type: 'date' } }, + ]); + }); + + it.each` + inputField | mappedField + ${'host_status'} | ${'status'} + ${'metadata.host.hostname'} | ${'united.endpoint.host.hostname'} + ${'metadata.Endpoint.policy.applied.name'} | ${'united.endpoint.Endpoint.policy.applied.name'} + `('correctly maps field $inputField', async ({ inputField, mappedField }) => { + const query = await buildUnitedIndexQuery(soClient, { + page: 1, + pageSize: 10, + sortField: inputField, + sortDirection: 'asc', + }); + + expect(query.body.sort).toEqual([{ [mappedField]: 'asc' }]); + }); + + it.each` + inputField | mappedField + ${EndpointSortableField.LAST_SEEN} | ${EndpointSortableField.LAST_SEEN} + ${EndpointSortableField.ENROLLED_AT} | ${'united.agent.enrolled_at'} + `('correctly maps date field $inputField', async ({ inputField, mappedField }) => { + const query = await buildUnitedIndexQuery(soClient, { + page: 1, + pageSize: 10, + sortField: inputField, + sortDirection: 'asc', + }); + + expect(query.body.sort).toEqual([ + { [mappedField]: { order: 'asc', unmapped_type: 'date' } }, + ]); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index b790283c03c575..e51655010b40c3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -9,9 +9,12 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { buildAgentStatusRuntimeField } from '@kbn/fleet-plugin/server'; import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { EndpointSortableField } from '../../../../common/endpoint/types'; import { ENDPOINT_DEFAULT_PAGE, ENDPOINT_DEFAULT_PAGE_SIZE, + ENDPOINT_DEFAULT_SORT_DIRECTION, + ENDPOINT_DEFAULT_SORT_FIELD, metadataCurrentIndexPattern, METADATA_UNITED_INDEX, } from '../../../../common/endpoint/constants'; @@ -55,9 +58,25 @@ export const MetadataSortMethod: estypes.SortCombinations[] = [ }, ]; -const UnitedMetadataSortMethod: estypes.SortCombinations[] = [ - { 'united.agent.enrolled_at': { order: 'desc', unmapped_type: 'date' } }, -]; +const getUnitedMetadataSortMethod = ( + sortField: EndpointSortableField, + sortDirection: 'asc' | 'desc' +): estypes.SortCombinations[] => { + const DATE_FIELDS = [EndpointSortableField.LAST_SEEN, EndpointSortableField.ENROLLED_AT]; + + const mappedUnitedMetadataSortField = + sortField === EndpointSortableField.HOST_STATUS + ? 'status' + : sortField === EndpointSortableField.ENROLLED_AT + ? 'united.agent.enrolled_at' + : sortField.replace('metadata.', 'united.endpoint.'); + + if (DATE_FIELDS.includes(sortField)) { + return [{ [mappedUnitedMetadataSortField]: { order: sortDirection, unmapped_type: 'date' } }]; + } else { + return [{ [mappedUnitedMetadataSortField]: sortDirection }]; + } +}; export function getESQueryHostMetadataByID(agentID: string): estypes.SearchRequest { return { @@ -128,6 +147,17 @@ export function getESQueryHostMetadataByIDs(agentIDs: string[]) { }; } +const lastCheckinRuntimeField = { + last_checkin: { + type: 'date', + script: { + lang: 'painless', + source: + "emit(doc['united.agent.last_checkin'].size() > 0 ? doc['united.agent.last_checkin'].value.toInstant().toEpochMilli() : doc['united.endpoint.@timestamp'].value.toInstant().toEpochMilli());", + }, + }, +}; + interface BuildUnitedIndexQueryResponse { body: { query: Record; @@ -151,6 +181,8 @@ export async function buildUnitedIndexQuery( pageSize = ENDPOINT_DEFAULT_PAGE_SIZE, hostStatuses = [], kuery = '', + sortField = ENDPOINT_DEFAULT_SORT_FIELD, + sortDirection = ENDPOINT_DEFAULT_SORT_DIRECTION, } = queryOptions || {}; const statusesKuery = buildStatusesKuery(hostStatuses); @@ -204,13 +236,15 @@ export async function buildUnitedIndexQuery( }; } - const runtimeMappings = await buildAgentStatusRuntimeField(soClient, 'united.agent.'); + const statusRuntimeField = await buildAgentStatusRuntimeField(soClient, 'united.agent.'); + const runtimeMappings = { ...statusRuntimeField, ...lastCheckinRuntimeField }; + const fields = Object.keys(runtimeMappings); return { body: { query, track_total_hits: true, - sort: UnitedMetadataSortMethod, + sort: getUnitedMetadataSortMethod(sortField as EndpointSortableField, sortDirection), fields, runtime_mappings: runtimeMappings, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.ts b/x-pack/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.ts index 658ff9f2a327ed..1f3df9d6a67d3a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.ts @@ -13,6 +13,8 @@ import type { PackagePolicyClient, PackageClient, } from '@kbn/fleet-plugin/server'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; +import { createInternalSoClient } from '../../utils/create_internal_so_client'; import { createInternalReadonlySoClient } from '../../utils/create_internal_readonly_so_client'; export interface EndpointFleetServicesFactoryInterface { @@ -42,7 +44,10 @@ export class EndpointFleetServicesFactory implements EndpointFleetServicesFactor packages: packageService.asInternalUser, packagePolicy, + endpointPolicyKuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "endpoint"`, + internalReadonlySoClient: createInternalReadonlySoClient(this.savedObjectsStart), + internalSoClient: createInternalSoClient(this.savedObjectsStart), }; } } @@ -55,6 +60,8 @@ export interface EndpointFleetServicesInterface { agentPolicy: AgentPolicyServiceInterface; packages: PackageClient; packagePolicy: PackagePolicyClient; + /** The `kuery` that can be used to filter for Endpoint integration policies */ + endpointPolicyKuery: string; } export interface EndpointInternalFleetServicesInterface extends EndpointFleetServicesInterface { @@ -62,4 +69,7 @@ export interface EndpointInternalFleetServicesInterface extends EndpointFleetSer * An internal SO client (readonly) that can be used with the Fleet services that require it */ internalReadonlySoClient: SavedObjectsClientContract; + + /** Internal SO client. USE ONLY WHEN ABSOLUTELY NEEDED. Else, use the `internalReadonlySoClient` */ + internalSoClient: SavedObjectsClientContract; } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts index 61008a99b55154..d5a973593f2250 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts @@ -13,7 +13,7 @@ import type { } from '@kbn/core/server'; import type { SearchResponse, SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; -import type { Agent, AgentPolicy, AgentStatus, PackagePolicy } from '@kbn/fleet-plugin/common'; +import type { Agent, AgentPolicy, PackagePolicy } from '@kbn/fleet-plugin/common'; import type { AgentPolicyServiceInterface, PackagePolicyClient } from '@kbn/fleet-plugin/server'; import { AgentNotFoundError } from '@kbn/fleet-plugin/server'; import type { @@ -295,7 +295,7 @@ export class EndpointMetadataService { }, }, last_checkin: - _fleetAgent?.last_checkin || new Date(endpointMetadata['@timestamp']).toISOString(), + fleetAgent?.last_checkin || new Date(endpointMetadata['@timestamp']).toISOString(), }; } @@ -438,12 +438,16 @@ export class EndpointMetadataService { const agentPolicy = agentPoliciesMap[_agent.policy_id!]; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const endpointPolicy = endpointPoliciesMap[_agent.policy_id!]; - // add the agent status from the fleet runtime field to - // the agent object + + const runtimeFields: Partial = { + status: doc?.fields?.status?.[0], + last_checkin: doc?.fields?.last_checkin?.[0], + }; const agent: typeof _agent = { ..._agent, - status: doc?.fields?.status?.[0] as AgentStatus, + ...runtimeFields, }; + hosts.push( await this.enrichHostMetadata(fleetServices, metadata, agent, agentPolicy, endpointPolicy) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.ts b/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.ts index d8bf7badec846b..b621222e79c0ab 100644 --- a/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.ts @@ -5,12 +5,8 @@ * 2.0. */ -import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; -import type { - KibanaRequest, - SavedObjectsClientContract, - SavedObjectsServiceStart, -} from '@kbn/core/server'; +import type { SavedObjectsClientContract, SavedObjectsServiceStart } from '@kbn/core/server'; +import { createInternalSoClient } from './create_internal_so_client'; import { EndpointError } from '../../../common/endpoint/errors'; type SavedObjectsClientContractKeys = keyof SavedObjectsClientContract; @@ -37,18 +33,7 @@ export class InternalReadonlySoClientMethodNotAllowedError extends EndpointError export const createInternalReadonlySoClient = ( savedObjectsServiceStart: SavedObjectsServiceStart ): SavedObjectsClientContract => { - const fakeRequest = { - headers: {}, - getBasePath: () => '', - path: '/', - route: { settings: {} }, - url: { href: {} }, - raw: { req: { url: '/' } }, - } as unknown as KibanaRequest; - - const internalSoClient = savedObjectsServiceStart.getScopedClient(fakeRequest, { - excludedExtensions: [SECURITY_EXTENSION_ID], - }); + const internalSoClient = createInternalSoClient(savedObjectsServiceStart); return new Proxy(internalSoClient, { get( diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_so_client.ts b/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_so_client.ts new file mode 100644 index 00000000000000..88e0d7a70a4c39 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_so_client.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsServiceStart } from '@kbn/core-saved-objects-server'; +import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; +import type { KibanaRequest, SavedObjectsClientContract } from '@kbn/core/server'; + +export const createInternalSoClient = ( + savedObjectsServiceStart: SavedObjectsServiceStart +): SavedObjectsClientContract => { + const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { href: {} }, + raw: { req: { url: '/' } }, + } as unknown as KibanaRequest; + + return savedObjectsServiceStart.getScopedClient(fakeRequest, { + excludedExtensions: [SECURITY_EXTENSION_ID], + }); +}; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 54258638f12301..f26531296b6a21 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -54,6 +54,9 @@ import { createMockPolicyData } from '../endpoint/services/feature_usage/mocks'; import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../common/endpoint/service/artifacts/constants'; import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { disableProtections } from '../../common/endpoint/models/policy_config_helpers'; +import type { AppFeatures } from '../lib/app_features'; +import { createAppFeaturesMock } from '../lib/app_features/mocks'; +import { ALL_APP_FEATURE_KEYS } from '../../common'; jest.mock('uuid', () => ({ v4: (): string => 'NEW_UUID', @@ -74,6 +77,7 @@ describe('ingest_integration tests ', () => { }); const generator = new EndpointDocGenerator(); const cloudService = cloudMock.createSetup(); + let appFeatures: AppFeatures; beforeEach(() => { endpointAppContextMock = createMockEndpointAppContextServiceStartContract(); @@ -82,6 +86,7 @@ describe('ingest_integration tests ', () => { licenseEmitter = new Subject(); licenseService = new LicenseService(); licenseService.start(licenseEmitter); + appFeatures = endpointAppContextMock.appFeatures; jest .spyOn(endpointAppContextMock.endpointMetadataService, 'getFleetEndpointPackagePolicy') @@ -129,7 +134,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.alerting, licenseService, exceptionListClient, - cloudService + cloudService, + appFeatures ); return callback( @@ -363,6 +369,7 @@ describe('ingest_integration tests ', () => { ); }); }); + describe('package policy update callback (when the license is below platinum)', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -379,7 +386,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.featureUsageService, endpointAppContextMock.endpointMetadataService, cloudService, - esClient + esClient, + appFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -397,7 +405,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.featureUsageService, endpointAppContextMock.endpointMetadataService, cloudService, - esClient + esClient, + appFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -419,6 +428,7 @@ describe('ingest_integration tests ', () => { beforeEach(() => { licenseEmitter.next(Platinum); // set license level to platinum }); + it('updates successfully when paid features are turned on', async () => { const mockPolicy = policyFactory(); mockPolicy.windows.popup.malware.message = 'paid feature'; @@ -429,7 +439,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.featureUsageService, endpointAppContextMock.endpointMetadataService, cloudService, - esClient + esClient, + appFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -442,6 +453,50 @@ describe('ingest_integration tests ', () => { ); expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy); }); + + it('should turn off protections if endpointPolicyProtections appFeature is disabled', async () => { + appFeatures = createAppFeaturesMock( + ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections') + ); + const callback = getPackagePolicyUpdateCallback( + endpointAppContextMock.logger, + licenseService, + endpointAppContextMock.featureUsageService, + endpointAppContextMock.endpointMetadataService, + cloudService, + esClient, + appFeatures + ); + + const updatedPolicy = await callback( + generator.generatePolicyPackagePolicy(), + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + + expect(updatedPolicy.inputs?.[0]?.config?.policy.value).toMatchObject({ + linux: { + behavior_protection: { mode: 'off' }, + malware: { mode: 'off' }, + memory_protection: { mode: 'off' }, + }, + mac: { + behavior_protection: { mode: 'off' }, + malware: { mode: 'off' }, + memory_protection: { mode: 'off' }, + }, + windows: { + antivirus_registration: { enabled: false }, + attack_surface_reduction: { credential_hardening: { enabled: false } }, + behavior_protection: { mode: 'off' }, + malware: { blocklist: false }, + memory_protection: { mode: 'off' }, + ransomware: { mode: 'off' }, + }, + }); + }); }); describe('package policy update callback when meta fields should be updated', () => { @@ -486,7 +541,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.featureUsageService, endpointAppContextMock.endpointMetadataService, cloudService, - esClient + esClient, + appFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); @@ -520,7 +576,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.featureUsageService, endpointAppContextMock.endpointMetadataService, cloudService, - esClient + esClient, + appFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); // values should be updated diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index b897441fe1e049..04bc9afa6d3a1e 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -22,6 +22,12 @@ import type { } from '@kbn/fleet-plugin/common'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types'; +import { AppFeatureSecurityKey } from '../../common/types/app_features'; +import { + isPolicySetToEventCollectionOnly, + ensureOnlyEventCollectionIsAllowed, +} from '../../common/endpoint/models/policy_config_helpers'; +import type { AppFeatures } from '../lib/app_features'; import type { NewPolicyData, PolicyConfig } from '../../common/endpoint/types'; import type { LicenseService } from '../../common/license'; import type { ManifestManager } from '../endpoint/services'; @@ -72,7 +78,8 @@ export const getPackagePolicyCreateCallback = ( alerts: AlertsStartContract, licenseService: LicenseService, exceptionsClient: ExceptionListClient | undefined, - cloud: CloudSetup + cloud: CloudSetup, + appFeatures: AppFeatures ): PostPackagePolicyCreateCallback => { return async ( newPackagePolicy, @@ -140,7 +147,8 @@ export const getPackagePolicyCreateCallback = ( licenseService, endpointIntegrationConfig, cloud, - esClientInfo + esClientInfo, + appFeatures ); return { @@ -175,31 +183,38 @@ export const getPackagePolicyUpdateCallback = ( featureUsageService: FeatureUsageService, endpointMetadataService: EndpointMetadataService, cloud: CloudSetup, - esClient: ElasticsearchClient + esClient: ElasticsearchClient, + appFeatures: AppFeatures ): PutPackagePolicyUpdateCallback => { return async (newPackagePolicy: NewPackagePolicy): Promise => { if (!isEndpointPackagePolicy(newPackagePolicy)) { return newPackagePolicy; } + const endpointIntegrationData = newPackagePolicy as NewPolicyData; + // Validate that Endpoint Security policy is valid against current license validatePolicyAgainstLicense( // The cast below is needed in order to ensure proper typing for // the policy configuration specific for endpoint - newPackagePolicy.inputs[0].config?.policy?.value as PolicyConfig, + endpointIntegrationData.inputs[0].config?.policy?.value as PolicyConfig, licenseService, logger ); - notifyProtectionFeatureUsage(newPackagePolicy, featureUsageService, endpointMetadataService); + notifyProtectionFeatureUsage( + endpointIntegrationData, + featureUsageService, + endpointMetadataService + ); - const newEndpointPackagePolicy = newPackagePolicy.inputs[0].config?.policy + const newEndpointPackagePolicy = endpointIntegrationData.inputs[0].config?.policy ?.value as PolicyConfig; const esClientInfo: InfoResponse = await esClient.info(); if ( - newPackagePolicy.inputs[0].config?.policy?.value && + endpointIntegrationData.inputs[0].config?.policy?.value && shouldUpdateMetaValues( newEndpointPackagePolicy, licenseService.getLicenseType(), @@ -214,10 +229,25 @@ export const getPackagePolicyUpdateCallback = ( newEndpointPackagePolicy.meta.cluster_name = esClientInfo.cluster_name; newEndpointPackagePolicy.meta.cluster_uuid = esClientInfo.cluster_uuid; newEndpointPackagePolicy.meta.license_uid = licenseService.getLicenseUID(); - newPackagePolicy.inputs[0].config.policy.value = newEndpointPackagePolicy; + + endpointIntegrationData.inputs[0].config.policy.value = newEndpointPackagePolicy; + } + + // If no Policy Protection allowed (ex. serverless) + const eventsOnlyPolicy = isPolicySetToEventCollectionOnly(newEndpointPackagePolicy); + if ( + !appFeatures.isEnabled(AppFeatureSecurityKey.endpointPolicyProtections) && + !eventsOnlyPolicy.isOnlyCollectingEvents + ) { + logger.warn( + `Endpoint integration policy [${endpointIntegrationData.id}][${endpointIntegrationData.name}] adjusted due to [endpointPolicyProtections] appFeature not being enabled. Trigger [${eventsOnlyPolicy.message}]` + ); + + endpointIntegrationData.inputs[0].config.policy.value = + ensureOnlyEventCollectionIsAllowed(newEndpointPackagePolicy); } - return newPackagePolicy; + return endpointIntegrationData; }; }; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts index b707199aa47387..9208f9f7f22cd5 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts @@ -19,6 +19,9 @@ import type { PolicyCreateCloudConfig, PolicyCreateEndpointConfig, } from '../types'; +import type { AppFeatures } from '../../lib/app_features'; +import { createAppFeaturesMock } from '../../lib/app_features/mocks'; +import { ALL_APP_FEATURE_KEYS } from '../../../common'; describe('Create Default Policy tests ', () => { const cloud = cloudMock.createSetup(); @@ -28,6 +31,7 @@ describe('Create Default Policy tests ', () => { const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold', uid: '' } }); let licenseEmitter: Subject; let licenseService: LicenseService; + let appFeatures: AppFeatures; const createDefaultPolicyCallback = async ( config: AnyPolicyCreateConfig | undefined @@ -35,7 +39,7 @@ describe('Create Default Policy tests ', () => { const esClientInfo = await elasticsearchServiceMock.createClusterClient().asInternalUser.info(); esClientInfo.cluster_name = ''; esClientInfo.cluster_uuid = ''; - return createDefaultPolicy(licenseService, config, cloud, esClientInfo); + return createDefaultPolicy(licenseService, config, cloud, esClientInfo, appFeatures); }; beforeEach(() => { @@ -43,7 +47,9 @@ describe('Create Default Policy tests ', () => { licenseService = new LicenseService(); licenseService.start(licenseEmitter); licenseEmitter.next(Platinum); // set license level to platinum + appFeatures = createAppFeaturesMock(); }); + describe('When no config is set', () => { it('Should return PolicyConfig for events only when license is at least platinum', async () => { const defaultPolicy = policyFactory(); @@ -174,6 +180,7 @@ describe('Create Default Policy tests ', () => { }); }); }); + it('Should return process, file and network events enabled when preset is EDR Essential', async () => { const config = createEndpointConfig({ preset: 'EDREssential' }); const policy = await createDefaultPolicyCallback(config); @@ -190,6 +197,7 @@ describe('Create Default Policy tests ', () => { }); }); }); + it('Should return the default config when preset is EDR Complete', async () => { const config = createEndpointConfig({ preset: 'EDRComplete' }); const policy = await createDefaultPolicyCallback(config); @@ -199,7 +207,37 @@ describe('Create Default Policy tests ', () => { defaultPolicy.meta.cloud = true; expect(policy).toMatchObject(defaultPolicy); }); + + it('should set policy to event collection only if endpointPolicyProtections appFeature is disabled', async () => { + appFeatures = createAppFeaturesMock( + ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections') + ); + + await expect( + createDefaultPolicyCallback(createEndpointConfig({ preset: 'EDRComplete' })) + ).resolves.toMatchObject({ + linux: { + behavior_protection: { mode: 'off' }, + malware: { mode: 'off' }, + memory_protection: { mode: 'off' }, + }, + mac: { + behavior_protection: { mode: 'off' }, + malware: { mode: 'off' }, + memory_protection: { mode: 'off' }, + }, + windows: { + antivirus_registration: { enabled: false }, + attack_surface_reduction: { credential_hardening: { enabled: false } }, + behavior_protection: { mode: 'off' }, + malware: { blocklist: false }, + memory_protection: { mode: 'off' }, + ransomware: { mode: 'off' }, + }, + }); + }); }); + describe('When cloud config is set', () => { const createCloudConfig = (): PolicyCreateCloudConfig => ({ type: 'cloud', diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts index d7c3994c05dc92..db053fd5c3b0e2 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts @@ -7,6 +7,8 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types'; +import { AppFeatureSecurityKey } from '../../../common/types/app_features'; +import type { AppFeatures } from '../../lib/app_features'; import { policyFactory as policyConfigFactory, policyFactoryWithoutPaidFeatures as policyConfigFactoryWithoutPaidFeatures, @@ -20,7 +22,10 @@ import { ENDPOINT_CONFIG_PRESET_NGAV, ENDPOINT_CONFIG_PRESET_DATA_COLLECTION, } from '../constants'; -import { disableProtections } from '../../../common/endpoint/models/policy_config_helpers'; +import { + disableProtections, + ensureOnlyEventCollectionIsAllowed, +} from '../../../common/endpoint/models/policy_config_helpers'; /** * Create the default endpoint policy based on the current license and configuration type @@ -29,7 +34,8 @@ export const createDefaultPolicy = ( licenseService: LicenseService, config: AnyPolicyCreateConfig | undefined, cloud: CloudSetup, - esClientInfo: InfoResponse + esClientInfo: InfoResponse, + appFeatures: AppFeatures ): PolicyConfig => { const factoryPolicy = policyConfigFactory(); @@ -44,15 +50,21 @@ export const createDefaultPolicy = ( : factoryPolicy.meta.cluster_uuid; factoryPolicy.meta.license_uid = licenseService.getLicenseUID(); - const defaultPolicyPerType = + let defaultPolicyPerType: PolicyConfig = config?.type === 'cloud' ? getCloudPolicyConfig(factoryPolicy) : getEndpointPolicyWithIntegrationConfig(factoryPolicy, config); - // Apply license limitations in the final step, so it's not overriden (see malware popup) - return licenseService.isPlatinumPlus() - ? defaultPolicyPerType - : policyConfigFactoryWithoutPaidFeatures(defaultPolicyPerType); + if (!licenseService.isPlatinumPlus()) { + defaultPolicyPerType = policyConfigFactoryWithoutPaidFeatures(defaultPolicyPerType); + } + + // If no Policy Protection allowed (ex. serverless) + if (!appFeatures.isEnabled(AppFeatureSecurityKey.endpointPolicyProtections)) { + defaultPolicyPerType = ensureOnlyEventCollectionIsAllowed(defaultPolicyPerType); + } + + return defaultPolicyPerType; }; /** diff --git a/x-pack/plugins/security_solution/server/lib/app_features/app_features.test.ts b/x-pack/plugins/security_solution/server/lib/app_features/app_features.test.ts index 5de9c57e2938f6..1951f6d8b00fa4 100644 --- a/x-pack/plugins/security_solution/server/lib/app_features/app_features.test.ts +++ b/x-pack/plugins/security_solution/server/lib/app_features/app_features.test.ts @@ -48,6 +48,25 @@ const CASES_APP_FEATURE_CONFIG = { }, }; +const ASSISTANT_BASE_CONFIG = { + bar: 'bar', +}; + +const ASSISTANT_APP_FEATURE_CONFIG = { + 'test-assistant-feature': { + privileges: { + all: { + ui: ['test-assistant-capability'], + api: ['test-assistant-capability'], + }, + read: { + ui: ['test-assistant-capability'], + api: ['test-assistant-capability'], + }, + }, + }, +}; + jest.mock('./security_kibana_features', () => { return { getSecurityBaseKibanaFeature: jest.fn(() => SECURITY_BASE_CONFIG), @@ -75,6 +94,20 @@ jest.mock('./security_cases_kibana_sub_features', () => { }; }); +jest.mock('./security_assistant_kibana_features', () => { + return { + getAssistantBaseKibanaFeature: jest.fn(() => ASSISTANT_BASE_CONFIG), + getAssistantBaseKibanaSubFeatureIds: jest.fn(() => ['subFeature1']), + getAssistantAppFeaturesConfig: jest.fn(() => ASSISTANT_APP_FEATURE_CONFIG), + }; +}); + +jest.mock('./security_assistant_kibana_sub_features', () => { + return { + assistantSubFeaturesMap: new Map([['subFeature1', { baz: 'baz' }]]), + }; +}); + describe('AppFeatures', () => { it('should register enabled kibana features', () => { const featuresSetup = { @@ -118,4 +151,25 @@ describe('AppFeatures', () => { subFeatures: [{ baz: 'baz' }], }); }); + + it('should register enabled assistant features', () => { + const featuresSetup = { + registerKibanaFeature: jest.fn(), + } as unknown as PluginSetupContract; + + const appFeatureKeys = ['test-assistant-feature'] as unknown as AppFeatureKeys; + + const appFeatures = new AppFeatures( + loggingSystemMock.create().get('mock'), + [] as unknown as ExperimentalFeatures + ); + appFeatures.init(featuresSetup); + appFeatures.set(appFeatureKeys); + + expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({ + ...ASSISTANT_BASE_CONFIG, + ...ASSISTANT_APP_FEATURE_CONFIG['test-assistant-feature'], + subFeatures: [{ baz: 'baz' }], + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts index 69c6c33d335c3e..0b17f6d71d00dd 100644 --- a/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts +++ b/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts @@ -22,9 +22,16 @@ import { import { AppFeaturesConfigMerger } from './app_features_config_merger'; import { casesSubFeaturesMap } from './security_cases_kibana_sub_features'; import { securitySubFeaturesMap } from './security_kibana_sub_features'; +import { assistantSubFeaturesMap } from './security_assistant_kibana_sub_features'; +import { + getAssistantAppFeaturesConfig, + getAssistantBaseKibanaFeature, + getAssistantBaseKibanaSubFeatureIds, +} from './security_assistant_kibana_features'; export class AppFeatures { private securityFeatureConfigMerger: AppFeaturesConfigMerger; + private assistantFeatureConfigMerger: AppFeaturesConfigMerger; private casesFeatureConfigMerger: AppFeaturesConfigMerger; private appFeatures?: Set; private featuresSetup?: FeaturesPluginSetup; @@ -38,6 +45,10 @@ export class AppFeatures { securitySubFeaturesMap ); this.casesFeatureConfigMerger = new AppFeaturesConfigMerger(this.logger, casesSubFeaturesMap); + this.assistantFeatureConfigMerger = new AppFeaturesConfigMerger( + this.logger, + assistantSubFeaturesMap + ); } public init(featuresSetup: FeaturesPluginSetup) { @@ -59,7 +70,7 @@ export class AppFeatures { return this.appFeatures.has(appFeatureKey); } - private registerEnabledKibanaFeatures() { + protected registerEnabledKibanaFeatures() { if (this.featuresSetup == null) { throw new Error( 'Cannot sync kibana features as featuresSetup is not present. Did you call init?' @@ -98,6 +109,23 @@ export class AppFeatures { this.logger.info(JSON.stringify(completeCasesAppFeatureConfig)); this.featuresSetup.registerKibanaFeature(completeCasesAppFeatureConfig); + + // register security assistant Kibana features + const securityAssistantBaseKibanaFeature = getAssistantBaseKibanaFeature(); + const securityAssistantBaseKibanaSubFeatureIds = getAssistantBaseKibanaSubFeatureIds(); + const enabledAssistantAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs( + getAssistantAppFeaturesConfig() + ); + const completeAssistantAppFeatureConfig = + this.assistantFeatureConfigMerger.mergeAppFeatureConfigs( + securityAssistantBaseKibanaFeature, + securityAssistantBaseKibanaSubFeatureIds, + enabledAssistantAppFeaturesConfigs + ); + + this.logger.info(JSON.stringify(completeAssistantAppFeatureConfig)); + + this.featuresSetup.registerKibanaFeature(completeAssistantAppFeatureConfig); } private getEnabledAppFeaturesConfigs( diff --git a/x-pack/plugins/security_solution/server/lib/app_features/mocks.ts b/x-pack/plugins/security_solution/server/lib/app_features/mocks.ts new file mode 100644 index 00000000000000..1a5efc9c64e37b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features/mocks.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 type { Logger } from '@kbn/core/server'; +import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; +import { AppFeatures } from './app_features'; +import type { AppFeatureKeys, ExperimentalFeatures } from '../../../common'; +import { ALL_APP_FEATURE_KEYS, allowedExperimentalValues } from '../../../common'; + +class AppFeaturesMock extends AppFeatures { + protected registerEnabledKibanaFeatures() { + // NOOP + } +} + +export const createAppFeaturesMock = ( + /** What features keys should be enabled. Default is all */ + enabledFeatureKeys: AppFeatureKeys = [...ALL_APP_FEATURE_KEYS], + experimentalFeatures: ExperimentalFeatures = { ...allowedExperimentalValues }, + featuresPluginSetupContract: FeaturesPluginSetup = featuresPluginMock.createSetup(), + logger: Logger = loggingSystemMock.create().get('appFeatureMock') +) => { + const appFeatures = new AppFeaturesMock(logger, experimentalFeatures); + + appFeatures.init(featuresPluginSetupContract); + + if (enabledFeatureKeys) { + appFeatures.set(enabledFeatureKeys); + } + + return appFeatures; +}; diff --git a/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_features.ts new file mode 100644 index 00000000000000..1927591da202f9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_features.ts @@ -0,0 +1,74 @@ +/* + * 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 { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import type { AppFeaturesAssistantConfig, BaseKibanaFeatureConfig } from './types'; +import { APP_ID, ASSISTANT_FEATURE_ID } from '../../../common/constants'; +import { AppFeatureAssistantKey } from '../../../common/types/app_features'; +import type { AssistantSubFeatureId } from './security_assistant_kibana_sub_features'; + +export const getAssistantBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({ + id: ASSISTANT_FEATURE_ID, + name: i18n.translate( + 'xpack.securitySolution.featureRegistry.linkSecuritySolutionAssistantTitle', + { + defaultMessage: 'Elastic AI Assistant', + } + ), + order: 1100, + category: DEFAULT_APP_CATEGORIES.security, + app: [ASSISTANT_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + minimumLicense: 'enterprise', + privileges: { + all: { + api: [], + app: [ASSISTANT_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + // No read-only mode currently supported + disabled: true, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, +}); + +export const getAssistantBaseKibanaSubFeatureIds = (): AssistantSubFeatureId[] => [ + // This is a sample sub-feature that can be used for future implementations + // AssistantSubFeatureId.createConversation, +]; + +/** + * Maps the AppFeatures keys to Kibana privileges that will be merged + * into the base privileges config for the Security app. + * + * Privileges can be added in different ways: + * - `privileges`: the privileges that will be added directly into the main Security Assistant feature. + * - `subFeatureIds`: the ids of the sub-features that will be added into the Assistant subFeatures entry. + * - `subFeaturesPrivileges`: the privileges that will be added into the existing Assistant subFeature with the privilege `id` specified. + */ +export const getAssistantAppFeaturesConfig = (): AppFeaturesAssistantConfig => ({ + [AppFeatureAssistantKey.assistant]: { + privileges: { + all: { + ui: ['ai-assistant'], + }, + }, + }, +}); diff --git a/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_sub_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_sub_features.ts new file mode 100644 index 00000000000000..bc495e8c24d608 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_sub_features.ts @@ -0,0 +1,59 @@ +/* + * 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 { SubFeatureConfig } from '@kbn/features-plugin/common'; + +// This is a sample sub-feature that can be used for future implementations +// @ts-expect-error unused variable +const createConversationSubFeature: SubFeatureConfig = { + name: i18n.translate( + 'xpack.securitySolution.featureRegistry.assistant.createConversationSubFeatureName', + { + defaultMessage: 'Create Conversations', + } + ), + description: i18n.translate( + 'xpack.securitySolution.featureRegistry.subFeatures.assistant.description', + { defaultMessage: 'Create custom conversations.' } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: [], + id: 'create_conversation', + name: i18n.translate( + 'xpack.securitySolution.featureRegistry.assistant.createConversationSubFeatureDetails', + { + defaultMessage: 'Create conversations', + } + ), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + ui: ['createConversation'], + }, + ], + }, + ], +}; + +export enum AssistantSubFeatureId { + createConversation = 'createConversationSubFeature', +} + +// Defines all the ordered Security Assistant subFeatures available +export const assistantSubFeaturesMap = Object.freeze( + new Map([ + // This is a sample sub-feature that can be used for future implementations + // [AssistantSubFeatureId.createConversation, createConversationSubFeature], + ]) +); diff --git a/x-pack/plugins/security_solution/server/lib/app_features/types.ts b/x-pack/plugins/security_solution/server/lib/app_features/types.ts index 67480b33a20895..e6a4fd8db03047 100644 --- a/x-pack/plugins/security_solution/server/lib/app_features/types.ts +++ b/x-pack/plugins/security_solution/server/lib/app_features/types.ts @@ -7,7 +7,11 @@ import type { KibanaFeatureConfig, SubFeaturePrivilegeConfig } from '@kbn/features-plugin/common'; import type { AppFeatureKey } from '../../../common'; -import type { AppFeatureSecurityKey, AppFeatureCasesKey } from '../../../common/types/app_features'; +import type { + AppFeatureSecurityKey, + AppFeatureCasesKey, + AppFeatureAssistantKey, +} from '../../../common/types/app_features'; import type { RecursivePartial } from '../../../common/utility_types'; export type BaseKibanaFeatureConfig = Omit; @@ -29,3 +33,7 @@ export type AppFeaturesCasesConfig = Record< AppFeatureCasesKey, AppFeatureKibanaConfig >; +export type AppFeaturesAssistantConfig = Record< + AppFeatureAssistantKey, + AppFeatureKibanaConfig +>; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json index c1a62bf7ca31f7..133083cec26018 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json @@ -32,6 +32,7 @@ "feature": { "ml": ["all"], "siem": ["all", "read_alerts", "crud_alerts"], + "securitySolutionAssistant": ["all"], "securitySolutionCases": ["all"], "actions": ["read"], "builtInAlerts": ["all"], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json index 42ef9ba1122c71..23a1256dac4aac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json @@ -34,6 +34,7 @@ "feature": { "ml": ["read"], "siem": ["all", "read_alerts", "crud_alerts"], + "securitySolutionAssistant": ["all"], "securitySolutionCases": ["all"], "actions": ["read"], "builtInAlerts": ["all"] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter_no_actions/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter_no_actions/detections_role.json index e8000d6bb50e7b..6b392c18f8caa4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter_no_actions/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter_no_actions/detections_role.json @@ -34,6 +34,7 @@ "feature": { "ml": ["read"], "siem": ["all", "read_alerts", "crud_alerts"], + "securitySolutionAssistant": ["all"], "securitySolutionCases": ["all"], "builtInAlerts": ["all"] }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json index 88d863631a90b2..17b6e45f8c72d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json @@ -38,6 +38,7 @@ "feature": { "ml": ["all"], "siem": ["all", "read_alerts", "crud_alerts"], + "securitySolutionAssistant": ["all"], "securitySolutionCases": ["all"], "actions": ["all"], "builtInAlerts": ["all"] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json index 95be607cf71814..137091bc7f7955 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json @@ -27,6 +27,7 @@ "feature": { "ml": ["read"], "siem": ["read", "read_alerts"], + "securitySolutionAssistant": ["none"], "securitySolutionCases": ["read"], "actions": ["read"], "builtInAlerts": ["read"] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json index ea1fb2bf1433f5..dafe85548d4d0c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json @@ -37,6 +37,7 @@ "feature": { "ml": ["read"], "siem": ["all", "read_alerts", "crud_alerts"], + "securitySolutionAssistant": ["all"], "securitySolutionCases": ["all"], "actions": ["read"], "builtInAlerts": ["all"] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json index 4ad6488d0b5ab6..5e3aa868f61477 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json @@ -37,6 +37,7 @@ "feature": { "ml": ["read"], "siem": ["all", "read_alerts", "crud_alerts"], + "securitySolutionAssistant": ["all"], "securitySolutionCases": ["all"], "actions": ["all"], "builtInAlerts": ["all"] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json index 2f555bebbff901..d670fd9555f597 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json @@ -26,6 +26,7 @@ "feature": { "ml": ["read"], "siem": ["read", "read_alerts"], + "securitySolutionAssistant": ["all"], "securitySolutionCases": ["read"], "actions": ["read"], "builtInAlerts": ["read"] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json index f8216a613cb5a0..4db91de93709ac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json @@ -31,6 +31,7 @@ "feature": { "ml": ["read"], "siem": ["read", "read_alerts"], + "securitySolutionAssistant": ["all"], "securitySolutionCases": ["read"], "actions": ["read"], "builtInAlerts": ["read"] diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index e64a5808eb3ddc..4e7beb558d4de9 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -17,6 +17,7 @@ import { Dataset } from '@kbn/rule-registry-plugin/server'; import type { ListPluginSetup } from '@kbn/lists-plugin/server'; import type { ILicense } from '@kbn/licensing-plugin/server'; +import { turnOffPolicyProtectionsIfNotSupported } from './endpoint/migrations/turn_off_policy_protections'; import { endpointSearchStrategyProvider } from './search_strategy/endpoint'; import { getScheduleNotificationResponseActionsService } from './lib/detection_engine/rule_response_actions/schedule_notification_response_actions'; import { siemGuideId, siemGuideConfig } from '../common/guided_onboarding/siem_guide_config'; @@ -438,6 +439,15 @@ export class Plugin implements ISecuritySolutionPlugin { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion plugins.fleet!; let manifestManager: ManifestManager | undefined; + const endpointFleetServicesFactory = new EndpointFleetServicesFactory( + { + agentService, + packageService, + packagePolicyService, + agentPolicyService, + }, + core.savedObjects + ); this.licensing$ = plugins.licensing.license$; @@ -459,17 +469,23 @@ export class Plugin implements ISecuritySolutionPlugin { esClient: core.elasticsearch.client.asInternalUser, }); - // Migrate artifacts to fleet and then start the minifest task after that is done + // Migrate artifacts to fleet and then start the manifest task after that is done plugins.fleet.fleetSetupCompleted().then(() => { - logger.info('Dependent plugin setup complete - Starting ManifestTask'); - if (this.manifestTask) { + logger.info('Dependent plugin setup complete - Starting ManifestTask'); this.manifestTask.start({ taskManager, }); } else { logger.error(new Error('User artifacts task not available.')); } + + turnOffPolicyProtectionsIfNotSupported( + core.elasticsearch.client.asInternalUser, + endpointFleetServicesFactory.asInternalUser(), + this.appFeatures, + logger + ); }); // License related start @@ -493,15 +509,7 @@ export class Plugin implements ISecuritySolutionPlugin { packagePolicyService, logger ), - endpointFleetServicesFactory: new EndpointFleetServicesFactory( - { - agentService, - packageService, - packagePolicyService, - agentPolicyService, - }, - core.savedObjects - ), + endpointFleetServicesFactory, security: plugins.security, alerting: plugins.alerting, config: this.config, @@ -522,6 +530,7 @@ export class Plugin implements ISecuritySolutionPlugin { ), createFleetActionsClient, esClient: core.elasticsearch.client.asInternalUser, + appFeatures: this.appFeatures, }); this.telemetryReceiver.start( diff --git a/x-pack/plugins/security_solution/server/ui_settings.ts b/x-pack/plugins/security_solution/server/ui_settings.ts index f5ff542a7833f8..6a41844e9e0325 100644 --- a/x-pack/plugins/security_solution/server/ui_settings.ts +++ b/x-pack/plugins/security_solution/server/ui_settings.ts @@ -36,6 +36,7 @@ import { EXTENDED_RULE_EXECUTION_LOGGING_MIN_LEVEL_SETTING, DEFAULT_ALERT_TAGS_KEY, DEFAULT_ALERT_TAGS_VALUE, + ENABLE_EXPANDABLE_FLYOUT_SETTING, } from '../common/constants'; import type { ExperimentalFeatures } from '../common/experimental_features'; import { LogLevelSetting } from '../common/api/detection_engine/rule_monitoring'; @@ -163,6 +164,22 @@ export const initUiSettings = ( requiresPageReload: true, schema: schema.boolean(), }, + [ENABLE_EXPANDABLE_FLYOUT_SETTING]: { + name: i18n.translate('xpack.securitySolution.uiSettings.enableExpandableFlyoutLabel', { + defaultMessage: 'Expandable flyout', + }), + value: true, + description: i18n.translate( + 'xpack.securitySolution.uiSettings.enableExpandableFlyoutDescription', + { + defaultMessage: '

    Enables the expandable flyout

    ', + } + ), + type: 'boolean', + category: [APP_ID], + requiresPageReload: true, + schema: schema.boolean(), + }, [DEFAULT_RULES_TABLE_REFRESH_SETTING]: { name: i18n.translate('xpack.securitySolution.uiSettings.rulesTableRefresh', { defaultMessage: 'Rules auto refresh', diff --git a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts index 82f180d4a541e9..779f874b266da2 100644 --- a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts +++ b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts @@ -17,6 +17,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = { essentials: [AppFeatureKey.endpointHostManagement, AppFeatureKey.endpointPolicyManagement], complete: [ AppFeatureKey.advancedInsights, + AppFeatureKey.assistant, AppFeatureKey.investigationGuide, AppFeatureKey.threatIntelligence, AppFeatureKey.casesConnectors, diff --git a/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts b/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts index 2ec94d78722e00..b47450eca235c9 100644 --- a/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts +++ b/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts @@ -113,13 +113,15 @@ export class EndpointMeteringService { creation_timestamp: timestampStr, usage: { type: 'security_solution_endpoint', - sub_type: this.tier, period_seconds: SAMPLE_PERIOD_SECONDS, quantity: 1, }, source: { id: taskId, instance_group_id: projectId, + metadata: { + tier: this.tier, + }, }, }; } diff --git a/x-pack/plugins/security_solution_serverless/server/types.ts b/x-pack/plugins/security_solution_serverless/server/types.ts index ac45cadd21c6d6..91b1750e62c81f 100644 --- a/x-pack/plugins/security_solution_serverless/server/types.ts +++ b/x-pack/plugins/security_solution_serverless/server/types.ts @@ -19,6 +19,8 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { SecuritySolutionEssPluginSetup } from '@kbn/security-solution-ess/server'; import type { MlPluginSetup } from '@kbn/ml-plugin/server'; +import type { ProductTier } from '../common/product'; + import type { ServerlessSecurityConfig } from './config'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -63,6 +65,11 @@ export interface UsageMetrics { export interface UsageSource { id: string; instance_group_id: string; + metadata?: UsageSourceMetadata; +} + +export interface UsageSourceMetadata { + tier?: ProductTier; } export interface SecurityUsageReportingTaskSetupContract { diff --git a/x-pack/plugins/serverless_search/public/application/components/indexing_api.tsx b/x-pack/plugins/serverless_search/public/application/components/indexing_api.tsx index eb34e345be1425..35674a82695b2f 100644 --- a/x-pack/plugins/serverless_search/public/application/components/indexing_api.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/indexing_api.tsx @@ -23,18 +23,20 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useQuery } from '@tanstack/react-query'; +import { OverviewPanel, LanguageClientPanel, CodeBox } from '@kbn/search-api-panels'; +import type { + LanguageDefinition, + LanguageDefinitionSnippetArguments, +} from '@kbn/search-api-panels'; +import { PLUGIN_ID } from '../../../common'; import { IndexData, FetchIndicesResult } from '../../../common/types'; import { FETCH_INDICES_PATH } from '../routes'; import { API_KEY_PLACEHOLDER, ELASTICSEARCH_URL_PLACEHOLDER } from '../constants'; import { useKibanaServices } from '../hooks/use_kibana'; -import { CodeBox } from './code_box'; import { javascriptDefinition } from './languages/javascript'; import { languageDefinitions } from './languages/languages'; -import { LanguageDefinition, LanguageDefinitionSnippetArguments } from './languages/types'; - -import { OverviewPanel } from './overview_panels/overview_panel'; -import { LanguageClientPanel } from './overview_panels/language_client_panel'; +import { getCodeSnippet, showTryInConsole } from './languages/utils'; const NoIndicesContent = () => ( <> @@ -115,7 +117,7 @@ const IndicesContent = ({ }; export const ElasticsearchIndexingApi = () => { - const { cloud, http } = useKibanaServices(); + const { cloud, http, share } = useKibanaServices(); const [selectedLanguage, setSelectedLanguage] = useState(javascriptDefinition); const [indexSearchQuery, setIndexSearchQuery] = useState(undefined); @@ -200,17 +202,26 @@ export const ElasticsearchIndexingApi = () => { language={language} setSelectedLanguage={setSelectedLanguage} isSelectedLanguage={selectedLanguage === language} + http={http} + pluginId={PLUGIN_ID} /> ))}
    } diff --git a/x-pack/plugins/serverless_search/public/application/components/languages/console.ts b/x-pack/plugins/serverless_search/public/application/components/languages/console.ts index c2fceaae4f85b1..d234262d93f136 100644 --- a/x-pack/plugins/serverless_search/public/application/components/languages/console.ts +++ b/x-pack/plugins/serverless_search/public/application/components/languages/console.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { LanguageDefinition } from '@kbn/search-api-panels'; import { INDEX_NAME_PLACEHOLDER } from '../../constants'; -import { LanguageDefinition } from './types'; export const consoleDefinition: Partial = { buildSearchQuery: `POST /books/_search?pretty diff --git a/x-pack/plugins/serverless_search/public/application/components/languages/curl.ts b/x-pack/plugins/serverless_search/public/application/components/languages/curl.ts index cc98b35c876961..a0ed0f89723c97 100644 --- a/x-pack/plugins/serverless_search/public/application/components/languages/curl.ts +++ b/x-pack/plugins/serverless_search/public/application/components/languages/curl.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { i18n } from '@kbn/i18n'; import { docLinks } from '../../../../common/doc_links'; -import { LanguageDefinition, Languages } from './types'; export const curlDefinition: LanguageDefinition = { buildSearchQuery: `curl -X POST "\$\{ES_URL\}/books/_search?pretty" \\ diff --git a/x-pack/plugins/serverless_search/public/application/components/languages/go.ts b/x-pack/plugins/serverless_search/public/application/components/languages/go.ts index f7cd2b3ac2cdc3..00e70f7ce7a99e 100644 --- a/x-pack/plugins/serverless_search/public/application/components/languages/go.ts +++ b/x-pack/plugins/serverless_search/public/application/components/languages/go.ts @@ -6,8 +6,8 @@ */ import { i18n } from '@kbn/i18n'; +import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { docLinks } from '../../../../common/doc_links'; -import { LanguageDefinition, Languages } from './types'; export const goDefinition: LanguageDefinition = { advancedConfig: docLinks.goAdvancedConfig, diff --git a/x-pack/plugins/serverless_search/public/application/components/languages/javascript.ts b/x-pack/plugins/serverless_search/public/application/components/languages/javascript.ts index d2ed8c016df851..bac5452ca51059 100644 --- a/x-pack/plugins/serverless_search/public/application/components/languages/javascript.ts +++ b/x-pack/plugins/serverless_search/public/application/components/languages/javascript.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { i18n } from '@kbn/i18n'; import { docLinks } from '../../../../common/doc_links'; -import { LanguageDefinition, Languages } from './types'; export const javascriptDefinition: LanguageDefinition = { advancedConfig: docLinks.jsAdvancedConfig, diff --git a/x-pack/plugins/serverless_search/public/application/components/languages/languages.ts b/x-pack/plugins/serverless_search/public/application/components/languages/languages.ts index 38b7cf2beacb0f..754b1c3386f8f4 100644 --- a/x-pack/plugins/serverless_search/public/application/components/languages/languages.ts +++ b/x-pack/plugins/serverless_search/public/application/components/languages/languages.ts @@ -5,13 +5,14 @@ * 2.0. */ +import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; + import { curlDefinition } from './curl'; import { goDefinition } from './go'; import { javascriptDefinition } from './javascript'; import { phpDefinition } from './php'; import { pythonDefinition } from './python'; import { rubyDefinition } from './ruby'; -import { Languages, LanguageDefinition } from './types'; const languageDefinitionRecords: Partial> = { [Languages.CURL]: curlDefinition, diff --git a/x-pack/plugins/serverless_search/public/application/components/languages/php.ts b/x-pack/plugins/serverless_search/public/application/components/languages/php.ts index 5e6824dc174b22..a13a1ea9b7177f 100644 --- a/x-pack/plugins/serverless_search/public/application/components/languages/php.ts +++ b/x-pack/plugins/serverless_search/public/application/components/languages/php.ts @@ -6,9 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { docLinks } from '../../../../common/doc_links'; import { INDEX_NAME_PLACEHOLDER } from '../../constants'; -import { LanguageDefinition, Languages } from './types'; export const phpDefinition: LanguageDefinition = { advancedConfig: docLinks.phpAdvancedConfig, diff --git a/x-pack/plugins/serverless_search/public/application/components/languages/python.ts b/x-pack/plugins/serverless_search/public/application/components/languages/python.ts index 9f5031a0993ca1..4fa3da03238686 100644 --- a/x-pack/plugins/serverless_search/public/application/components/languages/python.ts +++ b/x-pack/plugins/serverless_search/public/application/components/languages/python.ts @@ -6,8 +6,8 @@ */ import { i18n } from '@kbn/i18n'; +import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { docLinks } from '../../../../common/doc_links'; -import { LanguageDefinition, Languages } from './types'; import { INDEX_NAME_PLACEHOLDER } from '../../constants'; export const pythonDefinition: LanguageDefinition = { diff --git a/x-pack/plugins/serverless_search/public/application/components/languages/ruby.ts b/x-pack/plugins/serverless_search/public/application/components/languages/ruby.ts index b6b8ce3c244289..4339d5f8261cc5 100644 --- a/x-pack/plugins/serverless_search/public/application/components/languages/ruby.ts +++ b/x-pack/plugins/serverless_search/public/application/components/languages/ruby.ts @@ -6,9 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import { Languages, LanguageDefinition } from '@kbn/search-api-panels'; import { docLinks } from '../../../../common/doc_links'; import { INDEX_NAME_PLACEHOLDER } from '../../constants'; -import { LanguageDefinition, Languages } from './types'; export const rubyDefinition: LanguageDefinition = { advancedConfig: docLinks.rubyAdvancedConfig, diff --git a/x-pack/plugins/serverless_search/public/application/components/languages/utils.ts b/x-pack/plugins/serverless_search/public/application/components/languages/utils.ts new file mode 100644 index 00000000000000..84f575ea0cc02e --- /dev/null +++ b/x-pack/plugins/serverless_search/public/application/components/languages/utils.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 { LanguageDefinition, LanguageDefinitionSnippetArguments } from '@kbn/search-api-panels'; +import { consoleDefinition } from './console'; + +export const showTryInConsole = (code: keyof LanguageDefinition) => code in consoleDefinition; + +export const getCodeSnippet = ( + language: Partial, + key: keyof LanguageDefinition, + args: LanguageDefinitionSnippetArguments +): string => { + const snippetVal = language[key]; + if (snippetVal === undefined) return ''; + if (typeof snippetVal === 'string') return snippetVal; + return snippetVal(args); +}; 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 a683f648207857..79652e78e815ed 100644 --- a/x-pack/plugins/serverless_search/public/application/components/overview.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/overview.tsx @@ -19,33 +19,38 @@ import { EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { + WelcomeBanner, + IngestData, + SelectClientPanel, + OverviewPanel, + CodeBox, + LanguageClientPanel, + InstallClientPanel, +} from '@kbn/search-api-panels'; + import React, { useMemo, useState } from 'react'; +import type { + LanguageDefinition, + LanguageDefinitionSnippetArguments, +} from '@kbn/search-api-panels'; import { docLinks } from '../../../common/doc_links'; import { PLUGIN_ID } from '../../../common'; import { useKibanaServices } from '../hooks/use_kibana'; import { API_KEY_PLACEHOLDER, ELASTICSEARCH_URL_PLACEHOLDER } from '../constants'; -import { CodeBox } from './code_box'; import { javascriptDefinition } from './languages/javascript'; import { languageDefinitions } from './languages/languages'; -import { LanguageDefinition, LanguageDefinitionSnippetArguments } from './languages/types'; -import { InstallClientPanel } from './overview_panels/install_client'; -import { OverviewPanel } from './overview_panels/overview_panel'; import './overview.scss'; -import { IngestData } from './overview_panels/ingest_data'; -import { SelectClientPanel } from './overview_panels/select_client'; import { ApiKeyPanel } from './api_key/api_key'; -import { LanguageClientPanel } from './overview_panels/language_client_panel'; +import { getCodeSnippet, showTryInConsole } from './languages/utils'; export const ElasticsearchOverview = () => { const [selectedLanguage, setSelectedLanguage] = useState(javascriptDefinition); const [clientApiKey, setClientApiKey] = useState(API_KEY_PLACEHOLDER); - const { - application: { navigateToApp }, - cloud, - http, - userProfile, - } = useKibanaServices(); + const { application, cloud, http, userProfile, share } = useKibanaServices(); + const { navigateToApp } = application; + const elasticsearchURL = useMemo(() => { return cloud?.elasticsearchUrl ?? ELASTICSEARCH_URL_PLACEHOLDER; }, [cloud]); @@ -59,54 +64,19 @@ export const ElasticsearchOverview = () => { - - - {/* Reversing column direction here so screenreaders keep h1 as the first element */} - - - -

    - {i18n.translate('xpack.serverlessSearch.header.title', { - defaultMessage: 'Get started with Elasticsearch', - })} -

    -
    -
    - - -

    - {i18n.translate('xpack.serverlessSearch.header.greeting.title', { - defaultMessage: 'Hi {name}!', - values: { name: userProfile.user.full_name || userProfile.user.username }, - })} -

    -
    -
    -
    - - - {i18n.translate('xpack.serverlessSearch.header.description', { - defaultMessage: - "Set up your programming language client, ingest some data, and you'll be ready to start searching within minutes.", - })} - - -
    - - - - -
    +
    - + {languageDefinitions.map((language, index) => ( ))} @@ -115,9 +85,15 @@ export const ElasticsearchOverview = () => { @@ -147,11 +123,19 @@ export const ElasticsearchOverview = () => { })} leftPanelContent={ } links={[ @@ -195,11 +179,15 @@ export const ElasticsearchOverview = () => { })} leftPanelContent={ } links={[]} @@ -210,9 +198,16 @@ export const ElasticsearchOverview = () => { @@ -223,11 +218,19 @@ export const ElasticsearchOverview = () => { })} leftPanelContent={ } links={[]} diff --git a/x-pack/plugins/serverless_search/public/application/components/overview_panels/ingest_data.scss b/x-pack/plugins/serverless_search/public/application/components/overview_panels/ingest_data.scss deleted file mode 100644 index ff48f49cfaf9e5..00000000000000 --- a/x-pack/plugins/serverless_search/public/application/components/overview_panels/ingest_data.scss +++ /dev/null @@ -1,3 +0,0 @@ -.serverlessSearchIntegrationsPanel { - background-color: $euiColorDarkestShade; -} diff --git a/x-pack/plugins/serverless_search/public/application/components/overview_panels/install_client.tsx b/x-pack/plugins/serverless_search/public/application/components/overview_panels/install_client.tsx deleted file mode 100644 index 259644074ba124..00000000000000 --- a/x-pack/plugins/serverless_search/public/application/components/overview_panels/install_client.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiSpacer, EuiCallOut, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { CodeBox } from '../code_box'; -import { languageDefinitions } from '../languages/languages'; -import { OverviewPanel } from './overview_panel'; -import { - LanguageDefinition, - Languages, - LanguageDefinitionSnippetArguments, -} from '../languages/types'; -import { GithubLink } from '../shared/github_link'; - -interface InstallClientProps { - codeArguments: LanguageDefinitionSnippetArguments; - language: LanguageDefinition; - setSelectedLanguage: (language: LanguageDefinition) => void; -} - -const Link: React.FC<{ language: Languages }> = ({ language }) => { - switch (language) { - case Languages.CURL: - return ( - - ); - case Languages.JAVASCRIPT: - return ( - - ); - case Languages.RUBY: - return ( - - ); - } - return null; -}; - -export const InstallClientPanel: React.FC = ({ - codeArguments, - language, - setSelectedLanguage, -}) => { - return ( - - - - - - - - {i18n.translate('xpack.serverlessSearch.apiCallout.content', { - defaultMessage: - 'Console enables you to call Elasticsearch and Kibana REST APIs directly, without needing to install a language client.', - })} - - - - } - /> - ); -}; diff --git a/x-pack/plugins/serverless_search/public/application/components/overview_panels/select_client.tsx b/x-pack/plugins/serverless_search/public/application/components/overview_panels/select_client.tsx deleted file mode 100644 index 7d78a54f8e1c72..00000000000000 --- a/x-pack/plugins/serverless_search/public/application/components/overview_panels/select_client.tsx +++ /dev/null @@ -1,99 +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 { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React from 'react'; - -import { useKibanaServices } from '../../hooks/use_kibana'; -import { OverviewPanel } from './overview_panel'; -import { docLinks } from '../../../../common/doc_links'; -import './select_client.scss'; - -export const SelectClientPanel: React.FC = ({ children }) => { - const { http } = useKibanaServices(); - - return ( - - {i18n.translate('xpack.serverlessSearch.selectClient.description.console.link', { - defaultMessage: 'Console', - })} - - ), - }} - /> - } - leftPanelContent={ - <> - - - - - {i18n.translate('xpack.serverlessSearch.selectClient.heading', { - defaultMessage: 'Choose one', - })} - - - - - - - {children} - - - -

    - {i18n.translate('xpack.serverlessSearch.selectClient.callout.description', { - defaultMessage: - 'With Console, you can get started right away with our REST API’s. No installation required. ', - })} - - - - {i18n.translate('xpack.serverlessSearch.selectClient.callout.link', { - defaultMessage: 'Try Console now', - })} - - -

    -
    - - } - links={[ - { - href: docLinks.elasticsearchClients, - label: i18n.translate('xpack.serverlessSearch.selectClient.elasticsearchClientDocLink', { - defaultMessage: 'Elasticsearch clients ', - }), - }, - { - href: docLinks.kibanaRunApiInConsole, - label: i18n.translate('xpack.serverlessSearch.selectClient.apiRequestConsoleDocLink', { - defaultMessage: 'Run API requests in Console ', - }), - }, - ]} - title={i18n.translate('xpack.serverlessSearch.selectClient.title', { - defaultMessage: 'Select your client', - })} - /> - ); -}; diff --git a/x-pack/plugins/serverless_search/tsconfig.json b/x-pack/plugins/serverless_search/tsconfig.json index 91a3f465ca4c63..5e1624175f763e 100644 --- a/x-pack/plugins/serverless_search/tsconfig.json +++ b/x-pack/plugins/serverless_search/tsconfig.json @@ -30,5 +30,6 @@ "@kbn/ml-plugin", "@kbn/management-cards-navigation", "@kbn/core-elasticsearch-server", + "@kbn/search-api-panels", ] } diff --git a/x-pack/plugins/stack_alerts/server/rule_types/constants.ts b/x-pack/plugins/stack_alerts/server/rule_types/constants.ts new file mode 100644 index 00000000000000..68ab6698f2d312 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/rule_types/constants.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 const STACK_AAD_INDEX_NAME = 'stack'; diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts index b42623d91cabf6..c33457fab43b12 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.test.ts @@ -45,11 +45,19 @@ jest.mock('./lib/fetch_search_source_query', () => ({ mockFetchSearchSourceQuery(...args), })); -const scheduleActions = jest.fn(); -const replaceState = jest.fn(() => ({ scheduleActions })); -const mockCreateAlert = jest.fn(() => ({ replaceState })); const mockGetRecoveredAlerts = jest.fn().mockReturnValue([]); const mockSetLimitReached = jest.fn(); +const mockReport = jest.fn(); +const mockSetAlertData = jest.fn(); +const mockGetAlertLimitValue = jest.fn().mockReturnValue(1000); + +const mockAlertClient = { + report: mockReport, + getAlertLimitValue: mockGetAlertLimitValue, + setAlertLimitReached: mockSetLimitReached, + getRecoveredAlerts: mockGetRecoveredAlerts, + setAlertData: mockSetAlertData, +}; const mockNow = jest.getRealSystemTime(); @@ -87,16 +95,7 @@ describe('es_query executor', () => { get: () => ({ attributes: { consumer: 'alerts' } }), }, searchSourceClient: searchSourceClientMock, - alertFactory: { - create: mockCreateAlert, - alertLimit: { - getValue: jest.fn().mockReturnValue(1000), - setLimitReached: mockSetLimitReached, - }, - done: () => ({ - getRecoveredAlerts: mockGetRecoveredAlerts, - }), - }, + alertsClient: mockAlertClient, alertWithLifecycle: jest.fn(), logger, shouldWriteAlerts: () => true, @@ -210,7 +209,7 @@ describe('es_query executor', () => { params: { ...defaultProps, threshold: [500], thresholdComparator: '>=' as Comparator }, }); - expect(mockCreateAlert).not.toHaveBeenCalled(); + expect(mockReport).not.toHaveBeenCalled(); expect(mockSetLimitReached).toHaveBeenCalledTimes(1); expect(mockSetLimitReached).toHaveBeenCalledWith(false); }); @@ -237,22 +236,47 @@ describe('es_query executor', () => { params: { ...defaultProps, threshold: [200], thresholdComparator: '>=' as Comparator }, }); - expect(mockCreateAlert).toHaveBeenCalledTimes(1); - expect(mockCreateAlert).toHaveBeenNthCalledWith(1, 'query matched'); - expect(scheduleActions).toHaveBeenCalledTimes(1); - expect(scheduleActions).toHaveBeenNthCalledWith(1, 'query matched', { - conditions: 'Number of matching documents is greater than or equal to 200', - date: new Date(mockNow).toISOString(), - hits: [], - link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', - message: `rule 'test-rule-name' is active: + expect(mockReport).toHaveBeenCalledTimes(1); + expect(mockReport).toHaveBeenNthCalledWith(1, { + actionGroup: 'query matched', + context: { + conditions: 'Number of matching documents is greater than or equal to 200', + date: new Date(mockNow).toISOString(), + hits: [], + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + message: `rule 'test-rule-name' is active: - Value: 491 - Conditions Met: Number of matching documents is greater than or equal to 200 over 5m - Timestamp: ${new Date(mockNow).toISOString()} - Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, - title: "rule 'test-rule-name' matched query", - value: 491, + title: "rule 'test-rule-name' matched query", + value: 491, + }, + id: 'query matched', + state: { + dateEnd: new Date(mockNow).toISOString(), + dateStart: new Date(mockNow).toISOString(), + latestTimestamp: undefined, + }, + payload: { + kibana: { + alert: { + url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + reason: `rule 'test-rule-name' is active: + +- Value: 491 +- Conditions Met: Number of matching documents is greater than or equal to 200 over 5m +- Timestamp: ${new Date(mockNow).toISOString()} +- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, + title: "rule 'test-rule-name' matched query", + evaluation: { + conditions: 'Number of matching documents is greater than or equal to 200', + value: 491, + }, + }, + }, + }, }); expect(mockSetLimitReached).toHaveBeenCalledTimes(1); expect(mockSetLimitReached).toHaveBeenCalledWith(false); @@ -297,55 +321,135 @@ describe('es_query executor', () => { }, }); - expect(mockCreateAlert).toHaveBeenCalledTimes(3); - expect(mockCreateAlert).toHaveBeenNthCalledWith(1, 'host-1'); - expect(mockCreateAlert).toHaveBeenNthCalledWith(2, 'host-2'); - expect(mockCreateAlert).toHaveBeenNthCalledWith(3, 'host-3'); - expect(scheduleActions).toHaveBeenCalledTimes(3); - expect(scheduleActions).toHaveBeenNthCalledWith(1, 'query matched', { - conditions: - 'Number of matching documents for group "host-1" is greater than or equal to 200', - date: new Date(mockNow).toISOString(), - hits: [], - link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', - message: `rule 'test-rule-name' is active: + expect(mockReport).toHaveBeenCalledTimes(3); + expect(mockReport).toHaveBeenNthCalledWith(1, { + actionGroup: 'query matched', + context: { + conditions: + 'Number of matching documents for group "host-1" is greater than or equal to 200', + date: new Date(mockNow).toISOString(), + hits: [], + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + message: `rule 'test-rule-name' is active: - Value: 291 - Conditions Met: Number of matching documents for group "host-1" is greater than or equal to 200 over 5m - Timestamp: ${new Date(mockNow).toISOString()} - Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, - title: "rule 'test-rule-name' matched query for group host-1", - value: 291, + title: "rule 'test-rule-name' matched query for group host-1", + value: 291, + }, + id: 'host-1', + state: { + dateEnd: new Date(mockNow).toISOString(), + dateStart: new Date(mockNow).toISOString(), + latestTimestamp: undefined, + }, + payload: { + kibana: { + alert: { + url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + reason: `rule 'test-rule-name' is active: + +- Value: 291 +- Conditions Met: Number of matching documents for group "host-1" is greater than or equal to 200 over 5m +- Timestamp: ${new Date(mockNow).toISOString()} +- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, + title: "rule 'test-rule-name' matched query for group host-1", + evaluation: { + conditions: + 'Number of matching documents for group "host-1" is greater than or equal to 200', + value: 291, + }, + }, + }, + }, }); - expect(scheduleActions).toHaveBeenNthCalledWith(2, 'query matched', { - conditions: - 'Number of matching documents for group "host-2" is greater than or equal to 200', - date: new Date(mockNow).toISOString(), - hits: [], - link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', - message: `rule 'test-rule-name' is active: + expect(mockReport).toHaveBeenNthCalledWith(2, { + actionGroup: 'query matched', + context: { + conditions: + 'Number of matching documents for group "host-2" is greater than or equal to 200', + date: new Date(mockNow).toISOString(), + hits: [], + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + message: `rule 'test-rule-name' is active: + +- Value: 477 +- Conditions Met: Number of matching documents for group "host-2" is greater than or equal to 200 over 5m +- Timestamp: ${new Date(mockNow).toISOString()} +- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, + title: "rule 'test-rule-name' matched query for group host-2", + value: 477, + }, + id: 'host-2', + state: { + dateEnd: new Date(mockNow).toISOString(), + dateStart: new Date(mockNow).toISOString(), + latestTimestamp: undefined, + }, + payload: { + kibana: { + alert: { + url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + reason: `rule 'test-rule-name' is active: - Value: 477 - Conditions Met: Number of matching documents for group "host-2" is greater than or equal to 200 over 5m - Timestamp: ${new Date(mockNow).toISOString()} - Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, - title: "rule 'test-rule-name' matched query for group host-2", - value: 477, + title: "rule 'test-rule-name' matched query for group host-2", + evaluation: { + conditions: + 'Number of matching documents for group "host-2" is greater than or equal to 200', + value: 477, + }, + }, + }, + }, }); - expect(scheduleActions).toHaveBeenNthCalledWith(3, 'query matched', { - conditions: - 'Number of matching documents for group "host-3" is greater than or equal to 200', - date: new Date(mockNow).toISOString(), - hits: [], - link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', - message: `rule 'test-rule-name' is active: + expect(mockReport).toHaveBeenNthCalledWith(3, { + actionGroup: 'query matched', + context: { + conditions: + 'Number of matching documents for group "host-3" is greater than or equal to 200', + date: new Date(mockNow).toISOString(), + hits: [], + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + message: `rule 'test-rule-name' is active: - Value: 999 - Conditions Met: Number of matching documents for group "host-3" is greater than or equal to 200 over 5m - Timestamp: ${new Date(mockNow).toISOString()} - Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, - title: "rule 'test-rule-name' matched query for group host-3", - value: 999, + title: "rule 'test-rule-name' matched query for group host-3", + value: 999, + }, + id: 'host-3', + state: { + dateEnd: new Date(mockNow).toISOString(), + dateStart: new Date(mockNow).toISOString(), + latestTimestamp: undefined, + }, + payload: { + kibana: { + alert: { + url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + reason: `rule 'test-rule-name' is active: + +- Value: 999 +- Conditions Met: Number of matching documents for group \"host-3\" is greater than or equal to 200 over 5m +- Timestamp: ${new Date(mockNow).toISOString()} +- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, + title: "rule 'test-rule-name' matched query for group host-3", + evaluation: { + conditions: + 'Number of matching documents for group "host-3" is greater than or equal to 200', + value: 999, + }, + }, + }, + }, }); expect(mockSetLimitReached).toHaveBeenCalledTimes(1); expect(mockSetLimitReached).toHaveBeenCalledWith(false); @@ -389,21 +493,20 @@ describe('es_query executor', () => { }, }); - expect(mockCreateAlert).toHaveBeenCalledTimes(3); - expect(mockCreateAlert).toHaveBeenNthCalledWith(1, 'host-1'); - expect(mockCreateAlert).toHaveBeenNthCalledWith(2, 'host-2'); - expect(mockCreateAlert).toHaveBeenNthCalledWith(3, 'host-3'); - expect(scheduleActions).toHaveBeenCalledTimes(3); + expect(mockReport).toHaveBeenCalledTimes(3); + expect(mockReport).toHaveBeenNthCalledWith(1, expect.objectContaining({ id: 'host-1' })); + expect(mockReport).toHaveBeenNthCalledWith(2, expect.objectContaining({ id: 'host-2' })); + expect(mockReport).toHaveBeenNthCalledWith(3, expect.objectContaining({ id: 'host-3' })); expect(mockSetLimitReached).toHaveBeenCalledTimes(1); expect(mockSetLimitReached).toHaveBeenCalledWith(true); }); it('should correctly handle recovered alerts for ungrouped alert', async () => { - const mockSetContext = jest.fn(); mockGetRecoveredAlerts.mockReturnValueOnce([ { - getId: () => 'query matched', - setContext: mockSetContext, + alert: { + getId: () => 'query matched', + }, }, ]); mockFetchEsQuery.mockResolvedValueOnce({ @@ -427,36 +530,58 @@ describe('es_query executor', () => { params: { ...defaultProps, threshold: [500], thresholdComparator: '>=' as Comparator }, }); - expect(mockCreateAlert).not.toHaveBeenCalled(); - expect(mockSetContext).toHaveBeenCalledTimes(1); - expect(mockSetContext).toHaveBeenNthCalledWith(1, { - conditions: 'Number of matching documents is NOT greater than or equal to 500', - date: new Date(mockNow).toISOString(), - hits: [], - link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', - message: `rule 'test-rule-name' is recovered: + expect(mockReport).not.toHaveBeenCalled(); + expect(mockSetAlertData).toHaveBeenCalledTimes(1); + expect(mockSetAlertData).toHaveBeenNthCalledWith(1, { + id: 'query matched', + context: { + conditions: 'Number of matching documents is NOT greater than or equal to 500', + date: new Date(mockNow).toISOString(), + hits: [], + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + message: `rule 'test-rule-name' is recovered: - Value: 0 - Conditions Met: Number of matching documents is NOT greater than or equal to 500 over 5m - Timestamp: ${new Date(mockNow).toISOString()} - Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, - title: "rule 'test-rule-name' recovered", - value: 0, + title: "rule 'test-rule-name' recovered", + value: 0, + }, + payload: { + kibana: { + alert: { + evaluation: { + conditions: 'Number of matching documents is NOT greater than or equal to 500', + value: 0, + }, + reason: `rule 'test-rule-name' is recovered: + +- Value: 0 +- Conditions Met: Number of matching documents is NOT greater than or equal to 500 over 5m +- Timestamp: ${new Date(mockNow).toISOString()} +- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, + title: "rule 'test-rule-name' recovered", + url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + }, + }, + }, }); expect(mockSetLimitReached).toHaveBeenCalledTimes(1); expect(mockSetLimitReached).toHaveBeenCalledWith(false); }); it('should correctly handle recovered alerts for grouped alerts', async () => { - const mockSetContext = jest.fn(); mockGetRecoveredAlerts.mockReturnValueOnce([ { - getId: () => 'host-1', - setContext: mockSetContext, + alert: { + getId: () => 'host-1', + }, }, { - getId: () => 'host-2', - setContext: mockSetContext, + alert: { + getId: () => 'host-2', + }, }, ]); mockFetchEsQuery.mockResolvedValueOnce({ @@ -478,35 +603,79 @@ describe('es_query executor', () => { }, }); - expect(mockCreateAlert).not.toHaveBeenCalled(); - expect(mockSetContext).toHaveBeenCalledTimes(2); - expect(mockSetContext).toHaveBeenNthCalledWith(1, { - conditions: `Number of matching documents for group "host-1" is NOT greater than or equal to 200`, - date: new Date(mockNow).toISOString(), - hits: [], - link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', - message: `rule 'test-rule-name' is recovered: + expect(mockReport).not.toHaveBeenCalled(); + expect(mockSetAlertData).toHaveBeenCalledTimes(2); + expect(mockSetAlertData).toHaveBeenNthCalledWith(1, { + id: 'host-1', + context: { + conditions: `Number of matching documents for group "host-1" is NOT greater than or equal to 200`, + date: new Date(mockNow).toISOString(), + hits: [], + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + message: `rule 'test-rule-name' is recovered: - Value: 0 - Conditions Met: Number of matching documents for group "host-1" is NOT greater than or equal to 200 over 5m - Timestamp: ${new Date(mockNow).toISOString()} - Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, - title: "rule 'test-rule-name' recovered", - value: 0, + title: "rule 'test-rule-name' recovered", + value: 0, + }, + payload: { + kibana: { + alert: { + evaluation: { + conditions: + 'Number of matching documents for group "host-1" is NOT greater than or equal to 200', + value: 0, + }, + reason: `rule 'test-rule-name' is recovered: + +- Value: 0 +- Conditions Met: Number of matching documents for group \"host-1\" is NOT greater than or equal to 200 over 5m +- Timestamp: ${new Date(mockNow).toISOString()} +- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, + title: "rule 'test-rule-name' recovered", + url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + }, + }, + }, }); - expect(mockSetContext).toHaveBeenNthCalledWith(2, { - conditions: `Number of matching documents for group "host-2" is NOT greater than or equal to 200`, - date: new Date(mockNow).toISOString(), - hits: [], - link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', - message: `rule 'test-rule-name' is recovered: + expect(mockSetAlertData).toHaveBeenNthCalledWith(2, { + id: 'host-2', + context: { + conditions: `Number of matching documents for group "host-2" is NOT greater than or equal to 200`, + date: new Date(mockNow).toISOString(), + hits: [], + link: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + message: `rule 'test-rule-name' is recovered: - Value: 0 - Conditions Met: Number of matching documents for group "host-2" is NOT greater than or equal to 200 over 5m - Timestamp: ${new Date(mockNow).toISOString()} - Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, - title: "rule 'test-rule-name' recovered", - value: 0, + title: "rule 'test-rule-name' recovered", + value: 0, + }, + payload: { + kibana: { + alert: { + evaluation: { + conditions: + 'Number of matching documents for group "host-2" is NOT greater than or equal to 200', + value: 0, + }, + reason: `rule 'test-rule-name' is recovered: + +- Value: 0 +- Conditions Met: Number of matching documents for group \"host-2\" is NOT greater than or equal to 200 over 5m +- Timestamp: ${new Date(mockNow).toISOString()} +- Link: https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id`, + title: "rule 'test-rule-name' recovered", + url: 'https://localhost:5601/app/management/insightsAndAlerting/triggersActions/rule/test-rule-id', + }, + }, + }, }); expect(mockSetLimitReached).toHaveBeenCalledTimes(1); expect(mockSetLimitReached).toHaveBeenCalledWith(false); diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts index 1b0f0437b74d8c..ae8ae99ba26a26 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/executor.ts @@ -9,6 +9,10 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup } from '@kbn/core/server'; import { parseDuration } from '@kbn/alerting-plugin/server'; import { isGroupAggregation, UngroupedGroupId } from '@kbn/triggers-actions-ui-plugin/common'; +import { ALERT_EVALUATION_VALUE, ALERT_REASON, ALERT_URL } from '@kbn/rule-data-utils'; + +import { expandFlattenedAlert } from '@kbn/alerting-plugin/server/alerts_client/lib'; +import { ALERT_TITLE, ALERT_EVALUATION_CONDITIONS } from './fields'; import { ComparatorFns } from '../../../common'; import { addMessages, @@ -32,11 +36,11 @@ export async function executor(core: CoreSetup, options: ExecutorOptions { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(mockNow); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + afterAll(() => { + jest.useRealTimers(); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('rule type creation structure is the expected value', async () => { expect(ruleType.id).toBe('.es-query'); expect(ruleType.name).toBe('Elasticsearch query'); @@ -168,7 +179,7 @@ describe('ruleType', () => { const result = await invokeExecutor({ params, ruleServices }); - expect(ruleServices.alertFactory.create).not.toHaveBeenCalled(); + expect(ruleServices.alertsClient.report).not.toHaveBeenCalled(); expect(result).toMatchInlineSnapshot(` Object { @@ -215,13 +226,17 @@ describe('ruleType', () => { const result = await invokeExecutor({ params, ruleServices }); - expect(ruleServices.alertFactory.create).toHaveBeenCalledWith(ConditionMetAlertInstanceId); - const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: undefined, - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); + expect(ruleServices.alertsClient.report).toHaveBeenCalledWith( + expect.objectContaining({ + id: ConditionMetAlertInstanceId, + actionGroup: ActionGroupId, + state: { + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }, + }) + ); expect(result).toMatchObject({ state: { @@ -269,13 +284,18 @@ describe('ruleType', () => { }, }); - const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - // ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward - latestTimestamp: new Date(previousTimestamp).toISOString(), - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); + expect(ruleServices.alertsClient.report).toHaveBeenCalledWith( + expect.objectContaining({ + id: ConditionMetAlertInstanceId, + actionGroup: ActionGroupId, + state: { + // ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward + latestTimestamp: new Date(previousTimestamp).toISOString(), + dateStart: expect.any(String), + dateEnd: expect.any(String), + }, + }) + ); expect(result).toMatchObject({ state: { @@ -318,12 +338,17 @@ describe('ruleType', () => { const result = await invokeExecutor({ params, ruleServices }); - const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: undefined, - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); + expect(ruleServices.alertsClient.report).toHaveBeenCalledWith( + expect.objectContaining({ + id: ConditionMetAlertInstanceId, + actionGroup: ActionGroupId, + state: { + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }, + }) + ); expect(result).toMatchObject({ state: { @@ -363,12 +388,17 @@ describe('ruleType', () => { const result = await invokeExecutor({ params, ruleServices }); - const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: undefined, - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); + expect(ruleServices.alertsClient.report).toHaveBeenCalledWith( + expect.objectContaining({ + id: ConditionMetAlertInstanceId, + actionGroup: ActionGroupId, + state: { + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }, + }) + ); expect(result?.state).toMatchObject({ latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), @@ -394,13 +424,17 @@ describe('ruleType', () => { state: result?.state as EsQueryRuleState, }); - const existingInstance: AlertInstanceMock = - ruleServices.alertFactory.create.mock.results[1].value; - expect(existingInstance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); + expect(ruleServices.alertsClient.report).toHaveBeenCalledWith( + expect.objectContaining({ + id: ConditionMetAlertInstanceId, + actionGroup: ActionGroupId, + state: { + latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), + dateStart: expect.any(String), + dateEnd: expect.any(String), + }, + }) + ); expect(secondResult).toMatchObject({ state: { @@ -446,12 +480,17 @@ describe('ruleType', () => { const result = await invokeExecutor({ params, ruleServices }); - const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: undefined, - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); + expect(ruleServices.alertsClient.report).toHaveBeenCalledWith( + expect.objectContaining({ + id: ConditionMetAlertInstanceId, + actionGroup: ActionGroupId, + state: { + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }, + }) + ); expect(result).toMatchObject({ state: { @@ -498,12 +537,17 @@ describe('ruleType', () => { const result = await invokeExecutor({ params, ruleServices }); - const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: undefined, - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); + expect(ruleServices.alertsClient.report).toHaveBeenCalledWith( + expect.objectContaining({ + id: ConditionMetAlertInstanceId, + actionGroup: ActionGroupId, + state: { + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }, + }) + ); expect(result).toMatchObject({ state: { @@ -599,7 +643,7 @@ describe('ruleType', () => { await invokeExecutor({ params, ruleServices }); - expect(ruleServices.alertFactory.create).not.toHaveBeenCalled(); + expect(ruleServices.alertsClient.report).not.toHaveBeenCalled(); }); it('rule executor throws an error when index does not have time field', async () => { @@ -637,10 +681,33 @@ describe('ruleType', () => { hits: { total: 3, hits: [{}, {}, {}] }, }); - await invokeExecutor({ params, ruleServices }); + await invokeExecutor({ + params, + ruleServices, + state: { latestTimestamp: new Date(mockNow).toISOString(), dateStart: '', dateEnd: '' }, + }); - const instance: AlertInstanceMock = ruleServices.alertFactory.create.mock.results[0].value; - expect(instance.scheduleActions).toHaveBeenCalled(); + expect(ruleServices.alertsClient.report).toHaveBeenCalledTimes(1); + + expect(ruleServices.alertsClient.report).toHaveBeenCalledWith( + expect.objectContaining({ + actionGroup: 'query matched', + id: 'query matched', + payload: expect.objectContaining({ + kibana: { + alert: { + url: expect.any(String), + reason: expect.any(String), + title: "rule 'rule-name' matched query", + evaluation: { + conditions: 'Number of matching documents is greater than or equal to 3', + value: 3, + }, + }, + }, + }), + }) + ); }); }); }); @@ -711,7 +778,7 @@ async function invokeExecutor({ spaceId: uuidv4(), rule: { id: uuidv4(), - name: uuidv4(), + name: 'rule-name', tags: [], consumer: '', producer: '', diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts index ef1008f360c8c0..01d6ee1497b3cc 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts @@ -8,6 +8,11 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup } from '@kbn/core/server'; import { extractReferences, injectReferences } from '@kbn/data-plugin/common'; +import { IRuleTypeAlerts } from '@kbn/alerting-plugin/server'; +import { ALERT_EVALUATION_VALUE } from '@kbn/rule-data-utils'; +import { StackAlert } from '@kbn/alerts-as-data-utils'; +import { STACK_AAD_INDEX_NAME } from '..'; +import { ALERT_TITLE, ALERT_EVALUATION_CONDITIONS } from './fields'; import { RuleType } from '../../types'; import { ActionContext } from './action_context'; import { @@ -30,7 +35,9 @@ export function getRuleType( EsQueryRuleState, {}, ActionContext, - typeof ActionGroupId + typeof ActionGroupId, + never, + StackAlert > { const ruleTypeName = i18n.translate('xpack.stackAlerts.esQuery.alertTypeTitle', { defaultMessage: 'Elasticsearch query', @@ -135,6 +142,18 @@ export function getRuleType( } ); + const alerts: IRuleTypeAlerts = { + context: STACK_AAD_INDEX_NAME, + mappings: { + fieldMap: { + [ALERT_TITLE]: { type: 'keyword', array: false, required: false }, + [ALERT_EVALUATION_CONDITIONS]: { type: 'keyword', array: false, required: false }, + [ALERT_EVALUATION_VALUE]: { type: 'keyword', array: false, required: false }, + }, + }, + shouldWrite: true, + }; + return { id: ES_QUERY_ID, name: ruleTypeName, @@ -188,5 +207,6 @@ export function getRuleType( }, producer: STACK_ALERTS_FEATURE_ID, doesSetRecoveryContext: true, + alerts, }; } diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts index c8844b19a678ad..b20f52f03ebe58 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { StackAlert } from '@kbn/alerts-as-data-utils'; import { RuleExecutorOptions, RuleTypeParams } from '../../types'; import { ActionContext } from './action_context'; import { EsQueryRuleParams, EsQueryRuleState } from './rule_type_params'; @@ -24,5 +25,6 @@ export type ExecutorOptions

    = RuleExecutorOptions< EsQueryRuleState, {}, ActionContext, - typeof ActionGroupId + typeof ActionGroupId, + StackAlert >; diff --git a/x-pack/plugins/stack_alerts/server/rule_types/index.ts b/x-pack/plugins/stack_alerts/server/rule_types/index.ts index 5bc7f4cc1c7d52..0bd47f6c2572a7 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/index.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/index.ts @@ -10,6 +10,8 @@ import { register as registerIndexThreshold } from './index_threshold'; import { register as registerGeoContainment } from './geo_containment'; import { register as registerEsQuery } from './es_query'; +export * from './constants'; + export function registerBuiltInRuleTypes(params: RegisterRuleTypesParams) { registerIndexThreshold(params); registerGeoContainment(params); diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json index 81e9d5b57bcf85..207e883aa89028 100644 --- a/x-pack/plugins/stack_alerts/tsconfig.json +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -42,6 +42,8 @@ "@kbn/logging-mocks", "@kbn/share-plugin", "@kbn/discover-plugin", + "@kbn/rule-data-utils", + "@kbn/alerts-as-data-utils", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index 9782d6ae08dbfb..c196a334931ba0 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -23,6 +23,7 @@ describe('config validation', () => { }, "max_attempts": 3, "max_workers": 10, + "metrics_reset_interval": 30000, "monitored_aggregated_stats_refresh_rate": 60000, "monitored_stats_health_verbose_log": Object { "enabled": false, @@ -81,6 +82,7 @@ describe('config validation', () => { }, "max_attempts": 3, "max_workers": 10, + "metrics_reset_interval": 30000, "monitored_aggregated_stats_refresh_rate": 60000, "monitored_stats_health_verbose_log": Object { "enabled": false, @@ -137,6 +139,7 @@ describe('config validation', () => { }, "max_attempts": 3, "max_workers": 10, + "metrics_reset_interval": 30000, "monitored_aggregated_stats_refresh_rate": 60000, "monitored_stats_health_verbose_log": Object { "enabled": false, diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index c2d4940d36450c..490d25a7bdfb0e 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -20,6 +20,8 @@ export const DEFAULT_MONITORING_REFRESH_RATE = 60 * 1000; export const DEFAULT_MONITORING_STATS_RUNNING_AVERAGE_WINDOW = 50; export const DEFAULT_MONITORING_STATS_WARN_DELAYED_TASK_START_IN_SECONDS = 60; +export const DEFAULT_METRICS_RESET_INTERVAL = 30 * 1000; // 30 seconds + // At the default poll interval of 3sec, this averages over the last 15sec. export const DEFAULT_WORKER_UTILIZATION_RUNNING_AVERAGE_WINDOW = 5; @@ -52,46 +54,40 @@ const eventLoopDelaySchema = schema.object({ }); const requeueInvalidTasksConfig = schema.object({ - enabled: schema.boolean({ defaultValue: false }), delay: schema.number({ defaultValue: 3000, min: 0 }), + enabled: schema.boolean({ defaultValue: false }), max_attempts: schema.number({ defaultValue: 100, min: 1, max: 500 }), }); export const configSchema = schema.object( { + allow_reading_invalid_state: schema.boolean({ defaultValue: true }), + ephemeral_tasks: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + /* How many requests can Task Manager buffer before it rejects new requests. */ + request_capacity: schema.number({ + // a nice round contrived number, feel free to change as we learn how it behaves + defaultValue: 10, + min: 1, + max: DEFAULT_MAX_EPHEMERAL_REQUEST_CAPACITY, + }), + }), + event_loop_delay: eventLoopDelaySchema, /* The maximum number of times a task will be attempted before being abandoned as failed */ max_attempts: schema.number({ defaultValue: 3, min: 1, }), - /* How often, in milliseconds, the task manager will look for more work. */ - poll_interval: schema.number({ - defaultValue: DEFAULT_POLL_INTERVAL, - min: 100, - }), - /* How many requests can Task Manager buffer before it rejects new requests. */ - request_capacity: schema.number({ - // a nice round contrived number, feel free to change as we learn how it behaves - defaultValue: 1000, - min: 1, - }), /* The maximum number of tasks that this Kibana instance will run simultaneously. */ max_workers: schema.number({ defaultValue: DEFAULT_MAX_WORKERS, // disable the task manager rather than trying to specify it with 0 workers min: 1, }), - /* The threshold percenatge for workers experiencing version conflicts for shifting the polling interval. */ - version_conflict_threshold: schema.number({ - defaultValue: DEFAULT_VERSION_CONFLICT_THRESHOLD, - min: 50, - max: 100, - }), - /* The rate at which we emit fresh monitored stats. By default we'll use the poll_interval (+ a slight buffer) */ - monitored_stats_required_freshness: schema.number({ - defaultValue: (config?: unknown) => - ((config as { poll_interval: number })?.poll_interval ?? DEFAULT_POLL_INTERVAL) + 1000, - min: 100, + /* The interval at which monotonically increasing metrics counters will reset */ + metrics_reset_interval: schema.number({ + defaultValue: DEFAULT_METRICS_RESET_INTERVAL, + min: 10 * 1000, // minimum 10 seconds }), /* The rate at which we refresh monitored stats that require aggregation queries against ES. */ monitored_aggregated_stats_refresh_rate: schema.number({ @@ -99,6 +95,22 @@ export const configSchema = schema.object( /* don't run monitored stat aggregations any faster than once every 5 seconds */ min: 5000, }), + monitored_stats_health_verbose_log: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + level: schema.oneOf([schema.literal('debug'), schema.literal('info')], { + defaultValue: 'debug', + }), + /* The amount of seconds we allow a task to delay before printing a warning server log */ + warn_delayed_task_start_in_seconds: schema.number({ + defaultValue: DEFAULT_MONITORING_STATS_WARN_DELAYED_TASK_START_IN_SECONDS, + }), + }), + /* The rate at which we emit fresh monitored stats. By default we'll use the poll_interval (+ a slight buffer) */ + monitored_stats_required_freshness: schema.number({ + defaultValue: (config?: unknown) => + ((config as { poll_interval: number })?.poll_interval ?? DEFAULT_POLL_INTERVAL) + 1000, + min: 100, + }), /* The size of the running average window for monitored stats. */ monitored_stats_running_average_window: schema.number({ defaultValue: DEFAULT_MONITORING_STATS_RUNNING_AVERAGE_WINDOW, @@ -107,44 +119,39 @@ export const configSchema = schema.object( }), /* Task Execution result warn & error thresholds. */ monitored_task_execution_thresholds: schema.object({ - default: taskExecutionFailureThresholdSchema, custom: schema.recordOf(schema.string(), taskExecutionFailureThresholdSchema, { defaultValue: {}, }), + default: taskExecutionFailureThresholdSchema, }), - monitored_stats_health_verbose_log: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - level: schema.oneOf([schema.literal('debug'), schema.literal('info')], { - defaultValue: 'debug', - }), - /* The amount of seconds we allow a task to delay before printing a warning server log */ - warn_delayed_task_start_in_seconds: schema.number({ - defaultValue: DEFAULT_MONITORING_STATS_WARN_DELAYED_TASK_START_IN_SECONDS, - }), - }), - ephemeral_tasks: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - /* How many requests can Task Manager buffer before it rejects new requests. */ - request_capacity: schema.number({ - // a nice round contrived number, feel free to change as we learn how it behaves - defaultValue: 10, - min: 1, - max: DEFAULT_MAX_EPHEMERAL_REQUEST_CAPACITY, - }), + /* How often, in milliseconds, the task manager will look for more work. */ + poll_interval: schema.number({ + defaultValue: DEFAULT_POLL_INTERVAL, + min: 100, }), - event_loop_delay: eventLoopDelaySchema, - worker_utilization_running_average_window: schema.number({ - defaultValue: DEFAULT_WORKER_UTILIZATION_RUNNING_AVERAGE_WINDOW, - max: 100, + /* How many requests can Task Manager buffer before it rejects new requests. */ + request_capacity: schema.number({ + // a nice round contrived number, feel free to change as we learn how it behaves + defaultValue: 1000, min: 1, }), + requeue_invalid_tasks: requeueInvalidTasksConfig, /* These are not designed to be used by most users. Please use caution when changing these */ unsafe: schema.object({ - exclude_task_types: schema.arrayOf(schema.string(), { defaultValue: [] }), authenticate_background_task_utilization: schema.boolean({ defaultValue: true }), + exclude_task_types: schema.arrayOf(schema.string(), { defaultValue: [] }), + }), + /* The threshold percenatge for workers experiencing version conflicts for shifting the polling interval. */ + version_conflict_threshold: schema.number({ + defaultValue: DEFAULT_VERSION_CONFLICT_THRESHOLD, + min: 50, + max: 100, + }), + worker_utilization_running_average_window: schema.number({ + defaultValue: DEFAULT_WORKER_UTILIZATION_RUNNING_AVERAGE_WINDOW, + max: 100, + min: 1, }), - requeue_invalid_tasks: requeueInvalidTasksConfig, - allow_reading_invalid_state: schema.boolean({ defaultValue: true }), }, { validate: (config) => { 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 863b5d986d3da2..6a06ea93f3dcb4 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 @@ -84,6 +84,7 @@ describe('EphemeralTaskLifecycle', () => { delay: 3000, max_attempts: 20, }, + metrics_reset_interval: 3000, ...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 e2d290d256ec25..f034feb1544625 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 @@ -79,6 +79,7 @@ describe('managed configuration', () => { delay: 3000, max_attempts: 20, }, + metrics_reset_interval: 3000, }); logger = context.logger.get('taskManager'); diff --git a/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts b/x-pack/plugins/task_manager/server/lib/runtime_statistics_aggregator.ts similarity index 100% rename from x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts rename to x-pack/plugins/task_manager/server/lib/runtime_statistics_aggregator.ts 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 new file mode 100644 index 00000000000000..96716983294473 --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts @@ -0,0 +1,1070 @@ +/* + * 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 sinon from 'sinon'; +import { Subject, Observable } from 'rxjs'; +import { take, bufferCount, skip } from 'rxjs/operators'; +import { isTaskPollingCycleEvent, isTaskRunEvent } from '../task_events'; +import { TaskLifecycleEvent } from '../polling_lifecycle'; +import { AggregatedStat } from '../lib/runtime_statistics_aggregator'; +import { taskPollingLifecycleMock } from '../polling_lifecycle.mock'; +import { TaskManagerConfig } from '../config'; +import { createAggregator } from './create_aggregator'; +import { TaskClaimMetric, TaskClaimMetricsAggregator } from './task_claim_metrics_aggregator'; +import { taskClaimFailureEvent, taskClaimSuccessEvent } from './task_claim_metrics_aggregator.test'; +import { getTaskRunFailedEvent, getTaskRunSuccessEvent } from './task_run_metrics_aggregator.test'; +import { TaskRunMetric, TaskRunMetricsAggregator } from './task_run_metrics_aggregator'; +import * as TaskClaimMetricsAggregatorModule from './task_claim_metrics_aggregator'; +import { metricsAggregatorMock } from './metrics_aggregator.mock'; + +const mockMetricsAggregator = metricsAggregatorMock.create(); +const config: TaskManagerConfig = { + allow_reading_invalid_state: false, + ephemeral_tasks: { + enabled: true, + request_capacity: 10, + }, + event_loop_delay: { + monitor: true, + warn_threshold: 5000, + }, + max_attempts: 9, + max_workers: 10, + metrics_reset_interval: 30000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_health_verbose_log: { + enabled: false, + level: 'debug' as const, + warn_delayed_task_start_in_seconds: 60, + }, + monitored_stats_required_freshness: 6000000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + custom: {}, + default: { + error_threshold: 90, + warn_threshold: 80, + }, + }, + poll_interval: 6000000, + request_capacity: 1000, + requeue_invalid_tasks: { + enabled: false, + delay: 3000, + max_attempts: 20, + }, + unsafe: { + authenticate_background_task_utilization: true, + exclude_task_types: [], + }, + version_conflict_threshold: 80, + worker_utilization_running_average_window: 5, +}; + +describe('createAggregator', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('with TaskClaimMetricsAggregator', () => { + test('returns a cumulative count of successful polling cycles and total polling cycles', async () => { + const pollingCycleEvents = [ + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + ]; + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const taskClaimAggregator = createAggregator({ + key: 'task_claim', + taskPollingLifecycle, + config, + resetMetrics$: new Subject(), + taskEventFilter: (taskEvent: TaskLifecycleEvent) => isTaskPollingCycleEvent(taskEvent), + metricsAggregator: new TaskClaimMetricsAggregator(), + }); + + return new Promise((resolve) => { + taskClaimAggregator + .pipe( + // skip initial metric which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + take(pollingCycleEvents.length), + bufferCount(pollingCycleEvents.length) + ) + .subscribe((metrics: Array>) => { + expect(metrics[0]).toEqual({ + key: 'task_claim', + value: { success: 1, total: 1, duration: { counts: [1], values: [100] } }, + }); + expect(metrics[1]).toEqual({ + key: 'task_claim', + value: { success: 2, total: 2, duration: { counts: [2], values: [100] } }, + }); + expect(metrics[2]).toEqual({ + key: 'task_claim', + value: { success: 3, total: 3, duration: { counts: [3], values: [100] } }, + }); + expect(metrics[3]).toEqual({ + key: 'task_claim', + value: { success: 4, total: 4, duration: { counts: [4], values: [100] } }, + }); + expect(metrics[4]).toEqual({ + key: 'task_claim', + value: { success: 4, total: 5, duration: { counts: [4], values: [100] } }, + }); + expect(metrics[5]).toEqual({ + key: 'task_claim', + value: { success: 5, total: 6, duration: { counts: [5], values: [100] } }, + }); + expect(metrics[6]).toEqual({ + key: 'task_claim', + value: { success: 6, total: 7, duration: { counts: [6], values: [100] } }, + }); + expect(metrics[7]).toEqual({ + key: 'task_claim', + value: { success: 7, total: 8, duration: { counts: [7], values: [100] } }, + }); + expect(metrics[8]).toEqual({ + key: 'task_claim', + value: { success: 8, total: 9, duration: { counts: [8], values: [100] } }, + }); + expect(metrics[9]).toEqual({ + key: 'task_claim', + value: { success: 8, total: 10, duration: { counts: [8], values: [100] } }, + }); + expect(metrics[10]).toEqual({ + key: 'task_claim', + value: { success: 9, total: 11, duration: { counts: [9], values: [100] } }, + }); + resolve(); + }); + + for (const event of pollingCycleEvents) { + events$.next(event); + } + }); + }); + + test('resets count when resetMetric$ event is received', async () => { + const resetMetrics$ = new Subject(); + const pollingCycleEvents1 = [ + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + ]; + + const pollingCycleEvents2 = [ + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + ]; + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const taskClaimAggregator = createAggregator({ + key: 'task_claim', + taskPollingLifecycle, + config, + resetMetrics$, + taskEventFilter: (taskEvent: TaskLifecycleEvent) => isTaskPollingCycleEvent(taskEvent), + metricsAggregator: new TaskClaimMetricsAggregator(), + }); + + return new Promise((resolve) => { + taskClaimAggregator + .pipe( + // skip initial metric which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + take(pollingCycleEvents1.length + pollingCycleEvents2.length), + bufferCount(pollingCycleEvents1.length + pollingCycleEvents2.length) + ) + .subscribe((metrics: Array>) => { + expect(metrics[0]).toEqual({ + key: 'task_claim', + value: { success: 1, total: 1, duration: { counts: [1], values: [100] } }, + }); + expect(metrics[1]).toEqual({ + key: 'task_claim', + value: { success: 2, total: 2, duration: { counts: [2], values: [100] } }, + }); + expect(metrics[2]).toEqual({ + key: 'task_claim', + value: { success: 3, total: 3, duration: { counts: [3], values: [100] } }, + }); + expect(metrics[3]).toEqual({ + key: 'task_claim', + value: { success: 4, total: 4, duration: { counts: [4], values: [100] } }, + }); + expect(metrics[4]).toEqual({ + key: 'task_claim', + value: { success: 4, total: 5, duration: { counts: [4], values: [100] } }, + }); + expect(metrics[5]).toEqual({ + key: 'task_claim', + value: { success: 5, total: 6, duration: { counts: [5], values: [100] } }, + }); + // reset event should have been received here + expect(metrics[6]).toEqual({ + key: 'task_claim', + value: { success: 1, total: 1, duration: { counts: [1], values: [100] } }, + }); + expect(metrics[7]).toEqual({ + key: 'task_claim', + value: { success: 1, total: 2, duration: { counts: [1], values: [100] } }, + }); + expect(metrics[8]).toEqual({ + key: 'task_claim', + value: { success: 1, total: 3, duration: { counts: [1], values: [100] } }, + }); + expect(metrics[9]).toEqual({ + key: 'task_claim', + value: { success: 2, total: 4, duration: { counts: [2], values: [100] } }, + }); + expect(metrics[10]).toEqual({ + key: 'task_claim', + value: { success: 3, total: 5, duration: { counts: [3], values: [100] } }, + }); + resolve(); + }); + + for (const event of pollingCycleEvents1) { + events$.next(event); + } + resetMetrics$.next(true); + for (const event of pollingCycleEvents2) { + events$.next(event); + } + }); + }); + + test('resets count when configured metrics reset interval expires', async () => { + const clock = sinon.useFakeTimers(); + clock.tick(0); + const pollingCycleEvents1 = [ + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + ]; + + const pollingCycleEvents2 = [ + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + ]; + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const taskClaimAggregator = createAggregator({ + key: 'task_claim', + taskPollingLifecycle, + config: { + ...config, + metrics_reset_interval: 10, + }, + resetMetrics$: new Subject(), + taskEventFilter: (taskEvent: TaskLifecycleEvent) => isTaskPollingCycleEvent(taskEvent), + metricsAggregator: new TaskClaimMetricsAggregator(), + }); + + return new Promise((resolve) => { + taskClaimAggregator + .pipe( + // skip initial metric which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + take(pollingCycleEvents1.length + pollingCycleEvents2.length), + bufferCount(pollingCycleEvents1.length + pollingCycleEvents2.length) + ) + .subscribe((metrics: Array>) => { + expect(metrics[0]).toEqual({ + key: 'task_claim', + value: { success: 1, total: 1, duration: { counts: [1], values: [100] } }, + }); + expect(metrics[1]).toEqual({ + key: 'task_claim', + value: { success: 2, total: 2, duration: { counts: [2], values: [100] } }, + }); + expect(metrics[2]).toEqual({ + key: 'task_claim', + value: { success: 3, total: 3, duration: { counts: [3], values: [100] } }, + }); + expect(metrics[3]).toEqual({ + key: 'task_claim', + value: { success: 4, total: 4, duration: { counts: [4], values: [100] } }, + }); + expect(metrics[4]).toEqual({ + key: 'task_claim', + value: { success: 4, total: 5, duration: { counts: [4], values: [100] } }, + }); + expect(metrics[5]).toEqual({ + key: 'task_claim', + value: { success: 5, total: 6, duration: { counts: [5], values: [100] } }, + }); + // reset interval should have fired here + expect(metrics[6]).toEqual({ + key: 'task_claim', + value: { success: 1, total: 1, duration: { counts: [1], values: [100] } }, + }); + expect(metrics[7]).toEqual({ + key: 'task_claim', + value: { success: 1, total: 2, duration: { counts: [1], values: [100] } }, + }); + expect(metrics[8]).toEqual({ + key: 'task_claim', + value: { success: 1, total: 3, duration: { counts: [1], values: [100] } }, + }); + expect(metrics[9]).toEqual({ + key: 'task_claim', + value: { success: 2, total: 4, duration: { counts: [2], values: [100] } }, + }); + expect(metrics[10]).toEqual({ + key: 'task_claim', + value: { success: 3, total: 5, duration: { counts: [3], values: [100] } }, + }); + resolve(); + }); + + for (const event of pollingCycleEvents1) { + events$.next(event); + } + clock.tick(20); + for (const event of pollingCycleEvents2) { + events$.next(event); + } + + clock.restore(); + }); + }); + }); + + describe('with TaskRunMetricsAggregator', () => { + test('returns a cumulative count of successful task runs and total task runs, broken down by type', async () => { + const taskRunEvents = [ + getTaskRunSuccessEvent('alerting:example'), + getTaskRunSuccessEvent('telemetry'), + getTaskRunSuccessEvent('alerting:example'), + getTaskRunSuccessEvent('report'), + getTaskRunFailedEvent('alerting:example'), + getTaskRunSuccessEvent('alerting:.index-threshold'), + getTaskRunSuccessEvent('alerting:example'), + getTaskRunFailedEvent('alerting:example'), + getTaskRunSuccessEvent('alerting:example'), + getTaskRunFailedEvent('actions:webhook'), + ]; + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const taskRunAggregator = createAggregator({ + key: 'task_run', + taskPollingLifecycle, + config, + resetMetrics$: new Subject(), + taskEventFilter: (taskEvent: TaskLifecycleEvent) => isTaskRunEvent(taskEvent), + metricsAggregator: new TaskRunMetricsAggregator(), + }); + + return new Promise((resolve) => { + taskRunAggregator + .pipe( + // skip initial metric which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + take(taskRunEvents.length), + bufferCount(taskRunEvents.length) + ) + .subscribe((metrics: Array>) => { + expect(metrics[0]).toEqual({ + key: 'task_run', + value: { + overall: { success: 1, total: 1 }, + by_type: { + alerting: { success: 1, total: 1 }, + 'alerting:example': { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[1]).toEqual({ + key: 'task_run', + value: { + overall: { success: 2, total: 2 }, + by_type: { + alerting: { success: 1, total: 1 }, + 'alerting:example': { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[2]).toEqual({ + key: 'task_run', + value: { + overall: { success: 3, total: 3 }, + by_type: { + alerting: { success: 2, total: 2 }, + 'alerting:example': { success: 2, total: 2 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[3]).toEqual({ + key: 'task_run', + value: { + overall: { success: 4, total: 4 }, + by_type: { + alerting: { success: 2, total: 2 }, + 'alerting:example': { success: 2, total: 2 }, + report: { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[4]).toEqual({ + key: 'task_run', + value: { + overall: { success: 4, total: 5 }, + by_type: { + alerting: { success: 2, total: 3 }, + 'alerting:example': { success: 2, total: 3 }, + report: { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[5]).toEqual({ + key: 'task_run', + value: { + overall: { success: 5, total: 6 }, + by_type: { + alerting: { success: 3, total: 4 }, + 'alerting:.index-threshold': { success: 1, total: 1 }, + 'alerting:example': { success: 2, total: 3 }, + report: { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[6]).toEqual({ + key: 'task_run', + value: { + overall: { success: 6, total: 7 }, + by_type: { + alerting: { success: 4, total: 5 }, + 'alerting:.index-threshold': { success: 1, total: 1 }, + 'alerting:example': { success: 3, total: 4 }, + report: { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[7]).toEqual({ + key: 'task_run', + value: { + overall: { success: 6, total: 8 }, + by_type: { + alerting: { success: 4, total: 6 }, + 'alerting:.index-threshold': { success: 1, total: 1 }, + 'alerting:example': { success: 3, total: 5 }, + report: { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[8]).toEqual({ + key: 'task_run', + value: { + overall: { success: 7, total: 9 }, + by_type: { + alerting: { success: 5, total: 7 }, + 'alerting:.index-threshold': { success: 1, total: 1 }, + 'alerting:example': { success: 4, total: 6 }, + report: { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[9]).toEqual({ + key: 'task_run', + value: { + overall: { success: 7, total: 10 }, + by_type: { + actions: { success: 0, total: 1 }, + alerting: { success: 5, total: 7 }, + 'actions:webhook': { success: 0, total: 1 }, + 'alerting:.index-threshold': { success: 1, total: 1 }, + 'alerting:example': { success: 4, total: 6 }, + report: { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + resolve(); + }); + + for (const event of taskRunEvents) { + events$.next(event); + } + }); + }); + + test('resets count when resetMetric$ event is received', async () => { + const resetMetrics$ = new Subject(); + const taskRunEvents1 = [ + getTaskRunSuccessEvent('alerting:example'), + getTaskRunSuccessEvent('telemetry'), + getTaskRunSuccessEvent('alerting:example'), + getTaskRunSuccessEvent('report'), + getTaskRunFailedEvent('alerting:example'), + ]; + + const taskRunEvents2 = [ + getTaskRunSuccessEvent('alerting:example'), + getTaskRunSuccessEvent('alerting:example'), + getTaskRunFailedEvent('alerting:example'), + getTaskRunSuccessEvent('alerting:example'), + getTaskRunFailedEvent('actions:webhook'), + ]; + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const taskRunAggregator = createAggregator({ + key: 'task_run', + taskPollingLifecycle, + config, + resetMetrics$, + taskEventFilter: (taskEvent: TaskLifecycleEvent) => isTaskRunEvent(taskEvent), + metricsAggregator: new TaskRunMetricsAggregator(), + }); + + return new Promise((resolve) => { + taskRunAggregator + .pipe( + // skip initial metric which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + take(taskRunEvents1.length + taskRunEvents2.length), + bufferCount(taskRunEvents1.length + taskRunEvents2.length) + ) + .subscribe((metrics: Array>) => { + expect(metrics[0]).toEqual({ + key: 'task_run', + value: { + overall: { success: 1, total: 1 }, + by_type: { + alerting: { success: 1, total: 1 }, + 'alerting:example': { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[1]).toEqual({ + key: 'task_run', + value: { + overall: { success: 2, total: 2 }, + by_type: { + alerting: { success: 1, total: 1 }, + 'alerting:example': { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[2]).toEqual({ + key: 'task_run', + value: { + overall: { success: 3, total: 3 }, + by_type: { + alerting: { success: 2, total: 2 }, + 'alerting:example': { success: 2, total: 2 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[3]).toEqual({ + key: 'task_run', + value: { + overall: { success: 4, total: 4 }, + by_type: { + alerting: { success: 2, total: 2 }, + 'alerting:example': { success: 2, total: 2 }, + report: { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[4]).toEqual({ + key: 'task_run', + value: { + overall: { success: 4, total: 5 }, + by_type: { + alerting: { success: 2, total: 3 }, + 'alerting:example': { success: 2, total: 3 }, + report: { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + // reset event should have been received here + expect(metrics[5]).toEqual({ + key: 'task_run', + value: { + overall: { success: 1, total: 1 }, + by_type: { + alerting: { success: 1, total: 1 }, + 'alerting:example': { success: 1, total: 1 }, + report: { success: 0, total: 0 }, + telemetry: { success: 0, total: 0 }, + }, + }, + }); + expect(metrics[6]).toEqual({ + key: 'task_run', + value: { + overall: { success: 2, total: 2 }, + by_type: { + alerting: { success: 2, total: 2 }, + 'alerting:example': { success: 2, total: 2 }, + report: { success: 0, total: 0 }, + telemetry: { success: 0, total: 0 }, + }, + }, + }); + expect(metrics[7]).toEqual({ + key: 'task_run', + value: { + overall: { success: 2, total: 3 }, + by_type: { + alerting: { success: 2, total: 3 }, + 'alerting:example': { success: 2, total: 3 }, + report: { success: 0, total: 0 }, + telemetry: { success: 0, total: 0 }, + }, + }, + }); + expect(metrics[8]).toEqual({ + key: 'task_run', + value: { + overall: { success: 3, total: 4 }, + by_type: { + alerting: { success: 3, total: 4 }, + 'alerting:example': { success: 3, total: 4 }, + report: { success: 0, total: 0 }, + telemetry: { success: 0, total: 0 }, + }, + }, + }); + expect(metrics[9]).toEqual({ + key: 'task_run', + value: { + overall: { success: 3, total: 5 }, + by_type: { + actions: { success: 0, total: 1 }, + alerting: { success: 3, total: 4 }, + 'actions:webhook': { success: 0, total: 1 }, + 'alerting:example': { success: 3, total: 4 }, + report: { success: 0, total: 0 }, + telemetry: { success: 0, total: 0 }, + }, + }, + }); + resolve(); + }); + + for (const event of taskRunEvents1) { + events$.next(event); + } + resetMetrics$.next(true); + for (const event of taskRunEvents2) { + events$.next(event); + } + }); + }); + + test('resets count when configured metrics reset interval expires', async () => { + const clock = sinon.useFakeTimers(); + clock.tick(0); + const taskRunEvents1 = [ + getTaskRunSuccessEvent('alerting:example'), + getTaskRunSuccessEvent('telemetry'), + getTaskRunSuccessEvent('alerting:example'), + getTaskRunSuccessEvent('report'), + getTaskRunFailedEvent('alerting:example'), + ]; + + const taskRunEvents2 = [ + getTaskRunSuccessEvent('alerting:example'), + getTaskRunSuccessEvent('alerting:example'), + getTaskRunFailedEvent('alerting:example'), + getTaskRunSuccessEvent('alerting:example'), + getTaskRunFailedEvent('actions:webhook'), + ]; + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const taskRunAggregator = createAggregator({ + key: 'task_run', + taskPollingLifecycle, + config: { + ...config, + metrics_reset_interval: 10, + }, + resetMetrics$: new Subject(), + taskEventFilter: (taskEvent: TaskLifecycleEvent) => isTaskRunEvent(taskEvent), + metricsAggregator: new TaskRunMetricsAggregator(), + }); + + return new Promise((resolve) => { + taskRunAggregator + .pipe( + // skip initial metric which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + take(taskRunEvents1.length + taskRunEvents2.length), + bufferCount(taskRunEvents1.length + taskRunEvents2.length) + ) + .subscribe((metrics: Array>) => { + expect(metrics[0]).toEqual({ + key: 'task_run', + value: { + overall: { success: 1, total: 1 }, + by_type: { + alerting: { success: 1, total: 1 }, + 'alerting:example': { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[1]).toEqual({ + key: 'task_run', + value: { + overall: { success: 2, total: 2 }, + by_type: { + alerting: { success: 1, total: 1 }, + 'alerting:example': { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[2]).toEqual({ + key: 'task_run', + value: { + overall: { success: 3, total: 3 }, + by_type: { + alerting: { success: 2, total: 2 }, + 'alerting:example': { success: 2, total: 2 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[3]).toEqual({ + key: 'task_run', + value: { + overall: { success: 4, total: 4 }, + by_type: { + alerting: { success: 2, total: 2 }, + 'alerting:example': { success: 2, total: 2 }, + report: { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + expect(metrics[4]).toEqual({ + key: 'task_run', + value: { + overall: { success: 4, total: 5 }, + by_type: { + alerting: { success: 2, total: 3 }, + 'alerting:example': { success: 2, total: 3 }, + report: { success: 1, total: 1 }, + telemetry: { success: 1, total: 1 }, + }, + }, + }); + // reset event should have been received here + expect(metrics[5]).toEqual({ + key: 'task_run', + value: { + overall: { success: 1, total: 1 }, + by_type: { + alerting: { success: 1, total: 1 }, + 'alerting:example': { success: 1, total: 1 }, + report: { success: 0, total: 0 }, + telemetry: { success: 0, total: 0 }, + }, + }, + }); + expect(metrics[6]).toEqual({ + key: 'task_run', + value: { + overall: { success: 2, total: 2 }, + by_type: { + alerting: { success: 2, total: 2 }, + 'alerting:example': { success: 2, total: 2 }, + report: { success: 0, total: 0 }, + telemetry: { success: 0, total: 0 }, + }, + }, + }); + expect(metrics[7]).toEqual({ + key: 'task_run', + value: { + overall: { success: 2, total: 3 }, + by_type: { + alerting: { success: 2, total: 3 }, + 'alerting:example': { success: 2, total: 3 }, + report: { success: 0, total: 0 }, + telemetry: { success: 0, total: 0 }, + }, + }, + }); + expect(metrics[8]).toEqual({ + key: 'task_run', + value: { + overall: { success: 3, total: 4 }, + by_type: { + alerting: { success: 3, total: 4 }, + 'alerting:example': { success: 3, total: 4 }, + report: { success: 0, total: 0 }, + telemetry: { success: 0, total: 0 }, + }, + }, + }); + expect(metrics[9]).toEqual({ + key: 'task_run', + value: { + overall: { success: 3, total: 5 }, + by_type: { + actions: { success: 0, total: 1 }, + alerting: { success: 3, total: 4 }, + 'actions:webhook': { success: 0, total: 1 }, + 'alerting:example': { success: 3, total: 4 }, + report: { success: 0, total: 0 }, + telemetry: { success: 0, total: 0 }, + }, + }, + }); + resolve(); + }); + + for (const event of taskRunEvents1) { + events$.next(event); + } + clock.tick(20); + for (const event of taskRunEvents2) { + events$.next(event); + } + + clock.restore(); + }); + }); + }); + + test('should filter task lifecycle events using specified taskEventFilter', () => { + const pollingCycleEvents = [ + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + ]; + const taskEventFilter = jest.fn().mockReturnValue(true); + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + const aggregator = createAggregator({ + key: 'test', + taskPollingLifecycle, + config, + resetMetrics$: new Subject(), + taskEventFilter, + metricsAggregator: new TaskClaimMetricsAggregator(), + }); + + return new Promise((resolve) => { + aggregator + .pipe( + // skip initial metric which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + take(pollingCycleEvents.length), + bufferCount(pollingCycleEvents.length) + ) + .subscribe(() => { + resolve(); + }); + + for (const event of pollingCycleEvents) { + events$.next(event); + } + + expect(taskEventFilter).toHaveBeenCalledTimes(pollingCycleEvents.length); + }); + }); + + test('should call metricAggregator to process task lifecycle events', () => { + const spy = jest + .spyOn(TaskClaimMetricsAggregatorModule, 'TaskClaimMetricsAggregator') + .mockImplementation(() => mockMetricsAggregator); + + const pollingCycleEvents = [ + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + ]; + const taskEventFilter = jest.fn().mockReturnValue(true); + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + const aggregator = createAggregator({ + key: 'test', + taskPollingLifecycle, + config, + resetMetrics$: new Subject(), + taskEventFilter, + metricsAggregator: mockMetricsAggregator, + }); + + return new Promise((resolve) => { + aggregator + .pipe( + // skip initial metric which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + take(pollingCycleEvents.length), + bufferCount(pollingCycleEvents.length) + ) + .subscribe(() => { + resolve(); + }); + + for (const event of pollingCycleEvents) { + events$.next(event); + } + + expect(mockMetricsAggregator.initialMetric).toHaveBeenCalledTimes(1); + expect(mockMetricsAggregator.processTaskLifecycleEvent).toHaveBeenCalledTimes( + pollingCycleEvents.length + ); + expect(mockMetricsAggregator.collect).toHaveBeenCalledTimes(pollingCycleEvents.length); + expect(mockMetricsAggregator.reset).not.toHaveBeenCalled(); + spy.mockRestore(); + }); + }); + + test('should call metricAggregator reset when resetMetric$ event is received', () => { + const spy = jest + .spyOn(TaskClaimMetricsAggregatorModule, 'TaskClaimMetricsAggregator') + .mockImplementation(() => mockMetricsAggregator); + + const resetMetrics$ = new Subject(); + const pollingCycleEvents = [ + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + ]; + const taskEventFilter = jest.fn().mockReturnValue(true); + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + const aggregator = createAggregator({ + key: 'test', + taskPollingLifecycle, + config, + resetMetrics$, + taskEventFilter, + metricsAggregator: mockMetricsAggregator, + }); + + return new Promise((resolve) => { + aggregator + .pipe( + // skip initial metric which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + take(pollingCycleEvents.length), + bufferCount(pollingCycleEvents.length) + ) + .subscribe(() => { + resolve(); + }); + + for (const event of pollingCycleEvents) { + events$.next(event); + } + + for (let i = 0; i < 5; i++) { + events$.next(pollingCycleEvents[i]); + } + resetMetrics$.next(true); + for (let i = 0; i < pollingCycleEvents.length; i++) { + events$.next(pollingCycleEvents[i]); + } + + expect(mockMetricsAggregator.initialMetric).toHaveBeenCalledTimes(1); + expect(mockMetricsAggregator.processTaskLifecycleEvent).toHaveBeenCalledTimes( + pollingCycleEvents.length + ); + expect(mockMetricsAggregator.collect).toHaveBeenCalledTimes(pollingCycleEvents.length); + expect(mockMetricsAggregator.reset).toHaveBeenCalledTimes(1); + spy.mockRestore(); + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/metrics/create_aggregator.ts b/x-pack/plugins/task_manager/server/metrics/create_aggregator.ts new file mode 100644 index 00000000000000..cece8c0f70b23f --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/create_aggregator.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 { combineLatest, filter, interval, map, merge, Observable, startWith } from 'rxjs'; +import { JsonValue } from '@kbn/utility-types'; +import { TaskLifecycleEvent, TaskPollingLifecycle } from '../polling_lifecycle'; +import { AggregatedStat, AggregatedStatProvider } from '../lib/runtime_statistics_aggregator'; +import { TaskManagerConfig } from '../config'; +import { ITaskMetricsAggregator } from './types'; + +export interface CreateMetricsAggregatorOpts { + key: string; + config: TaskManagerConfig; + resetMetrics$: Observable; + taskPollingLifecycle: TaskPollingLifecycle; + taskEventFilter: (taskEvent: TaskLifecycleEvent) => boolean; + metricsAggregator: ITaskMetricsAggregator; +} + +export function createAggregator({ + key, + taskPollingLifecycle, + config, + resetMetrics$, + taskEventFilter, + metricsAggregator, +}: CreateMetricsAggregatorOpts): AggregatedStatProvider { + // Resets the aggregators either when the reset interval has passed or + // a resetMetrics$ event is received + merge( + interval(config.metrics_reset_interval).pipe(map(() => true)), + resetMetrics$.pipe(map(() => true)) + ).subscribe(() => { + metricsAggregator.reset(); + }); + + const taskEvents$: Observable = taskPollingLifecycle.events.pipe( + filter((taskEvent: TaskLifecycleEvent) => taskEventFilter(taskEvent)), + map((taskEvent: TaskLifecycleEvent) => { + metricsAggregator.processTaskLifecycleEvent(taskEvent); + return metricsAggregator.collect(); + }) + ); + + return combineLatest([taskEvents$.pipe(startWith(metricsAggregator.initialMetric()))]).pipe( + map(([value]: [T]) => { + return { + key, + value, + } as AggregatedStat; + }) + ); +} diff --git a/x-pack/plugins/task_manager/server/metrics/index.ts b/x-pack/plugins/task_manager/server/metrics/index.ts new file mode 100644 index 00000000000000..5e2a73f91dd73e --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/index.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 { Observable } from 'rxjs'; +import { TaskManagerConfig } from '../config'; +import { Metrics, createMetricsAggregators, createMetricsStream } from './metrics_stream'; +import { TaskPollingLifecycle } from '../polling_lifecycle'; +export type { Metrics } from './metrics_stream'; + +export function metricsStream( + config: TaskManagerConfig, + resetMetrics$: Observable, + taskPollingLifecycle?: TaskPollingLifecycle +): Observable { + return createMetricsStream( + createMetricsAggregators({ + config, + resetMetrics$, + taskPollingLifecycle, + }) + ); +} diff --git a/x-pack/plugins/task_manager/server/metrics/metrics_aggregator.mock.ts b/x-pack/plugins/task_manager/server/metrics/metrics_aggregator.mock.ts new file mode 100644 index 00000000000000..691ba9d0290d21 --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/metrics_aggregator.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createIMetricsAggregatorMock = () => { + return jest.fn().mockImplementation(() => { + return { + initialMetric: jest.fn().mockReturnValue({ count: 0 }), + reset: jest.fn(), + collect: jest.fn(), + processTaskLifecycleEvent: jest.fn(), + }; + }); +}; + +export const metricsAggregatorMock = { + create: createIMetricsAggregatorMock(), +}; diff --git a/x-pack/plugins/task_manager/server/metrics/metrics_stream.test.ts b/x-pack/plugins/task_manager/server/metrics/metrics_stream.test.ts new file mode 100644 index 00000000000000..5aec856a7a4f05 --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/metrics_stream.test.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. + */ + +import { Subject } from 'rxjs'; +import { take, bufferCount } from 'rxjs/operators'; +import { createMetricsStream } from './metrics_stream'; +import { JsonValue } from '@kbn/utility-types'; +import { AggregatedStat } from '../lib/runtime_statistics_aggregator'; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('createMetricsStream', () => { + it('incrementally updates the metrics returned by the endpoint', async () => { + const aggregatedStats$ = new Subject(); + + return new Promise((resolve) => { + createMetricsStream(aggregatedStats$) + .pipe(take(3), bufferCount(3)) + .subscribe(([initialValue, secondValue, thirdValue]) => { + expect(initialValue.metrics).toMatchObject({ + lastUpdate: expect.any(String), + metrics: {}, + }); + + expect(secondValue).toMatchObject({ + lastUpdate: expect.any(String), + metrics: { + newAggregatedStat: { + timestamp: expect.any(String), + value: { + some: { + complex: { + value: 123, + }, + }, + }, + }, + }, + }); + + expect(thirdValue).toMatchObject({ + lastUpdate: expect.any(String), + metrics: { + newAggregatedStat: { + timestamp: expect.any(String), + value: { + some: { + updated: { + value: 456, + }, + }, + }, + }, + }, + }); + }); + + aggregatedStats$.next({ + key: 'newAggregatedStat', + value: { + some: { + complex: { + value: 123, + }, + }, + } as JsonValue, + }); + + aggregatedStats$.next({ + key: 'newAggregatedStat', + value: { + some: { + updated: { + value: 456, + }, + }, + } as JsonValue, + }); + + resolve(); + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/metrics/metrics_stream.ts b/x-pack/plugins/task_manager/server/metrics/metrics_stream.ts new file mode 100644 index 00000000000000..29558308c51963 --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/metrics_stream.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. + */ + +import { merge, of, Observable } from 'rxjs'; +import { map, scan } from 'rxjs/operators'; +import { set } from '@kbn/safer-lodash-set'; +import { TaskLifecycleEvent, TaskPollingLifecycle } from '../polling_lifecycle'; +import { TaskManagerConfig } from '../config'; +import { AggregatedStatProvider } from '../lib/runtime_statistics_aggregator'; +import { isTaskPollingCycleEvent, isTaskRunEvent } from '../task_events'; +import { TaskClaimMetric, TaskClaimMetricsAggregator } from './task_claim_metrics_aggregator'; +import { createAggregator } from './create_aggregator'; +import { TaskRunMetric, TaskRunMetricsAggregator } from './task_run_metrics_aggregator'; +export interface Metrics { + last_update: string; + metrics: { + task_claim?: Metric; + task_run?: Metric; + }; +} + +export interface Metric { + timestamp: string; + value: T; +} + +interface CreateMetricsAggregatorsOpts { + config: TaskManagerConfig; + resetMetrics$: Observable; + taskPollingLifecycle?: TaskPollingLifecycle; +} +export function createMetricsAggregators({ + config, + resetMetrics$, + taskPollingLifecycle, +}: CreateMetricsAggregatorsOpts): AggregatedStatProvider { + const aggregators: AggregatedStatProvider[] = []; + if (taskPollingLifecycle) { + aggregators.push( + createAggregator({ + key: 'task_claim', + taskPollingLifecycle, + config, + resetMetrics$, + taskEventFilter: (taskEvent: TaskLifecycleEvent) => isTaskPollingCycleEvent(taskEvent), + metricsAggregator: new TaskClaimMetricsAggregator(), + }), + createAggregator({ + key: 'task_run', + taskPollingLifecycle, + config, + resetMetrics$, + taskEventFilter: (taskEvent: TaskLifecycleEvent) => isTaskRunEvent(taskEvent), + metricsAggregator: new TaskRunMetricsAggregator(), + }) + ); + } + return merge(...aggregators); +} + +export function createMetricsStream(provider$: AggregatedStatProvider): Observable { + const initialMetrics = { + last_update: new Date().toISOString(), + metrics: {}, + }; + return merge( + // emit the initial metrics + of(initialMetrics), + // emit updated metrics whenever a provider updates a specific key on the stats + provider$.pipe( + map(({ key, value }) => { + return { + value: { timestamp: new Date().toISOString(), value }, + key, + }; + }), + scan((metrics: Metrics, { key, value }) => { + // incrementally merge stats as they come in + set(metrics.metrics, key, value); + metrics.last_update = new Date().toISOString(); + return metrics; + }, initialMetrics) + ) + ); +} diff --git a/x-pack/plugins/task_manager/server/metrics/simple_histogram.test.ts b/x-pack/plugins/task_manager/server/metrics/simple_histogram.test.ts new file mode 100644 index 00000000000000..30b5d0bd6b21a7 --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/simple_histogram.test.ts @@ -0,0 +1,179 @@ +/* + * 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 { SimpleHistogram } from './simple_histogram'; + +describe('SimpleHistogram', () => { + test('should throw error when bucketSize is greater than range', () => { + expect(() => { + new SimpleHistogram(10, 100); + }).toThrowErrorMatchingInlineSnapshot(`"bucket size cannot be greater than value range"`); + }); + + test('should correctly initialize when bucketSize evenly divides range', () => { + const histogram = new SimpleHistogram(100, 10); + expect(histogram.get()).toEqual([ + { value: 10, count: 0 }, + { value: 20, count: 0 }, + { value: 30, count: 0 }, + { value: 40, count: 0 }, + { value: 50, count: 0 }, + { value: 60, count: 0 }, + { value: 70, count: 0 }, + { value: 80, count: 0 }, + { value: 90, count: 0 }, + { value: 100, count: 0 }, + ]); + }); + + test('should correctly initialize when bucketSize does not evenly divides range', () => { + const histogram = new SimpleHistogram(100, 7); + expect(histogram.get()).toEqual([ + { value: 7, count: 0 }, + { value: 14, count: 0 }, + { value: 21, count: 0 }, + { value: 28, count: 0 }, + { value: 35, count: 0 }, + { value: 42, count: 0 }, + { value: 49, count: 0 }, + { value: 56, count: 0 }, + { value: 63, count: 0 }, + { value: 70, count: 0 }, + { value: 77, count: 0 }, + { value: 84, count: 0 }, + { value: 91, count: 0 }, + { value: 98, count: 0 }, + { value: 105, count: 0 }, + ]); + }); + + test('should correctly record values', () => { + const histogram = new SimpleHistogram(100, 10); + histogram.record(23); + histogram.record(34); + histogram.record(21); + histogram.record(56); + histogram.record(78); + histogram.record(33); + histogram.record(99); + histogram.record(1); + histogram.record(2); + + expect(histogram.get()).toEqual([ + { value: 10, count: 2 }, + { value: 20, count: 0 }, + { value: 30, count: 2 }, + { value: 40, count: 2 }, + { value: 50, count: 0 }, + { value: 60, count: 1 }, + { value: 70, count: 0 }, + { value: 80, count: 1 }, + { value: 90, count: 0 }, + { value: 100, count: 1 }, + ]); + }); + + test('should ignore values less than 0 and greater than max', () => { + const histogram = new SimpleHistogram(100, 10); + histogram.record(23); + histogram.record(34); + histogram.record(21); + histogram.record(56); + histogram.record(78); + histogram.record(33); + histogram.record(99); + histogram.record(1); + histogram.record(2); + + const hist1 = histogram.get(); + + histogram.record(-1); + histogram.record(200); + + expect(histogram.get()).toEqual(hist1); + }); + + test('should correctly reset values', () => { + const histogram = new SimpleHistogram(100, 10); + histogram.record(23); + histogram.record(34); + histogram.record(21); + histogram.record(56); + histogram.record(78); + histogram.record(33); + histogram.record(99); + histogram.record(1); + histogram.record(2); + + expect(histogram.get()).toEqual([ + { value: 10, count: 2 }, + { value: 20, count: 0 }, + { value: 30, count: 2 }, + { value: 40, count: 2 }, + { value: 50, count: 0 }, + { value: 60, count: 1 }, + { value: 70, count: 0 }, + { value: 80, count: 1 }, + { value: 90, count: 0 }, + { value: 100, count: 1 }, + ]); + + histogram.reset(); + + expect(histogram.get()).toEqual([ + { value: 10, count: 0 }, + { value: 20, count: 0 }, + { value: 30, count: 0 }, + { value: 40, count: 0 }, + { value: 50, count: 0 }, + { value: 60, count: 0 }, + { value: 70, count: 0 }, + { value: 80, count: 0 }, + { value: 90, count: 0 }, + { value: 100, count: 0 }, + ]); + }); + + test('should correctly truncate zero values', () => { + const histogram = new SimpleHistogram(100, 10); + histogram.record(23); + histogram.record(34); + histogram.record(21); + histogram.record(56); + histogram.record(33); + histogram.record(1); + histogram.record(2); + + expect(histogram.get()).toEqual([ + { value: 10, count: 2 }, + { value: 20, count: 0 }, + { value: 30, count: 2 }, + { value: 40, count: 2 }, + { value: 50, count: 0 }, + { value: 60, count: 1 }, + { value: 70, count: 0 }, + { value: 80, count: 0 }, + { value: 90, count: 0 }, + { value: 100, count: 0 }, + ]); + + expect(histogram.get(true)).toEqual([ + { value: 10, count: 2 }, + { value: 20, count: 0 }, + { value: 30, count: 2 }, + { value: 40, count: 2 }, + { value: 50, count: 0 }, + { value: 60, count: 1 }, + ]); + }); + + test('should correctly truncate zero values when all values are zero', () => { + const histogram = new SimpleHistogram(100, 10); + + expect(histogram.get(true)).toEqual([]); + }); +}); diff --git a/x-pack/plugins/task_manager/server/metrics/simple_histogram.ts b/x-pack/plugins/task_manager/server/metrics/simple_histogram.ts new file mode 100644 index 00000000000000..3b2cb89a7f5dd1 --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/simple_histogram.ts @@ -0,0 +1,82 @@ +/* + * 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 { last } from 'lodash'; + +interface Bucket { + min: number; // inclusive + max: number; // exclusive + count: number; +} + +export class SimpleHistogram { + private maxValue: number; + private bucketSize: number; + private histogramBuckets: Bucket[] = []; + + constructor(max: number, bucketSize: number) { + if (bucketSize > max) { + throw new Error(`bucket size cannot be greater than value range`); + } + + this.maxValue = max; + this.bucketSize = bucketSize; + this.initializeBuckets(); + } + + public reset() { + for (let i = 0; i < this.histogramBuckets.length; i++) { + this.histogramBuckets[i].count = 0; + } + } + + public record(value: number) { + if (value < 0 || value > this.maxValue) { + return; + } + + for (let i = 0; i < this.histogramBuckets.length; i++) { + if (value >= this.histogramBuckets[i].min && value < this.histogramBuckets[i].max) { + this.histogramBuckets[i].count++; + + break; + } + } + } + + public get(truncate: boolean = false) { + let histogramToReturn = this.histogramBuckets; + + if (truncate) { + // find the index of the last bucket with a non-zero value + const nonZeroCountsWithIndex = this.histogramBuckets + .map((bucket: Bucket, index: number) => ({ count: bucket.count, index })) + .filter(({ count }) => count > 0); + const lastNonZeroIndex: number = + nonZeroCountsWithIndex.length > 0 ? last(nonZeroCountsWithIndex)?.index ?? -1 : -1; + histogramToReturn = + lastNonZeroIndex >= 0 ? this.histogramBuckets.slice(0, lastNonZeroIndex + 1) : []; + } + + return histogramToReturn.map((bucket: Bucket) => ({ + count: bucket.count, + value: bucket.max, + })); + } + + private initializeBuckets() { + let i = 0; + while (i < this.maxValue) { + this.histogramBuckets.push({ + min: i, + max: i + Math.min(this.bucketSize, this.maxValue), + count: 0, + }); + i += this.bucketSize; + } + } +} diff --git a/x-pack/plugins/task_manager/server/metrics/success_rate_counter.test.ts b/x-pack/plugins/task_manager/server/metrics/success_rate_counter.test.ts new file mode 100644 index 00000000000000..eb34f3a34c005f --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/success_rate_counter.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SuccessRateCounter } from './success_rate_counter'; + +describe('SuccessRateCounter', () => { + let successRateCounter: SuccessRateCounter; + beforeEach(() => { + successRateCounter = new SuccessRateCounter(); + }); + + test('should correctly initialize', () => { + expect(successRateCounter.get()).toEqual({ success: 0, total: 0 }); + }); + + test('should correctly return initialMetrics', () => { + expect(successRateCounter.initialMetric()).toEqual({ success: 0, total: 0 }); + }); + + test('should correctly increment counter when success is true', () => { + successRateCounter.increment(true); + successRateCounter.increment(true); + expect(successRateCounter.get()).toEqual({ success: 2, total: 2 }); + }); + + test('should correctly increment counter when success is false', () => { + successRateCounter.increment(false); + successRateCounter.increment(false); + expect(successRateCounter.get()).toEqual({ success: 0, total: 2 }); + }); + + test('should correctly reset counter', () => { + successRateCounter.increment(true); + successRateCounter.increment(true); + successRateCounter.increment(false); + successRateCounter.increment(false); + successRateCounter.increment(true); + successRateCounter.increment(true); + successRateCounter.increment(false); + expect(successRateCounter.get()).toEqual({ success: 4, total: 7 }); + + successRateCounter.reset(); + expect(successRateCounter.get()).toEqual({ success: 0, total: 0 }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/metrics/success_rate_counter.ts b/x-pack/plugins/task_manager/server/metrics/success_rate_counter.ts new file mode 100644 index 00000000000000..d9c61575a2698b --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/success_rate_counter.ts @@ -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 { JsonObject } from '@kbn/utility-types'; + +export interface SuccessRate extends JsonObject { + success: number; + total: number; +} + +export class SuccessRateCounter { + private success = 0; + private total = 0; + + public initialMetric(): SuccessRate { + return { + success: 0, + total: 0, + }; + } + + public get(): SuccessRate { + return { + success: this.success, + total: this.total, + }; + } + + public increment(success: boolean) { + if (success) { + this.success++; + } + this.total++; + } + + public reset() { + this.success = 0; + this.total = 0; + } +} diff --git a/x-pack/plugins/task_manager/server/metrics/task_claim_metrics_aggregator.test.ts b/x-pack/plugins/task_manager/server/metrics/task_claim_metrics_aggregator.test.ts new file mode 100644 index 00000000000000..cfcf4bfdf8d0b6 --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/task_claim_metrics_aggregator.test.ts @@ -0,0 +1,102 @@ +/* + * 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 { none } from 'fp-ts/lib/Option'; +import { FillPoolResult } from '../lib/fill_pool'; +import { asOk, asErr } from '../lib/result_type'; +import { PollingError, PollingErrorType } from '../polling'; +import { asTaskPollingCycleEvent } from '../task_events'; +import { TaskClaimMetricsAggregator } from './task_claim_metrics_aggregator'; + +export const taskClaimSuccessEvent = asTaskPollingCycleEvent( + asOk({ + result: FillPoolResult.PoolFilled, + stats: { + tasksUpdated: 0, + tasksConflicted: 0, + tasksClaimed: 0, + }, + }), + { + start: 1689698780490, + stop: 1689698780500, + } +); +export const taskClaimFailureEvent = asTaskPollingCycleEvent( + asErr( + new PollingError( + 'Failed to poll for work: Error: failed to work', + PollingErrorType.WorkError, + none + ) + ) +); + +describe('TaskClaimMetricsAggregator', () => { + let taskClaimMetricsAggregator: TaskClaimMetricsAggregator; + beforeEach(() => { + taskClaimMetricsAggregator = new TaskClaimMetricsAggregator(); + }); + + test('should correctly initialize', () => { + expect(taskClaimMetricsAggregator.collect()).toEqual({ + success: 0, + total: 0, + duration: { counts: [], values: [] }, + }); + }); + + test('should correctly return initialMetrics', () => { + expect(taskClaimMetricsAggregator.initialMetric()).toEqual({ + success: 0, + total: 0, + duration: { counts: [], values: [] }, + }); + }); + + test('should correctly process task lifecycle success event', () => { + taskClaimMetricsAggregator.processTaskLifecycleEvent(taskClaimSuccessEvent); + taskClaimMetricsAggregator.processTaskLifecycleEvent(taskClaimSuccessEvent); + expect(taskClaimMetricsAggregator.collect()).toEqual({ + success: 2, + total: 2, + duration: { counts: [2], values: [100] }, + }); + }); + + test('should correctly process task lifecycle failure event', () => { + taskClaimMetricsAggregator.processTaskLifecycleEvent(taskClaimFailureEvent); + taskClaimMetricsAggregator.processTaskLifecycleEvent(taskClaimFailureEvent); + expect(taskClaimMetricsAggregator.collect()).toEqual({ + success: 0, + total: 2, + duration: { counts: [], values: [] }, + }); + }); + + test('should correctly reset counter', () => { + taskClaimMetricsAggregator.processTaskLifecycleEvent(taskClaimSuccessEvent); + taskClaimMetricsAggregator.processTaskLifecycleEvent(taskClaimSuccessEvent); + taskClaimMetricsAggregator.processTaskLifecycleEvent(taskClaimFailureEvent); + taskClaimMetricsAggregator.processTaskLifecycleEvent(taskClaimFailureEvent); + taskClaimMetricsAggregator.processTaskLifecycleEvent(taskClaimSuccessEvent); + taskClaimMetricsAggregator.processTaskLifecycleEvent(taskClaimSuccessEvent); + taskClaimMetricsAggregator.processTaskLifecycleEvent(taskClaimFailureEvent); + expect(taskClaimMetricsAggregator.collect()).toEqual({ + success: 4, + total: 7, + duration: { counts: [4], values: [100] }, + }); + + taskClaimMetricsAggregator.reset(); + expect(taskClaimMetricsAggregator.collect()).toEqual({ + success: 0, + total: 0, + duration: { counts: [], values: [] }, + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/metrics/task_claim_metrics_aggregator.ts b/x-pack/plugins/task_manager/server/metrics/task_claim_metrics_aggregator.ts new file mode 100644 index 00000000000000..2dc1a50e8d00e4 --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/task_claim_metrics_aggregator.ts @@ -0,0 +1,68 @@ +/* + * 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 { isOk } from '../lib/result_type'; +import { TaskLifecycleEvent } from '../polling_lifecycle'; +import { TaskRun } from '../task_events'; +import { SimpleHistogram } from './simple_histogram'; +import { SuccessRate, SuccessRateCounter } from './success_rate_counter'; +import { ITaskMetricsAggregator } from './types'; + +const HDR_HISTOGRAM_MAX = 30000; // 30 seconds +const HDR_HISTOGRAM_BUCKET_SIZE = 100; // 100 millis + +export type TaskClaimMetric = SuccessRate & { + duration: { + counts: number[]; + values: number[]; + }; +}; + +export class TaskClaimMetricsAggregator implements ITaskMetricsAggregator { + private claimSuccessRate = new SuccessRateCounter(); + private durationHistogram = new SimpleHistogram(HDR_HISTOGRAM_MAX, HDR_HISTOGRAM_BUCKET_SIZE); + + public initialMetric(): TaskClaimMetric { + return { + ...this.claimSuccessRate.initialMetric(), + duration: { counts: [], values: [] }, + }; + } + public collect(): TaskClaimMetric { + return { + ...this.claimSuccessRate.get(), + duration: this.serializeHistogram(), + }; + } + + public reset() { + this.claimSuccessRate.reset(); + this.durationHistogram.reset(); + } + + public processTaskLifecycleEvent(taskEvent: TaskLifecycleEvent) { + const success = isOk((taskEvent as TaskRun).event); + this.claimSuccessRate.increment(success); + + if (taskEvent.timing) { + const durationInMs = taskEvent.timing.stop - taskEvent.timing.start; + this.durationHistogram.record(durationInMs); + } + } + + private serializeHistogram() { + const counts: number[] = []; + const values: number[] = []; + + for (const { count, value } of this.durationHistogram.get(true)) { + counts.push(count); + values.push(value); + } + + return { counts, values }; + } +} diff --git a/x-pack/plugins/task_manager/server/metrics/task_run_metrics_aggregator.test.ts b/x-pack/plugins/task_manager/server/metrics/task_run_metrics_aggregator.test.ts new file mode 100644 index 00000000000000..e3654fd9a21d53 --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/task_run_metrics_aggregator.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 * as uuid from 'uuid'; +import { asOk, asErr } from '../lib/result_type'; +import { TaskStatus } from '../task'; +import { asTaskRunEvent, TaskPersistence } from '../task_events'; +import { TaskRunResult } from '../task_running'; +import { TaskRunMetricsAggregator } from './task_run_metrics_aggregator'; + +export const getTaskRunSuccessEvent = (type: string) => { + const id = uuid.v4(); + return asTaskRunEvent( + id, + asOk({ + task: { + id, + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: type, + params: {}, + ownerId: null, + }, + persistence: TaskPersistence.Recurring, + result: TaskRunResult.Success, + }), + { + start: 1689698780490, + stop: 1689698780500, + } + ); +}; + +export const getTaskRunFailedEvent = (type: string) => { + const id = uuid.v4(); + return asTaskRunEvent( + id, + asErr({ + error: new Error('task failed to run'), + task: { + id, + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: type, + params: {}, + ownerId: null, + }, + persistence: TaskPersistence.Recurring, + result: TaskRunResult.Failed, + }) + ); +}; + +describe('TaskRunMetricsAggregator', () => { + let taskRunMetricsAggregator: TaskRunMetricsAggregator; + beforeEach(() => { + taskRunMetricsAggregator = new TaskRunMetricsAggregator(); + }); + + test('should correctly initialize', () => { + expect(taskRunMetricsAggregator.collect()).toEqual({ + overall: { success: 0, total: 0 }, + by_type: {}, + }); + }); + + test('should correctly return initialMetrics', () => { + expect(taskRunMetricsAggregator.initialMetric()).toEqual({ + overall: { success: 0, total: 0 }, + by_type: {}, + }); + }); + + test('should correctly process task run success event', () => { + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('telemetry')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('telemetry')); + expect(taskRunMetricsAggregator.collect()).toEqual({ + overall: { success: 2, total: 2 }, + by_type: { + telemetry: { success: 2, total: 2 }, + }, + }); + }); + + test('should correctly process task run failure event', () => { + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunFailedEvent('telemetry')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunFailedEvent('telemetry')); + expect(taskRunMetricsAggregator.collect()).toEqual({ + overall: { success: 0, total: 2 }, + by_type: { + telemetry: { success: 0, total: 2 }, + }, + }); + }); + + test('should correctly process different task types', () => { + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('telemetry')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('report')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('report')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunFailedEvent('telemetry')); + expect(taskRunMetricsAggregator.collect()).toEqual({ + overall: { success: 3, total: 4 }, + by_type: { + report: { success: 2, total: 2 }, + telemetry: { success: 1, total: 2 }, + }, + }); + }); + + test('should correctly group alerting and action task types', () => { + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('telemetry')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('report')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('report')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunFailedEvent('telemetry')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('alerting:example')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('alerting:example')); + taskRunMetricsAggregator.processTaskLifecycleEvent( + getTaskRunSuccessEvent('alerting:.index-threshold') + ); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('actions:webhook')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunFailedEvent('alerting:example')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('actions:webhook')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('alerting:example')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunFailedEvent('alerting:example')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('actions:.email')); + taskRunMetricsAggregator.processTaskLifecycleEvent( + getTaskRunSuccessEvent('alerting:.index-threshold') + ); + expect(taskRunMetricsAggregator.collect()).toEqual({ + overall: { success: 11, total: 14 }, + by_type: { + actions: { success: 3, total: 3 }, + 'actions:.email': { success: 1, total: 1 }, + 'actions:webhook': { success: 2, total: 2 }, + alerting: { success: 5, total: 7 }, + 'alerting:example': { success: 3, total: 5 }, + 'alerting:.index-threshold': { success: 2, total: 2 }, + report: { success: 2, total: 2 }, + telemetry: { success: 1, total: 2 }, + }, + }); + }); + + test('should correctly reset counter', () => { + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('telemetry')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('report')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('report')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunFailedEvent('telemetry')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('alerting:example')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('alerting:example')); + taskRunMetricsAggregator.processTaskLifecycleEvent( + getTaskRunSuccessEvent('alerting:.index-threshold') + ); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('actions:webhook')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunFailedEvent('alerting:example')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('actions:webhook')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('alerting:example')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunFailedEvent('alerting:example')); + taskRunMetricsAggregator.processTaskLifecycleEvent(getTaskRunSuccessEvent('actions:.email')); + taskRunMetricsAggregator.processTaskLifecycleEvent( + getTaskRunSuccessEvent('alerting:.index-threshold') + ); + expect(taskRunMetricsAggregator.collect()).toEqual({ + overall: { success: 11, total: 14 }, + by_type: { + actions: { success: 3, total: 3 }, + 'actions:.email': { success: 1, total: 1 }, + 'actions:webhook': { success: 2, total: 2 }, + alerting: { success: 5, total: 7 }, + 'alerting:example': { success: 3, total: 5 }, + 'alerting:.index-threshold': { success: 2, total: 2 }, + report: { success: 2, total: 2 }, + telemetry: { success: 1, total: 2 }, + }, + }); + + taskRunMetricsAggregator.reset(); + expect(taskRunMetricsAggregator.collect()).toEqual({ + overall: { success: 0, total: 0 }, + by_type: { + actions: { success: 0, total: 0 }, + 'actions:.email': { success: 0, total: 0 }, + 'actions:webhook': { success: 0, total: 0 }, + alerting: { success: 0, total: 0 }, + 'alerting:example': { success: 0, total: 0 }, + 'alerting:.index-threshold': { success: 0, total: 0 }, + report: { success: 0, total: 0 }, + telemetry: { success: 0, total: 0 }, + }, + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/metrics/task_run_metrics_aggregator.ts b/x-pack/plugins/task_manager/server/metrics/task_run_metrics_aggregator.ts new file mode 100644 index 00000000000000..c25d80f112df1d --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/task_run_metrics_aggregator.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 { JsonObject } from '@kbn/utility-types'; +import { isOk, unwrap } from '../lib/result_type'; +import { TaskLifecycleEvent } from '../polling_lifecycle'; +import { ErroredTask, RanTask, TaskRun } from '../task_events'; +import { SuccessRate, SuccessRateCounter } from './success_rate_counter'; +import { ITaskMetricsAggregator } from './types'; + +const taskTypeGrouping = new Set(['alerting:', 'actions:']); + +export interface TaskRunMetric extends JsonObject { + overall: SuccessRate; + by_type: { + [key: string]: SuccessRate; + }; +} + +export class TaskRunMetricsAggregator implements ITaskMetricsAggregator { + private taskRunSuccessRate = new SuccessRateCounter(); + private taskRunCounter: Map = new Map(); + + public initialMetric(): TaskRunMetric { + return { + overall: this.taskRunSuccessRate.initialMetric(), + by_type: {}, + }; + } + + public collect(): TaskRunMetric { + return { + overall: this.taskRunSuccessRate.get(), + by_type: this.collectTaskTypeEntries(), + }; + } + + public reset() { + this.taskRunSuccessRate.reset(); + for (const taskType of this.taskRunCounter.keys()) { + this.taskRunCounter.get(taskType)!.reset(); + } + } + + public processTaskLifecycleEvent(taskEvent: TaskLifecycleEvent) { + const { task }: RanTask | ErroredTask = unwrap((taskEvent as TaskRun).event); + const taskType = task.taskType; + + const taskTypeSuccessRate: SuccessRateCounter = + this.taskRunCounter.get(taskType) ?? new SuccessRateCounter(); + + const success = isOk((taskEvent as TaskRun).event); + this.taskRunSuccessRate.increment(success); + taskTypeSuccessRate.increment(success); + this.taskRunCounter.set(taskType, taskTypeSuccessRate); + + const taskTypeGroup = this.getTaskTypeGroup(taskType); + if (taskTypeGroup) { + const taskTypeGroupSuccessRate: SuccessRateCounter = + this.taskRunCounter.get(taskTypeGroup) ?? new SuccessRateCounter(); + taskTypeGroupSuccessRate.increment(success); + this.taskRunCounter.set(taskTypeGroup, taskTypeGroupSuccessRate); + } + } + + private collectTaskTypeEntries() { + const collected: Record = {}; + for (const [key, value] of this.taskRunCounter) { + collected[key] = value.get(); + } + return collected; + } + + private getTaskTypeGroup(taskType: string): string | undefined { + for (const group of taskTypeGrouping) { + if (taskType.startsWith(group)) { + return group.replaceAll(':', ''); + } + } + } +} diff --git a/x-pack/plugins/task_manager/server/metrics/types.ts b/x-pack/plugins/task_manager/server/metrics/types.ts new file mode 100644 index 00000000000000..7fbee1fe8abdd9 --- /dev/null +++ b/x-pack/plugins/task_manager/server/metrics/types.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 { TaskLifecycleEvent } from '../polling_lifecycle'; + +export interface ITaskMetricsAggregator { + initialMetric: () => T; + collect: () => T; + reset: () => void; + processTaskLifecycleEvent: (taskEvent: TaskLifecycleEvent) => void; +} diff --git a/x-pack/plugins/task_manager/server/monitoring/background_task_utilization_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/background_task_utilization_statistics.test.ts index cdd67a07ff9e71..9507b3ab0e4cd3 100644 --- a/x-pack/plugins/task_manager/server/monitoring/background_task_utilization_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/background_task_utilization_statistics.test.ts @@ -19,7 +19,7 @@ import { import { asOk } from '../lib/result_type'; import { TaskLifecycleEvent } from '../polling_lifecycle'; import { TaskRunResult } from '../task_running'; -import { AggregatedStat } from './runtime_statistics_aggregator'; +import { AggregatedStat } from '../lib/runtime_statistics_aggregator'; import { taskPollingLifecycleMock } from '../polling_lifecycle.mock'; import { BackgroundTaskUtilizationStat, diff --git a/x-pack/plugins/task_manager/server/monitoring/background_task_utilization_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/background_task_utilization_statistics.ts index fd116cbdd71d82..837f29c83f108b 100644 --- a/x-pack/plugins/task_manager/server/monitoring/background_task_utilization_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/background_task_utilization_statistics.ts @@ -20,7 +20,7 @@ import { TaskTiming, } from '../task_events'; import { MonitoredStat } from './monitoring_stats_stream'; -import { AggregatedStat, AggregatedStatProvider } from './runtime_statistics_aggregator'; +import { AggregatedStat, AggregatedStatProvider } from '../lib/runtime_statistics_aggregator'; import { createRunningAveragedStat } from './task_run_calcultors'; import { DEFAULT_WORKER_UTILIZATION_RUNNING_AVERAGE_WINDOW } from '../config'; 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 98493ae89b6838..689c9c882bee32 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 @@ -52,6 +52,7 @@ describe('Configuration Statistics Aggregator', () => { delay: 3000, max_attempts: 20, }, + metrics_reset_interval: 3000, }; const managedConfig = { diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.ts index 6414c9e80ce06a..2212affcc8db3a 100644 --- a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.ts @@ -8,7 +8,7 @@ import { combineLatest, of } from 'rxjs'; import { pick, merge } from 'lodash'; import { map, startWith } from 'rxjs/operators'; -import { AggregatedStatProvider } from './runtime_statistics_aggregator'; +import { AggregatedStatProvider } from '../lib/runtime_statistics_aggregator'; import { TaskManagerConfig } from '../config'; import { ManagedConfiguration } from '../lib/create_managed_configuration'; diff --git a/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.test.ts index 8a2305c3076a50..8d4ef4fab2ebad 100644 --- a/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.test.ts @@ -26,7 +26,7 @@ import { SummarizedEphemeralTaskStat, EphemeralTaskStat, } from './ephemeral_task_statistics'; -import { AggregatedStat } from './runtime_statistics_aggregator'; +import { AggregatedStat } from '../lib/runtime_statistics_aggregator'; import { ephemeralTaskLifecycleMock } from '../ephemeral_task_lifecycle.mock'; import { times, takeRight, take as takeLeft } from 'lodash'; diff --git a/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts index 52aa2b1eead251..8a6ade503b041d 100644 --- a/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts @@ -9,7 +9,7 @@ import { map, filter, startWith, buffer, share } from 'rxjs/operators'; import { JsonObject } from '@kbn/utility-types'; import { combineLatest, Observable, zip } from 'rxjs'; import { isOk, Ok } from '../lib/result_type'; -import { AggregatedStat, AggregatedStatProvider } from './runtime_statistics_aggregator'; +import { AggregatedStat, AggregatedStatProvider } from '../lib/runtime_statistics_aggregator'; import { EphemeralTaskLifecycle } from '../ephemeral_task_lifecycle'; import { TaskLifecycleEvent } from '../polling_lifecycle'; import { isTaskRunEvent, isTaskManagerStatEvent } from '../task_events'; 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 995db14fa09eaa..daf3f2baf085df 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 @@ -8,8 +8,9 @@ import { TaskManagerConfig } from '../config'; import { of, Subject } from 'rxjs'; import { take, bufferCount } from 'rxjs/operators'; -import { createMonitoringStatsStream, AggregatedStat } from './monitoring_stats_stream'; +import { createMonitoringStatsStream } from './monitoring_stats_stream'; import { JsonValue } from '@kbn/utility-types'; +import { AggregatedStat } from '../lib/runtime_statistics_aggregator'; beforeEach(() => { jest.resetAllMocks(); @@ -56,6 +57,7 @@ describe('createMonitoringStatsStream', () => { delay: 3000, max_attempts: 20, }, + metrics_reset_interval: 3000, }; it('returns the initial config used to configure Task Manager', async () => { diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts index e1ff38d1c96078..62505a34d7f89a 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts @@ -37,13 +37,11 @@ import { import { ConfigStat, createConfigurationAggregator } from './configuration_statistics'; import { TaskManagerConfig } from '../config'; -import { AggregatedStatProvider } from './runtime_statistics_aggregator'; import { ManagedConfiguration } from '../lib/create_managed_configuration'; import { EphemeralTaskLifecycle } from '../ephemeral_task_lifecycle'; import { CapacityEstimationStat, withCapacityEstimate } from './capacity_estimation'; import { AdHocTaskCounter } from '../lib/adhoc_task_counter'; - -export type { AggregatedStatProvider, AggregatedStat } from './runtime_statistics_aggregator'; +import { AggregatedStatProvider } from '../lib/runtime_statistics_aggregator'; export interface MonitoringStats { last_update: string; diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts index 4d69b23b699b79..91e81013b726f7 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts @@ -30,7 +30,7 @@ import { TaskRunStat, SummarizedTaskRunStat, } from './task_run_statistics'; -import { AggregatedStat } from './runtime_statistics_aggregator'; +import { AggregatedStat } from '../lib/runtime_statistics_aggregator'; import { FillPoolResult } from '../lib/fill_pool'; import { taskPollingLifecycleMock } from '../polling_lifecycle.mock'; import { configSchema } from '../config'; diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts index 0c6063af19286c..7b7db8cb25eed5 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts @@ -10,7 +10,7 @@ import { filter, startWith, map } from 'rxjs/operators'; import { JsonObject, JsonValue } from '@kbn/utility-types'; import { isNumber, mapValues } from 'lodash'; import { Logger } from '@kbn/core/server'; -import { AggregatedStatProvider, AggregatedStat } from './runtime_statistics_aggregator'; +import { AggregatedStatProvider, AggregatedStat } from '../lib/runtime_statistics_aggregator'; import { TaskLifecycleEvent } from '../polling_lifecycle'; import { isTaskRunEvent, diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index bacd05dcb6a06a..b4d5db14a12e4d 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -12,7 +12,7 @@ import { JsonObject } from '@kbn/utility-types'; import { keyBy, mapValues } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { AggregationResultOf } from '@kbn/es-types'; -import { AggregatedStatProvider } from './runtime_statistics_aggregator'; +import { AggregatedStatProvider } from '../lib/runtime_statistics_aggregator'; import { parseIntervalAsSecond, asInterval, parseIntervalAsMillisecond } from '../lib/intervals'; import { HealthStatus } from './monitoring_stats_stream'; import { TaskStore } from '../task_store'; diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 4c0c96c7f76a65..1e7215d6d7a1b1 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -77,6 +77,7 @@ const pluginInitializerContextParams = { delay: 3000, max_attempts: 20, }, + metrics_reset_interval: 3000, }; describe('TaskManagerPlugin', () => { diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index e65574cef779a8..3b8ab4a54be1fb 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -27,7 +27,7 @@ import { TaskDefinitionRegistry, TaskTypeDictionary, REMOVED_TYPES } from './tas import { AggregationOpts, FetchResult, SearchOpts, TaskStore } from './task_store'; import { createManagedConfiguration } from './lib/create_managed_configuration'; import { TaskScheduling } from './task_scheduling'; -import { backgroundTaskUtilizationRoute, healthRoute } from './routes'; +import { backgroundTaskUtilizationRoute, healthRoute, metricsRoute } from './routes'; import { createMonitoringStats, MonitoringStats } from './monitoring'; import { EphemeralTaskLifecycle } from './ephemeral_task_lifecycle'; import { EphemeralTask, ConcreteTaskInstance } from './task'; @@ -35,6 +35,7 @@ import { registerTaskManagerUsageCollector } from './usage'; import { TASK_MANAGER_INDEX } from './constants'; import { AdHocTaskCounter } from './lib/adhoc_task_counter'; import { setupIntervalLogging } from './lib/log_health_metrics'; +import { metricsStream, Metrics } from './metrics'; export interface TaskManagerSetupContract { /** @@ -82,6 +83,8 @@ export class TaskManagerPlugin private middleware: Middleware = createInitialMiddleware(); private elasticsearchAndSOAvailability$?: Observable; private monitoringStats$ = new Subject(); + private metrics$ = new Subject(); + private resetMetrics$ = new Subject(); private shouldRunBackgroundTasks: boolean; private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; private adHocTaskCounter: AdHocTaskCounter; @@ -155,6 +158,12 @@ export class TaskManagerPlugin getClusterClient: () => startServicesPromise.then(({ elasticsearch }) => elasticsearch.client), }); + metricsRoute({ + router, + metrics$: this.metrics$, + resetMetrics$: this.resetMetrics$, + taskManagerId: this.taskManagerId, + }); core.status.derivedStatus$.subscribe((status) => this.logger.debug(`status core.status.derivedStatus now set to ${status.level}`) @@ -276,6 +285,10 @@ export class TaskManagerPlugin this.ephemeralTaskLifecycle ).subscribe((stat) => this.monitoringStats$.next(stat)); + metricsStream(this.config!, this.resetMetrics$, this.taskPollingLifecycle).subscribe((metric) => + this.metrics$.next(metric) + ); + const taskScheduling = new TaskScheduling({ logger: this.logger, taskStore, 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 62e6be589b4cfc..79b153f42a88d6 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -82,6 +82,7 @@ describe('TaskPollingLifecycle', () => { delay: 3000, max_attempts: 20, }, + metrics_reset_interval: 3000, }, taskStore: mockTaskStore, logger: taskManagerLogger, diff --git a/x-pack/plugins/task_manager/server/routes/index.ts b/x-pack/plugins/task_manager/server/routes/index.ts index f3ba539323f8e9..372996f7cea3df 100644 --- a/x-pack/plugins/task_manager/server/routes/index.ts +++ b/x-pack/plugins/task_manager/server/routes/index.ts @@ -7,3 +7,4 @@ export { healthRoute } from './health'; export { backgroundTaskUtilizationRoute } from './background_task_utilization'; +export { metricsRoute } from './metrics'; diff --git a/x-pack/plugins/task_manager/server/routes/metrics.test.ts b/x-pack/plugins/task_manager/server/routes/metrics.test.ts new file mode 100644 index 00000000000000..a9703aa7548dd1 --- /dev/null +++ b/x-pack/plugins/task_manager/server/routes/metrics.test.ts @@ -0,0 +1,82 @@ +/* + * 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 { of, Subject } from 'rxjs'; +import { v4 as uuidv4 } from 'uuid'; +import { httpServiceMock } from '@kbn/core/server/mocks'; +import { metricsRoute } from './metrics'; +import { mockHandlerArguments } from './_mock_handler_arguments'; + +describe('metricsRoute', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('registers route', async () => { + const router = httpServiceMock.createRouter(); + metricsRoute({ + router, + metrics$: of(), + resetMetrics$: new Subject(), + taskManagerId: uuidv4(), + }); + + const [config] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/task_manager/metrics"`); + }); + + it('emits resetMetric$ event when route is accessed and reset query param is true', async () => { + let resetCalledTimes = 0; + const resetMetrics$ = new Subject(); + + resetMetrics$.subscribe(() => { + resetCalledTimes++; + }); + const router = httpServiceMock.createRouter(); + metricsRoute({ + router, + metrics$: of(), + resetMetrics$, + taskManagerId: uuidv4(), + }); + + const [config, handler] = router.get.mock.calls[0]; + const [context, req, res] = mockHandlerArguments({}, { query: { reset: true } }, ['ok']); + + expect(config.path).toMatchInlineSnapshot(`"/api/task_manager/metrics"`); + + await handler(context, req, res); + + expect(resetCalledTimes).toEqual(1); + }); + + it('does not emit resetMetric$ event when route is accessed and reset query param is false', async () => { + let resetCalledTimes = 0; + const resetMetrics$ = new Subject(); + + resetMetrics$.subscribe(() => { + resetCalledTimes++; + }); + const router = httpServiceMock.createRouter(); + metricsRoute({ + router, + metrics$: of(), + resetMetrics$, + taskManagerId: uuidv4(), + }); + + const [config, handler] = router.get.mock.calls[0]; + const [context, req, res] = mockHandlerArguments({}, { query: { reset: false } }, ['ok']); + + expect(config.path).toMatchInlineSnapshot(`"/api/task_manager/metrics"`); + + await handler(context, req, res); + + expect(resetCalledTimes).toEqual(0); + }); +}); diff --git a/x-pack/plugins/task_manager/server/routes/metrics.ts b/x-pack/plugins/task_manager/server/routes/metrics.ts new file mode 100644 index 00000000000000..737f2b44fd79e4 --- /dev/null +++ b/x-pack/plugins/task_manager/server/routes/metrics.ts @@ -0,0 +1,74 @@ +/* + * 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 { + IRouter, + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, +} from '@kbn/core/server'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { Observable, Subject } from 'rxjs'; +import { Metrics } from '../metrics'; + +export interface NodeMetrics { + process_uuid: string; + timestamp: string; + last_update: string; + metrics: Metrics['metrics'] | null; +} + +export interface MetricsRouteParams { + router: IRouter; + metrics$: Observable; + resetMetrics$: Subject; + taskManagerId: string; +} + +const QuerySchema = schema.object({ + reset: schema.boolean({ defaultValue: true }), +}); + +export function metricsRoute(params: MetricsRouteParams) { + const { router, metrics$, resetMetrics$, taskManagerId } = params; + + let lastMetrics: NodeMetrics | null = null; + + metrics$.subscribe((metrics) => { + lastMetrics = { process_uuid: taskManagerId, timestamp: new Date().toISOString(), ...metrics }; + }); + + router.get( + { + path: `/api/task_manager/metrics`, + options: { + access: 'public', + }, + // Uncomment when we determine that we can restrict API usage to Global admins based on telemetry + // options: { tags: ['access:taskManager'] }, + validate: { + query: QuerySchema, + }, + }, + async function ( + _: RequestHandlerContext, + req: KibanaRequest, unknown>, + res: KibanaResponseFactory + ): Promise { + if (req.query.reset) { + resetMetrics$.next(true); + } + + return res.ok({ + body: lastMetrics + ? lastMetrics + : { process_uuid: taskManagerId, timestamp: new Date().toISOString(), metrics: {} }, + }); + } + ); +} 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 97eeb17f0cd4e0..7e897840f72c7b 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 @@ -1298,6 +1298,45 @@ describe('TaskManagerRunner', () => { ); }); + test('emits TaskEvent when a recurring task returns a success result with hasError=true', async () => { + const id = _.random(1, 20).toString(); + const runAt = minutesFromNow(_.random(5)); + const onTaskEvent = jest.fn(); + const { runner, instance } = await readyToRunStageSetup({ + onTaskEvent, + instance: { + id, + schedule: { interval: '1m' }, + }, + definitions: { + bar: { + title: 'Bar!', + createTaskRunner: () => ({ + async run() { + return { runAt, state: {}, hasError: true }; + }, + }), + }, + }, + }); + + await runner.run(); + + expect(onTaskEvent).toHaveBeenCalledWith( + withAnyTiming( + asTaskRunEvent( + id, + asErr({ + task: instance, + persistence: TaskPersistence.Recurring, + result: TaskRunResult.Success, + error: new Error(`Alerting task failed to run.`), + }) + ) + ) + ); + }); + test('emits TaskEvent when a task run throws an error', async () => { const id = _.random(1, 20).toString(); const error = new Error('Dangit!'); 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 8ad020684e2696..e8ec5cb0f2d917 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 @@ -47,6 +47,7 @@ import { FailedRunResult, FailedTaskResult, isFailedRunResult, + RunContext, SuccessfulRunResult, TaskDefinition, TaskStatus, @@ -321,9 +322,9 @@ export class TaskManagerRunner implements TaskRunner { let taskParamsValidation; if (this.requeueInvalidTasksConfig.enabled) { - taskParamsValidation = this.validateTaskParams(); + taskParamsValidation = this.validateTaskParams(modifiedContext); if (!taskParamsValidation.error) { - taskParamsValidation = await this.validateIndirectTaskParams(); + taskParamsValidation = await this.validateIndirectTaskParams(modifiedContext); } } @@ -359,9 +360,9 @@ export class TaskManagerRunner implements TaskRunner { } } - private validateTaskParams() { + private validateTaskParams({ taskInstance }: RunContext) { let error; - const { state, taskType, params, id, numSkippedRuns = 0 } = this.instance.task; + const { state, taskType, params, id, numSkippedRuns = 0 } = taskInstance; const { max_attempts: maxAttempts } = this.requeueInvalidTasksConfig; try { @@ -383,9 +384,9 @@ export class TaskManagerRunner implements TaskRunner { return { ...(error ? { error } : {}), state }; } - private async validateIndirectTaskParams() { + private async validateIndirectTaskParams({ taskInstance }: RunContext) { let error; - const { state, taskType, id, numSkippedRuns = 0 } = this.instance.task; + const { state, taskType, id, numSkippedRuns = 0 } = taskInstance; const { max_attempts: maxAttempts } = this.requeueInvalidTasksConfig; const indirectParamsSchema = this.definition.indirectParamsSchema; @@ -735,23 +736,30 @@ export class TaskManagerRunner implements TaskRunner { await eitherAsync( result, - async ({ runAt, schedule }: SuccessfulRunResult) => { - this.onTaskEvent( - asTaskRunEvent( - this.id, - asOk({ - task, - persistence: - schedule || task.schedule - ? TaskPersistence.Recurring - : TaskPersistence.NonRecurring, - result: await (runAt || schedule || task.schedule - ? this.processResultForRecurringTask(result) - : this.processResultWhenDone()), - }), - taskTiming - ) - ); + async ({ runAt, schedule, hasError }: SuccessfulRunResult) => { + const processedResult = { + task, + persistence: + schedule || task.schedule ? TaskPersistence.Recurring : TaskPersistence.NonRecurring, + result: await (runAt || schedule || task.schedule + ? this.processResultForRecurringTask(result) + : this.processResultWhenDone()), + }; + + // Alerting task runner returns SuccessfulRunResult with hasError=true + // when the alerting task fails, so we check for this condition in order + // to emit the correct task run event for metrics collection + const taskRunEvent = hasError + ? asTaskRunEvent( + this.id, + asErr({ + ...processedResult, + error: new Error(`Alerting task failed to run.`), + }), + taskTiming + ) + : asTaskRunEvent(this.id, asOk(processedResult), taskTiming); + this.onTaskEvent(taskRunEvent); }, async ({ error }: FailedRunResult) => { this.onTaskEvent( diff --git a/x-pack/plugins/transform/common/privilege/has_privilege_factory.ts b/x-pack/plugins/transform/common/privilege/has_privilege_factory.ts index 972f8e727f50d1..9dee0c1a73cf18 100644 --- a/x-pack/plugins/transform/common/privilege/has_privilege_factory.ts +++ b/x-pack/plugins/transform/common/privilege/has_privilege_factory.ts @@ -12,6 +12,11 @@ import { cloneDeep } from 'lodash'; import { APP_INDEX_PRIVILEGES } from '../constants'; import { Privileges } from '../types/privileges'; +export interface PrivilegesAndCapabilities { + privileges: Privileges; + capabilities: Capabilities; +} + export interface TransformCapabilities { canGetTransform: boolean; canDeleteTransform: boolean; @@ -89,7 +94,7 @@ export const getPrivilegesAndCapabilities = ( clusterPrivileges: Record, hasOneIndexWithAllPrivileges: boolean, hasAllPrivileges: boolean -) => { +): PrivilegesAndCapabilities => { const privilegesResult: Privileges = { hasAllPrivileges: true, missingPrivileges: { diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index f10b48de27e38e..ac4b7fe49a38cb 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -8,11 +8,6 @@ // actual mocks export const expandLiteralStrings = jest.fn(); export const XJsonMode = jest.fn(); -export const useRequest = jest.fn(() => ({ - isLoading: false, - error: null, - data: undefined, -})); export const getSavedSearch = jest.fn(); // just passing through the reimports diff --git a/x-pack/plugins/transform/public/app/app.tsx b/x-pack/plugins/transform/public/app/app.tsx index af411d0aeda6fb..ba4a43bfa0876c 100644 --- a/x-pack/plugins/transform/public/app/app.tsx +++ b/x-pack/plugins/transform/public/app/app.tsx @@ -7,14 +7,13 @@ import React, { useContext, FC } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Router, Routes, Route } from '@kbn/shared-ux-router'; - -import { ScopedHistory } from '@kbn/core/public'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { EuiErrorBoundary } from '@elastic/eui'; +import { Router, Routes, Route } from '@kbn/shared-ux-router'; +import { ScopedHistory } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; - import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { addInternalBasePath } from '../../common/constants'; @@ -23,7 +22,6 @@ import { SectionError } from './components'; import { SECTION_SLUG } from './common/constants'; import { AuthorizationContext, AuthorizationProvider } from './lib/authorization'; import { AppDependencies } from './app_dependencies'; - import { CloneTransformSection } from './sections/clone_transform'; import { CreateTransformSection } from './sections/create_transform'; import { TransformManagementSection } from './sections/transform_management'; @@ -63,20 +61,23 @@ export const App: FC<{ history: ScopedHistory }> = ({ history }) => { export const renderApp = (element: HTMLElement, appDependencies: AppDependencies) => { const I18nContext = appDependencies.i18n.Context; + const queryClient = new QueryClient(); render( - - - - - - - - - + + + + + + + + + + + , element ); diff --git a/x-pack/plugins/transform/public/app/hooks/index.ts b/x-pack/plugins/transform/public/app/hooks/index.ts index eb2be5f4b9b237..f6a4c72b39a44f 100644 --- a/x-pack/plugins/transform/public/app/hooks/index.ts +++ b/x-pack/plugins/transform/public/app/hooks/index.ts @@ -12,4 +12,3 @@ export { useResetTransforms } from './use_reset_transform'; export { useScheduleNowTransforms } from './use_schedule_now_transform'; export { useStartTransforms } from './use_start_transform'; export { useStopTransforms } from './use_stop_transform'; -export { useRequest } from './use_request'; diff --git a/x-pack/plugins/transform/public/app/hooks/use_request.ts b/x-pack/plugins/transform/public/app/hooks/use_request.ts deleted file mode 100644 index de1df3e5616126..00000000000000 --- a/x-pack/plugins/transform/public/app/hooks/use_request.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { UseRequestConfig, useRequest as _useRequest } from '../../shared_imports'; - -import { useAppDependencies } from '../app_dependencies'; - -export const useRequest = (config: UseRequestConfig) => { - const { http } = useAppDependencies(); - return _useRequest(http, config); -}; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx index 4271e1447b1e87..02bbe4e40a9691 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx @@ -6,16 +6,20 @@ */ import React, { createContext } from 'react'; +import { useQuery } from '@tanstack/react-query'; -import { Privileges } from '../../../../../common/types/privileges'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; -import { useRequest } from '../../../hooks'; +import type { Privileges } from '../../../../../common/types/privileges'; import { - TransformCapabilities, + type PrivilegesAndCapabilities, + type TransformCapabilities, INITIAL_CAPABILITIES, } from '../../../../../common/privilege/has_privilege_factory'; +import { useAppDependencies } from '../../../app_dependencies'; + interface Authorization { isLoading: boolean; apiError: Error | null; @@ -41,22 +45,36 @@ interface Props { } export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) => { + const { http } = useAppDependencies(); + const { path, version } = privilegesEndpoint; + const { isLoading, error, data: privilegesData, - } = useRequest({ - path, - version, - method: 'get', - }); + } = useQuery( + ['transform-privileges-and-capabilities'], + async ({ signal }) => { + return await http.fetch(path, { + version, + method: 'GET', + signal, + }); + } + ); const value = { isLoading, - privileges: isLoading ? { ...initialValue.privileges } : privilegesData.privileges, - capabilities: isLoading ? { ...INITIAL_CAPABILITIES } : privilegesData.capabilities, - apiError: error ? (error as Error) : null, + privileges: + isLoading || privilegesData === undefined + ? { ...initialValue.privileges } + : privilegesData.privileges, + capabilities: + isLoading || privilegesData === undefined + ? { ...INITIAL_CAPABILITIES } + : privilegesData.capabilities, + apiError: error ? error : null, }; return ( diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index c24f792eacab00..63276cecc7a866 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -6,8 +6,6 @@ */ export { XJsonMode } from '@kbn/ace'; -export type { UseRequestConfig } from '@kbn/es-ui-shared-plugin/public'; -export { useRequest } from '@kbn/es-ui-shared-plugin/public'; export type { GetMlSharedImportsReturnType } from '@kbn/ml-plugin/public'; export { getMlSharedImports } from '@kbn/ml-plugin/public'; diff --git a/x-pack/plugins/transform/server/routes/api/privileges.ts b/x-pack/plugins/transform/server/routes/api/privileges.ts index cd6817f8be63c4..0b93c8e503fc6b 100644 --- a/x-pack/plugins/transform/server/routes/api/privileges.ts +++ b/x-pack/plugins/transform/server/routes/api/privileges.ts @@ -5,15 +5,17 @@ * 2.0. */ -import type { IScopedClusterClient } from '@kbn/core/server'; +import type { IKibanaResponse, IScopedClusterClient } from '@kbn/core/server'; import type { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types'; -import { getPrivilegesAndCapabilities } from '../../../common/privilege/has_privilege_factory'; +import { + getPrivilegesAndCapabilities, + type PrivilegesAndCapabilities, +} from '../../../common/privilege/has_privilege_factory'; import { addInternalBasePath, APP_CLUSTER_PRIVILEGES, APP_INDEX_PRIVILEGES, } from '../../../common/constants'; -import type { Privileges } from '../../../common/types/privileges'; import type { RouteDependencies } from '../../types'; @@ -23,64 +25,60 @@ export function registerPrivilegesRoute({ router, license }: RouteDependencies) path: addInternalBasePath('privileges'), access: 'internal', }) - .addVersion( + .addVersion( { version: '1', validate: false, }, - license.guardApiRoute(async (ctx, req, res) => { - const privilegesResult: Privileges = { - hasAllPrivileges: true, - missingPrivileges: { - cluster: [], - index: [], - }, - }; - - if (license.getStatus().isSecurityEnabled === false) { - // If security isn't enabled, let the user use app. - return res.ok({ body: privilegesResult }); - } + license.guardApiRoute( + async (ctx, req, res): Promise> => { + if (license.getStatus().isSecurityEnabled === false) { + // If security isn't enabled, let the user use app. + return res.ok({ + body: getPrivilegesAndCapabilities({}, true, true), + }); + } - const esClient: IScopedClusterClient = (await ctx.core).elasticsearch.client; + const esClient: IScopedClusterClient = (await ctx.core).elasticsearch.client; - const esClusterPrivilegesReq: Promise = - esClient.asCurrentUser.security.hasPrivileges({ - body: { - cluster: APP_CLUSTER_PRIVILEGES, - }, - }); - const [esClusterPrivileges, userPrivileges] = await Promise.all([ - // Get cluster privileges - esClusterPrivilegesReq, - // // Get all index privileges the user has - esClient.asCurrentUser.security.getUserPrivileges(), - ]); + const esClusterPrivilegesReq: Promise = + esClient.asCurrentUser.security.hasPrivileges({ + body: { + cluster: APP_CLUSTER_PRIVILEGES, + }, + }); + const [esClusterPrivileges, userPrivileges] = await Promise.all([ + // Get cluster privileges + esClusterPrivilegesReq, + // // Get all index privileges the user has + esClient.asCurrentUser.security.getUserPrivileges(), + ]); - const { has_all_requested: hasAllPrivileges, cluster } = esClusterPrivileges; - const { indices } = userPrivileges; + const { has_all_requested: hasAllPrivileges, cluster } = esClusterPrivileges; + const { indices } = userPrivileges; - // Check if they have all the required index privileges for at least one index - const hasOneIndexWithAllPrivileges = - indices.find(({ privileges }: { privileges: string[] }) => { - if (privileges.includes('all')) { - return true; - } + // Check if they have all the required index privileges for at least one index + const hasOneIndexWithAllPrivileges = + indices.find(({ privileges }: { privileges: string[] }) => { + if (privileges.includes('all')) { + return true; + } - const indexHasAllPrivileges = APP_INDEX_PRIVILEGES.every((privilege) => - privileges.includes(privilege) - ); + const indexHasAllPrivileges = APP_INDEX_PRIVILEGES.every((privilege) => + privileges.includes(privilege) + ); - return indexHasAllPrivileges; - }) !== undefined; + return indexHasAllPrivileges; + }) !== undefined; - return res.ok({ - body: getPrivilegesAndCapabilities( - cluster, - hasOneIndexWithAllPrivileges, - hasAllPrivileges - ), - }); - }) + return res.ok({ + body: getPrivilegesAndCapabilities( + cluster, + hasOneIndexWithAllPrivileges, + hasAllPrivileges + ), + }); + } + ) ); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index dcd9d175820be4..720b0e8d549a2d 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -399,7 +399,6 @@ "controls.controlGroup.manageControl.controlTypeSettings.formGroupDescription": "Paramètres personnalisés pour votre contrôle {controlType}.", "controls.controlGroup.manageControl.controlTypeSettings.formGroupTitle": "Paramètres de {controlType}", "controls.optionsList.controlAndPopover.exists": "{negate, plural, one {Existe} many {Existent} other {Existent}}", - "controls.optionsList.errors.dataViewNotFound": "Impossible de localiser la vue de données : {dataViewId}", "controls.optionsList.errors.fieldNotFound": "Impossible de localiser le champ : {fieldName}", "controls.optionsList.popover.ariaLabel": "Fenêtre contextuelle pour le contrôle {fieldName}", "controls.optionsList.popover.cardinalityLabel": "{totalOptions, number} {totalOptions, plural, one {option} many {options disponibles} other {options}}", @@ -409,7 +408,6 @@ "controls.optionsList.popover.invalidSelectionsLabel": "{selectedOptions} {selectedOptions, plural, one {sélection ignorée} many {Sélections ignorées} other {sélections ignorées}}", "controls.optionsList.popover.invalidSelectionsSectionTitle": "{invalidSelectionCount, plural, one {Sélection ignorée} many {Sélections ignorées} other {Sélections ignorées}}", "controls.optionsList.popover.suggestionsAriaLabel": "{optionCount, plural, one {option disponible} many {options disponibles} other {options disponibles}} pour {fieldName}", - "controls.rangeSlider.errors.dataViewNotFound": "Impossible de localiser la vue de données : {dataViewId}", "controls.rangeSlider.errors.fieldNotFound": "Impossible de localiser le champ : {fieldName}", "controls.controlGroup.emptyState.addControlButtonTitle": "Ajouter un contrôle", "controls.controlGroup.emptyState.badgeText": "Nouveauté", @@ -12154,7 +12152,6 @@ "xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.isInvalid.error": "{indexName} n'est pas un nom d'index valide", "xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.nameInputHelpText.lineOne": "Votre index sera nommé : {indexName}", "xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description": "Un index supprimé appelé {indexName} était, à l'origine, lié à une configuration de connecteur. Voulez-vous remplacer cette configuration de connecteur par la nouvelle ?", - "xpack.enterpriseSearch.content.overview.documentExample.description.text": "Générez une clé API et lisez la {documentation} concernant l’envoi de documents au point de terminaison de l’API Elasticsearch. Utilisez des {clients} Elastic pour une intégration rationalisée.", "xpack.enterpriseSearch.content.searchIndex.documents.documentList.description": "Affichage de {results} sur {total}. Nombre maximal de résultats de recherche de {maximum} documents.", "xpack.enterpriseSearch.content.searchIndex.documents.documentList.pagination.itemsPerPage": "Documents par page : {docPerPage}", "xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option": "{docCount} documents", @@ -13770,10 +13767,6 @@ "xpack.enterpriseSearch.content.overview.documentExample.clientLibraries.python": "Python", "xpack.enterpriseSearch.content.overview.documentExample.clientLibraries.ruby": "Ruby", "xpack.enterpriseSearch.content.overview.documentExample.clientLibraries.rust": "Rust", - "xpack.enterpriseSearch.content.overview.documentExample.description.clientsLink": "clients de langages de programmation", - "xpack.enterpriseSearch.content.overview.documentExample.description.documentationLink": "documentation", - "xpack.enterpriseSearch.content.overview.documentExample.generateApiKeyButton.label": "Gérer les clés d'API", - "xpack.enterpriseSearch.content.overview.documentExample.title": "Ajout de documents à votre index", "xpack.enterpriseSearch.content.overview.emptyPrompt.body": "Nous déconseillons l'ajout de documents à un index géré en externe.", "xpack.enterpriseSearch.content.overview.emptyPrompt.title": "Index géré en externe", "xpack.enterpriseSearch.content.overview.generateApiKeyModal.apiKeyWarning": "Elastic ne stocke pas les clés d’API. Une fois la clé générée, vous ne pourrez la visualiser qu'une seule fois. Veillez à l'enregistrer dans un endroit sûr. Si vous n'y avez plus accès, vous devrez générer une nouvelle clé d’API à partir de cet écran.", @@ -13783,7 +13776,6 @@ "xpack.enterpriseSearch.content.overview.generateApiKeyModal.info": "Avant de pouvoir commencer à publier des documents dans votre index Elasticsearch, vous devez créer au moins une clé d’API.", "xpack.enterpriseSearch.content.overview.generateApiKeyModal.learnMore": "En savoir plus sur les clés d’API", "xpack.enterpriseSearch.content.overview.generateApiKeyModal.title": "Générer une clé d’API", - "xpack.enterpriseSearch.content.overview.optimizedRequest.label": "Afficher la requête optimisée d'Enterprise Search", "xpack.enterpriseSearch.content.searchIndex.cancelSyncs.successMessage": "Annulation réussie des synchronisations", "xpack.enterpriseSearch.content.searchIndex.configurationTabLabel": "Configuration", "xpack.enterpriseSearch.content.searchIndex.connectorErrorCallOut.title": "Votre connecteur a rapporté une erreur", @@ -14167,16 +14159,9 @@ "xpack.enterpriseSearch.curations.settings.licenseUpgradeLink": "En savoir plus sur les mises à niveau incluses dans la licence", "xpack.enterpriseSearch.curations.settings.start30DayTrialButtonLabel": "Démarrer un essai gratuit de 30 jours", "xpack.enterpriseSearch.descriptionLabel": "Description", - "xpack.enterpriseSearch.elasticsearch.features.buildSearchExperiences": "Créer des expériences de recherche personnalisées", - "xpack.enterpriseSearch.elasticsearch.features.buildTooling": "Créer des outils personnalisés", - "xpack.enterpriseSearch.elasticsearch.features.integrate": "Intégrer à des bases de données, des sites web, etc.", "xpack.enterpriseSearch.elasticsearch.productCardDescription": "Idéal pour les applications sur mesure, Elasticsearch vous aide à créer des recherches hautement personnalisables et offre de nombreuses méthodes d'ingestion différentes.", "xpack.enterpriseSearch.elasticsearch.productDescription": "Outils de bas niveau pour la création d'expériences performantes et pertinentes.", "xpack.enterpriseSearch.elasticsearch.productName": "Elasticsearch", - "xpack.enterpriseSearch.elasticsearch.resources.createNewIndexLabel": "Créer un nouvel index", - "xpack.enterpriseSearch.elasticsearch.resources.gettingStartedLabel": "Prise en main d'Elasticsearch", - "xpack.enterpriseSearch.elasticsearch.resources.languageClientLabel": "Configurer un client de langage", - "xpack.enterpriseSearch.elasticsearch.resources.searchUILabel": "Search UI pour Elasticsearch", "xpack.enterpriseSearch.emailLabel": "E-mail", "xpack.enterpriseSearch.emptyState.description": "Votre contenu est stocké dans un index Elasticsearch. Commencez par créer un index Elasticsearch et sélectionnez une méthode d'ingestion. Les options comprennent le robot d'indexation Elastic, les intégrations de données tierces ou l'utilisation des points de terminaison d'API Elasticsearch.", "xpack.enterpriseSearch.emptyState.description.line2": "Qu’il s’agisse de créer une expérience de recherche avec App Search ou Elasticsearch, vous pouvez commencer ici.", @@ -14411,8 +14396,6 @@ "xpack.enterpriseSearch.overview.elasticsearchResources.gettingStarted": "Prise en main d'Elasticsearch", "xpack.enterpriseSearch.overview.elasticsearchResources.searchUi": "Search UI pour Elasticsearch", "xpack.enterpriseSearch.overview.elasticsearchResources.title": "Ressources", - "xpack.enterpriseSearch.overview.emptyPromptButtonLabel": "Créer un index Elasticsearch", - "xpack.enterpriseSearch.overview.emptyPromptTitle": "Ajouter des données et démarrer les recherches", "xpack.enterpriseSearch.overview.emptyState.buttonTitle": "Ajouter un contenu dans Enterprise Search", "xpack.enterpriseSearch.overview.emptyState.footerLinkTitle": "En savoir plus", "xpack.enterpriseSearch.overview.emptyState.heading": "Ajouter un contenu dans Enterprise Search", @@ -14438,12 +14421,9 @@ "xpack.enterpriseSearch.overview.iconRow.sharePoint.title": "Microsoft SharePoint", "xpack.enterpriseSearch.overview.iconRow.sharePoint.tooltip": "Indexer des contenus depuis Microsoft SharePoint", "xpack.enterpriseSearch.overview.navTitle": "Aperçu", - "xpack.enterpriseSearch.overview.pageTitle": "Bienvenue dans Enterprise Search", - "xpack.enterpriseSearch.overview.productSelector.title": "Des expériences de recherche pour chaque cas d'utilisation", "xpack.enterpriseSearch.overview.searchIndices.image.altText": "Illustration d'index de recherche", "xpack.enterpriseSearch.overview.setupCta.description": "Ajoutez des fonctions de recherche à votre application ou à votre organisation interne avec Elastic App Search et Workplace Search. Regardez la vidéo pour savoir ce qu'il est possible de faire lorsque la recherche est facilitée.", "xpack.enterpriseSearch.passwordLabel": "Mot de passe", - "xpack.enterpriseSearch.productCard.resourcesTitle": "Ressources", "xpack.enterpriseSearch.productSelectorCalloutTitle": "Mettez à niveau pour obtenir des fonctionnalités de niveau entreprise pour votre équipe", "xpack.enterpriseSearch.readOnlyMode.warning": "Enterprise Search est en mode de lecture seule. Vous ne pourrez pas effectuer de changements tels que création, modification ou suppression.", "xpack.enterpriseSearch.roleMapping.addRoleMappingButtonLabel": "Ajouter un mapping", @@ -27312,9 +27292,7 @@ "xpack.observability.threshold.rule.alertDetailsAppSection.criterion.subtitle": "Dernière {lookback} {timeLabel}", "xpack.observability.threshold.rule.alertFlyout.alertPerRedundantFilterError": "Il est possible que cette règle signale {matchedGroups} moins que prévu, car la requête de filtre comporte une correspondance pour {groupCount, plural, one {ce champ} many {ces champs} other {ces champs}}. Pour en savoir plus, consultez notre {filteringAndGroupingLink}.", "xpack.observability.threshold.rule.alertFlyout.customEquationEditor.aggregationLabel": "Agrégation {name}", - "xpack.observability.threshold.rule.alertFlyout.customEquationEditor.fieldLabel": "Champ {name}", "xpack.observability.threshold.rule.alertFlyout.customEquationEditor.filterLabel": "Filtre KQL {name}", - "xpack.observability.threshold.rule.alertFlyout.ofExpression.helpTextDetail": "Vous ne trouvez pas un indicateur ? {documentationLink}.", "xpack.observability.threshold.rule.alerts.dataTimeRangeLabel": "Dernière {lookback} {timeLabel}", "xpack.observability.threshold.rule.alerts.dataTimeRangeLabelWithGrouping": "Dernières {lookback} {timeLabel} de données pour {id}", "xpack.observability.threshold.rule.threshold.errorAlertReason": "Elasticsearch a échoué lors de l'interrogation des données pour {metric}", @@ -27827,9 +27805,7 @@ "xpack.observability.threshold.rule.alertFlyout.error.thresholdRequired": "Le seuil est requis.", "xpack.observability.threshold.rule.alertFlyout.error.thresholdTypeRequired": "Les seuils doivent contenir un nombre valide.", "xpack.observability.threshold.rule.alertFlyout.error.timeRequred": "La taille de temps est requise.", - "xpack.observability.threshold.rule.alertFlyout.expandRowLabel": "Développer la ligne.", "xpack.observability.threshold.rule.alertFlyout.groupDisappearHelpText": "Activez cette option pour déclencher l’action si un groupe précédemment détecté cesse de signaler des résultats. Ce n’est pas recommandé pour les infrastructures à montée en charge dynamique qui peuvent rapidement lancer ou stopper des nœuds automatiquement.", - "xpack.observability.threshold.rule.alertFlyout.ofExpression.popoverLinkLabel": "Apprenez comment ajouter davantage de données", "xpack.observability.threshold.rule.alertFlyout.outsideRangeLabel": "N'est pas entre", "xpack.observability.threshold.rule.alertFlyout.removeCondition": "Retirer la condition", "xpack.observability.threshold.rule.alerting.noDataFormattedValue": "[AUCUNE DONNÉE]", @@ -27869,7 +27845,6 @@ "xpack.observability.threshold.ruleExplorer.groupByAriaLabel": "Graphique par", "xpack.observability.threshold.ruleExplorer.groupByLabel": "Tout", "xpack.observability.threshold.ruleName": "Seuil (Version d'évaluation technique)", - "xpack.observability.thresholdRule.expressionItems.descriptionLabel": "quand", "xpack.observability.uiSettings.betaLabel": "bêta", "xpack.observability.uiSettings.technicalPreviewLabel": "version d'évaluation technique", "xpack.observability.uiSettings.throttlingDocsLinkText": "lisez la notification ici.", @@ -28305,10 +28280,8 @@ "xpack.profiling.noDataConfig.action.buttonLabel": "Configurer Universal Profiling", "xpack.profiling.noDataConfig.action.buttonLoadingLabel": "Configuration de Universal Profiling...", "xpack.profiling.noDataConfig.action.dataRetention.link": "contrôle de la conservation des données", - "xpack.profiling.noDataConfig.action.legalBetaTerms": "En utilisant cette fonctionnalité, vous reconnaissez avoir lu et accepté ", "xpack.profiling.noDataConfig.action.permissionsWarning": "Pour configurer Universal Profiling, vous devez être connecté en tant que superutilisateur.", "xpack.profiling.noDataConfig.action.title": "Universal Profiling fournit un profilage continu sur tout le serveur Fleet et à travers tout le système sans aucune instrumentation.\n Découvrez quelles lignes de code consomment des ressources informatiques, à tout moment et dans votre infrastructure tout entière.", - "xpack.profiling.noDataConfig.betaTerms.linkLabel": "Conditions d'utilisation de la version bêta d'Elastic", "xpack.profiling.noDataConfig.loading.loaderText": "Chargement des sources de données", "xpack.profiling.noDataConfig.pageTitle": "Universal Profiling (maintenant en version bêta)", "xpack.profiling.noDataConfig.solutionName": "Universal Profiling", @@ -28447,7 +28420,6 @@ "xpack.remoteClusters.listBreadcrumbTitle": "Clusters distants", "xpack.remoteClusters.readDocsButtonLabel": "Documents du cluster distant", "xpack.remoteClusters.refreshAction.errorTitle": "Erreur lors de l'actualisation des clusters distants", - "xpack.remoteClusters.remoteClusterForm.actions.savingText": "Enregistrement", "xpack.remoteClusters.remoteClusterForm.addressError.invalidPortMessage": "Un port est requis.", "xpack.remoteClusters.remoteClusterForm.cancelButtonLabel": "Annuler", "xpack.remoteClusters.remoteClusterForm.cloudUrlHelp.buttonLabel": "Besoin d'aide ?", @@ -28483,7 +28455,6 @@ "xpack.remoteClusters.remoteClusterForm.manualModeFieldLabel": "Entrer manuellement l'adresse proxy et le nom du serveur", "xpack.remoteClusters.remoteClusterForm.proxyError.invalidCharactersMessage": "L'adresse doit utiliser le format host:port. Exemple : 127.0.0.1:9400, localhost:9400. Les hôtes ne peuvent comprendre que des lettres, des chiffres et des tirets.", "xpack.remoteClusters.remoteClusterForm.proxyError.missingProxyMessage": "Une adresse proxy est requise.", - "xpack.remoteClusters.remoteClusterForm.saveButtonLabel": "Enregistrer", "xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription": "Configurez automatiquement le cluster distant à l'aide de l'URL de point de terminaison Elasticsearch du déploiement distant ou entrez l'adresse proxy et le nom du serveur manuellement.", "xpack.remoteClusters.remoteClusterForm.sectionModeDescription": "Utilisez les nœuds initiaux par défaut, ou passez au mode proxy.", "xpack.remoteClusters.remoteClusterForm.sectionModeTitle": "Mode de connexion", @@ -30089,7 +30060,6 @@ "xpack.securitySolution.flyout.correlations.relatedCasesHeading": "{count} associé {count, plural, one {cas} many {aux cas suivants} other {aux cas suivants}}", "xpack.securitySolution.flyout.correlations.sessionAlertsHeading": "{count, plural, one {# alerte} many {# alertes} other {Alertes #}} associé(es) par session", "xpack.securitySolution.flyout.correlations.sourceAlertsHeading": "{count, plural, one {# alerte} many {# alertes} other {Alertes #}} associé(es) par événement source", - "xpack.securitySolution.flyout.documentDetails.overviewTab.viewAllButton": "Afficher tous les {text}", "xpack.securitySolution.flyout.errorMessage": "Une erreur est survenue lors de l'affichage de {message}", "xpack.securitySolution.flyout.errorTitle": "Impossible d'afficher {title}", "xpack.securitySolution.footer.autoRefreshActiveTooltip": "Lorsque le rafraîchissement automatique est activé, la chronologie vous montre les {numberOfItems} derniers événements qui correspondent à votre requête.", @@ -33359,7 +33329,6 @@ "xpack.securitySolution.flyout.correlations.timestampColumnTitle": "Horodatage", "xpack.securitySolution.flyout.documentDetails.alertReasonTitle": "Raison d'alerte", "xpack.securitySolution.flyout.documentDetails.analyzerGraphButton": "Graph Analyseur", - "xpack.securitySolution.flyout.documentDetails.analyzerPreviewText": "aperçu de l'analyseur.", "xpack.securitySolution.flyout.documentDetails.analyzerPreviewTitle": "Aperçu de l'analyseur", "xpack.securitySolution.flyout.documentDetails.collapseDetailButton": "Réduire les détails de l'alerte", "xpack.securitySolution.flyout.documentDetails.correlationsButton": "Corrélations", @@ -33388,14 +33357,11 @@ "xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlert": "alerte associée par le même événement source", "xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlerts": "alertes associées par le même événement source", "xpack.securitySolution.flyout.documentDetails.overviewTab.correlationsText": "champs de corrélation", - "xpack.securitySolution.flyout.documentDetails.overviewTab.entitiesText": "les entités sélectionnées", "xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceRowText": "est inhabituel", - "xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceText": "champs de prévalence", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichment": "champ enrichi avec la Threat Intelligence", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichments": "champs enrichis avec la Threat Intelligence", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch": "correspondance de menace détectée", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatches": "correspondances de menaces détectées", - "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligenceText": "champs de Threat Intelligence", "xpack.securitySolution.flyout.documentDetails.prevalenceButton": "Prévalence", "xpack.securitySolution.flyout.documentDetails.prevalenceTitle": "Prévalence", "xpack.securitySolution.flyout.documentDetails.riskScoreTitle": "Score de risque", @@ -34747,12 +34713,6 @@ "xpack.securitySolution.zeek.shrDescription": "L'équipe de réponse a envoyé un SYN ACK suivi d'un FIN, pas de SYN de la part de l'initiateur", "xpack.serverlessSearch.apiKey.activeKeys": "Vous avez {number} clés actives.", "xpack.serverlessSearch.apiKey.expiresHelpText": "Cette clé d’API expirera le {expirationDate}", - "xpack.serverlessSearch.header.greeting.title": "Bonjour {name} !", - "xpack.serverlessSearch.ingestData.clientDocLink": "Référence d’API {languageName}", - "xpack.serverlessSearch.installClient.clientDocLink": "Documentation du client {languageName}", - "xpack.serverlessSearch.selectClient.description": "Elastic construit et assure la maintenance des clients dans plusieurs langues populaires et notre communauté a contribué à beaucoup d'autres. Sélectionnez votre client linguistique favori or explorez la {console} pour commencer.", - "xpack.serverlessSearch.apiCallout.content": "La console vous permet d’appeler directement les API REST d’Elasticsearch et de Kibana, sans avoir à installer de client de langage.", - "xpack.serverlessSearch.apiCallOut.title": "Appeler l’API depuis la console", "xpack.serverlessSearch.apiKey.apiKeyStepDescription": "Cette clé ne s’affichera qu’une fois, conservez-la donc en lieu sûr. Nous ne conservons pas vos clés d’API, vous devrez donc générer une clé de remplacement si vous la perdez.", "xpack.serverlessSearch.apiKey.apiKeyStepTitle": "Stocker cette clé d'API", "xpack.serverlessSearch.apiKey.description": "Vous aurez besoin de ces identifiants uniques pour vous connecter en toute sécurité à votre projet Elasticsearch.", @@ -34785,8 +34745,6 @@ "xpack.serverlessSearch.apiKey.userFieldLabel": "Utilisateur", "xpack.serverlessSearch.back": "Retour", "xpack.serverlessSearch.cancel": "Annuler", - "xpack.serverlessSearch.codeBox.copyButtonLabel": "Copier", - "xpack.serverlessSearch.codeBox.selectAriaLabel": "Sélectionner un langage de programmation", "xpack.serverlessSearch.configureClient.advancedConfigLabel": "Configuration avancée", "xpack.serverlessSearch.configureClient.basicConfigLabel": "Configuration de base", "xpack.serverlessSearch.configureClient.description": "Initialiser votre client avec votre clé d’API et votre identifiant de cloud uniques", @@ -34807,30 +34765,7 @@ "xpack.serverlessSearch.footer.searchUI.description": "L’interface utilisateur Search est une bibliothèque JavaScript libre et gratuite maintenue par Elastic pour un développement rapide d’expériences de recherche modernes et attrayantes.", "xpack.serverlessSearch.footer.searchUI.title": "Créer une interface utilisateur avec Search UI", "xpack.serverlessSearch.footer.title": "Et ensuite ?", - "xpack.serverlessSearch.githubLink.curl.label": "curl", - "xpack.serverlessSearch.githubLink.javascript.label": "elasticsearch", - "xpack.serverlessSearch.githubLink.ruby.label": "elasticsearch-ruby", - "xpack.serverlessSearch.header.description": "Configurez votre client de langage de programmation, ingérez des données, et vous serez prêt à commencer vos recherches en quelques minutes.", "xpack.serverlessSearch.header.title": "Lancez-vous avec Elasticsearch", - "xpack.serverlessSearch.ingestData.beatsDescription": "Des agents légers conçus pour le transfert de données pour Elasticsearch. Utilisez Beats pour envoyer des données opérationnelles depuis vos serveurs.", - "xpack.serverlessSearch.ingestData.beatsLink": "beats", - "xpack.serverlessSearch.ingestData.beatsTitle": "Beats", - "xpack.serverlessSearch.ingestData.connectorsDescription": "Des intégrations spécialisées pour synchroniser des données de sources tierces avec Elasticsearch. Utilisez des connecteurs Elastic pour synchroniser du contenu d’une plage de bases de données et de stockage d’objets.", - "xpack.serverlessSearch.ingestData.connectorsPythonLink": "connecteurs-python", - "xpack.serverlessSearch.ingestData.connectorsTitle": "Client de connecteur", - "xpack.serverlessSearch.ingestData.description": "Ajoutez des données à votre flux de données ou à votre index pour les rendre interrogeables. Choisissez une méthode d’ingestion qui correspond à votre application et à votre workflow.", - "xpack.serverlessSearch.ingestData.ingestApiDescription": "La façon la plus flexible d’indexer des données, ce qui vous donne un contrôle total sur vos options de personnalisation et d’optimisation.", - "xpack.serverlessSearch.ingestData.ingestApiLabel": "Ingérer via une API", - "xpack.serverlessSearch.ingestData.ingestIntegrationDescription": "Des outils d’ingestion spécialisés optimisés pour transformer des données et les transférer à Elasticsearch.", - "xpack.serverlessSearch.ingestData.ingestIntegrationLabel": "Ingérer via l’intégration", - "xpack.serverlessSearch.ingestData.ingestLegendLabel": "Sélectionner une méthode d'ingestion", - "xpack.serverlessSearch.ingestData.integrationsLink": "À propos des intégrations", - "xpack.serverlessSearch.ingestData.logstashDescription": "Ajoutez des données à votre flux de données ou à votre index pour les rendre interrogeables. Choisissez une méthode d’ingestion qui correspond à votre application et à votre workflow.", - "xpack.serverlessSearch.ingestData.logstashLink": "Logstash", - "xpack.serverlessSearch.ingestData.logstashTitle": "Logstash", - "xpack.serverlessSearch.ingestData.title": "Ingérer des données", - "xpack.serverlessSearch.installClient.description": "Elastic construit et assure la maintenance des clients dans plusieurs langues populaires et notre communauté a contribué à beaucoup d'autres. Installez votre client de langage favori pour commencer.", - "xpack.serverlessSearch.installClient.title": "Installer un client", "xpack.serverlessSearch.invalidJsonError": "JSON non valide", "xpack.serverlessSearch.languages.cURL": "cURL", "xpack.serverlessSearch.languages.javascript": "JavaScript / Node.js", @@ -34848,17 +34783,8 @@ "xpack.serverlessSearch.required": "Obligatoire", "xpack.serverlessSearch.searchQuery.description": "Vous êtes maintenant prêt à expérimenter la recherche et l'exécution d'agrégations sur vos données Elasticsearch.", "xpack.serverlessSearch.searchQuery.title": "Créer votre première requête de recherche", - "xpack.serverlessSearch.selectClient.apiRequestConsoleDocLink": "Exécuter des requêtes d’API dans la console ", - "xpack.serverlessSearch.selectClient.callout.description": "Avec la console, vous pouvez directement commencer à utiliser nos API REST. Aucune installation n’est requise. ", - "xpack.serverlessSearch.selectClient.callout.link": "Essayez la console maintenant", - "xpack.serverlessSearch.selectClient.callout.title": "Lancez-vous dans la console", - "xpack.serverlessSearch.selectClient.description.console.link": "Console", - "xpack.serverlessSearch.selectClient.elasticsearchClientDocLink": "Clients d'Elasticsearch ", - "xpack.serverlessSearch.selectClient.heading": "Choisissez-en un", - "xpack.serverlessSearch.selectClient.title": "Sélectionner votre client", "xpack.serverlessSearch.testConnection.description": "Envoyez une requête de test pour confirmer que votre client de langage et votre instance Elasticsearch sont opérationnels.", "xpack.serverlessSearch.testConnection.title": "Tester votre connexion", - "xpack.serverlessSearch.tryInConsoleButton": "Essayer dans la console", "xpack.sessionView.alertFilteredCountStatusLabel": " Affichage de {count} alertes", "xpack.sessionView.alertTotalCountStatusLabel": "Affichage de {count} alertes", "xpack.sessionView.processTree.loadMore": "Afficher les {pageSize} événements suivants", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f49042924be5dd..a239b3d1e04772 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -399,7 +399,6 @@ "controls.controlGroup.manageControl.controlTypeSettings.formGroupDescription": "{controlType}コントロールのカスタム設定", "controls.controlGroup.manageControl.controlTypeSettings.formGroupTitle": "{controlType}設定", "controls.optionsList.controlAndPopover.exists": "{negate, plural, other {存在します}}", - "controls.optionsList.errors.dataViewNotFound": "データビューが見つかりませんでした:{dataViewId}", "controls.optionsList.errors.fieldNotFound": "フィールドが見つかりませんでした:{fieldName}", "controls.optionsList.popover.ariaLabel": "{fieldName}コントロールのポップオーバー", "controls.optionsList.popover.cardinalityLabel": "{totalOptions, number}{totalOptions, plural, other {オプション}}", @@ -409,7 +408,6 @@ "controls.optionsList.popover.invalidSelectionsLabel": "{selectedOptions}{selectedOptions, plural, other {選択項目}}が無視されました", "controls.optionsList.popover.invalidSelectionsSectionTitle": "{invalidSelectionCount, plural, other {選択項目}}が無視されました", "controls.optionsList.popover.suggestionsAriaLabel": "{fieldName}の{optionCount, plural, other {オプション}}があります", - "controls.rangeSlider.errors.dataViewNotFound": "データビューが見つかりませんでした:{dataViewId}", "controls.rangeSlider.errors.fieldNotFound": "フィールドが見つかりませんでした:{fieldName}", "controls.controlGroup.emptyState.addControlButtonTitle": "コントロールを追加", "controls.controlGroup.emptyState.badgeText": "新規", @@ -12168,7 +12166,6 @@ "xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.isInvalid.error": "{indexName}は無効なインデックス名です", "xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.nameInputHelpText.lineOne": "インデックスは次の名前になります:{indexName}", "xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description": "削除されたインデックス{indexName}は、既存のコネクター構成に関連付けられていました。既存のコネクター構成を新しいコネクター構成で置き換えますか?", - "xpack.enterpriseSearch.content.overview.documentExample.description.text": "APIキーを生成し、ドキュメントをElasticsearch APIエンドポイントに送信する方法に関する{documentation}を読みます。統合を合理化するには、Elastic {clients}を使用します。", "xpack.enterpriseSearch.content.searchIndex.documents.documentList.description": "{total}件中{results}件を表示中。{maximum}ドキュメントが検索結果の最大数です。", "xpack.enterpriseSearch.content.searchIndex.documents.documentList.pagination.itemsPerPage": "毎秒あたりのドキュメント:{docPerPage}", "xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option": "{docCount}ドキュメント", @@ -13784,10 +13781,6 @@ "xpack.enterpriseSearch.content.overview.documentExample.clientLibraries.python": "Python", "xpack.enterpriseSearch.content.overview.documentExample.clientLibraries.ruby": "Ruby", "xpack.enterpriseSearch.content.overview.documentExample.clientLibraries.rust": "Rust", - "xpack.enterpriseSearch.content.overview.documentExample.description.clientsLink": "プログラミング言語クライアント", - "xpack.enterpriseSearch.content.overview.documentExample.description.documentationLink": "ドキュメンテーション", - "xpack.enterpriseSearch.content.overview.documentExample.generateApiKeyButton.label": "APIキーの管理", - "xpack.enterpriseSearch.content.overview.documentExample.title": "ドキュメントをインデックスに追加しています", "xpack.enterpriseSearch.content.overview.emptyPrompt.body": "外部で管理されているインデックスにはドキュメントを追加しないようにすることをお勧めします。", "xpack.enterpriseSearch.content.overview.emptyPrompt.title": "外部で管理されているインデックス", "xpack.enterpriseSearch.content.overview.generateApiKeyModal.apiKeyWarning": "ElasticはAPIキーを保存しません。生成後は、1回だけキーを表示できます。必ず安全に保管してください。アクセスできなくなった場合は、この画面から新しいAPIキーを生成する必要があります。", @@ -13797,7 +13790,6 @@ "xpack.enterpriseSearch.content.overview.generateApiKeyModal.info": "ElasticsearchドキュメントをElasticsearchインデックスに送信する前に、少なくとも1つのAPIキーを作成する必要があります。", "xpack.enterpriseSearch.content.overview.generateApiKeyModal.learnMore": "APIキーの詳細", "xpack.enterpriseSearch.content.overview.generateApiKeyModal.title": "APIキーを生成", - "xpack.enterpriseSearch.content.overview.optimizedRequest.label": "エンタープライズ サーチで最適化されたリクエストを表示", "xpack.enterpriseSearch.content.searchIndex.cancelSyncs.successMessage": "同期が正常にキャンセルされました", "xpack.enterpriseSearch.content.searchIndex.configurationTabLabel": "構成", "xpack.enterpriseSearch.content.searchIndex.connectorErrorCallOut.title": "コネクターでエラーが発生しました", @@ -14181,16 +14173,9 @@ "xpack.enterpriseSearch.curations.settings.licenseUpgradeLink": "ライセンスアップグレードの詳細", "xpack.enterpriseSearch.curations.settings.start30DayTrialButtonLabel": "30 日間のトライアルの開始", "xpack.enterpriseSearch.descriptionLabel": "説明", - "xpack.enterpriseSearch.elasticsearch.features.buildSearchExperiences": "カスタム検索エクスペリエンスを構築", - "xpack.enterpriseSearch.elasticsearch.features.buildTooling": "カスタムツールを作成", - "xpack.enterpriseSearch.elasticsearch.features.integrate": "データベース、Webサイトなどを統合", "xpack.enterpriseSearch.elasticsearch.productCardDescription": "カスタムアプリケーションに最適なElasticsearchでは、非常にカスタマイズ性の高い検索を構築し、多数の異なるインジェスチョン方法を利用できます。", "xpack.enterpriseSearch.elasticsearch.productDescription": "高パフォーマンスで関連性の高い検索エクスペリエンスを作成するための低レベルのツール。", "xpack.enterpriseSearch.elasticsearch.productName": "Elasticsearch", - "xpack.enterpriseSearch.elasticsearch.resources.createNewIndexLabel": "新しいインデックスを作成", - "xpack.enterpriseSearch.elasticsearch.resources.gettingStartedLabel": "Elasticsearchを使い始める", - "xpack.enterpriseSearch.elasticsearch.resources.languageClientLabel": "言語クライアントのセットアップ", - "xpack.enterpriseSearch.elasticsearch.resources.searchUILabel": "ElasticsearchのUIを検索", "xpack.enterpriseSearch.emailLabel": "メール", "xpack.enterpriseSearch.emptyState.description": "コンテンツはElasticsearchインデックスに保存されます。まず、Elasticsearchインデックスを作成し、インジェスチョン方法を選択します。オプションには、Elastic Webクローラー、サードパーティデータ統合、Elasticsearch APIエンドポイントの使用があります。", "xpack.enterpriseSearch.emptyState.description.line2": "App SearchまたはElasticsearchのどちらで検索エクスペリエンスを構築しても、これが最初のステップです。", @@ -14425,8 +14410,6 @@ "xpack.enterpriseSearch.overview.elasticsearchResources.gettingStarted": "Elasticsearchを使い始める", "xpack.enterpriseSearch.overview.elasticsearchResources.searchUi": "ElasticsearchのUIを検索", "xpack.enterpriseSearch.overview.elasticsearchResources.title": "リソース", - "xpack.enterpriseSearch.overview.emptyPromptButtonLabel": "Elasticsearchインデックスを作成", - "xpack.enterpriseSearch.overview.emptyPromptTitle": "データを追加して検索を開始", "xpack.enterpriseSearch.overview.emptyState.buttonTitle": "コンテンツをエンタープライズ サーチに追加", "xpack.enterpriseSearch.overview.emptyState.footerLinkTitle": "詳細", "xpack.enterpriseSearch.overview.emptyState.heading": "コンテンツをエンタープライズ サーチに追加", @@ -14452,12 +14435,9 @@ "xpack.enterpriseSearch.overview.iconRow.sharePoint.title": "Microsoft SharePoint", "xpack.enterpriseSearch.overview.iconRow.sharePoint.tooltip": "Microsoft SharePointのコンテンツにインデックスを作成", "xpack.enterpriseSearch.overview.navTitle": "概要", - "xpack.enterpriseSearch.overview.pageTitle": "エンタープライズ サーチへようこそ", - "xpack.enterpriseSearch.overview.productSelector.title": "すべてのユースケースの検索エクスペリエンス", "xpack.enterpriseSearch.overview.searchIndices.image.altText": "検索インデックスの例", "xpack.enterpriseSearch.overview.setupCta.description": "Elastic App Search および Workplace Search を使用して、アプリまたは社内組織に検索を追加できます。検索が簡単になるとどのような利点があるのかについては、動画をご覧ください。", "xpack.enterpriseSearch.passwordLabel": "パスワード", - "xpack.enterpriseSearch.productCard.resourcesTitle": "リソース", "xpack.enterpriseSearch.productSelectorCalloutTitle": "チームのためのエンタープライズレベルの機能を実現できるようにアップグレード", "xpack.enterpriseSearch.readOnlyMode.warning": "エンタープライズ サーチは読み取り専用モードです。作成、編集、削除などの変更を実行できません。", "xpack.enterpriseSearch.roleMapping.addRoleMappingButtonLabel": "マッピングを追加", @@ -27312,9 +27292,7 @@ "xpack.observability.threshold.rule.alertDetailsAppSection.criterion.subtitle": "最後の{lookback} {timeLabel}", "xpack.observability.threshold.rule.alertFlyout.alertPerRedundantFilterError": "フィルタークエリには{groupCount, plural, other {これらのフィールド}}に対する一致が含まれているため、このルールによって、想定を下回る{matchedGroups}に関するアラートが発行される場合があります。詳細については、{filteringAndGroupingLink}を参照してください。", "xpack.observability.threshold.rule.alertFlyout.customEquationEditor.aggregationLabel": "アグリゲーション{name}", - "xpack.observability.threshold.rule.alertFlyout.customEquationEditor.fieldLabel": "フィールド{name}", "xpack.observability.threshold.rule.alertFlyout.customEquationEditor.filterLabel": "KQLフィルター{name}", - "xpack.observability.threshold.rule.alertFlyout.ofExpression.helpTextDetail": "メトリックが見つからない場合は、{documentationLink}。", "xpack.observability.threshold.rule.alerts.dataTimeRangeLabel": "最後の{lookback} {timeLabel}", "xpack.observability.threshold.rule.alerts.dataTimeRangeLabelWithGrouping": "{id}のデータの最後の{lookback} {timeLabel}", "xpack.observability.threshold.rule.threshold.errorAlertReason": "{metric}のデータのクエリを試行しているときに、Elasticsearchが失敗しました", @@ -27827,9 +27805,7 @@ "xpack.observability.threshold.rule.alertFlyout.error.thresholdRequired": "しきい値が必要です。", "xpack.observability.threshold.rule.alertFlyout.error.thresholdTypeRequired": "しきい値には有効な数値を含める必要があります。", "xpack.observability.threshold.rule.alertFlyout.error.timeRequred": "ページサイズが必要です。", - "xpack.observability.threshold.rule.alertFlyout.expandRowLabel": "行を展開します。", "xpack.observability.threshold.rule.alertFlyout.groupDisappearHelpText": "以前に検出されたグループが結果を報告しなくなった場合は、これを有効にすると、アクションがトリガーされます。自動的に急速にノードを開始および停止することがある動的に拡張するインフラストラクチャーでは、これは推奨されません。", - "xpack.observability.threshold.rule.alertFlyout.ofExpression.popoverLinkLabel": "データの追加方法", "xpack.observability.threshold.rule.alertFlyout.outsideRangeLabel": "is not between", "xpack.observability.threshold.rule.alertFlyout.removeCondition": "条件を削除", "xpack.observability.threshold.rule.alerting.noDataFormattedValue": "[データなし]", @@ -27869,7 +27845,6 @@ "xpack.observability.threshold.ruleExplorer.groupByAriaLabel": "graph/", "xpack.observability.threshold.ruleExplorer.groupByLabel": "すべて", "xpack.observability.threshold.ruleName": "しきい値(テクニカルプレビュー)", - "xpack.observability.thresholdRule.expressionItems.descriptionLabel": "タイミング", "xpack.observability.uiSettings.betaLabel": "ベータ", "xpack.observability.uiSettings.technicalPreviewLabel": "テクニカルプレビュー", "xpack.observability.uiSettings.throttlingDocsLinkText": "こちらで通知をお読みください。", @@ -28305,10 +28280,8 @@ "xpack.profiling.noDataConfig.action.buttonLabel": "ユニバーサルプロファイリングの設定", "xpack.profiling.noDataConfig.action.buttonLoadingLabel": "ユニバーサルプロファイリングの設定中...", "xpack.profiling.noDataConfig.action.dataRetention.link": "データ保持を管理中", - "xpack.profiling.noDataConfig.action.legalBetaTerms": "この機能を使用すると、読んで同意したことを承諾します ", "xpack.profiling.noDataConfig.action.permissionsWarning": "ユニバーサルプロファイリングを設定するには、スーパーユーザーとしてログインする必要があります。", "xpack.profiling.noDataConfig.action.title": "ユニバーサルプロファイリングは、インストルメンテーションしで、フリート全体、システム全体、連続的なプロファイリングを提供します。\n インフラストラクチャー全体で、どのコード行が常にコンピューティングリソースを消費しているかを把握できます。", - "xpack.profiling.noDataConfig.betaTerms.linkLabel": "Elasticベータリリース条件", "xpack.profiling.noDataConfig.loading.loaderText": "データソースを読み込み中", "xpack.profiling.noDataConfig.pageTitle": "ユニバーサルプロファイリング(ベータ版)", "xpack.profiling.noDataConfig.solutionName": "ユニバーサルプロファイリング", @@ -28447,7 +28420,6 @@ "xpack.remoteClusters.listBreadcrumbTitle": "リモートクラスター", "xpack.remoteClusters.readDocsButtonLabel": "リモートクラスタードキュメント", "xpack.remoteClusters.refreshAction.errorTitle": "リモートクラスターの更新中にエラーが発生", - "xpack.remoteClusters.remoteClusterForm.actions.savingText": "保存中", "xpack.remoteClusters.remoteClusterForm.addressError.invalidPortMessage": "ポートが必要です。", "xpack.remoteClusters.remoteClusterForm.cancelButtonLabel": "キャンセル", "xpack.remoteClusters.remoteClusterForm.cloudUrlHelp.buttonLabel": "ヘルプが必要な場合", @@ -28483,7 +28455,6 @@ "xpack.remoteClusters.remoteClusterForm.manualModeFieldLabel": "手動でプロキシアドレスとサーバー名を入力", "xpack.remoteClusters.remoteClusterForm.proxyError.invalidCharactersMessage": "アドレスはホスト:ポートの形式にする必要があります。例:127.0.0.1:9400、localhost:9400ホストには文字、数字、ハイフンのみが使用できます。", "xpack.remoteClusters.remoteClusterForm.proxyError.missingProxyMessage": "プロキシアドレスが必要です。", - "xpack.remoteClusters.remoteClusterForm.saveButtonLabel": "保存", "xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription": "リモートデプロイのElasticsearchエンドポイントURLを使用して、リモートクラスターを自動的に構成するか、プロキシアドレスとサーバー名を手動で入力します。", "xpack.remoteClusters.remoteClusterForm.sectionModeDescription": "既定でシードノードを使用するか、プロキシモードに切り替えます。", "xpack.remoteClusters.remoteClusterForm.sectionModeTitle": "接続モード", @@ -30088,7 +30059,6 @@ "xpack.securitySolution.flyout.correlations.relatedCasesHeading": "{count}件の関連する{count, plural, other {ケース}}", "xpack.securitySolution.flyout.correlations.sessionAlertsHeading": "セッションに関連する{count, plural, other {#件のアラート}}", "xpack.securitySolution.flyout.correlations.sourceAlertsHeading": "ソースイベントに関連する{count, plural, other {#件のアラート}}", - "xpack.securitySolution.flyout.documentDetails.overviewTab.viewAllButton": "すべての{text}を表示", "xpack.securitySolution.flyout.errorMessage": "{message}の表示中にエラーが発生しました", "xpack.securitySolution.flyout.errorTitle": "{title}を表示できません", "xpack.securitySolution.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する直近{numberOfItems}件のイベントを表示します。", @@ -33358,7 +33328,6 @@ "xpack.securitySolution.flyout.correlations.timestampColumnTitle": "タイムスタンプ", "xpack.securitySolution.flyout.documentDetails.alertReasonTitle": "アラートの理由", "xpack.securitySolution.flyout.documentDetails.analyzerGraphButton": "アナライザーグラフ", - "xpack.securitySolution.flyout.documentDetails.analyzerPreviewText": "アナライザープレビュー。", "xpack.securitySolution.flyout.documentDetails.analyzerPreviewTitle": "アナライザープレビュー", "xpack.securitySolution.flyout.documentDetails.collapseDetailButton": "アラート詳細を折りたたむ", "xpack.securitySolution.flyout.documentDetails.correlationsButton": "相関関係", @@ -33387,14 +33356,11 @@ "xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlert": "同じソースイベントに関連するアラート", "xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlerts": "同じソースイベントに関連するアラート", "xpack.securitySolution.flyout.documentDetails.overviewTab.correlationsText": "相関のフィールド", - "xpack.securitySolution.flyout.documentDetails.overviewTab.entitiesText": "エンティティ", "xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceRowText": "共通しない", - "xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceText": "発生率のフィールド", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichment": "脅威インテリジェンスで拡張されたフィールド", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichments": "脅威インテリジェンスで拡張されたフィールド", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch": "脅威一致が検出されました", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatches": "脅威一致が検出されました", - "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligenceText": "脅威インテリジェンスのフィールド", "xpack.securitySolution.flyout.documentDetails.prevalenceButton": "発生率", "xpack.securitySolution.flyout.documentDetails.prevalenceTitle": "発生率", "xpack.securitySolution.flyout.documentDetails.riskScoreTitle": "リスクスコア", @@ -34746,12 +34712,6 @@ "xpack.securitySolution.zeek.shrDescription": "レスポンダーがFINに続きSYNを送信しました。接続元からSYN-ACKはありません", "xpack.serverlessSearch.apiKey.activeKeys": "{number}個のアクティブなキーがあります。", "xpack.serverlessSearch.apiKey.expiresHelpText": "このAPIキーは{expirationDate}に有効期限切れになります", - "xpack.serverlessSearch.header.greeting.title": "{name}様", - "xpack.serverlessSearch.ingestData.clientDocLink": "{languageName}APIリファレンス", - "xpack.serverlessSearch.installClient.clientDocLink": "{languageName}クライアントドキュメント", - "xpack.serverlessSearch.selectClient.description": "Elasticは複数の一般的な言語でクライアントを構築および保守します。Elasticのコミュニティはさらに多くを提供しています。お気に入りの言語クライアントを選択するか、{console}を起動して開始します。", - "xpack.serverlessSearch.apiCallout.content": "Consoleを使用すると、言語クライアントをインストールせずに、ElasticsearchとKibanaのREST APIを直接呼び出すことができます。", - "xpack.serverlessSearch.apiCallOut.title": "コンソールでAPIを呼び出し", "xpack.serverlessSearch.apiKey.apiKeyStepDescription": "このキーは一度しか表示されないため、安全な場所に保存しておいてください。当社はお客様のAPIキーを保存しません。キーを紛失した場合は、代替キーを生成する必要があります。", "xpack.serverlessSearch.apiKey.apiKeyStepTitle": "このAPIキーを保存", "xpack.serverlessSearch.apiKey.description": "Elasticsearchプロジェクトに安全に接続するには、これらの一意の識別子が必要です。", @@ -34784,8 +34744,6 @@ "xpack.serverlessSearch.apiKey.userFieldLabel": "ユーザー", "xpack.serverlessSearch.back": "戻る", "xpack.serverlessSearch.cancel": "キャンセル", - "xpack.serverlessSearch.codeBox.copyButtonLabel": "コピー", - "xpack.serverlessSearch.codeBox.selectAriaLabel": "プログラミング言語を選択", "xpack.serverlessSearch.configureClient.advancedConfigLabel": "高度な構成", "xpack.serverlessSearch.configureClient.basicConfigLabel": "基本構成", "xpack.serverlessSearch.configureClient.description": "一意のAPIキーとCloud IDでクライアントを初期化", @@ -34806,30 +34764,7 @@ "xpack.serverlessSearch.footer.searchUI.description": "Search UIはElasticが管理している無料のオープンソースJavaScriptライブラリで、モダンで魅力的な検索エクスペリエンスをすばやく開発できます。", "xpack.serverlessSearch.footer.searchUI.title": "Search UIでユーザーインターフェースを構築", "xpack.serverlessSearch.footer.title": "次のステップ", - "xpack.serverlessSearch.githubLink.curl.label": "curl", - "xpack.serverlessSearch.githubLink.javascript.label": "elasticsearch", - "xpack.serverlessSearch.githubLink.ruby.label": "elasticsearch-ruby", - "xpack.serverlessSearch.header.description": "プログラミング言語のクライアントを設定し、データを取り込めば、数分で検索を開始できます。", "xpack.serverlessSearch.header.title": "Elasticsearchをはじめよう", - "xpack.serverlessSearch.ingestData.beatsDescription": "Elasticsearch向けの軽量の、専用データ転送機能。Beatsを使用して、サーバーから運用データを送信します。", - "xpack.serverlessSearch.ingestData.beatsLink": "beats", - "xpack.serverlessSearch.ingestData.beatsTitle": "ビート", - "xpack.serverlessSearch.ingestData.connectorsDescription": "サードパーティのソースからElasticsearchにデータを同期するための特別な統合。Elasticコネクターを使って、さまざまなデータベースやオブジェクトストアからコンテンツを同期できます。", - "xpack.serverlessSearch.ingestData.connectorsPythonLink": "connectors-python", - "xpack.serverlessSearch.ingestData.connectorsTitle": "コネクタークライアント", - "xpack.serverlessSearch.ingestData.description": "データストリームやインデックスにデータを追加して、データを検索可能にします。アプリケーションとワークフローに合ったインジェスト方法を選択します。", - "xpack.serverlessSearch.ingestData.ingestApiDescription": "データをインデックス化する最も柔軟な方法で、カスタマイズや最適化オプションを完全に制御できます。", - "xpack.serverlessSearch.ingestData.ingestApiLabel": "API経由でインジェスト", - "xpack.serverlessSearch.ingestData.ingestIntegrationDescription": "データを変換してElasticsearchに送信するために最適化された専用のインジェストツール。", - "xpack.serverlessSearch.ingestData.ingestIntegrationLabel": "統合経由でインジェスト", - "xpack.serverlessSearch.ingestData.ingestLegendLabel": "インジェスチョン方法を選択", - "xpack.serverlessSearch.ingestData.integrationsLink": "統合について", - "xpack.serverlessSearch.ingestData.logstashDescription": "データストリームやインデックスにデータを追加して、データを検索可能にします。アプリケーションとワークフローに合ったインジェスト方法を選択します。", - "xpack.serverlessSearch.ingestData.logstashLink": "Logstash", - "xpack.serverlessSearch.ingestData.logstashTitle": "Logstash", - "xpack.serverlessSearch.ingestData.title": "データをインジェスト", - "xpack.serverlessSearch.installClient.description": "Elasticは複数の一般的な言語でクライアントを構築および保守します。Elasticのコミュニティはさらに多くを提供しています。開始するには、お気に入りの言語クライアントをインストールします。", - "xpack.serverlessSearch.installClient.title": "クライアントをインスト-ル", "xpack.serverlessSearch.invalidJsonError": "無効なJSON", "xpack.serverlessSearch.languages.cURL": "cURL", "xpack.serverlessSearch.languages.javascript": "JavaScript / Node.js", @@ -34847,17 +34782,8 @@ "xpack.serverlessSearch.required": "必須", "xpack.serverlessSearch.searchQuery.description": "これで、Elasticsearchデータの検索や集約の実験を始める準備が整いました。", "xpack.serverlessSearch.searchQuery.title": "最初の検索クエリを作成", - "xpack.serverlessSearch.selectClient.apiRequestConsoleDocLink": "コンソールでAPIリクエストを実行 ", - "xpack.serverlessSearch.selectClient.callout.description": "コンソールでは、REST APIを使用してすぐに開始できます。インストールは不要です。", - "xpack.serverlessSearch.selectClient.callout.link": "今すぐコンソールを試す", - "xpack.serverlessSearch.selectClient.callout.title": "今すぐコンソールで試す", - "xpack.serverlessSearch.selectClient.description.console.link": "コンソール", - "xpack.serverlessSearch.selectClient.elasticsearchClientDocLink": "Elasticsearchクライアント ", - "xpack.serverlessSearch.selectClient.heading": "1つ選択", - "xpack.serverlessSearch.selectClient.title": "クライアントを選択", "xpack.serverlessSearch.testConnection.description": "テストリクエストを送信して、言語クライアントとElasticsearchインスタンスが起動し、実行中であることを確認してください。", "xpack.serverlessSearch.testConnection.title": "接続をテスト", - "xpack.serverlessSearch.tryInConsoleButton": "コンソールで試す", "xpack.sessionView.alertFilteredCountStatusLabel": " {count}件のアラートを表示中", "xpack.sessionView.alertTotalCountStatusLabel": "{count}件のアラートを表示中", "xpack.sessionView.processTree.loadMore": "{pageSize}次のイベントを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1556ea23a7cc36..fa22bef2151542 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -399,7 +399,6 @@ "controls.controlGroup.manageControl.controlTypeSettings.formGroupDescription": "{controlType} 控件的定制设置。", "controls.controlGroup.manageControl.controlTypeSettings.formGroupTitle": "{controlType} 设置", "controls.optionsList.controlAndPopover.exists": "{negate, plural, other {存在}}", - "controls.optionsList.errors.dataViewNotFound": "找不到数据视图:{dataViewId}", "controls.optionsList.errors.fieldNotFound": "找不到字段:{fieldName}", "controls.optionsList.popover.ariaLabel": "{fieldName} 控件的弹出框", "controls.optionsList.popover.cardinalityLabel": "{totalOptions, number} 个{totalOptions, plural, other {选项}}", @@ -409,7 +408,6 @@ "controls.optionsList.popover.invalidSelectionsLabel": "已忽略 {selectedOptions} 个{selectedOptions, plural, other {选择的内容}}", "controls.optionsList.popover.invalidSelectionsSectionTitle": "已忽略{invalidSelectionCount, plural, other {选择的内容}}", "controls.optionsList.popover.suggestionsAriaLabel": "{fieldName} 的可用{optionCount, plural, other {选项}}", - "controls.rangeSlider.errors.dataViewNotFound": "找不到数据视图:{dataViewId}", "controls.rangeSlider.errors.fieldNotFound": "找不到字段:{fieldName}", "controls.controlGroup.emptyState.addControlButtonTitle": "添加控件", "controls.controlGroup.emptyState.badgeText": "新建", @@ -12168,7 +12166,6 @@ "xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.isInvalid.error": "{indexName} 为无效索引名称", "xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.nameInputHelpText.lineOne": "您的索引将命名为:{indexName}", "xpack.enterpriseSearch.content.newIndex.steps.buildConnector.confirmModal.description": "名为 {indexName} 的已删除索引最初绑定到现有连接器配置。是否要将现有连接器配置替换成新的?", - "xpack.enterpriseSearch.content.overview.documentExample.description.text": "生成 API 密钥并阅读{documentation},了解如何将文档发布到 Elasticsearch API 终端。将 Elastic {clients} 用于精简集成。", "xpack.enterpriseSearch.content.searchIndex.documents.documentList.description": "显示 {results} 个,共 {total} 个。搜索结果最多包含 {maximum} 个文档。", "xpack.enterpriseSearch.content.searchIndex.documents.documentList.pagination.itemsPerPage": "每页文档数:{docPerPage}", "xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option": "{docCount} 个文档", @@ -13784,10 +13781,6 @@ "xpack.enterpriseSearch.content.overview.documentExample.clientLibraries.python": "Python", "xpack.enterpriseSearch.content.overview.documentExample.clientLibraries.ruby": "Ruby", "xpack.enterpriseSearch.content.overview.documentExample.clientLibraries.rust": "Rust", - "xpack.enterpriseSearch.content.overview.documentExample.description.clientsLink": "编程语言客户端", - "xpack.enterpriseSearch.content.overview.documentExample.description.documentationLink": "文档", - "xpack.enterpriseSearch.content.overview.documentExample.generateApiKeyButton.label": "管理 API 密钥", - "xpack.enterpriseSearch.content.overview.documentExample.title": "正在添加文档到您的索引", "xpack.enterpriseSearch.content.overview.emptyPrompt.body": "不建议将文档添加到外部管理的索引。", "xpack.enterpriseSearch.content.overview.emptyPrompt.title": "外部管理的索引", "xpack.enterpriseSearch.content.overview.generateApiKeyModal.apiKeyWarning": "Elastic 不会存储 API 密钥。一旦生成,您只能查看密钥一次。请确保将其保存在某个安全位置。如果失去它的访问权限,您需要从此屏幕生成新的 API 密钥。", @@ -13797,7 +13790,6 @@ "xpack.enterpriseSearch.content.overview.generateApiKeyModal.info": "在开始将文档发布到 Elasticsearch 索引之前,您至少需要创建一个 API 密钥。", "xpack.enterpriseSearch.content.overview.generateApiKeyModal.learnMore": "进一步了解 API 密钥", "xpack.enterpriseSearch.content.overview.generateApiKeyModal.title": "生成 API 密钥", - "xpack.enterpriseSearch.content.overview.optimizedRequest.label": "查看 Enterprise Search 优化的请求", "xpack.enterpriseSearch.content.searchIndex.cancelSyncs.successMessage": "已成功取消同步", "xpack.enterpriseSearch.content.searchIndex.configurationTabLabel": "配置", "xpack.enterpriseSearch.content.searchIndex.connectorErrorCallOut.title": "您的连接器报告了错误", @@ -14181,16 +14173,9 @@ "xpack.enterpriseSearch.curations.settings.licenseUpgradeLink": "详细了解许可证升级", "xpack.enterpriseSearch.curations.settings.start30DayTrialButtonLabel": "开始为期 30 天的试用", "xpack.enterpriseSearch.descriptionLabel": "描述", - "xpack.enterpriseSearch.elasticsearch.features.buildSearchExperiences": "构建定制搜索体验", - "xpack.enterpriseSearch.elasticsearch.features.buildTooling": "构建定制工具", - "xpack.enterpriseSearch.elasticsearch.features.integrate": "集成数据库、网站等", "xpack.enterpriseSearch.elasticsearch.productCardDescription": "适用于专门定制的应用程序,Elasticsearch 将帮助您构建高度可定制的搜索,并提供许多不同的采集方法。", "xpack.enterpriseSearch.elasticsearch.productDescription": "用于打造高效、相关的搜索体验的低级工具。", "xpack.enterpriseSearch.elasticsearch.productName": "Elasticsearch", - "xpack.enterpriseSearch.elasticsearch.resources.createNewIndexLabel": "创建新索引", - "xpack.enterpriseSearch.elasticsearch.resources.gettingStartedLabel": "Elasticsearch 入门", - "xpack.enterpriseSearch.elasticsearch.resources.languageClientLabel": "设置语言客户端", - "xpack.enterpriseSearch.elasticsearch.resources.searchUILabel": "Elasticsearch 的搜索 UI", "xpack.enterpriseSearch.emailLabel": "电子邮件", "xpack.enterpriseSearch.emptyState.description": "您的内容存储在 Elasticsearch 索引中。通过创建 Elasticsearch 索引并选择采集方法开始使用。选项包括 Elastic 网络爬虫、第三方数据集成或使用 Elasticsearch API 终端。", "xpack.enterpriseSearch.emptyState.description.line2": "无论是使用 App Search 还是 Elasticsearch 构建搜索体验,您都可以从此处立即开始。", @@ -14425,8 +14410,6 @@ "xpack.enterpriseSearch.overview.elasticsearchResources.gettingStarted": "Elasticsearch 入门", "xpack.enterpriseSearch.overview.elasticsearchResources.searchUi": "Elasticsearch 的搜索 UI", "xpack.enterpriseSearch.overview.elasticsearchResources.title": "资源", - "xpack.enterpriseSearch.overview.emptyPromptButtonLabel": "创建 Elasticsearch 索引", - "xpack.enterpriseSearch.overview.emptyPromptTitle": "添加数据并开始搜索", "xpack.enterpriseSearch.overview.emptyState.buttonTitle": "将内容添加到 Enterprise Search", "xpack.enterpriseSearch.overview.emptyState.footerLinkTitle": "了解详情", "xpack.enterpriseSearch.overview.emptyState.heading": "将内容添加到 Enterprise Search", @@ -14452,12 +14435,9 @@ "xpack.enterpriseSearch.overview.iconRow.sharePoint.title": "Microsoft SharePoint", "xpack.enterpriseSearch.overview.iconRow.sharePoint.tooltip": "索引来自 Microsoft SharePoint 的内容", "xpack.enterpriseSearch.overview.navTitle": "概览", - "xpack.enterpriseSearch.overview.pageTitle": "欢迎使用 Enterprise Search", - "xpack.enterpriseSearch.overview.productSelector.title": "每个用例的搜索体验", "xpack.enterpriseSearch.overview.searchIndices.image.altText": "搜索索引图示", "xpack.enterpriseSearch.overview.setupCta.description": "通过 Elastic App Search 和 Workplace Search,将搜索添加到您的应用或内部组织中。观看视频,了解方便易用的搜索功能可以帮您做些什么。", "xpack.enterpriseSearch.passwordLabel": "密码", - "xpack.enterpriseSearch.productCard.resourcesTitle": "资源", "xpack.enterpriseSearch.productSelectorCalloutTitle": "进行升级以便为您的团队获取企业级功能", "xpack.enterpriseSearch.readOnlyMode.warning": "企业搜索处于只读模式。您将无法执行更改,例如创建、编辑或删除。", "xpack.enterpriseSearch.roleMapping.addRoleMappingButtonLabel": "添加映射", @@ -27310,9 +27290,7 @@ "xpack.observability.threshold.rule.alertDetailsAppSection.criterion.subtitle": "过去 {lookback} {timeLabel}", "xpack.observability.threshold.rule.alertFlyout.alertPerRedundantFilterError": "此规则可能针对低于预期的 {matchedGroups} 告警,因为筛选查询包含{groupCount, plural, other {这些字段}}的匹配项。有关更多信息,请参阅 {filteringAndGroupingLink}。", "xpack.observability.threshold.rule.alertFlyout.customEquationEditor.aggregationLabel": "聚合 {name}", - "xpack.observability.threshold.rule.alertFlyout.customEquationEditor.fieldLabel": "字段 {name}", "xpack.observability.threshold.rule.alertFlyout.customEquationEditor.filterLabel": "KQL 筛选 {name}", - "xpack.observability.threshold.rule.alertFlyout.ofExpression.helpTextDetail": "找不到指标?{documentationLink}。", "xpack.observability.threshold.rule.alerts.dataTimeRangeLabel": "过去 {lookback} {timeLabel}", "xpack.observability.threshold.rule.alerts.dataTimeRangeLabelWithGrouping": "{id} 过去 {lookback} {timeLabel}的数据", "xpack.observability.threshold.rule.threshold.errorAlertReason": "Elasticsearch 尝试查询 {metric} 的数据时出现故障", @@ -27825,9 +27803,7 @@ "xpack.observability.threshold.rule.alertFlyout.error.thresholdRequired": "“阈值”必填。", "xpack.observability.threshold.rule.alertFlyout.error.thresholdTypeRequired": "阈值必须包含有效数字。", "xpack.observability.threshold.rule.alertFlyout.error.timeRequred": "“时间大小”必填。", - "xpack.observability.threshold.rule.alertFlyout.expandRowLabel": "展开行。", "xpack.observability.threshold.rule.alertFlyout.groupDisappearHelpText": "启用此选项可在之前检测的组开始不报告任何数据时触发操作。不建议将此选项用于可能会快速自动启动和停止节点的动态扩展基础架构。", - "xpack.observability.threshold.rule.alertFlyout.ofExpression.popoverLinkLabel": "了解如何添加更多数据", "xpack.observability.threshold.rule.alertFlyout.outsideRangeLabel": "不介于", "xpack.observability.threshold.rule.alertFlyout.removeCondition": "删除条件", "xpack.observability.threshold.rule.alerting.noDataFormattedValue": "[无数据]", @@ -27867,7 +27843,6 @@ "xpack.observability.threshold.ruleExplorer.groupByAriaLabel": "图表绘制依据", "xpack.observability.threshold.ruleExplorer.groupByLabel": "所有内容", "xpack.observability.threshold.ruleName": "阈值(技术预览)", - "xpack.observability.thresholdRule.expressionItems.descriptionLabel": "当", "xpack.observability.uiSettings.betaLabel": "公测版", "xpack.observability.uiSettings.technicalPreviewLabel": "技术预览", "xpack.observability.uiSettings.throttlingDocsLinkText": "在此处阅读通知。", @@ -28303,10 +28278,8 @@ "xpack.profiling.noDataConfig.action.buttonLabel": "设置 Universal Profiling", "xpack.profiling.noDataConfig.action.buttonLoadingLabel": "正在设置 Universal Profiling......", "xpack.profiling.noDataConfig.action.dataRetention.link": "正在控制数据保留", - "xpack.profiling.noDataConfig.action.legalBetaTerms": "使用此功能即表示您已阅读并同意 ", "xpack.profiling.noDataConfig.action.permissionsWarning": "要设置 Universal Profiling,您必须以超级用户的身份登录。", "xpack.profiling.noDataConfig.action.title": "Universal Profiling 无需检测即可提供 Fleet 范围的全系统持续分析。\n 了解哪些代码行一直在跨整个基础架构消耗计算资源。", - "xpack.profiling.noDataConfig.betaTerms.linkLabel": "Elastic 公测版条款", "xpack.profiling.noDataConfig.loading.loaderText": "正在加载数据源", "xpack.profiling.noDataConfig.pageTitle": "Universal Profiling(现在为公测版)", "xpack.profiling.noDataConfig.solutionName": "Universal Profiling", @@ -28445,7 +28418,6 @@ "xpack.remoteClusters.listBreadcrumbTitle": "远程集群", "xpack.remoteClusters.readDocsButtonLabel": "远程集群文档", "xpack.remoteClusters.refreshAction.errorTitle": "刷新远程集群时出错", - "xpack.remoteClusters.remoteClusterForm.actions.savingText": "正在保存", "xpack.remoteClusters.remoteClusterForm.addressError.invalidPortMessage": "端口必填。", "xpack.remoteClusters.remoteClusterForm.cancelButtonLabel": "取消", "xpack.remoteClusters.remoteClusterForm.cloudUrlHelp.buttonLabel": "需要帮助?", @@ -28481,7 +28453,6 @@ "xpack.remoteClusters.remoteClusterForm.manualModeFieldLabel": "手动输入代理地址和服务器名称", "xpack.remoteClusters.remoteClusterForm.proxyError.invalidCharactersMessage": "地址必须使用 host:port 格式。例如:127.0.0.1:9400、localhost:9400。主机只能由字母、数字和短划线构成。", "xpack.remoteClusters.remoteClusterForm.proxyError.missingProxyMessage": "必须指定代理地址。", - "xpack.remoteClusters.remoteClusterForm.saveButtonLabel": "保存", "xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription": "通过使用远程部署的 Elasticsearch 终端 URL 自动配置运程集群或手动输入代理地址和服务器名称。", "xpack.remoteClusters.remoteClusterForm.sectionModeDescription": "默认使用种子节点或切换到代理模式。", "xpack.remoteClusters.remoteClusterForm.sectionModeTitle": "连接模式", @@ -30084,7 +30055,6 @@ "xpack.securitySolution.flyout.correlations.relatedCasesHeading": "{count} 个相关{count, plural, other {案例}}", "xpack.securitySolution.flyout.correlations.sessionAlertsHeading": "{count, plural, other {# 个告警}}与会话相关", "xpack.securitySolution.flyout.correlations.sourceAlertsHeading": "{count, plural, other {# 个告警}}与源事件相关", - "xpack.securitySolution.flyout.documentDetails.overviewTab.viewAllButton": "查看所有 {text}", "xpack.securitySolution.flyout.errorMessage": "显示 {message} 时出现错误", "xpack.securitySolution.flyout.errorTitle": "无法显示 {title}", "xpack.securitySolution.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。", @@ -33354,7 +33324,6 @@ "xpack.securitySolution.flyout.correlations.timestampColumnTitle": "时间戳", "xpack.securitySolution.flyout.documentDetails.alertReasonTitle": "告警原因", "xpack.securitySolution.flyout.documentDetails.analyzerGraphButton": "分析器图表", - "xpack.securitySolution.flyout.documentDetails.analyzerPreviewText": "分析器预览。", "xpack.securitySolution.flyout.documentDetails.analyzerPreviewTitle": "分析器预览", "xpack.securitySolution.flyout.documentDetails.collapseDetailButton": "折叠告警详情", "xpack.securitySolution.flyout.documentDetails.correlationsButton": "相关性", @@ -33383,14 +33352,11 @@ "xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlert": "告警与同一源事件相关", "xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlerts": "告警与同一源事件相关", "xpack.securitySolution.flyout.documentDetails.overviewTab.correlationsText": "相关性字段", - "xpack.securitySolution.flyout.documentDetails.overviewTab.entitiesText": "实体", "xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceRowText": "不常见", - "xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceText": "普及率字段", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichment": "已使用威胁情报扩充字段", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichments": "已使用威胁情报扩充字段", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch": "检测到威胁匹配", "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatches": "检测到威胁匹配", - "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligenceText": "威胁情报字段", "xpack.securitySolution.flyout.documentDetails.prevalenceButton": "普及率", "xpack.securitySolution.flyout.documentDetails.prevalenceTitle": "普及率", "xpack.securitySolution.flyout.documentDetails.riskScoreTitle": "风险分数", @@ -34742,12 +34708,6 @@ "xpack.securitySolution.zeek.shrDescription": "响应方已发送 SYN ACK,后跟 FIN,发起方未发送 SYN", "xpack.serverlessSearch.apiKey.activeKeys": "您有 {number} 个活动密钥。", "xpack.serverlessSearch.apiKey.expiresHelpText": "此 API 密钥将于 {expirationDate}到期", - "xpack.serverlessSearch.header.greeting.title": "{name}您好!", - "xpack.serverlessSearch.ingestData.clientDocLink": "{languageName} API 参考", - "xpack.serverlessSearch.installClient.clientDocLink": "{languageName} 客户端文档", - "xpack.serverlessSearch.selectClient.description": "Elastic 以几种流行语言构建和维护客户端,我们的社区也做出了许多贡献。选择您常用的语言客户端或深入分析 {console} 以开始使用。", - "xpack.serverlessSearch.apiCallout.content": "使用 Console,您可以直接调用 Elasticsearch 和 Kibana REST API,而无需安装语言客户端。", - "xpack.serverlessSearch.apiCallOut.title": "通过 Console 调用 API", "xpack.serverlessSearch.apiKey.apiKeyStepDescription": "此密钥仅显示一次,因此请将其保存到某个安全位置。我们不存储您的 API 密钥,因此,如果您丢失了密钥,则需要生成替代密钥。", "xpack.serverlessSearch.apiKey.apiKeyStepTitle": "存储此 API 密钥", "xpack.serverlessSearch.apiKey.description": "您需要这些唯一标识符才能安全连接到 Elasticsearch 项目。", @@ -34780,8 +34740,6 @@ "xpack.serverlessSearch.apiKey.userFieldLabel": "用户", "xpack.serverlessSearch.back": "返回", "xpack.serverlessSearch.cancel": "取消", - "xpack.serverlessSearch.codeBox.copyButtonLabel": "复制", - "xpack.serverlessSearch.codeBox.selectAriaLabel": "选择编程语言", "xpack.serverlessSearch.configureClient.advancedConfigLabel": "高级配置", "xpack.serverlessSearch.configureClient.basicConfigLabel": "基本配置", "xpack.serverlessSearch.configureClient.description": "使用唯一 API 密钥和云 ID 对客户端进行初始化", @@ -34802,30 +34760,7 @@ "xpack.serverlessSearch.footer.searchUI.description": "搜索 UI 是一个由 Elastic 维护的免费开源 JavaScript 库,用于快速打造现代、富于吸引力的搜索体验。", "xpack.serverlessSearch.footer.searchUI.title": "通过搜索 UI 构建用户界面", "xpack.serverlessSearch.footer.title": "后续操作", - "xpack.serverlessSearch.githubLink.curl.label": "curl", - "xpack.serverlessSearch.githubLink.javascript.label": "Elasticsearch", - "xpack.serverlessSearch.githubLink.ruby.label": "elasticsearch-ruby", - "xpack.serverlessSearch.header.description": "设置您的编程语言客户端,采集一些数据,如此即可在数分钟内开始搜索。", "xpack.serverlessSearch.header.title": "Elasticsearch 入门", - "xpack.serverlessSearch.ingestData.beatsDescription": "用于 Elasticsearch 的轻量级、单一用途数据采集器。使用 Beats 从您的服务器发送运营数据。", - "xpack.serverlessSearch.ingestData.beatsLink": "Beats", - "xpack.serverlessSearch.ingestData.beatsTitle": "Beats", - "xpack.serverlessSearch.ingestData.connectorsDescription": "用于将数据从第三方源同步到 Elasticsearch 的专用集成。使用 Elastic 连接器同步来自一系列数据库和对象存储的内容。", - "xpack.serverlessSearch.ingestData.connectorsPythonLink": "connectors-python", - "xpack.serverlessSearch.ingestData.connectorsTitle": "连接器客户端", - "xpack.serverlessSearch.ingestData.description": "将数据添加到数据流或索引,使其可进行搜索。选择适合您的应用程序和工作流的集成方法。", - "xpack.serverlessSearch.ingestData.ingestApiDescription": "最灵活的数据索引方法,允许您全面控制定制和优化选项。", - "xpack.serverlessSearch.ingestData.ingestApiLabel": "通过 API 采集", - "xpack.serverlessSearch.ingestData.ingestIntegrationDescription": "针对转换数据并将其传输到 Elasticsearch 而优化的专用采集工具。", - "xpack.serverlessSearch.ingestData.ingestIntegrationLabel": "通过集成采集", - "xpack.serverlessSearch.ingestData.ingestLegendLabel": "选择采集方法", - "xpack.serverlessSearch.ingestData.integrationsLink": "关于集成", - "xpack.serverlessSearch.ingestData.logstashDescription": "将数据添加到数据流或索引,使其可进行搜索。选择适合您的应用程序和工作流的集成方法。", - "xpack.serverlessSearch.ingestData.logstashLink": "Logstash", - "xpack.serverlessSearch.ingestData.logstashTitle": "Logstash", - "xpack.serverlessSearch.ingestData.title": "采集数据", - "xpack.serverlessSearch.installClient.description": "Elastic 以几种流行语言构建和维护客户端,我们的社区也做出了许多贡献。安装您常用的语言客户端以开始使用。", - "xpack.serverlessSearch.installClient.title": "安装客户端", "xpack.serverlessSearch.invalidJsonError": "JSON 无效", "xpack.serverlessSearch.languages.cURL": "cURL", "xpack.serverlessSearch.languages.javascript": "JavaScript/Node.js", @@ -34843,17 +34778,8 @@ "xpack.serverlessSearch.required": "必需", "xpack.serverlessSearch.searchQuery.description": "现在您已做好准备,可以开始体验搜索并对您的 Elasticsearch 数据执行聚合。", "xpack.serverlessSearch.searchQuery.title": "构建您的首个搜索查询", - "xpack.serverlessSearch.selectClient.apiRequestConsoleDocLink": "在 Console 中运行 API 请求 ", - "xpack.serverlessSearch.selectClient.callout.description": "借助 Console,您可以立即开始使用我们的 REST API。无需进行安装。", - "xpack.serverlessSearch.selectClient.callout.link": "立即试用 Console", - "xpack.serverlessSearch.selectClient.callout.title": "立即在 Console 中试用", - "xpack.serverlessSearch.selectClient.description.console.link": "控制台", - "xpack.serverlessSearch.selectClient.elasticsearchClientDocLink": "Elasticsearch 客户端 ", - "xpack.serverlessSearch.selectClient.heading": "选择一个", - "xpack.serverlessSearch.selectClient.title": "选择客户端", "xpack.serverlessSearch.testConnection.description": "发送测试请求,以确认您的语言客户端和 Elasticsearch 实例已启动并正在运行。", "xpack.serverlessSearch.testConnection.title": "测试您的连接", - "xpack.serverlessSearch.tryInConsoleButton": "在 Console 中试用", "xpack.sessionView.alertFilteredCountStatusLabel": " 正在显示 {count} 个告警", "xpack.sessionView.alertTotalCountStatusLabel": "正在显示 {count} 个告警", "xpack.sessionView.processTree.loadMore": "显示 {pageSize} 个后续事件", diff --git a/x-pack/test/alerting_api_integration/packages/helpers/es_test_index_tool.ts b/x-pack/test/alerting_api_integration/packages/helpers/es_test_index_tool.ts index 98d69309365e27..9b106700ee613f 100644 --- a/x-pack/test/alerting_api_integration/packages/helpers/es_test_index_tool.ts +++ b/x-pack/test/alerting_api_integration/packages/helpers/es_test_index_tool.ts @@ -110,6 +110,31 @@ export class ESTestIndexTool { return await this.es.search(params, { meta: true }); } + async getAll(size: number = 10) { + const params = { + index: this.index, + size, + body: { + query: { + match_all: {}, + }, + }, + }; + return await this.es.search(params, { meta: true }); + } + + async removeAll() { + const params = { + index: this.index, + body: { + query: { + match_all: {}, + }, + }, + }; + return await this.es.deleteByQuery(params); + } + async waitForDocs(source: string, reference: string, numDocs: number = 1) { return await this.retry.try(async () => { const searchResult = await this.search(source, reference); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts index 2c7a55134c7111..295350e6dc18e3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts @@ -19,8 +19,7 @@ const findTestUtils = ( supertest: SuperTest, supertestWithoutAuth: any ) => { - // FLAKY: https://github.com/elastic/kibana/issues/148660 - describe.skip(describeType, () => { + describe(describeType, () => { afterEach(() => objectRemover.removeAll()); for (const scenario of UserAtSpaceScenarios) { @@ -60,6 +59,8 @@ const findTestUtils = ( }) ) .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + const response = await supertestWithoutAuth .get( `${getUrlPrefix(space.id)}/${ @@ -105,6 +106,7 @@ const findTestUtils = ( id: createdAction.id, connector_type_id: 'test.noop', params: {}, + uuid: match.actions[0].uuid, frequency: { summary: false, notify_when: 'onThrottleInterval', @@ -121,6 +123,7 @@ const findTestUtils = ( notify_when: null, updated_by: 'elastic', api_key_owner: 'elastic', + api_key_created_by_user: false, mute_all: false, muted_alert_ids: [], revision: 0, @@ -328,6 +331,7 @@ const findTestUtils = ( params: {}, created_by: 'elastic', throttle: '1m', + api_key_created_by_user: null, updated_by: 'elastic', api_key_owner: null, mute_all: false, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/common.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/common.ts index 9ec5171d74d5cb..fc7a65978aaa0f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/common.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/common.ts @@ -6,6 +6,7 @@ */ import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; +import { STACK_AAD_INDEX_NAME } from '@kbn/stack-alerts-plugin/server/rule_types'; import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; import { Spaces } from '../../../../../scenarios'; import { getUrlPrefix, ObjectRemover } from '../../../../../../common/lib'; @@ -69,6 +70,11 @@ export function getRuleServices(getService: FtrProviderContext['getService']) { const esTestIndexTool = new ESTestIndexTool(es, retry); const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); const esTestIndexToolDataStream = new ESTestIndexTool(es, retry, ES_TEST_DATA_STREAM_NAME); + const esTestIndexToolAAD = new ESTestIndexTool( + es, + retry, + `.internal.alerts-${STACK_AAD_INDEX_NAME}.alerts-default-000001` + ); async function createEsDocumentsInGroups( groups: number, @@ -112,6 +118,14 @@ export function getRuleServices(getService: FtrProviderContext['getService']) { ); } + async function getAllAADDocs(size: number): Promise { + return await esTestIndexToolAAD.getAll(size); + } + + async function removeAllAADDocs(): Promise { + return await esTestIndexToolAAD.removeAll(); + } + return { retry, es, @@ -121,5 +135,7 @@ export function getRuleServices(getService: FtrProviderContext['getService']) { createEsDocumentsInGroups, createGroupedEsDocumentsInGroups, waitForDocs, + getAllAADDocs, + removeAllAADDocs, }; } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/rule.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/rule.ts index 622e4878a6750a..c0b9113fa61439 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/rule.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/builtin_alert_types/es_query/rule.ts @@ -38,6 +38,8 @@ export default function ruleTests({ getService }: FtrProviderContext) { esTestIndexToolDataStream, createEsDocumentsInGroups, createGroupedEsDocumentsInGroups, + removeAllAADDocs, + getAllAADDocs, } = getRuleServices(getService); describe('rule', async () => { @@ -66,6 +68,7 @@ export default function ruleTests({ getService }: FtrProviderContext) { await esTestIndexTool.destroy(); await esTestIndexToolOutput.destroy(); await deleteDataStream(es, ES_TEST_DATA_STREAM_NAME); + await removeAllAADDocs(); }); [ @@ -135,6 +138,9 @@ export default function ruleTests({ getService }: FtrProviderContext) { await initData(); const docs = await waitForDocs(2); + const messagePattern = + /rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + for (let i = 0; i < docs.length; i++) { const doc = docs[i]; const { previousTimestamp, hits } = doc._source; @@ -142,8 +148,6 @@ export default function ruleTests({ getService }: FtrProviderContext) { expect(name).to.be('always fire'); expect(title).to.be(`rule 'always fire' matched query`); - const messagePattern = - /rule 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 20s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; expect(message).to.match(messagePattern); expect(hits).not.to.be.empty(); @@ -155,6 +159,17 @@ export default function ruleTests({ getService }: FtrProviderContext) { expect(previousTimestamp).not.to.be.empty(); } } + + const aadDocs = await getAllAADDocs(1); + + const alertDoc = aadDocs.body.hits.hits[0]._source.kibana.alert; + expect(alertDoc.reason).to.match(messagePattern); + expect(alertDoc.title).to.be("rule 'always fire' matched query"); + expect(alertDoc.evaluation.conditions).to.be( + 'Number of matching documents is greater than -1' + ); + expect(alertDoc.evaluation.value).greaterThan(0); + expect(alertDoc.url).to.contain('/s/space1/app/'); }) ); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/generate_alert_schemas.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/generate_alert_schemas.ts index 09a780b70d134a..478a9b17a21f53 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/generate_alert_schemas.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/generate_alert_schemas.ts @@ -84,7 +84,13 @@ export default function checkAlertSchemasTest({ getService }: FtrProviderContext } }); - const { stdout } = await execa('git', ['ls-files', '--modified']); + const { stdout } = await execa('git', [ + 'ls-files', + '--modified', + '--others', + '--exclude-standard', + ]); + expect(stdout).not.to.contain('packages/kbn-alerts-as-data-utils/src/schemas/generated'); }); }); diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index fd91262d57a614..a80a39e4af5dc9 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -125,6 +125,7 @@ export default function ({ getService }: FtrProviderContext) { 'uptime', 'siem', 'slo', + 'securitySolutionAssistant', 'securitySolutionCases', 'fleet', 'fleetv2', diff --git a/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts b/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts index ccc9b92ac82986..4af49a3991611b 100644 --- a/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts +++ b/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts @@ -8,6 +8,10 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { CSV_QUOTE_VALUES_SETTING } from '@kbn/share-plugin/common/constants'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function featureControlsTests({ getService }: FtrProviderContext) { @@ -64,9 +68,11 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const basePath = spaceId ? `/s/${spaceId}` : ''; return await supertest - .post(`${basePath}/api/telemetry/v2/optIn`) + .post(`${basePath}/internal/telemetry/optIn`) .auth(username, password) .set('kbn-xsrf', 'foo') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ enabled: true }) .then((response: any) => ({ error: undefined, response })) .catch((error: any) => ({ error, response: undefined })); 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 8dbfca9a40a770..4883f01b5ac6ed 100644 --- a/x-pack/test/api_integration/apis/maps/maps_telemetry.ts +++ b/x-pack/test/api_integration/apis/maps/maps_telemetry.ts @@ -7,6 +7,10 @@ import expect from '@kbn/expect'; import { estypes } from '@elastic/elasticsearch'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -17,7 +21,9 @@ export default function ({ getService }: FtrProviderContext) { const { body: [{ stats: apiResponse }], } = await supertest - .post(`/api/telemetry/v2/clusters/_stats`) + .post(`/internal/telemetry/clusters/_stats`) + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .set('kbn-xsrf', 'xxxx') .send({ unencrypted: true, diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index ad9eb9b3bd6ebb..d49df52bfcd1c3 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -56,6 +56,7 @@ export default function ({ getService }: FtrProviderContext) { 'execute_operations_all', ], uptime: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 680bd9fd132986..c6982b3c6d53ea 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -42,6 +42,7 @@ export default function ({ getService }: FtrProviderContext) { osquery: ['all', 'read', 'minimal_all', 'minimal_read'], ml: ['all', 'read', 'minimal_all', 'minimal_read'], siem: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read'], fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'], fleet: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -130,6 +131,7 @@ export default function ({ getService }: FtrProviderContext) { 'execute_operations_all', ], uptime: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry.ts b/x-pack/test/api_integration/apis/telemetry/telemetry.ts index 2d02f0a9764213..601e2fddbd83ef 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry.ts +++ b/x-pack/test/api_integration/apis/telemetry/telemetry.ts @@ -21,6 +21,10 @@ import type { CacheDetails, } from '@kbn/telemetry-collection-manager-plugin/server/types'; import { assertTelemetryPayload } from '@kbn/telemetry-tools'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import basicClusterFixture from './fixtures/basiccluster.json'; import multiClusterFixture from './fixtures/multicluster.json'; import type { SecurityService } from '../../../../../test/common/services/security/security'; @@ -97,7 +101,7 @@ export default function ({ getService }: FtrProviderContext) { const esSupertest = getService('esSupertest'); const security = getService('security'); - describe('/api/telemetry/v2/clusters/_stats', () => { + describe('/internal/telemetry/clusters/_stats', () => { const timestamp = new Date().toISOString(); describe('monitoring/multicluster', () => { let localXPack: Record; @@ -112,8 +116,10 @@ export default function ({ getService }: FtrProviderContext) { await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp); const { body }: { body: UnencryptedTelemetryPayload } = await supertest - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true, refreshCache: true }) .expect(200); @@ -167,8 +173,10 @@ export default function ({ getService }: FtrProviderContext) { after(() => esArchiver.unload(archive)); it('should load non-expiring basic cluster', async () => { const { body }: { body: UnencryptedTelemetryPayload } = await supertest - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true, refreshCache: true }) .expect(200); @@ -193,8 +201,10 @@ export default function ({ getService }: FtrProviderContext) { await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp); // hit the endpoint to cache results await supertest - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true, refreshCache: true }) .expect(200); }); @@ -204,8 +214,10 @@ export default function ({ getService }: FtrProviderContext) { it('returns non-cached results when unencrypted', async () => { const now = Date.now(); const { body }: { body: UnencryptedTelemetryPayload } = await supertest - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true }) .expect(200); @@ -224,8 +236,10 @@ export default function ({ getService }: FtrProviderContext) { it('grabs a fresh copy on refresh', async () => { const now = Date.now(); const { body }: { body: UnencryptedTelemetryPayload } = await supertest - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true, refreshCache: true }) .expect(200); @@ -243,16 +257,20 @@ export default function ({ getService }: FtrProviderContext) { describe('superadmin user', () => { it('should return unencrypted telemetry for the admin user', async () => { await supertest - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true }) .expect(200); }); it('should return encrypted telemetry for the admin user', async () => { await supertest - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: false }) .expect(200); }); @@ -281,18 +299,22 @@ export default function ({ getService }: FtrProviderContext) { it('should return encrypted telemetry for the global-read user', async () => { await supertestWithoutAuth - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .auth(globalReadOnlyUser, password(globalReadOnlyUser)) .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: false }) .expect(200); }); it('should return unencrypted telemetry for the global-read user', async () => { await supertestWithoutAuth - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .auth(globalReadOnlyUser, password(globalReadOnlyUser)) .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true }) .expect(200); }); @@ -330,18 +352,22 @@ export default function ({ getService }: FtrProviderContext) { it('should return encrypted telemetry for the read-only user', async () => { await supertestWithoutAuth - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .auth(noGlobalUser, password(noGlobalUser)) .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: false }) .expect(200); }); it('should return 403 when the read-only user requests unencrypted telemetry', async () => { await supertestWithoutAuth - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .auth(noGlobalUser, password(noGlobalUser)) .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true }) .expect(403); }); diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts b/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts index f0bb15d29b87b1..51e60c2e22bd1b 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts +++ b/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts @@ -12,6 +12,10 @@ import ossPluginsTelemetrySchema from '@kbn/telemetry-plugin/schema/oss_plugins. import xpackRootTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_root.json'; import xpackPluginsTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_plugins.json'; import { assertTelemetryPayload } from '@kbn/telemetry-tools'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { flatKeys } from '../../../../../test/api_integration/apis/telemetry/utils'; import type { FtrProviderContext } from '../../ftr_provider_context'; @@ -31,7 +35,7 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); - describe('/api/telemetry/v2/clusters/_stats with monitoring disabled', () => { + describe('/internal/telemetry/clusters/_stats with monitoring disabled', () => { let stats: Record; before('disable monitoring and pull local stats', async () => { @@ -39,8 +43,10 @@ export default function ({ getService }: FtrProviderContext) { await new Promise((r) => setTimeout(r, 1000)); const { body } = await supertest - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true, refreshCache: true }) .expect(200); diff --git a/x-pack/test/api_integration/services/usage_api.ts b/x-pack/test/api_integration/services/usage_api.ts index fbcddfb3dc512c..500212d96ddfc7 100644 --- a/x-pack/test/api_integration/services/usage_api.ts +++ b/x-pack/test/api_integration/services/usage_api.ts @@ -6,6 +6,10 @@ */ import { UsageStatsPayload } from '@kbn/telemetry-collection-manager-plugin/server'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { FtrProviderContext } from '../ftr_provider_context'; export interface UsageStatsPayloadTestFriendly extends UsageStatsPayload { @@ -29,9 +33,10 @@ export function UsageAPIProvider({ getService }: FtrProviderContext) { refreshCache?: boolean; }): Promise> { const { body } = await supertest - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .set('kbn-xsrf', 'xxx') - .set('x-elastic-internal-origin', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ refreshCache: true, ...payload }) .expect(200); return body; diff --git a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.spec.ts b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.spec.ts index c3d5dbf6fe61cb..99205fb6e27274 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.spec.ts @@ -344,6 +344,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(firstItem.location).toMatchInline(` Object { "agentName": "dotnet", + "dependencyName": "opbeans:3000", "environment": "production", "id": "5948c153c2d8989f92a9c75ef45bb845f53e200d", "serviceName": "opbeans-dotnet", diff --git a/x-pack/test/cases_api_integration/common/lib/api/case.ts b/x-pack/test/cases_api_integration/common/lib/api/case.ts index b2c6ad9435dc34..a6605d8e83aab6 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/case.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/case.ts @@ -27,6 +27,7 @@ export const createCase = async ( const { body: theCase } = await apiCall .set('kbn-xsrf', 'true') + .set('x-elastic-internal-origin', 'foo') .set(headers) .send(params) .expect(expectedHttpCode); diff --git a/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts b/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts index 797a88881a87bd..e5867ccd748975 100644 --- a/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts +++ b/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts @@ -6,7 +6,10 @@ */ import expect from '@kbn/expect'; -import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { data, MockTelemetryFindings } from './data'; import type { FtrProviderContext } from '../ftr_provider_context'; @@ -67,7 +70,9 @@ export default function ({ getService }: FtrProviderContext) { const { body: [{ stats: apiResponse }], } = await supertest - .post(`/api/telemetry/v2/clusters/_stats`) + .post(`/internal/telemetry/clusters/_stats`) + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .set('kbn-xsrf', 'xxxx') .send({ unencrypted: true, @@ -119,8 +124,10 @@ export default function ({ getService }: FtrProviderContext) { const { body: [{ stats: apiResponse }], } = await supertest - .post(`/api/telemetry/v2/clusters/_stats`) + .post(`/internal/telemetry/clusters/_stats`) .set('kbn-xsrf', 'xxxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true, refreshCache: true, @@ -164,8 +171,10 @@ export default function ({ getService }: FtrProviderContext) { const { body: [{ stats: apiResponse }], } = await supertest - .post(`/api/telemetry/v2/clusters/_stats`) + .post(`/internal/telemetry/clusters/_stats`) .set('kbn-xsrf', 'xxxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true, refreshCache: true, @@ -240,8 +249,10 @@ export default function ({ getService }: FtrProviderContext) { const { body: [{ stats: apiResponse }], } = await supertest - .post(`/api/telemetry/v2/clusters/_stats`) + .post(`/internal/telemetry/clusters/_stats`) .set('kbn-xsrf', 'xxxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true, refreshCache: true, @@ -294,8 +305,10 @@ export default function ({ getService }: FtrProviderContext) { const { body: [{ stats: apiResponse }], } = await supertest - .post(`/api/telemetry/v2/clusters/_stats`) + .post(`/internal/telemetry/clusters/_stats`) .set('kbn-xsrf', 'xxxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true, refreshCache: true, diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/coverage_overview.ts b/x-pack/test/detection_engine_api_integration/basic/tests/coverage_overview.ts index d7427a24657faf..d5e827d545a68e 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/coverage_overview.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/coverage_overview.ts @@ -351,7 +351,7 @@ export default ({ getService }: FtrProviderContext): void => { threat: generateThreatArray(1), }), ]); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); const expectedRule = await createRule(supertest, log, { ...getSimpleRule('rule-1'), diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/bundled_prebuilt_rules_package/install_latest_bundled_prebuilt_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/bundled_prebuilt_rules_package/install_latest_bundled_prebuilt_rules.ts index 97717f00773d9b..d9f710ba6afcf1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/bundled_prebuilt_rules_package/install_latest_bundled_prebuilt_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/bundled_prebuilt_rules_package/install_latest_bundled_prebuilt_rules.ts @@ -15,6 +15,7 @@ import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { deleteAllPrebuiltRuleAssets, deleteAllRules } from '../../utils'; import { getPrebuiltRulesStatus } from '../../utils/prebuilt_rules/get_prebuilt_rules_status'; +import { installPrebuiltRulesPackageByVersion } from '../../utils/prebuilt_rules/install_fleet_package_by_url'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -55,18 +56,17 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusBeforePackageInstallation.stats.num_prebuilt_rules_to_install).toBe(0); expect(statusBeforePackageInstallation.stats.num_prebuilt_rules_to_upgrade).toBe(0); - const EPM_URL = `/api/fleet/epm/packages/security_detection_engine/99.0.0`; - - const bundledInstallResponse = await supertest - .post(EPM_URL) - .set('kbn-xsrf', 'xxxx') - .type('application/json') - .send({ force: true }) - .expect(200); + const bundledInstallResponse = await installPrebuiltRulesPackageByVersion( + es, + supertest, + '99.0.0' + ); // As opposed to "registry" - expect(bundledInstallResponse.body._meta.install_source).toBe('bundled'); + expect(bundledInstallResponse._meta.install_source).toBe('bundled'); + // Refresh ES indices to avoid race conditions between write and reading of indeces + // See implementation utility function at x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules_fleet_package.ts await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); // Verify that status is updated after package installation diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/bundled_prebuilt_rules_package/prerelease_packages.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/bundled_prebuilt_rules_package/prerelease_packages.ts index b1c32bf0e245e5..fd69e3128c3e7f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/bundled_prebuilt_rules_package/prerelease_packages.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/bundled_prebuilt_rules_package/prerelease_packages.ts @@ -4,11 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; -import { DETECTION_ENGINE_RULES_URL_FIND } from '@kbn/security-solution-plugin/common/constants'; import expect from 'expect'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { deleteAllPrebuiltRuleAssets, deleteAllRules } from '../../utils'; +import { getInstalledRules } from '../../utils/prebuilt_rules/get_installed_rules'; import { getPrebuiltRulesStatus } from '../../utils/prebuilt_rules/get_prebuilt_rules_status'; import { installPrebuiltRules } from '../../utils/prebuilt_rules/install_prebuilt_rules'; @@ -38,8 +37,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusBeforePackageInstallation.stats.num_prebuilt_rules_to_install).toBe(0); expect(statusBeforePackageInstallation.stats.num_prebuilt_rules_to_upgrade).toBe(0); - await installPrebuiltRules(supertest); - await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); + await installPrebuiltRules(es, supertest); // Verify that status is updated after package installation const statusAfterPackageInstallation = await getPrebuiltRulesStatus(supertest); @@ -48,11 +46,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusAfterPackageInstallation.stats.num_prebuilt_rules_to_upgrade).toBe(0); // Get installed rules - const { body: rulesResponse } = await supertest - .get(DETECTION_ENGINE_RULES_URL_FIND) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const rulesResponse = await getInstalledRules(supertest); // Assert that installed rules are from package 99.0.0 and not from prerelease (beta) package expect(rulesResponse.data.length).toBe(1); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/large_prebuilt_rules_package/install_large_prebuilt_rules_package.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/large_prebuilt_rules_package/install_large_prebuilt_rules_package.ts index 9dd5e695c37729..c047413bdb90ab 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/large_prebuilt_rules_package/install_large_prebuilt_rules_package.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/large_prebuilt_rules_package/install_large_prebuilt_rules_package.ts @@ -5,7 +5,6 @@ * 2.0. */ import expect from 'expect'; -import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { deleteAllRules, getPrebuiltRulesAndTimelinesStatus } from '../../utils'; import { deleteAllPrebuiltRuleAssets } from '../../utils/prebuilt_rules/delete_all_prebuilt_rule_assets'; @@ -36,8 +35,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusBeforePackageInstallation.rules_not_updated).toBe(0); // Install the package with 15000 prebuilt historical version of rules rules and 750 unique rules - await installPrebuiltRulesAndTimelines(supertest); - await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); + await installPrebuiltRulesAndTimelines(es, supertest); // Verify that status is updated after package installation const statusAfterPackageInstallation = await getPrebuiltRulesAndTimelinesStatus(supertest); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/fleet_integration.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/fleet_integration.ts index e48530ad16513b..1433cb7cac2fff 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/fleet_integration.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/fleet_integration.ts @@ -5,7 +5,6 @@ * 2.0. */ import expect from 'expect'; -import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { deleteAllRules, @@ -43,24 +42,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusBeforePackageInstallation.rules_not_updated).toBe(0); await installPrebuiltRulesFleetPackage({ + es, supertest, overrideExistingPackage: true, }); - // Before we proceed, we need to refresh saved object indices. This comment will explain why. - // At the previous step we installed the Fleet package with prebuilt detection rules. - // Prebuilt rules are assets that Fleet indexes as saved objects of a certain type. - // Fleet does this via a savedObjectsClient.import() call with explicit `refresh: false`. - // So, despite of the fact that the endpoint waits until the prebuilt rule assets will be - // successfully indexed, it doesn't wait until they become "visible" for subsequent read - // operations. Which is what we do next: we read these SOs in getPrebuiltRulesAndTimelinesStatus(). - // Now, the time left until the next refresh can be anything from 0 to the default value, and - // it depends on the time when savedObjectsClient.import() call happens relative to the time of - // the next refresh. Also, probably the refresh time can be delayed when ES is under load? - // Anyway, here we have a race condition between a write and subsequent read operation, and to - // fix it deterministically we have to refresh saved object indices and wait until it's done. - await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); - // Verify that status is updated after package installation const statusAfterPackageInstallation = await getPrebuiltRulesAndTimelinesStatus(supertest); expect(statusAfterPackageInstallation.rules_installed).toBe(0); @@ -68,19 +54,10 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusAfterPackageInstallation.rules_not_updated).toBe(0); // Verify that all previously not installed rules were installed - const response = await installPrebuiltRulesAndTimelines(supertest); + const response = await installPrebuiltRulesAndTimelines(es, supertest); expect(response.rules_installed).toBe(statusAfterPackageInstallation.rules_not_installed); expect(response.rules_updated).toBe(0); - // Similar to the previous refresh, we need to do it again between the two operations: - // - previous write operation: install prebuilt rules and timelines - // - subsequent read operation: get prebuilt rules and timelines status - // You may ask why? I'm not sure, probably because the write operation can install the Fleet - // package under certain circumstances, and it all works with `refresh: false` again. - // Anyway, there were flaky runs failing specifically at one of the next assertions, - // which means some kind of the same race condition we have here too. - await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); - // Verify that status is updated after rules installation const statusAfterRuleInstallation = await getPrebuiltRulesAndTimelinesStatus(supertest); expect(statusAfterRuleInstallation.rules_installed).toBe(response.rules_installed); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/get_prebuilt_rules_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/get_prebuilt_rules_status.ts index ee5730cee39a86..ae43e3bdd50982 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/get_prebuilt_rules_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/get_prebuilt_rules_status.ts @@ -82,7 +82,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the number of installed prebuilt rules after installing them', async () => { await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); const { stats } = await getPrebuiltRulesStatus(supertest); expect(stats).toMatchObject({ @@ -95,7 +95,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should notify the user again that a rule is available for install after it is deleted', async () => { await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); await deleteRule(supertest, 'rule-1'); const { stats } = await getPrebuiltRulesStatus(supertest); @@ -110,7 +110,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return available rule updates', async () => { const ruleAssetSavedObjects = getRuleAssetSavedObjects(); await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Clear previous rule assets await deleteAllPrebuiltRuleAssets(es); @@ -130,7 +130,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not return any available update if rule has been successfully upgraded', async () => { const ruleAssetSavedObjects = getRuleAssetSavedObjects(); await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Clear previous rule assets await deleteAllPrebuiltRuleAssets(es); @@ -138,7 +138,7 @@ export default ({ getService }: FtrProviderContext): void => { ruleAssetSavedObjects[0]['security-rule'].version += 1; await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); // Upgrade all rules - await upgradePrebuiltRules(supertest); + await upgradePrebuiltRules(es, supertest); const { stats } = await getPrebuiltRulesStatus(supertest); expect(stats).toMatchObject({ @@ -152,7 +152,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not return any updates if none are available', async () => { const ruleAssetSavedObjects = getRuleAssetSavedObjects(); await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Clear previous rule assets await deleteAllPrebuiltRuleAssets(es); @@ -193,7 +193,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the number of installed prebuilt rules after installing them', async () => { await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); const { stats } = await getPrebuiltRulesStatus(supertest); expect(stats).toMatchObject({ @@ -206,7 +206,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should notify the user again that a rule is available for install after it is deleted', async () => { await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); await deleteRule(supertest, 'rule-1'); const { stats } = await getPrebuiltRulesStatus(supertest); @@ -220,7 +220,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return available rule updates when previous historical versions available', async () => { await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Add a new version of one of the installed rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ @@ -238,7 +238,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return available rule updates when previous historical versions unavailable', async () => { await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Delete the previous versions of rule assets await deleteAllPrebuiltRuleAssets(es); @@ -261,7 +261,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not return available rule updates after rule has been upgraded', async () => { await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Delete the previous versions of rule assets await deleteAllPrebuiltRuleAssets(es); @@ -272,7 +272,7 @@ export default ({ getService }: FtrProviderContext): void => { ]); // Upgrade the rule - await upgradePrebuiltRules(supertest); + await upgradePrebuiltRules(es, supertest); const { stats } = await getPrebuiltRulesStatus(supertest); expect(stats).toMatchObject({ @@ -339,7 +339,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the number of installed prebuilt rules after installing them', async () => { await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); const body = await getPrebuiltRulesAndTimelinesStatus(supertest); expect(body).toMatchObject({ @@ -352,7 +352,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should notify the user again that a rule is available for install after it is deleted', async () => { await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); await deleteRule(supertest, 'rule-1'); const body = await getPrebuiltRulesAndTimelinesStatus(supertest); @@ -367,7 +367,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return available rule updates', async () => { const ruleAssetSavedObjects = getRuleAssetSavedObjects(); await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Clear previous rule assets await deleteAllPrebuiltRuleAssets(es); @@ -387,7 +387,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not return any updates if none are available', async () => { const ruleAssetSavedObjects = getRuleAssetSavedObjects(); await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Clear previous rule assets await deleteAllPrebuiltRuleAssets(es); @@ -428,7 +428,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the number of installed prebuilt rules after installing them', async () => { await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); const body = await getPrebuiltRulesAndTimelinesStatus(supertest); expect(body).toMatchObject({ @@ -441,7 +441,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should notify the user again that a rule is available for install after it is deleted', async () => { await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); await deleteRule(supertest, 'rule-1'); const body = await getPrebuiltRulesAndTimelinesStatus(supertest); @@ -455,7 +455,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return available rule updates when previous historical versions available', async () => { await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Add a new version of one of the installed rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ @@ -473,7 +473,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return available rule updates when previous historical versions unavailable', async () => { await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Delete the previous versions of rule assets await deleteAllPrebuiltRuleAssets(es); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/get_prebuilt_timelines_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/get_prebuilt_timelines_status.ts index 04275afe20dc95..05b34ffa98ed7e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/get_prebuilt_timelines_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/get_prebuilt_timelines_status.ts @@ -35,7 +35,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return the number of installed timeline templates after installing them', async () => { - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); const body = await getPrebuiltRulesAndTimelinesStatus(supertest); expect(body).toMatchObject({ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/install_and_upgrade_prebuilt_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/install_and_upgrade_prebuilt_rules.ts index c36b81f93cf7c3..85af64415c95e2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/install_and_upgrade_prebuilt_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/prebuilt_rules/install_and_upgrade_prebuilt_rules.ts @@ -6,7 +6,6 @@ */ import expect from 'expect'; -import { DETECTION_ENGINE_RULES_URL_FIND } from '@kbn/security-solution-plugin/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { deleteAllRules, @@ -24,6 +23,7 @@ import { installPrebuiltRulesAndTimelines } from '../../utils/prebuilt_rules/ins import { installPrebuiltRules } from '../../utils/prebuilt_rules/install_prebuilt_rules'; import { getPrebuiltRulesStatus } from '../../utils/prebuilt_rules/get_prebuilt_rules_status'; import { upgradePrebuiltRules } from '../../utils/prebuilt_rules/upgrade_prebuilt_rules'; +import { getInstalledRules } from '../../utils/prebuilt_rules/get_installed_rules'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('using legacy endpoint', () => { it('should install prebuilt rules', async () => { await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const body = await installPrebuiltRulesAndTimelines(supertest); + const body = await installPrebuiltRulesAndTimelines(es, supertest); expect(body.rules_installed).toBe(RULES_COUNT); expect(body.rules_updated).toBe(0); @@ -58,14 +58,10 @@ export default ({ getService }: FtrProviderContext): void => { it('should install correct prebuilt rule versions', async () => { await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Get installed rules - const { body: rulesResponse } = await supertest - .get(DETECTION_ENGINE_RULES_URL_FIND) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const rulesResponse = await getInstalledRules(supertest); // Check that all prebuilt rules were actually installed and their versions match the latest expect(rulesResponse.total).toBe(RULES_COUNT); @@ -82,7 +78,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should install missing prebuilt rules', async () => { // Install all prebuilt detection rules await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Delete one of the installed rules await deleteRule(supertest, 'rule-1'); @@ -92,7 +88,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.rules_not_installed).toBe(1); // Call the install prebuilt rules again and check that the missing rule was installed - const response = await installPrebuiltRulesAndTimelines(supertest); + const response = await installPrebuiltRulesAndTimelines(es, supertest); expect(response.rules_installed).toBe(1); expect(response.rules_updated).toBe(0); }); @@ -101,7 +97,7 @@ export default ({ getService }: FtrProviderContext): void => { // Install all prebuilt detection rules const ruleAssetSavedObjects = getRuleAssetSavedObjects(); await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Clear previous rule assets await deleteAllPrebuiltRuleAssets(es); @@ -114,7 +110,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.rules_not_updated).toBe(1); // Call the install prebuilt rules again and check that the outdated rule was updated - const response = await installPrebuiltRulesAndTimelines(supertest); + const response = await installPrebuiltRulesAndTimelines(es, supertest); expect(response.rules_installed).toBe(0); expect(response.rules_updated).toBe(1); }); @@ -122,7 +118,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not install prebuilt rules if they are up to date', async () => { // Install all prebuilt detection rules await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Check that all prebuilt rules were installed const statusResponse = await getPrebuiltRulesAndTimelinesStatus(supertest); @@ -130,7 +126,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.rules_not_updated).toBe(0); // Call the install prebuilt rules again and check that no rules were installed - const response = await installPrebuiltRulesAndTimelines(supertest); + const response = await installPrebuiltRulesAndTimelines(es, supertest); expect(response.rules_installed).toBe(0); expect(response.rules_updated).toBe(0); }); @@ -139,7 +135,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('using current endpoint', () => { it('should install prebuilt rules', async () => { await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const body = await installPrebuiltRules(supertest); + const body = await installPrebuiltRules(es, supertest); expect(body.summary.succeeded).toBe(RULES_COUNT); expect(body.summary.failed).toBe(0); @@ -148,7 +144,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should install correct prebuilt rule versions', async () => { await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const body = await installPrebuiltRules(supertest); + const body = await installPrebuiltRules(es, supertest); // Check that all prebuilt rules were actually installed and their versions match the latest expect(body.results.created).toEqual( @@ -164,7 +160,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should install missing prebuilt rules', async () => { // Install all prebuilt detection rules await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Delete one of the installed rules await deleteRule(supertest, 'rule-1'); @@ -174,7 +170,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(1); // Call the install prebuilt rules again and check that the missing rule was installed - const response = await installPrebuiltRules(supertest); + const response = await installPrebuiltRules(es, supertest); expect(response.summary.succeeded).toBe(1); }); @@ -182,7 +178,7 @@ export default ({ getService }: FtrProviderContext): void => { // Install all prebuilt detection rules const ruleAssetSavedObjects = getRuleAssetSavedObjects(); await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Clear previous rule assets await deleteAllPrebuiltRuleAssets(es); @@ -196,7 +192,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.stats.num_prebuilt_rules_to_upgrade).toBe(1); // Call the install prebuilt rules again and check that the outdated rule was updated - const response = await upgradePrebuiltRules(supertest); + const response = await upgradePrebuiltRules(es, supertest); expect(response.summary.succeeded).toBe(1); expect(response.summary.skipped).toBe(0); }); @@ -204,7 +200,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not install prebuilt rules if they are up to date', async () => { // Install all prebuilt detection rules await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Check that all prebuilt rules were installed const statusResponse = await getPrebuiltRulesStatus(supertest); @@ -212,12 +208,12 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.stats.num_prebuilt_rules_to_upgrade).toBe(0); // Call the install prebuilt rules again and check that no rules were installed - const installResponse = await installPrebuiltRules(supertest); + const installResponse = await installPrebuiltRules(es, supertest); expect(installResponse.summary.succeeded).toBe(0); expect(installResponse.summary.skipped).toBe(0); // Call the upgrade prebuilt rules endpoint and check that no rules were updated - const upgradeResponse = await upgradePrebuiltRules(supertest); + const upgradeResponse = await upgradePrebuiltRules(es, supertest); expect(upgradeResponse.summary.succeeded).toBe(0); expect(upgradeResponse.summary.skipped).toBe(0); }); @@ -237,7 +233,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('using legacy endpoint', () => { it('should install prebuilt rules', async () => { await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const body = await installPrebuiltRulesAndTimelines(supertest); + const body = await installPrebuiltRulesAndTimelines(es, supertest); expect(body.rules_installed).toBe(RULES_COUNT); expect(body.rules_updated).toBe(0); @@ -245,14 +241,10 @@ export default ({ getService }: FtrProviderContext): void => { it('should install correct prebuilt rule versions', async () => { await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Get installed rules - const { body: rulesResponse } = await supertest - .get(DETECTION_ENGINE_RULES_URL_FIND) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const rulesResponse = await getInstalledRules(supertest); // Check that all prebuilt rules were actually installed and their versions match the latest expect(rulesResponse.total).toBe(RULES_COUNT); @@ -267,14 +259,14 @@ export default ({ getService }: FtrProviderContext): void => { it('should not install prebuilt rules if they are up to date', async () => { // Install all prebuilt detection rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Check that all prebuilt rules were installed const statusResponse = await getPrebuiltRulesAndTimelinesStatus(supertest); expect(statusResponse.rules_not_installed).toBe(0); // Call the install prebuilt rules again and check that no rules were installed - const response = await installPrebuiltRulesAndTimelines(supertest); + const response = await installPrebuiltRulesAndTimelines(es, supertest); expect(response.rules_installed).toBe(0); expect(response.rules_updated).toBe(0); }); @@ -282,7 +274,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should install missing prebuilt rules', async () => { // Install all prebuilt detection rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Delete one of the installed rules await deleteRule(supertest, 'rule-1'); @@ -292,7 +284,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.rules_not_installed).toBe(1); // Call the install prebuilt rules endpoint again and check that the missing rule was installed - const response = await installPrebuiltRulesAndTimelines(supertest); + const response = await installPrebuiltRulesAndTimelines(es, supertest); expect(response.rules_installed).toBe(1); expect(response.rules_updated).toBe(0); }); @@ -300,7 +292,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should update outdated prebuilt rules when previous historical versions available', async () => { // Install all prebuilt detection rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Add a new version of one of the installed rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ @@ -312,7 +304,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.rules_not_updated).toBe(1); // Call the install prebuilt rules again and check that the outdated rule was updated - const response = await installPrebuiltRulesAndTimelines(supertest); + const response = await installPrebuiltRulesAndTimelines(es, supertest); expect(response.rules_installed).toBe(0); expect(response.rules_updated).toBe(1); @@ -324,7 +316,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should update outdated prebuilt rules when previous historical versions unavailable', async () => { // Install all prebuilt detection rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(supertest); + await installPrebuiltRulesAndTimelines(es, supertest); // Clear previous rule assets await deleteAllPrebuiltRuleAssets(es); @@ -340,7 +332,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.rules_not_installed).toBe(0); // Call the install prebuilt rules again and check that the outdated rule was updated - const response = await installPrebuiltRulesAndTimelines(supertest); + const response = await installPrebuiltRulesAndTimelines(es, supertest); expect(response.rules_installed).toBe(0); expect(response.rules_updated).toBe(1); @@ -353,14 +345,14 @@ export default ({ getService }: FtrProviderContext): void => { describe('using current endpoint', () => { it('should install prebuilt rules', async () => { await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const body = await installPrebuiltRules(supertest); + const body = await installPrebuiltRules(es, supertest); expect(body.summary.succeeded).toBe(RULES_COUNT); }); it('should install correct prebuilt rule versions', async () => { await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const response = await installPrebuiltRules(supertest); + const response = await installPrebuiltRules(es, supertest); // Check that all prebuilt rules were actually installed and their versions match the latest expect(response.summary.succeeded).toBe(RULES_COUNT); @@ -375,14 +367,14 @@ export default ({ getService }: FtrProviderContext): void => { it('should not install prebuilt rules if they are up to date', async () => { // Install all prebuilt detection rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Check that all prebuilt rules were installed const statusResponse = await getPrebuiltRulesStatus(supertest); expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(0); // Call the install prebuilt rules again and check that no rules were installed - const response = await installPrebuiltRules(supertest); + const response = await installPrebuiltRules(es, supertest); expect(response.summary.succeeded).toBe(0); expect(response.summary.total).toBe(0); }); @@ -390,7 +382,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should install missing prebuilt rules', async () => { // Install all prebuilt detection rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Delete one of the installed rules await deleteRule(supertest, 'rule-1'); @@ -400,7 +392,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(1); // Call the install prebuilt rules endpoint again and check that the missing rule was installed - const response = await installPrebuiltRules(supertest); + const response = await installPrebuiltRules(es, supertest); expect(response.summary.succeeded).toBe(1); expect(response.summary.total).toBe(1); }); @@ -408,7 +400,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should update outdated prebuilt rules when previous historical versions available', async () => { // Install all prebuilt detection rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Add a new version of one of the installed rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ @@ -420,7 +412,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.stats.num_prebuilt_rules_to_upgrade).toBe(1); // Call the upgrade prebuilt rules endpoint and check that the outdated rule was updated - const response = await upgradePrebuiltRules(supertest); + const response = await upgradePrebuiltRules(es, supertest); expect(response.summary.succeeded).toBe(1); expect(response.summary.total).toBe(1); @@ -432,7 +424,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should update outdated prebuilt rules when previous historical versions unavailable', async () => { // Install all prebuilt detection rules await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(supertest); + await installPrebuiltRules(es, supertest); // Clear previous rule assets await deleteAllPrebuiltRuleAssets(es); @@ -448,7 +440,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(0); // Call the upgrade prebuilt rules endpoint and check that the outdated rule was updated - const response = await upgradePrebuiltRules(supertest); + const response = await upgradePrebuiltRules(es, supertest); expect(response.summary.succeeded).toBe(1); expect(response.summary.total).toBe(1); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/update_prebuilt_rules_package/update_prebuilt_rules_package.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/update_prebuilt_rules_package/update_prebuilt_rules_package.ts index 0e25999a37e9b2..1d7939e83f9abe 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/update_prebuilt_rules_package/update_prebuilt_rules_package.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/update_prebuilt_rules_package/update_prebuilt_rules_package.ts @@ -13,7 +13,6 @@ import { REPO_ROOT } from '@kbn/repo-info'; import JSON5 from 'json5'; import expect from 'expect'; import { PackageSpecManifest } from '@kbn/fleet-plugin/common'; -import { DETECTION_ENGINE_RULES_URL_FIND } from '@kbn/security-solution-plugin/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { deleteAllPrebuiltRuleAssets, @@ -24,6 +23,8 @@ import { } from '../../utils'; import { reviewPrebuiltRulesToInstall } from '../../utils/prebuilt_rules/review_install_prebuilt_rules'; import { reviewPrebuiltRulesToUpgrade } from '../../utils/prebuilt_rules/review_upgrade_prebuilt_rules'; +import { installPrebuiltRulesPackageByVersion } from '../../utils/prebuilt_rules/install_fleet_package_by_url'; +import { getInstalledRules } from '../../utils/prebuilt_rules/get_installed_rules'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -103,17 +104,14 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusBeforePackageInstallation.stats.num_prebuilt_rules_to_upgrade).toBe(0); expect(statusBeforePackageInstallation.stats.num_prebuilt_rules_total_in_package).toBe(0); - const EPM_URL_FOR_PREVIOUS_VERSION = `/api/fleet/epm/packages/security_detection_engine/${previousVersion}`; - - const installPreviousPackageResponse = await supertest - .post(EPM_URL_FOR_PREVIOUS_VERSION) - .set('kbn-xsrf', 'xxxx') - .type('application/json') - .send({ force: true }) - .expect(200); + const installPreviousPackageResponse = await installPrebuiltRulesPackageByVersion( + es, + supertest, + previousVersion + ); - expect(installPreviousPackageResponse.body._meta.install_source).toBe('registry'); - expect(installPreviousPackageResponse.body.items.length).toBeGreaterThan(0); + expect(installPreviousPackageResponse._meta.install_source).toBe('registry'); + expect(installPreviousPackageResponse.items.length).toBeGreaterThan(0); // Verify that status is updated after the installation of package "N-1" const statusAfterPackageInstallation = await getPrebuiltRulesStatus(supertest); @@ -132,7 +130,8 @@ export default ({ getService }: FtrProviderContext): void => { // Verify that the _perform endpoint returns the same number of installed rules as the status endpoint // and the _review endpoint - const installPrebuiltRulesResponse = await installPrebuiltRules(supertest); + const installPrebuiltRulesResponse = await installPrebuiltRules(es, supertest); + expect(installPrebuiltRulesResponse.summary.succeeded).toBe( statusAfterPackageInstallation.stats.num_prebuilt_rules_to_install ); @@ -141,11 +140,7 @@ export default ({ getService }: FtrProviderContext): void => { ); // Get installed rules - const { body: rulesResponse } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL_FIND}?per_page=10000`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const rulesResponse = await getInstalledRules(supertest); // Check that all prebuilt rules were actually installed expect(rulesResponse.total).toBe(installPrebuiltRulesResponse.summary.succeeded); @@ -160,16 +155,13 @@ export default ({ getService }: FtrProviderContext): void => { ); // PART 2: Now install the lastest (current) package, defined in fleet_packages.json - const EPM_URL_FOR_CURRENT_VERSION = `/api/fleet/epm/packages/security_detection_engine/${currentVersion}`; - - const installLatestPackageResponse = await supertest - .post(EPM_URL_FOR_CURRENT_VERSION) - .set('kbn-xsrf', 'xxxx') - .type('application/json') - .send({ force: true }) - .expect(200); - expect(installLatestPackageResponse.body.items.length).toBeGreaterThanOrEqual(0); + const installLatestPackageResponse = await installPrebuiltRulesPackageByVersion( + es, + supertest, + currentVersion + ); + expect(installLatestPackageResponse.items.length).toBeGreaterThanOrEqual(0); // Verify status after intallation of the latest package const statusAfterLatestPackageInstallation = await getPrebuiltRulesStatus(supertest); @@ -196,8 +188,10 @@ export default ({ getService }: FtrProviderContext): void => { // Install available rules and verify that the _perform endpoint returns the same number of // installed rules as the status endpoint and the _review endpoint const installPrebuiltRulesResponseAfterLatestPackageInstallation = await installPrebuiltRules( + es, supertest ); + expect(installPrebuiltRulesResponseAfterLatestPackageInstallation.summary.succeeded).toBe( statusAfterLatestPackageInstallation.stats.num_prebuilt_rules_to_install ); @@ -208,11 +202,7 @@ export default ({ getService }: FtrProviderContext): void => { ); // Get installed rules - const { body: rulesResponseAfterPackageUpdate } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL_FIND}?per_page=10000`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const rulesResponseAfterPackageUpdate = await getInstalledRules(supertest); // Check that the expected new prebuilt rules from the latest package were actually installed expect( @@ -239,8 +229,10 @@ export default ({ getService }: FtrProviderContext): void => { // Call the upgrade _perform endpoint and verify that the number of upgraded rules is the same as the one // returned by the _review endpoint and the status endpoint const upgradePrebuiltRulesResponseAfterLatestPackageInstallation = await upgradePrebuiltRules( + es, supertest ); + expect(upgradePrebuiltRulesResponseAfterLatestPackageInstallation.summary.succeeded).toEqual( statusAfterLatestPackageInstallation.stats.num_prebuilt_rules_to_upgrade ); @@ -249,11 +241,8 @@ export default ({ getService }: FtrProviderContext): void => { ); // Get installed rules - const { body: rulesResponseAfterPackageUpdateAndRuleUpgrades } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL_FIND}?per_page=10000`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + + const rulesResponseAfterPackageUpdateAndRuleUpgrades = await getInstalledRules(supertest); // Check that the expected new prebuilt rules from the latest package were actually installed expect( diff --git a/x-pack/test/detection_engine_api_integration/utils/get_stats.ts b/x-pack/test/detection_engine_api_integration/utils/get_stats.ts index 0871012f8749f6..7f4a2bddbd833e 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_stats.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_stats.ts @@ -8,6 +8,10 @@ import type { ToolingLog } from '@kbn/tooling-log'; import type SuperTest from 'supertest'; import type { DetectionMetrics } from '@kbn/security-solution-plugin/server/usage/detections/types'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { getStatsUrl } from './get_stats_url'; import { getDetectionMetricsFromBody } from './get_detection_metrics_from_body'; @@ -24,6 +28,8 @@ export const getStats = async ( const response = await supertest .post(getStatsUrl()) .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true, refreshCache: true }); if (response.status !== 200) { log.error( diff --git a/x-pack/test/detection_engine_api_integration/utils/get_stats_url.ts b/x-pack/test/detection_engine_api_integration/utils/get_stats_url.ts index ac6537f670f77a..1cd397df922673 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_stats_url.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_stats_url.ts @@ -8,4 +8,4 @@ /** * Cluster stats URL. Replace this with any from kibana core if there is ever a constant there for this. */ -export const getStatsUrl = (): string => '/api/telemetry/v2/clusters/_stats'; +export const getStatsUrl = (): string => '/internal/telemetry/clusters/_stats'; diff --git a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/get_installed_rules.ts b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/get_installed_rules.ts new file mode 100644 index 00000000000000..85eaee80ed3e8f --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/get_installed_rules.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type SuperTest from 'supertest'; +import { DETECTION_ENGINE_RULES_URL_FIND } from '@kbn/security-solution-plugin/common/constants'; +import { FindRulesResponse } from '@kbn/security-solution-plugin/common/api/detection_engine'; + +/** + * Get all installed security rules (both prebuilt + custom) + * + * @param es Elasticsearch client + * @param supertest SuperTest instance + * @param version Semver version of the `security_detection_engine` package to install + * @returns Fleet install package response + */ + +export const getInstalledRules = async ( + supertest: SuperTest.SuperTest +): Promise => { + const { body: rulesResponse } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL_FIND}?per_page=10000`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + return rulesResponse; +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_fleet_package_by_url.ts b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_fleet_package_by_url.ts new file mode 100644 index 00000000000000..802626881b8e6d --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_fleet_package_by_url.ts @@ -0,0 +1,50 @@ +/* + * 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 { Client } from '@elastic/elasticsearch'; +import type SuperTest from 'supertest'; +import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; +import { InstallPackageResponse } from '@kbn/fleet-plugin/common/types'; + +/** + * Installs prebuilt rules package `security_detection_engine` by version. + * + * @param es Elasticsearch client + * @param supertest SuperTest instance + * @param version Semver version of the `security_detection_engine` package to install + * @returns Fleet install package response + */ + +export const installPrebuiltRulesPackageByVersion = async ( + es: Client, + supertest: SuperTest.SuperTest, + version: string +): Promise => { + const fleetResponse = await supertest + .post(`/api/fleet/epm/packages/security_detection_engine/${version}`) + .set('kbn-xsrf', 'xxxx') + .type('application/json') + .send({ force: true }) + .expect(200); + + // Before we proceed, we need to refresh saved object indices. + // At the previous step we installed the Fleet package with prebuilt detection rules. + // Prebuilt rules are assets that Fleet indexes as saved objects of a certain type. + // Fleet does this via a savedObjectsClient.import() call with explicit `refresh: false`. + // So, despite of the fact that the endpoint waits until the prebuilt rule assets will be + // successfully indexed, it doesn't wait until they become "visible" for subsequent read + // operations. + // And this is usually what we do next in integration tests: we read these SOs with utility + // function such as getPrebuiltRulesAndTimelinesStatus(). + // Now, the time left until the next refresh can be anything from 0 to the default value, and + // it depends on the time when savedObjectsClient.import() call happens relative to the time of + // the next refresh. Also, probably the refresh time can be delayed when ES is under load? + // Anyway, this can cause race condition between a write and subsequent read operation, and to + // fix it deterministically we have to refresh saved object indices and wait until it's done. + await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); + + return fleetResponse.body as InstallPackageResponse; +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_mock_prebuilt_rules.ts b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_mock_prebuilt_rules.ts index 6f9726ae6a194d..0e15f416e12388 100644 --- a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_mock_prebuilt_rules.ts +++ b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_mock_prebuilt_rules.ts @@ -24,5 +24,5 @@ export const installMockPrebuiltRules = async ( ): Promise => { // Ensure there are prebuilt rule saved objects before installing rules await createPrebuiltRuleAssetSavedObjects(es); - return installPrebuiltRulesAndTimelines(supertest); + return installPrebuiltRulesAndTimelines(es, supertest); }; diff --git a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules.ts b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules.ts index c11ccb7b37abd6..f05ea093cfc5d6 100644 --- a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules.ts +++ b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules.ts @@ -10,7 +10,9 @@ import { RuleVersionSpecifier, PerformRuleInstallationResponseBody, } from '@kbn/security-solution-plugin/common/api/detection_engine/prebuilt_rules'; +import type { Client } from '@elastic/elasticsearch'; import type SuperTest from 'supertest'; +import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; /** * Installs available prebuilt rules in Kibana. Rules are @@ -27,6 +29,7 @@ import type SuperTest from 'supertest'; * @returns Install prebuilt rules response */ export const installPrebuiltRules = async ( + es: Client, supertest: SuperTest.SuperTest, rules?: RuleVersionSpecifier[] ): Promise => { @@ -42,5 +45,17 @@ export const installPrebuiltRules = async ( .send(payload) .expect(200); + // Before we proceed, we need to refresh saved object indices. + // At the previous step we installed the prebuilt detection rules SO of type 'security-rule'. + // The savedObjectsClient does this with a call with explicit `refresh: false`. + // So, despite of the fact that the endpoint waits until the prebuilt rule will be + // successfully indexed, it doesn't wait until they become "visible" for subsequent read + // operations. + // And this is usually what we do next in integration tests: we read these SOs with utility + // function such as getPrebuiltRulesAndTimelinesStatus(). + // This can cause race conditions between a write and subsequent read operation, and to + // fix it deterministically we have to refresh saved object indices and wait until it's done. + await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); + return response.body; }; diff --git a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules_and_timelines.ts b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules_and_timelines.ts index 7954e2b47bbac4..fdf87a94391c91 100644 --- a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules_and_timelines.ts +++ b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules_and_timelines.ts @@ -9,7 +9,9 @@ import { InstallPrebuiltRulesAndTimelinesResponse, PREBUILT_RULES_URL, } from '@kbn/security-solution-plugin/common/api/detection_engine/prebuilt_rules'; +import type { Client } from '@elastic/elasticsearch'; import type SuperTest from 'supertest'; +import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; /** * (LEGACY) @@ -28,6 +30,7 @@ import type SuperTest from 'supertest'; * @returns Install prebuilt rules response */ export const installPrebuiltRulesAndTimelines = async ( + es: Client, supertest: SuperTest.SuperTest ): Promise => { const response = await supertest @@ -36,5 +39,17 @@ export const installPrebuiltRulesAndTimelines = async ( .send() .expect(200); + // Before we proceed, we need to refresh saved object indices. + // At the previous step we installed the prebuilt detection rules SO of type 'security-rule'. + // The savedObjectsClient does this with a call with explicit `refresh: false`. + // So, despite of the fact that the endpoint waits until the prebuilt rule will be + // successfully indexed, it doesn't wait until they become "visible" for subsequent read + // operations. + // And this is usually what we do next in integration tests: we read these SOs with utility + // function such as getPrebuiltRulesAndTimelinesStatus(). + // This can cause race condition between a write and subsequent read operation, and to + // fix it deterministically we have to refresh saved object indices and wait until it's done. + await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); + return response.body; }; diff --git a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules_fleet_package.ts b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules_fleet_package.ts index 30435caa5a7c31..cc899ecc1dccc4 100644 --- a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules_fleet_package.ts +++ b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/install_prebuilt_rules_fleet_package.ts @@ -5,7 +5,9 @@ * 2.0. */ +import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; import { epmRouteService } from '@kbn/fleet-plugin/common'; +import type { Client } from '@elastic/elasticsearch'; import type SuperTest from 'supertest'; /** @@ -17,10 +19,12 @@ import type SuperTest from 'supertest'; * @param overrideExistingPackage Whether or not to force the install */ export const installPrebuiltRulesFleetPackage = async ({ + es, supertest, version, overrideExistingPackage, }: { + es: Client; supertest: SuperTest.SuperTest; version?: string; overrideExistingPackage: boolean; @@ -46,6 +50,22 @@ export const installPrebuiltRulesFleetPackage = async ({ }) .expect(200); } + + // Before we proceed, we need to refresh saved object indices. + // At the previous step we installed the Fleet package with prebuilt detection rules. + // Prebuilt rules are assets that Fleet indexes as saved objects of a certain type. + // Fleet does this via a savedObjectsClient.import() call with explicit `refresh: false`. + // So, despite of the fact that the endpoint waits until the prebuilt rule assets will be + // successfully indexed, it doesn't wait until they become "visible" for subsequent read + // operations. + // And this is usually what we do next in integration tests: we read these SOs with utility + // function such as getPrebuiltRulesAndTimelinesStatus(). + // Now, the time left until the next refresh can be anything from 0 to the default value, and + // it depends on the time when savedObjectsClient.import() call happens relative to the time of + // the next refresh. Also, probably the refresh time can be delayed when ES is under load? + // Anyway, this can cause race condition between a write and subsequent read operation, and to + // fix it deterministically we have to refresh saved object indices and wait until it's done. + await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); }; /** diff --git a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/upgrade_prebuilt_rules.ts b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/upgrade_prebuilt_rules.ts index bb3299cb5dd9ed..d9ea277fb1421e 100644 --- a/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/upgrade_prebuilt_rules.ts +++ b/x-pack/test/detection_engine_api_integration/utils/prebuilt_rules/upgrade_prebuilt_rules.ts @@ -10,7 +10,9 @@ import { RuleVersionSpecifier, PerformRuleUpgradeResponseBody, } from '@kbn/security-solution-plugin/common/api/detection_engine/prebuilt_rules'; +import type { Client } from '@elastic/elasticsearch'; import type SuperTest from 'supertest'; +import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server'; /** * Upgrades available prebuilt rules in Kibana. @@ -23,6 +25,7 @@ import type SuperTest from 'supertest'; * @returns Upgrade prebuilt rules response */ export const upgradePrebuiltRules = async ( + es: Client, supertest: SuperTest.SuperTest, rules?: RuleVersionSpecifier[] ): Promise => { @@ -38,5 +41,18 @@ export const upgradePrebuiltRules = async ( .send(payload) .expect(200); + // Before we proceed, we need to refresh saved object indices. + // At the previous step we upgraded the prebuilt rules, which, under the hoods, installs new versions + // of the prebuilt detection rules SO of type 'security-rule'. + // The savedObjectsClient does this with a call with explicit `refresh: false`. + // So, despite of the fact that the endpoint waits until the prebuilt rule will be + // successfully indexed, it doesn't wait until they become "visible" for subsequent read + // operations. + // And this is usually what we do next in integration tests: we read these SOs with utility + // function such as getPrebuiltRulesAndTimelinesStatus(). + // This can cause race conditions between a write and subsequent read operation, and to + // fix it deterministically we have to refresh saved object indices and wait until it's done. + await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); + return response.body; }; diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index 47d3f93fd8ddf5..90426d9bdfa3ed 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -119,7 +119,6 @@ export default function (providerContext: FtrProviderContext) { expect(body.item.is_managed).to.equal(false); expect(body.item.inactivity_timeout).to.equal(1209600); expect(body.item.status).to.be('active'); - expect(body.item.is_protected).to.equal(false); }); it('sets given is_managed value', async () => { @@ -445,13 +444,13 @@ export default function (providerContext: FtrProviderContext) { status: 'active', description: 'Test', is_managed: false, - is_protected: false, namespace: 'default', monitoring_enabled: ['logs', 'metrics'], revision: 1, schema_version: FLEET_AGENT_POLICIES_SCHEMA_VERSION, updated_by: 'elastic', package_policies: [], + is_protected: false, }); }); @@ -732,7 +731,7 @@ export default function (providerContext: FtrProviderContext) { name: 'Updated name', description: 'Updated description', namespace: 'default', - is_protected: true, + is_protected: false, }) .expect(200); createdPolicyIds.push(updatedPolicy.id); @@ -750,7 +749,7 @@ export default function (providerContext: FtrProviderContext) { updated_by: 'elastic', inactivity_timeout: 1209600, package_policies: [], - is_protected: true, + is_protected: false, }); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts b/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts index 492d708da5ef7a..88d71edb74d47c 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_custom.ts @@ -10,7 +10,7 @@ import { PACKAGES_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; -const INTEGRATION_NAME = 'my_custom_nginx'; +const INTEGRATION_NAME = 'my_nginx'; const INTEGRATION_VERSION = '1.0.0'; export default function (providerContext: FtrProviderContext) { @@ -21,7 +21,8 @@ export default function (providerContext: FtrProviderContext) { const uninstallPackage = async () => { await supertest .delete(`/api/fleet/epm/packages/${INTEGRATION_NAME}/${INTEGRATION_VERSION}`) - .set('kbn-xsrf', 'xxxx'); + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); }; describe('Installing custom integrations', async () => { @@ -36,7 +37,7 @@ export default function (providerContext: FtrProviderContext) { .type('application/json') .send({ force: true, - integrationName: 'my_custom_nginx', + integrationName: INTEGRATION_NAME, datasets: [ { name: 'access', type: 'logs' }, { name: 'error', type: 'metrics' }, @@ -46,22 +47,22 @@ export default function (providerContext: FtrProviderContext) { .expect(200); const expectedIngestPipelines = [ - 'logs-my_custom_nginx.access-1.0.0', - 'metrics-my_custom_nginx.error-1.0.0', - 'logs-my_custom_nginx.warning-1.0.0', + `logs-${INTEGRATION_NAME}.access-1.0.0`, + `metrics-${INTEGRATION_NAME}.error-1.0.0`, + `logs-${INTEGRATION_NAME}.warning-1.0.0`, ]; const expectedIndexTemplates = [ - 'logs-my_custom_nginx.access', - 'metrics-my_custom_nginx.error', - 'logs-my_custom_nginx.warning', + `logs-${INTEGRATION_NAME}.access`, + `metrics-${INTEGRATION_NAME}.error`, + `logs-${INTEGRATION_NAME}.warning`, ]; const expectedComponentTemplates = [ - 'logs-my_custom_nginx.access@package', - 'logs-my_custom_nginx.access@custom', - 'metrics-my_custom_nginx.error@package', - 'metrics-my_custom_nginx.error@custom', - 'logs-my_custom_nginx.warning@package', - 'logs-my_custom_nginx.warning@custom', + `logs-${INTEGRATION_NAME}.access@package`, + `logs-${INTEGRATION_NAME}.access@custom`, + `metrics-${INTEGRATION_NAME}.error@package`, + `metrics-${INTEGRATION_NAME}.error@custom`, + `logs-${INTEGRATION_NAME}.warning@package`, + `logs-${INTEGRATION_NAME}.warning@custom`, ]; expect(response.body._meta.install_source).to.be('custom'); @@ -92,11 +93,65 @@ export default function (providerContext: FtrProviderContext) { type: PACKAGES_SAVED_OBJECT_TYPE, id: INTEGRATION_NAME, }); - expect(installation.attributes.name).to.be(INTEGRATION_NAME); expect(installation.attributes.version).to.be(INTEGRATION_VERSION); expect(installation.attributes.install_source).to.be('custom'); expect(installation.attributes.install_status).to.be('installed'); }); + + it('Throws an error when there is a naming collision with a current package installation', async () => { + await supertest + .post(`/api/fleet/epm/custom_integrations`) + .set('kbn-xsrf', 'xxxx') + .type('application/json') + .send({ + force: true, + integrationName: INTEGRATION_NAME, + datasets: [ + { name: 'access', type: 'logs' }, + { name: 'error', type: 'metrics' }, + { name: 'warning', type: 'logs' }, + ], + }) + .expect(200); + + const response = await supertest + .post(`/api/fleet/epm/custom_integrations`) + .set('kbn-xsrf', 'xxxx') + .type('application/json') + .send({ + force: true, + integrationName: INTEGRATION_NAME, + datasets: [ + { name: 'access', type: 'logs' }, + { name: 'error', type: 'metrics' }, + { name: 'warning', type: 'logs' }, + ], + }) + .expect(409); + + expect(response.body.message).to.be( + `Failed to create the integration as an installation with the name ${INTEGRATION_NAME} already exists.` + ); + }); + + it('Throws an error when there is a naming collision with a registry package', async () => { + const pkgName = 'apache'; + + const response = await supertest + .post(`/api/fleet/epm/custom_integrations`) + .set('kbn-xsrf', 'xxxx') + .type('application/json') + .send({ + force: true, + integrationName: pkgName, + datasets: [{ name: 'error', type: 'logs' }], + }) + .expect(409); + + expect(response.body.message).to.be( + `Failed to create the integration as an integration with the name ${pkgName} already exists in the package registry or as a bundled package.` + ); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts index 04b8d80fdbee19..3efb072907faca 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry, generateAgent } from '../helpers'; @@ -124,8 +128,10 @@ export default function (providerContext: FtrProviderContext) { const { body: [{ stats: apiResponse }], } = await supertest - .post(`/api/telemetry/v2/clusters/_stats`) + .post(`/internal/telemetry/clusters/_stats`) .set('kbn-xsrf', 'xxxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: true, refreshCache: true, diff --git a/x-pack/test/fleet_api_integration/apis/policy_secrets.ts b/x-pack/test/fleet_api_integration/apis/policy_secrets.ts index 52b614f389ba95..63878420084a23 100644 --- a/x-pack/test/fleet_api_integration/apis/policy_secrets.ts +++ b/x-pack/test/fleet_api_integration/apis/policy_secrets.ts @@ -12,6 +12,7 @@ import type { Client } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; import { FullAgentPolicy } from '@kbn/fleet-plugin/common'; +import { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common/constants'; import { v4 as uuidv4 } from 'uuid'; import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../helpers'; @@ -50,6 +51,126 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const createFleetServerAgentPolicy = async () => { + const agentPolicyResponse = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxx') + .send({ + name: `Fleet server policy ${uuidv4()}`, + namespace: 'default', + }) + .expect(200); + + const agentPolicyId = agentPolicyResponse.body.item.id; + + // create fleet_server package policy + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxx') + .send({ + force: true, + package: { + name: 'fleet_server', + version: '1.3.1', + }, + name: `Fleet Server ${uuidv4()}`, + namespace: 'default', + policy_id: agentPolicyId, + vars: {}, + inputs: { + 'fleet_server-fleet-server': { + enabled: true, + vars: { + custom: '', + }, + streams: {}, + }, + }, + }) + .expect(200); + + return agentPolicyId; + }; + + const createPolicyWithSecrets = async () => { + return supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: `secrets-${Date.now()}`, + description: '', + namespace: 'default', + policy_id: agentPolicyId, + inputs: { + 'secrets-test_input': { + enabled: true, + vars: { + input_var_secret: 'input_secret_val', + }, + streams: { + 'secrets.log': { + enabled: true, + vars: { + stream_var_secret: 'stream_secret_val', + }, + }, + }, + }, + }, + vars: { + package_var_secret: 'package_secret_val', + }, + package: { + name: 'secrets', + version: '1.0.0', + }, + }) + .expect(200); + }; + + const createFleetServerAgent = async ( + agentPolicyId: string, + hostname: string, + agentVersion: string + ) => { + const agentResponse = await es.index({ + index: '.fleet-agents', + refresh: true, + body: { + access_api_key_id: 'api-key-3', + active: true, + policy_id: agentPolicyId, + type: 'PERMANENT', + local_metadata: { + host: { hostname }, + elastic: { agent: { version: agentVersion } }, + }, + user_provided_metadata: {}, + enrolled_at: '2022-06-21T12:14:25Z', + last_checkin: '2022-06-27T12:28:29Z', + tags: ['tag1'], + }, + }); + + return agentResponse._id; + }; + + const clearAgents = async () => { + try { + await es.deleteByQuery({ + index: '.fleet-agents', + refresh: true, + body: { + query: { + match_all: {}, + }, + }, + }); + } catch (err) { + // index doesn't exist + } + }; + const getSecrets = async (ids?: string[]) => { const query = ids ? { terms: { _id: ids } } : { match_all: {} }; return es.search({ @@ -71,7 +192,7 @@ export default function (providerContext: FtrProviderContext) { }, }); } catch (err) { - // index doesnt exis + // index doesn't exist } }; @@ -80,6 +201,36 @@ export default function (providerContext: FtrProviderContext) { return body.item; }; + const enableSecrets = async () => { + try { + await kibanaServer.savedObjects.update({ + type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + id: 'fleet-default-settings', + attributes: { + secret_storage_requirements_met: true, + }, + overwrite: false, + }); + } catch (e) { + throw e; + } + }; + + const disableSecrets = async () => { + try { + await kibanaServer.savedObjects.update({ + type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + id: 'fleet-default-settings', + attributes: { + secret_storage_requirements_met: false, + }, + overwrite: false, + }); + } catch (e) { + throw e; + } + }; + const getFullAgentPolicyById = async (id: string) => { const { body } = await supertest.get(`/api/fleet/agent_policies/${id}/full`).expect(200); return body.item; @@ -141,10 +292,13 @@ export default function (providerContext: FtrProviderContext) { skipIfNoDockerRegistry(providerContext); let agentPolicyId: string; + let fleetServerAgentPolicyId: string; before(async () => { await kibanaServer.savedObjects.cleanStandardList(); await deleteAllSecrets(); + await clearAgents(); + await enableSecrets(); }); setupFleetAndAgents(providerContext); @@ -160,6 +314,8 @@ export default function (providerContext: FtrProviderContext) { .expect(200); agentPolicyId = agentPolicyResponse.item.id; + + fleetServerAgentPolicyId = await createFleetServerAgentPolicy(); }); after(async () => { @@ -427,5 +583,67 @@ export default function (providerContext: FtrProviderContext) { expect(searchRes.hits.hits.length).to.eql(0); }); + + it('should not store secrets if fleet server does not meet minimum version', async () => { + await createFleetServerAgent(fleetServerAgentPolicyId, 'server_1', '7.0.0'); + await disableSecrets(); + + const createdPolicy = await createPolicyWSecretVar(); + + // secret should be in plain text i.e not a secret refrerence + expect(createdPolicy.vars.package_var_secret.value).eql('package_secret_val'); + }); + + async function createPolicyWSecretVar() { + const { body: createResBody } = await createPolicyWithSecrets(); + const createdPolicy = createResBody.item; + return createdPolicy; + } + + it('should not store secrets if there are no fleet servers', async () => { + await clearAgents(); + + const { body: createResBody } = await createPolicyWithSecrets(); + + const createdPolicy = createResBody.item; + + // secret should be in plain text i.e not a secret refrerence + expect(createdPolicy.vars.package_var_secret.value).eql('package_secret_val'); + }); + + it('should convert plain text values to secrets once fleet server requirements are met', async () => { + await clearAgents(); + + const createdPolicy = await createPolicyWSecretVar(); + + await createFleetServerAgent(fleetServerAgentPolicyId, 'server_2', '9.0.0'); + + const updatedPolicy = createdPolicyToUpdatePolicy(createdPolicy); + delete updatedPolicy.name; + + updatedPolicy.vars.package_var_secret.value = 'package_secret_val_2'; + + const updateRes = await supertest + .put(`/api/fleet/package_policies/${createdPolicy.id}`) + .set('kbn-xsrf', 'xxxx') + .send(updatedPolicy) + .expect(200); + + const updatedPolicyRes = updateRes.body.item; + + expect(updatedPolicyRes.vars.package_var_secret.value.isSecretRef).eql(true); + expect(updatedPolicyRes.inputs[0].vars.input_var_secret.value.isSecretRef).eql(true); + expect(updatedPolicyRes.inputs[0].streams[0].vars.stream_var_secret.value.isSecretRef).eql( + true + ); + }); + + it('should not revert to plaintext values if the user adds an out of date fleet server', async () => { + await createFleetServerAgent(fleetServerAgentPolicyId, 'server_3', '7.0.0'); + + const createdPolicy = await createPolicyWSecretVar(); + + expect(createdPolicy.vars.package_var_secret.value.isSecretRef).eql(true); + }); }); } diff --git a/x-pack/test/functional/apps/discover_log_explorer/columns_selection.ts b/x-pack/test/functional/apps/discover_log_explorer/columns_selection.ts new file mode 100644 index 00000000000000..c1a9aee81758af --- /dev/null +++ b/x-pack/test/functional/apps/discover_log_explorer/columns_selection.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const defaultLogColumns = ['@timestamp', 'message']; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'discover']); + + describe('Columns selection initialization and update', () => { + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + ); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + ); + }); + + describe('when the log explorer profile loads', () => { + it("should initialize the table columns to logs' default selection", async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + + await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage(); + + await retry.try(async () => { + expect(await PageObjects.discover.getColumnHeaders()).to.eql(defaultLogColumns); + }); + }); + + it('should restore the table columns from the URL state if exists', async () => { + await PageObjects.common.navigateToApp('discover', { + hash: '/p/log-explorer?_a=(columns:!(message,data_stream.namespace))', + }); + + await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage(); + + await retry.try(async () => { + expect(await PageObjects.discover.getColumnHeaders()).to.eql([ + ...defaultLogColumns, + 'data_stream.namespace', + ]); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/discover_log_explorer/customization.ts b/x-pack/test/functional/apps/discover_log_explorer/customization.ts index 2dba2ed30b695b..6cd713a40f63a2 100644 --- a/x-pack/test/functional/apps/discover_log_explorer/customization.ts +++ b/x-pack/test/functional/apps/discover_log_explorer/customization.ts @@ -25,14 +25,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('DatasetSelector should replace the DataViewPicker', async () => { // Assert does not render on discover app await PageObjects.common.navigateToApp('discover'); - await testSubjects.missingOrFail('dataset-selector-popover'); + await testSubjects.missingOrFail('datasetSelectorPopover'); // Assert it renders on log-explorer profile await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); - await testSubjects.existOrFail('dataset-selector-popover'); + await testSubjects.existOrFail('datasetSelectorPopover'); }); - it('the TopNav bar should hide New, Open and Save options', async () => { + it('the TopNav bar should hide then New, Open and Save options', async () => { // Assert does not render on discover app await PageObjects.common.navigateToApp('discover'); await testSubjects.existOrFail('discoverNewButton'); @@ -59,6 +59,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const results = await PageObjects.navigationalSearch.getDisplayedResults(); expect(results[0].label).to.eql('Discover / Logs Explorer'); }); + + it('should render a filter controls section as part of the unified search bar', async () => { + // Assert does not render on discover app + await PageObjects.common.navigateToApp('discover'); + await testSubjects.missingOrFail('datasetFiltersCustomization'); + + // Assert it renders on log-explorer profile + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await testSubjects.existOrFail('datasetFiltersCustomization', { allowHidden: true }); + }); }); }); } diff --git a/x-pack/test/functional/apps/discover_log_explorer/dataset_selection_state.ts b/x-pack/test/functional/apps/discover_log_explorer/dataset_selection_state.ts index f795afb9664939..c1c2b335358bc2 100644 --- a/x-pack/test/functional/apps/discover_log_explorer/dataset_selection_state.ts +++ b/x-pack/test/functional/apps/discover_log_explorer/dataset_selection_state.ts @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('when the "index" query param exist', () => { + describe('when the "index" query param exists', () => { it('should decode and restore the selection from a valid encoded index', async () => { const azureActivitylogsIndex = 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; @@ -37,7 +37,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(datasetSelectionTitle).to.be('[Azure Logs] activitylogs'); }); - it('should fallback to "All log datasets" selection and notify the user for an invalid encoded index', async () => { + it('should fallback to the "All log datasets" selection and notify the user of an invalid encoded index', async () => { const invalidEncodedIndex = 'invalid-encoded-index'; await PageObjects.common.navigateToApp('discover', { hash: `/p/log-explorer?_a=(index:${encodeURIComponent(invalidEncodedIndex)})`, diff --git a/x-pack/test/functional/apps/discover_log_explorer/dataset_selector.ts b/x-pack/test/functional/apps/discover_log_explorer/dataset_selector.ts new file mode 100644 index 00000000000000..b456da8bddb2a2 --- /dev/null +++ b/x-pack/test/functional/apps/discover_log_explorer/dataset_selector.ts @@ -0,0 +1,664 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +const initialPackageMap = { + apache: 'Apache HTTP Server', + aws: 'AWS', + system: 'System', +}; +const initialPackagesTexts = Object.values(initialPackageMap); + +const expectedUncategorized = ['logs-gaming-*', 'logs-manufacturing-*', 'logs-retail-*']; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'discoverLogExplorer']); + + describe('Dataset Selector', () => { + before(async () => { + await PageObjects.discoverLogExplorer.removeInstalledPackages(); + }); + + describe('without installed integrations or uncategorized data streams', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + beforeEach(async () => { + await browser.refresh(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + }); + + describe('when open on the first navigation level', () => { + it('should always display the "All log datasets" entry as the first item', async () => { + const allLogDatasetButton = + await PageObjects.discoverLogExplorer.getAllLogDatasetsButton(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + const allLogDatasetTitle = await allLogDatasetButton.getVisibleText(); + const firstEntryTitle = await menuEntries[0].getVisibleText(); + + expect(allLogDatasetTitle).to.be('All log datasets'); + expect(allLogDatasetTitle).to.be(firstEntryTitle); + }); + + it('should always display the unmanaged datasets entry as the second item', async () => { + const unamanagedDatasetButton = + await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + const unmanagedDatasetTitle = await unamanagedDatasetButton.getVisibleText(); + const secondEntryTitle = await menuEntries[1].getVisibleText(); + + expect(unmanagedDatasetTitle).to.be('Uncategorized'); + expect(unmanagedDatasetTitle).to.be(secondEntryTitle); + }); + + it('should display an error prompt if could not retrieve the integrations', async function () { + // Skip the test in case network condition utils are not available + try { + await retry.try(async () => { + await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + }); + + await PageObjects.common.sleep(5000); + await browser.setNetworkConditions('OFFLINE'); + await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + + await retry.try(async () => { + await PageObjects.discoverLogExplorer.assertNoIntegrationsErrorExists(); + }); + + await browser.restoreNetworkConditions(); + } catch (error) { + this.skip(); + } + }); + + it('should display an empty prompt for no integrations', async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations.length).to.be(0); + + await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + }); + }); + + describe('when navigating into Uncategorized data streams', () => { + it('should display a loading skeleton while loading', async function () { + // Skip the test in case network condition utils are not available + try { + await browser.setNetworkConditions('SLOW_3G'); // Almost stuck network conditions + const unamanagedDatasetButton = + await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await unamanagedDatasetButton.click(); + + await PageObjects.discoverLogExplorer.assertLoadingSkeletonExists(); + + await browser.restoreNetworkConditions(); + } catch (error) { + this.skip(); + } + }); + + it('should display an error prompt if could not retrieve the data streams', async function () { + // Skip the test in case network condition utils are not available + try { + const unamanagedDatasetButton = + await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await unamanagedDatasetButton.click(); + + await retry.try(async () => { + await PageObjects.discoverLogExplorer.assertNoDataStreamsPromptExists(); + }); + + await browser.setNetworkConditions('OFFLINE'); + await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + + await retry.try(async () => { + await PageObjects.discoverLogExplorer.assertNoDataStreamsErrorExists(); + }); + + await browser.restoreNetworkConditions(); + } catch (error) { + this.skip(); + } + }); + + it('should display an empty prompt for no data streams', async () => { + const unamanagedDatasetButton = + await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await unamanagedDatasetButton.click(); + + const unamanagedDatasetEntries = + await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(unamanagedDatasetEntries.length).to.be(0); + + await PageObjects.discoverLogExplorer.assertNoDataStreamsPromptExists(); + }); + }); + }); + + describe('with installed integrations and uncategorized data streams', () => { + let cleanupIntegrationsSetup: () => Promise; + + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + ); + cleanupIntegrationsSetup = await PageObjects.discoverLogExplorer.setupInitialIntegrations(); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + ); + await cleanupIntegrationsSetup(); + }); + + describe('when open on the first navigation level', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + beforeEach(async () => { + await browser.refresh(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + }); + + it('should always display the "All log datasets" entry as the first item', async () => { + const allLogDatasetButton = + await PageObjects.discoverLogExplorer.getAllLogDatasetsButton(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + const allLogDatasetTitle = await allLogDatasetButton.getVisibleText(); + const firstEntryTitle = await menuEntries[0].getVisibleText(); + + expect(allLogDatasetTitle).to.be('All log datasets'); + expect(allLogDatasetTitle).to.be(firstEntryTitle); + }); + + it('should always display the unmanaged datasets entry as the second item', async () => { + const unamanagedDatasetButton = + await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + const unmanagedDatasetTitle = await unamanagedDatasetButton.getVisibleText(); + const secondEntryTitle = await menuEntries[1].getVisibleText(); + + expect(unmanagedDatasetTitle).to.be('Uncategorized'); + expect(unmanagedDatasetTitle).to.be(secondEntryTitle); + }); + + it('should display a list of installed integrations', async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + + expect(integrations.length).to.be(3); + expect(integrations).to.eql(initialPackagesTexts); + }); + + it('should sort the integrations list by the clicked sorting option', async () => { + // Test ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql(initialPackagesTexts); + }); + + // Test descending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql(initialPackagesTexts.slice().reverse()); + }); + + // Test back ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql(initialPackagesTexts); + }); + }); + + it('should filter the integrations list by the typed integration name', async () => { + await PageObjects.discoverLogExplorer.typeSearchFieldWith('system'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.system]); + }); + + await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.apache, initialPackageMap.aws]); + }); + }); + + it('should display an empty prompt when the search does not match any result', async () => { + await PageObjects.discoverLogExplorer.typeSearchFieldWith('no result search text'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations.length).to.be(0); + }); + + await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + }); + + it('should load more integrations by scrolling to the end of the list', async () => { + // Install more integrations and reload the page + const cleanupAdditionalSetup = + await PageObjects.discoverLogExplorer.setupAdditionalIntegrations(); + await browser.refresh(); + + await PageObjects.discoverLogExplorer.openDatasetSelector(); + + // Initially fetched integrations + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(nodes.length).to.be(15); + await nodes.at(-1)?.scrollIntoViewIfNecessary(); + }); + + // Load more integrations + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(nodes.length).to.be(20); + await nodes.at(-1)?.scrollIntoViewIfNecessary(); + }); + + // No other integrations to load after scrolling to last integration + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(nodes.length).to.be(20); + }); + + cleanupAdditionalSetup(); + }); + }); + + describe('when clicking on integration and moving into the second navigation level', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + beforeEach(async () => { + await browser.refresh(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + }); + + it('should display a list of available datasets', async () => { + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + await nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + }); + + it('should sort the datasets list by the clicked sorting option', async () => { + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + await nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + }); + + // Test ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + + // Test descending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('error'); + expect(await menuEntries[1].getVisibleText()).to.be('access'); + }); + + // Test back ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + }); + + it('should filter the datasets list by the typed dataset name', async () => { + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + await nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + }); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + + await PageObjects.discoverLogExplorer.typeSearchFieldWith('err'); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(menuEntries.length).to.be(1); + expect(await menuEntries[0].getVisibleText()).to.be('error'); + }); + }); + + it('should update the current selection with the clicked dataset', async () => { + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + await nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + }); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('access'); + menuEntries[0].click(); + }); + + await retry.try(async () => { + const selectorButton = await PageObjects.discoverLogExplorer.getDatasetSelectorButton(); + + expect(await selectorButton.getVisibleText()).to.be('[Apache HTTP Server] access'); + }); + }); + }); + + describe('when navigating into Uncategorized data streams', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + beforeEach(async () => { + await browser.refresh(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + }); + + it('should display a list of available datasets', async () => { + const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await button.click(); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); + expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); + expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); + expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[2]); + }); + }); + + it('should sort the datasets list by the clicked sorting option', async () => { + const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await button.click(); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); + }); + + // Test ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); + expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); + expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[2]); + }); + + // Test descending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[2]); + expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); + expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[0]); + }); + + // Test back ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); + expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); + expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[2]); + }); + }); + + it('should filter the datasets list by the typed dataset name', async () => { + const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await button.click(); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); + }); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); + expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); + expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[2]); + }); + + await PageObjects.discoverLogExplorer.typeSearchFieldWith('retail'); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(menuEntries.length).to.be(1); + expect(await menuEntries[0].getVisibleText()).to.be('logs-retail-*'); + }); + }); + + it('should update the current selection with the clicked dataset', async () => { + const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await button.click(); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); + }); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('logs-gaming-*'); + menuEntries[0].click(); + }); + + await retry.try(async () => { + const selectorButton = await PageObjects.discoverLogExplorer.getDatasetSelectorButton(); + + expect(await selectorButton.getVisibleText()).to.be('logs-gaming-*'); + }); + }); + }); + + describe('when open/close the selector', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + beforeEach(async () => { + await browser.refresh(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + }); + + it('should restore the latest navigation panel', async () => { + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + await nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + + await PageObjects.discoverLogExplorer.closeDatasetSelector(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + }); + + it('should restore the latest search results', async () => { + await PageObjects.discoverLogExplorer.typeSearchFieldWith('system'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.system]); + }); + + await PageObjects.discoverLogExplorer.closeDatasetSelector(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.system]); + }); + }); + }); + + describe('when switching between integration panels', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + it('should remember the latest search and restore its results for each integration', async () => { + await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.discoverLogExplorer.clearSearchField(); + + await PageObjects.discoverLogExplorer.typeSearchFieldWith('apache'); + + await retry.try(async () => { + const { nodes, integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.apache]); + nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + + await PageObjects.discoverLogExplorer.typeSearchFieldWith('err'); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(menuEntries.length).to.be(1); + expect(await menuEntries[0].getVisibleText()).to.be('error'); + }); + + // Navigate back to integrations + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + panelTitleNode.click(); + + await retry.try(async () => { + const { nodes, integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.apache]); + + const searchValue = await PageObjects.discoverLogExplorer.getSearchFieldValue(); + expect(searchValue).to.eql('apache'); + + nodes[0].click(); + }); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + const searchValue = await PageObjects.discoverLogExplorer.getSearchFieldValue(); + expect(searchValue).to.eql('err'); + + expect(menuEntries.length).to.be(1); + expect(await menuEntries[0].getVisibleText()).to.be('error'); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/discover_log_explorer/index.ts b/x-pack/test/functional/apps/discover_log_explorer/index.ts index 719bd8a7fcb286..dd8b99db79ad0d 100644 --- a/x-pack/test/functional/apps/discover_log_explorer/index.ts +++ b/x-pack/test/functional/apps/discover_log_explorer/index.ts @@ -9,7 +9,9 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Discover Log-Explorer profile', function () { + loadTestFile(require.resolve('./columns_selection')); loadTestFile(require.resolve('./customization')); loadTestFile(require.resolve('./dataset_selection_state')); + loadTestFile(require.resolve('./dataset_selector')); }); } diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index 8166af78482753..daf6296ed2c2c1 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -6,6 +6,10 @@ */ import expect from '@kbn/expect'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import { DATES } from './constants'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -133,8 +137,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await logsUi.logStreamPage.getStreamEntries(); const [{ stats }] = await supertest - .post(`/api/telemetry/v2/clusters/_stats`) + .post(`/internal/telemetry/clusters/_stats`) .set(COMMON_REQUEST_HEADERS) + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .set('Accept', 'application/json') .send({ unencrypted: true, diff --git a/x-pack/test/functional/apps/lens/group1/smokescreen.ts b/x-pack/test/functional/apps/lens/group1/smokescreen.ts index 4ae232f8fbf6bd..dbd734348ba7d0 100644 --- a/x-pack/test/functional/apps/lens/group1/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/group1/smokescreen.ts @@ -86,7 +86,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.waitForVisualization('xyVisChart'); // Verify that the field was persisted from the transition - expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`ip : *`, `geo.src : CN`]); + expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`"ip" : *`, `geo.src : CN`]); expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); }); diff --git a/x-pack/test/functional/es_archives/discover_log_explorer/data_streams/data.json.gz b/x-pack/test/functional/es_archives/discover_log_explorer/data_streams/data.json.gz new file mode 100644 index 00000000000000..4e72a78a4f8b9b Binary files /dev/null and b/x-pack/test/functional/es_archives/discover_log_explorer/data_streams/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/discover_log_explorer/data_streams/mappings.json b/x-pack/test/functional/es_archives/discover_log_explorer/data_streams/mappings.json new file mode 100644 index 00000000000000..ed9e5982f576fa --- /dev/null +++ b/x-pack/test/functional/es_archives/discover_log_explorer/data_streams/mappings.json @@ -0,0 +1,419 @@ +{ + "type": "data_stream", + "value": { + "data_stream": "logs-gaming-activity", + "template": { + "_meta": { + "description": "Template for my time series data", + "my-custom-meta-field": "More arbitrary metadata" + }, + "data_stream": { + "allow_custom_routing": false, + "hidden": false + }, + "index_patterns": [ + "logs-gaming-activity" + ], + "name": "logs-gaming-activity", + "priority": 500, + "template": { + "mappings": { + "properties": { + "@timestamp": { + "format": "date_optional_time||epoch_millis", + "type": "date" + }, + "data_stream": { + "properties": { + "namespace": { + "type": "constant_keyword" + } + } + }, + "message": { + "type": "wildcard" + } + } + } + } + } + } +} + +{ + "type": "data_stream", + "value": { + "data_stream": "logs-gaming-events", + "template": { + "_meta": { + "description": "Template for my time series data", + "my-custom-meta-field": "More arbitrary metadata" + }, + "data_stream": { + "allow_custom_routing": false, + "hidden": false + }, + "index_patterns": [ + "logs-gaming-events" + ], + "name": "logs-gaming-events", + "priority": 500, + "template": { + "mappings": { + "properties": { + "@timestamp": { + "format": "date_optional_time||epoch_millis", + "type": "date" + }, + "data_stream": { + "properties": { + "namespace": { + "type": "constant_keyword" + } + } + }, + "message": { + "type": "wildcard" + } + } + } + } + } + } +} + +{ + "type": "data_stream", + "value": { + "data_stream": "logs-gaming-scores", + "template": { + "_meta": { + "description": "Template for my time series data", + "my-custom-meta-field": "More arbitrary metadata" + }, + "data_stream": { + "allow_custom_routing": false, + "hidden": false + }, + "index_patterns": [ + "logs-gaming-scores" + ], + "name": "logs-gaming-scores", + "priority": 500, + "template": { + "mappings": { + "properties": { + "@timestamp": { + "format": "date_optional_time||epoch_millis", + "type": "date" + }, + "data_stream": { + "properties": { + "namespace": { + "type": "constant_keyword" + } + } + }, + "message": { + "type": "wildcard" + } + } + } + } + } + } +} + +{ + "type": "data_stream", + "value": { + "data_stream": "logs-manufacturing-downtime", + "template": { + "_meta": { + "description": "Template for my time series data", + "my-custom-meta-field": "More arbitrary metadata" + }, + "data_stream": { + "allow_custom_routing": false, + "hidden": false + }, + "index_patterns": [ + "logs-manufacturing-downtime" + ], + "name": "logs-manufacturing-downtime", + "priority": 500, + "template": { + "mappings": { + "properties": { + "@timestamp": { + "format": "date_optional_time||epoch_millis", + "type": "date" + }, + "data_stream": { + "properties": { + "namespace": { + "type": "constant_keyword" + } + } + }, + "message": { + "type": "wildcard" + } + } + } + } + } + } +} + +{ + "type": "data_stream", + "value": { + "data_stream": "logs-manufacturing-output", + "template": { + "_meta": { + "description": "Template for my time series data", + "my-custom-meta-field": "More arbitrary metadata" + }, + "data_stream": { + "allow_custom_routing": false, + "hidden": false + }, + "index_patterns": [ + "logs-manufacturing-output" + ], + "name": "logs-manufacturing-output", + "priority": 500, + "template": { + "mappings": { + "properties": { + "@timestamp": { + "format": "date_optional_time||epoch_millis", + "type": "date" + }, + "data_stream": { + "properties": { + "namespace": { + "type": "constant_keyword" + } + } + }, + "message": { + "type": "wildcard" + } + } + } + } + } + } +} + +{ + "type": "data_stream", + "value": { + "data_stream": "logs-manufacturing-quality", + "template": { + "_meta": { + "description": "Template for my time series data", + "my-custom-meta-field": "More arbitrary metadata" + }, + "data_stream": { + "allow_custom_routing": false, + "hidden": false + }, + "index_patterns": [ + "logs-manufacturing-quality" + ], + "name": "logs-manufacturing-quality", + "priority": 500, + "template": { + "mappings": { + "properties": { + "@timestamp": { + "format": "date_optional_time||epoch_millis", + "type": "date" + }, + "data_stream": { + "properties": { + "namespace": { + "type": "constant_keyword" + } + } + }, + "message": { + "type": "wildcard" + } + } + } + } + } + } +} + +{ + "type": "data_stream", + "value": { + "data_stream": "logs-retail-customers", + "template": { + "_meta": { + "description": "Template for my time series data", + "my-custom-meta-field": "More arbitrary metadata" + }, + "data_stream": { + "allow_custom_routing": false, + "hidden": false + }, + "index_patterns": [ + "logs-retail-customers" + ], + "name": "logs-retail-customers", + "priority": 500, + "template": { + "mappings": { + "properties": { + "@timestamp": { + "format": "date_optional_time||epoch_millis", + "type": "date" + }, + "data_stream": { + "properties": { + "namespace": { + "type": "constant_keyword" + } + } + }, + "message": { + "type": "wildcard" + } + } + } + } + } + } +} + +{ + "type": "data_stream", + "value": { + "data_stream": "logs-retail-inventory", + "template": { + "_meta": { + "description": "Template for my time series data", + "my-custom-meta-field": "More arbitrary metadata" + }, + "data_stream": { + "allow_custom_routing": false, + "hidden": false + }, + "index_patterns": [ + "logs-retail-inventory" + ], + "name": "logs-retail-inventory", + "priority": 500, + "template": { + "mappings": { + "properties": { + "@timestamp": { + "format": "date_optional_time||epoch_millis", + "type": "date" + }, + "data_stream": { + "properties": { + "namespace": { + "type": "constant_keyword" + } + } + }, + "message": { + "type": "wildcard" + } + } + } + } + } + } +} + +{ + "type": "data_stream", + "value": { + "data_stream": "logs-retail-promotions", + "template": { + "_meta": { + "description": "Template for my time series data", + "my-custom-meta-field": "More arbitrary metadata" + }, + "data_stream": { + "allow_custom_routing": false, + "hidden": false + }, + "index_patterns": [ + "logs-retail-promotions" + ], + "name": "logs-retail-promotions", + "priority": 500, + "template": { + "mappings": { + "properties": { + "@timestamp": { + "format": "date_optional_time||epoch_millis", + "type": "date" + }, + "data_stream": { + "properties": { + "namespace": { + "type": "constant_keyword" + } + } + }, + "message": { + "type": "wildcard" + } + } + } + } + } + } +} + +{ + "type": "data_stream", + "value": { + "data_stream": "logs-retail-sales", + "template": { + "_meta": { + "description": "Template for my time series data", + "my-custom-meta-field": "More arbitrary metadata" + }, + "data_stream": { + "allow_custom_routing": false, + "hidden": false + }, + "index_patterns": [ + "logs-retail-sales" + ], + "name": "logs-retail-sales", + "priority": 500, + "template": { + "mappings": { + "properties": { + "@timestamp": { + "format": "date_optional_time||epoch_millis", + "type": "date" + }, + "data_stream": { + "properties": { + "namespace": { + "type": "constant_keyword" + } + } + }, + "message": { + "type": "wildcard" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/discover_log_explorer.ts b/x-pack/test/functional/page_objects/discover_log_explorer.ts index 15c2dc8fbcc4ee..282a703863dc2f 100644 --- a/x-pack/test/functional/page_objects/discover_log_explorer.ts +++ b/x-pack/test/functional/page_objects/discover_log_explorer.ts @@ -7,13 +7,186 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; +export interface IntegrationPackage { + name: string; + version: string; +} + +const packages: IntegrationPackage[] = [ + { + name: 'apache', + version: '1.14.0', + }, + { + name: 'aws', + version: '1.51.0', + }, + { + name: 'system', + version: '1.38.1', + }, + { + name: '1password', + version: '1.18.0', + }, + { + name: 'activemq', + version: '0.13.0', + }, + { + name: 'akamai', + version: '2.14.0', + }, + { + name: 'apache_tomcat', + version: '0.12.1', + }, + { + name: 'apm', + version: '8.4.2', + }, + { + name: 'atlassian_bitbucket', + version: '1.14.0', + }, + { + name: 'atlassian_confluence', + version: '1.15.0', + }, + { + name: 'atlassian_jira', + version: '1.15.0', + }, + { + name: 'auditd', + version: '3.12.0', + }, + { + name: 'auditd_manager', + version: '1.12.0', + }, + { + name: 'auth0', + version: '1.10.0', + }, + { + name: 'aws_logs', + version: '0.5.0', + }, + { + name: 'azure', + version: '1.5.28', + }, + { + name: 'azure_app_service', + version: '0.0.1', + }, + { + name: 'azure_blob_storage', + version: '0.5.0', + }, + { + name: 'azure_frontdoor', + version: '1.1.0', + }, + { + name: 'azure_functions', + version: '0.0.1', + }, +]; + +const initialPackages = packages.slice(0, 3); +const additionalPackages = packages.slice(3); + export function DiscoverLogExplorerPageObject({ getService }: FtrProviderContext) { + const log = getService('log'); + const supertest = getService('supertest'); const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); return { - async getDatasetSelectorButton() { - return testSubjects.find('dataset-selector-popover-button'); + uninstallPackage: ({ name, version }: IntegrationPackage) => { + return supertest.delete(`/api/fleet/epm/packages/${name}/${version}`).set('kbn-xsrf', 'xxxx'); + }, + + installPackage: ({ name, version }: IntegrationPackage) => { + return supertest + .post(`/api/fleet/epm/packages/${name}/${version}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + }, + + getInstalledPackages: () => { + return supertest + .get(`/api/fleet/epm/packages/installed?dataStreamType=logs&perPage=1000`) + .set('kbn-xsrf', 'xxxx'); + }, + + async removeInstalledPackages(): Promise { + const response = await this.getInstalledPackages(); + + // Uninstall installed integration + await Promise.all( + response.body.items.map((pkg: IntegrationPackage) => this.uninstallPackage(pkg)) + ); + + return response.body.items; + }, + + async setupInitialIntegrations() { + log.info(`===== Setup initial integration packages. =====`); + log.info(`===== Uninstall initial integration packages. =====`); + const uninstalled = await this.removeInstalledPackages(); + log.info(`===== Install ${initialPackages.length} mock integration packages. =====`); + await Promise.all(initialPackages.map((pkg: IntegrationPackage) => this.installPackage(pkg))); + + return async () => { + log.info(`===== Uninstall ${initialPackages.length} mock integration packages. =====`); + await Promise.all( + initialPackages.map((pkg: IntegrationPackage) => this.uninstallPackage(pkg)) + ); + log.info(`===== Restore pre-existing integration packages. =====`); + await Promise.all(uninstalled.map((pkg: IntegrationPackage) => this.installPackage(pkg))); + }; + }, + + async setupAdditionalIntegrations() { + log.info(`===== Setup additional integration packages. =====`); + log.info(`===== Install ${additionalPackages.length} mock integration packages. =====`); + await Promise.all( + additionalPackages.map((pkg: IntegrationPackage) => this.installPackage(pkg)) + ); + + return async () => { + log.info(`===== Uninstall ${additionalPackages.length} mock integration packages. =====`); + await Promise.all( + additionalPackages.map((pkg: IntegrationPackage) => this.uninstallPackage(pkg)) + ); + }; + }, + + getDatasetSelector() { + return testSubjects.find('datasetSelectorPopover'); + }, + + getDatasetSelectorButton() { + return testSubjects.find('datasetSelectorPopoverButton'); + }, + + getDatasetSelectorContent() { + return testSubjects.find('datasetSelectorContent'); + }, + + getDatasetSelectorSearchControls() { + return testSubjects.find('datasetSelectorSearchControls'); + }, + + getDatasetSelectorContextMenu() { + return testSubjects.find('datasetSelectorContextMenu'); + }, + + getDatasetSelectorContextMenuPanelTitle() { + return testSubjects.find('contextMenuPanelTitleButton'); }, async getDatasetSelectorButtonText() { @@ -21,11 +194,115 @@ export function DiscoverLogExplorerPageObject({ getService }: FtrProviderContext return button.getVisibleText(); }, + async getCurrentPanelEntries() { + const contextMenu = await this.getDatasetSelectorContextMenu(); + return contextMenu.findAllByClassName('euiContextMenuItem', 2000); + }, + + getAllLogDatasetsButton() { + return testSubjects.find('allLogDatasets'); + }, + + getUnmanagedDatasetsButton() { + return testSubjects.find('unmanagedDatasets'); + }, + + async getIntegrations() { + const content = await this.getDatasetSelectorContent(); + + const nodes = await content.findAllByCssSelector('[data-test-subj*="integration-"]', 2000); + const integrations = await Promise.all(nodes.map((node) => node.getVisibleText())); + + return { + nodes, + integrations, + }; + }, + + async openDatasetSelector() { + const button = await this.getDatasetSelectorButton(); + return button.click(); + }, + + async closeDatasetSelector() { + const button = await this.getDatasetSelectorButton(); + const isOpen = await testSubjects.exists('datasetSelectorContent'); + + if (isOpen) return button.click(); + }, + + async clickSortButtonBy(direction: 'asc' | 'desc') { + const titleMap = { + asc: 'Ascending', + desc: 'Descending', + }; + const searchControlsContainer = await this.getDatasetSelectorSearchControls(); + const sortingButton = await searchControlsContainer.findByCssSelector( + `[title=${titleMap[direction]}]` + ); + + return sortingButton.click(); + }, + + async getSearchFieldValue() { + const searchControlsContainer = await this.getDatasetSelectorSearchControls(); + const searchField = await searchControlsContainer.findByCssSelector('input[type=search]'); + + return searchField.getAttribute('value'); + }, + + async typeSearchFieldWith(name: string) { + const searchControlsContainer = await this.getDatasetSelectorSearchControls(); + const searchField = await searchControlsContainer.findByCssSelector('input[type=search]'); + + await searchField.clearValueWithKeyboard(); + return searchField.type(name); + }, + + async clearSearchField() { + const searchControlsContainer = await this.getDatasetSelectorSearchControls(); + const searchField = await searchControlsContainer.findByCssSelector('input[type=search]'); + + return searchField.clearValueWithKeyboard(); + }, + async assertRestoreFailureToastExist() { const successToast = await toasts.getToastElement(1); expect(await successToast.getVisibleText()).to.contain( "We couldn't restore your datasets selection" ); }, + + assertLoadingSkeletonExists() { + return testSubjects.existOrFail('datasetSelectorSkeleton'); + }, + + async assertNoIntegrationsPromptExists() { + const integrationStatus = await testSubjects.find('integrationStatusItem'); + const promptTitle = await integrationStatus.findByTagName('h2'); + + expect(await promptTitle.getVisibleText()).to.be('No integrations found'); + }, + + async assertNoIntegrationsErrorExists() { + const integrationStatus = await testSubjects.find('integrationsErrorPrompt'); + const promptTitle = await integrationStatus.findByTagName('h2'); + + expect(await promptTitle.getVisibleText()).to.be('No integrations found'); + }, + + async assertNoDataStreamsPromptExists() { + const integrationStatus = await testSubjects.find('emptyDatasetPrompt'); + const promptTitle = await integrationStatus.findByTagName('h2'); + + expect(await promptTitle.getVisibleText()).to.be('No data streams found'); + }, + + async assertNoDataStreamsErrorExists() { + const integrationStatus = await testSubjects.find('datasetErrorPrompt'); + const promptTitle = await integrationStatus.findByTagName('h2'); + + expect(await promptTitle.getVisibleText()).to.be('No data streams found'); + }, }; } diff --git a/x-pack/test/functional/page_objects/remote_clusters_page.ts b/x-pack/test/functional/page_objects/remote_clusters_page.ts index b6ce2eb4a39bda..b9f24dd1854d2b 100644 --- a/x-pack/test/functional/page_objects/remote_clusters_page.ts +++ b/x-pack/test/functional/page_objects/remote_clusters_page.ts @@ -29,7 +29,14 @@ export function RemoteClustersPageProvider({ getService }: FtrProviderContext) { }); await testSubjects.setValue('remoteClusterFormNameInput', name); await comboBox.setCustom('comboBoxInput', seedNode); + + // Submit config form await testSubjects.click('remoteClusterFormSaveButton'); + + // Complete trust setup + await testSubjects.click('setupTrustDoneButton'); + await testSubjects.setCheckbox('remoteClusterTrustCheckbox', 'check'); + await testSubjects.click('remoteClusterTrustSubmitButton'); }, async getRemoteClustersList() { const table = await testSubjects.find('remoteClusterListTable'); diff --git a/x-pack/test/functional_execution_context/tests/server.ts b/x-pack/test/functional_execution_context/tests/server.ts index 1d854fed2b94d8..64035a0077966c 100644 --- a/x-pack/test/functional_execution_context/tests/server.ts +++ b/x-pack/test/functional_execution_context/tests/server.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; import expect from '@kbn/expect'; import type { FtrProviderContext } from '../ftr_provider_context'; import { assertLogContains, isExecutionContextLog, ANY } from '../test_utils'; @@ -111,8 +115,10 @@ export default function ({ getService }: FtrProviderContext) { it('propagates context for Telemetry collection', async () => { await supertest - .post('/api/telemetry/v2/clusters/_stats') + .post('/internal/telemetry/clusters/_stats') .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send({ unencrypted: false }) .expect(200); diff --git a/x-pack/test/localization/tests/lens/smokescreen.ts b/x-pack/test/localization/tests/lens/smokescreen.ts index 580bb2844dcf9d..a0a4119c21f461 100644 --- a/x-pack/test/localization/tests/lens/smokescreen.ts +++ b/x-pack/test/localization/tests/lens/smokescreen.ts @@ -243,7 +243,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.waitForVisualization('xyVisChart'); // Verify that the field was persisted from the transition - expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`ip : *`, `geo.src : CN`]); + expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`"ip" : *`, `geo.src : CN`]); expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); }); 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 87224810ed450c..42c4a0bd6d1c3f 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 @@ -85,24 +85,30 @@ export default ({ getService }: FtrProviderContext) => { }); describe('Rules table', () => { - let uptimeRuleId: string; + let metricThresholdRuleId: string; let logThresholdRuleId: string; before(async () => { - const uptimeRule = { + const metricThresholdRule = { params: { - search: '', - numTimes: 5, - timerangeUnit: 'm', - timerangeCount: 15, - shouldCheckStatus: true, - shouldCheckAvailability: true, - availability: { range: 30, rangeUnit: 'd', threshold: '99' }, + criteria: [ + { + aggType: 'avg', + comparator: '>', + threshold: [0.5], + timeSize: 5, + timeUnit: 'm', + metric: 'system.cpu.user.pct', + }, + ], + sourceId: 'default', + alertOnNoData: true, + alertOnGroupDisappear: true, }, - consumer: 'alerts', + consumer: 'infrastructure', + tags: ['infrastructure'], + name: 'metric-threshold', schedule: { interval: '1m' }, - tags: [], - name: 'uptime', - rule_type_id: 'xpack.uptime.alerts.monitorStatus', + rule_type_id: 'metrics.alert.threshold', notify_when: 'onActionGroupChange', actions: [], }; @@ -125,12 +131,12 @@ export default ({ getService }: FtrProviderContext) => { notify_when: 'onActionGroupChange', actions: [], }; - uptimeRuleId = await createRule(uptimeRule); + metricThresholdRuleId = await createRule(metricThresholdRule); logThresholdRuleId = await createRule(logThresholdRule); await observability.alerts.common.navigateToRulesPage(); }); after(async () => { - await deleteRuleById(uptimeRuleId); + await deleteRuleById(metricThresholdRuleId); await deleteRuleById(logThresholdRuleId); }); @@ -142,7 +148,7 @@ export default ({ getService }: FtrProviderContext) => { expect(rows.length).to.be(2); expect(rows[0].name).to.contain('error-log'); expect(rows[0].enabled).to.be('Enabled'); - expect(rows[1].name).to.contain('uptime'); + expect(rows[1].name).to.contain('metric-threshold'); expect(rows[1].enabled).to.be('Enabled'); }); diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts index af17d1b76ed993..420dfe795f322f 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('task_manager', function taskManagerSuite() { loadTestFile(require.resolve('./background_task_utilization_route')); + loadTestFile(require.resolve('./metrics_route')); loadTestFile(require.resolve('./health_route')); loadTestFile(require.resolve('./task_management')); loadTestFile(require.resolve('./task_management_scheduled_at')); diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/metrics_route.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/metrics_route.ts new file mode 100644 index 00000000000000..4da679b6839aca --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/metrics_route.ts @@ -0,0 +1,227 @@ +/* + * 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 url from 'url'; +import supertest from 'supertest'; +import { NodeMetrics } from '@kbn/task-manager-plugin/server/routes/metrics'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const config = getService('config'); + const retry = getService('retry'); + const request = supertest(url.format(config.get('servers.kibana'))); + + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + function getMetricsRequest(reset: boolean = false) { + return request + .get(`/api/task_manager/metrics${reset ? '' : '?reset=false'}`) + .set('kbn-xsrf', 'foo') + .expect(200) + .then((response) => response.body); + } + + function getMetrics( + reset: boolean = false, + callback: (metrics: NodeMetrics) => boolean + ): Promise { + return retry.try(async () => { + const metrics = await getMetricsRequest(reset); + + if (metrics.metrics && callback(metrics)) { + return metrics; + } + + await delay(500); + throw new Error('Expected metrics not received'); + }); + } + + describe('task manager metrics', () => { + describe('task claim', () => { + it('should increment task claim success/total counters', async () => { + // counters are reset every 30 seconds, so wait until the start of a + // fresh counter cycle to make sure values are incrementing + const initialMetrics = ( + await getMetrics(false, (metrics) => metrics?.metrics?.task_claim?.value.total === 1) + ).metrics; + expect(initialMetrics).not.to.be(null); + expect(initialMetrics?.task_claim).not.to.be(null); + expect(initialMetrics?.task_claim?.value).not.to.be(null); + + let previousTaskClaimSuccess = initialMetrics?.task_claim?.value.total!; + let previousTaskClaimTotal = initialMetrics?.task_claim?.value.success!; + let previousTaskClaimTimestamp: string = initialMetrics?.task_claim?.timestamp!; + + for (let i = 0; i < 5; ++i) { + const metrics = ( + await getMetrics( + false, + (m: NodeMetrics) => m.metrics?.task_claim?.timestamp !== previousTaskClaimTimestamp + ) + ).metrics; + expect(metrics).not.to.be(null); + expect(metrics?.task_claim).not.to.be(null); + expect(metrics?.task_claim?.value).not.to.be(null); + + expect(metrics?.task_claim?.value.success).to.be.greaterThan(previousTaskClaimSuccess); + expect(metrics?.task_claim?.value.total).to.be.greaterThan(previousTaskClaimTotal); + + previousTaskClaimTimestamp = metrics?.task_claim?.timestamp!; + previousTaskClaimSuccess = metrics?.task_claim?.value.success!; + previousTaskClaimTotal = metrics?.task_claim?.value.total!; + + // check that duration histogram exists + expect(metrics?.task_claim?.value.duration).not.to.be(null); + expect(Array.isArray(metrics?.task_claim?.value.duration.counts)).to.be(true); + expect(Array.isArray(metrics?.task_claim?.value.duration.values)).to.be(true); + } + }); + + it('should reset task claim success/total counters at an interval', async () => { + const initialCounterValue = 7; + const initialMetrics = ( + await getMetrics( + false, + (metrics) => metrics?.metrics?.task_claim?.value.total === initialCounterValue + ) + ).metrics; + expect(initialMetrics).not.to.be(null); + expect(initialMetrics?.task_claim).not.to.be(null); + expect(initialMetrics?.task_claim?.value).not.to.be(null); + + // retry until counter value resets + const resetMetrics = ( + await getMetrics(false, (m: NodeMetrics) => m?.metrics?.task_claim?.value.total === 1) + ).metrics; + expect(resetMetrics).not.to.be(null); + expect(resetMetrics?.task_claim).not.to.be(null); + expect(resetMetrics?.task_claim?.value).not.to.be(null); + }); + + it('should reset task claim success/total counters on request', async () => { + const initialCounterValue = 1; + const initialMetrics = ( + await getMetrics( + false, + (metrics) => metrics?.metrics?.task_claim?.value.total === initialCounterValue + ) + ).metrics; + expect(initialMetrics).not.to.be(null); + expect(initialMetrics?.task_claim).not.to.be(null); + expect(initialMetrics?.task_claim?.value).not.to.be(null); + + let previousTaskClaimTimestamp: string = initialMetrics?.task_claim?.timestamp!; + + for (let i = 0; i < 5; ++i) { + const metrics = ( + await getMetrics( + true, + (m: NodeMetrics) => m.metrics?.task_claim?.timestamp !== previousTaskClaimTimestamp + ) + ).metrics; + expect(metrics).not.to.be(null); + expect(metrics?.task_claim).not.to.be(null); + expect(metrics?.task_claim?.value).not.to.be(null); + + expect(metrics?.task_claim?.value.success).to.equal(1); + expect(metrics?.task_claim?.value.total).to.equal(1); + + previousTaskClaimTimestamp = metrics?.task_claim?.timestamp!; + + // check that duration histogram exists + expect(metrics?.task_claim?.value.duration).not.to.be(null); + expect(Array.isArray(metrics?.task_claim?.value.duration.counts)).to.be(true); + expect(Array.isArray(metrics?.task_claim?.value.duration.values)).to.be(true); + } + }); + }); + + describe('task run test', () => { + let ruleId: string | null = null; + before(async () => { + // create a rule that fires actions + const rule = await request + .post(`/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send({ + enabled: true, + name: 'test rule', + tags: [], + rule_type_id: '.es-query', + consumer: 'alerts', + // set schedule long so we can control when it runs + schedule: { interval: '1d' }, + actions: [], + params: { + aggType: 'count', + esQuery: '{\n "query":{\n "match_all" : {}\n }\n}', + excludeHitsFromPreviousRun: false, + groupBy: 'all', + index: ['.kibana-event-log*'], + searchType: 'esQuery', + size: 100, + termSize: 5, + threshold: [0], + thresholdComparator: '>', + timeField: '@timestamp', + timeWindowSize: 5, + timeWindowUnit: 'm', + }, + }) + .expect(200) + .then((response) => response.body); + + ruleId = rule.id; + }); + + after(async () => { + // delete rule + await request.delete(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'foo').expect(204); + }); + + it('should increment task run success/total counters', async () => { + const initialMetrics = ( + await getMetrics( + false, + (metrics) => + metrics?.metrics?.task_run?.value.by_type.alerting?.total === 1 && + metrics?.metrics?.task_run?.value.by_type.alerting?.success === 1 + ) + ).metrics; + expect(initialMetrics).not.to.be(null); + expect(initialMetrics?.task_claim).not.to.be(null); + expect(initialMetrics?.task_claim?.value).not.to.be(null); + + for (let i = 0; i < 1; ++i) { + // run the rule and expect counters to increment + await request + .post('/api/sample_tasks/run_soon') + .set('kbn-xsrf', 'xxx') + .send({ task: { id: ruleId } }) + .expect(200); + + await getMetrics( + false, + (metrics) => + metrics?.metrics?.task_run?.value.by_type.alerting?.total === i + 2 && + metrics?.metrics?.task_run?.value.by_type.alerting?.success === i + 2 + ); + } + + // counter should reset on its own + await getMetrics( + false, + (metrics) => + metrics?.metrics?.task_run?.value.by_type.alerting?.total === 0 && + metrics?.metrics?.task_run?.value.by_type.alerting?.success === 0 + ); + }); + }); + }); +} diff --git a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.1600_dataviews.json b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.1600_dataviews.json index 621bd21556d660..485208916d48ec 100644 --- a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.1600_dataviews.json +++ b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.1600_dataviews.json @@ -1,5 +1,5 @@ { - "journeyName": "POST /api/telemetry/v2/clusters/_stats - 1600 dataviews", + "journeyName": "POST /internal/telemetry/clusters/_stats - 1600 dataviews", "scalabilitySetup": { "warmup": [ { @@ -30,13 +30,15 @@ { "http": { "method": "POST", - "path": "/api/telemetry/v2/clusters/_stats", + "path": "/internal/telemetry/clusters/_stats", "body": "{}", "headers": { "Cookie": "", "Kbn-Version": "", "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json" + "Content-Type": "application/json", + "elastic-api-version": "2", + "x-elastic-internal-origin": "kibana" }, "statusCode": 200 } diff --git a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.json b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.json index eb5bf0808d3eae..041fb1fae31ea3 100644 --- a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.json +++ b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.json @@ -1,5 +1,5 @@ { - "journeyName": "POST /api/telemetry/v2/clusters/_stats", + "journeyName": "POST /internal/telemetry/clusters/_stats", "scalabilitySetup": { "warmup": [ { @@ -28,13 +28,15 @@ { "http": { "method": "POST", - "path": "/api/telemetry/v2/clusters/_stats", + "path": "/internal/telemetry/clusters/_stats", "body": "{}", "headers": { "Cookie": "", "Kbn-Version": "", "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json" + "Content-Type": "application/json", + "elastic-api-version": "2", + "x-elastic-internal-origin": "kibana" }, "statusCode": 200 } diff --git a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.1600_dataviews.json b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.1600_dataviews.json index 191d01c6a7424e..2a3095447e8b4b 100644 --- a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.1600_dataviews.json +++ b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.1600_dataviews.json @@ -1,5 +1,5 @@ { - "journeyName": "POST /api/telemetry/v2/clusters/_stats - no cache - 1600 dataviews", + "journeyName": "POST /internal/telemetry/clusters/_stats - no cache - 1600 dataviews", "scalabilitySetup": { "responseTimeThreshold": { "threshold1": 1000, @@ -35,13 +35,15 @@ { "http": { "method": "POST", - "path": "/api/telemetry/v2/clusters/_stats", + "path": "/internal/telemetry/clusters/_stats", "body": "{ \"refreshCache\": true }", "headers": { "Cookie": "", "Kbn-Version": "", "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json" + "Content-Type": "application/json", + "elastic-api-version": "2", + "x-elastic-internal-origin": "kibana" }, "timeout": 240000, "statusCode": 200 diff --git a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.json b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.json index b3099941180a34..c0521ffb2607ba 100644 --- a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.json +++ b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.json @@ -1,5 +1,5 @@ { - "journeyName": "POST /api/telemetry/v2/clusters/_stats - no cache", + "journeyName": "POST /internal/telemetry/clusters/_stats - no cache", "scalabilitySetup": { "responseTimeThreshold": { "threshold1": 1000, @@ -33,13 +33,15 @@ { "http": { "method": "POST", - "path": "/api/telemetry/v2/clusters/_stats", + "path": "/internal/telemetry/clusters/_stats", "body": "{ \"refreshCache\": true }", "headers": { "Cookie": "", "Kbn-Version": "", "Accept-Encoding": "gzip, deflate, br", - "Content-Type": "application/json" + "Content-Type": "application/json", + "elastic-api-version": "2", + "x-elastic-internal-origin": "kibana" }, "statusCode": 200 } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts index 586b07261e76c8..dc2183d6f0fee5 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts @@ -197,6 +197,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { let indexedAlerts: IndexedEndpointRuleAlerts; before(async () => { + await getService('kibanaServer').request({ + path: `internal/kibana/settings`, + method: 'POST', + body: { changes: { 'securitySolution:enableExpandableFlyout': false } }, + }); + indexedAlerts = await detectionsTestService.loadEndpointRuleAlerts(endpointAgentId); await detectionsTestService.waitForAlerts( diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.fixtures.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.fixtures.ts index 3a81ad1c71dcb1..fd67cd0a1d8bd5 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.fixtures.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.fixtures.ts @@ -5,7 +5,7 @@ * 2.0. */ -export function generateAgentDocs(timestamp: number, policyId: string) { +export function generateAgentDocs(timestamps: number[], policyId: string) { return [ { access_api_key_id: 'w4zJBHwBfQcM6aSYIRjO', @@ -15,7 +15,7 @@ export function generateAgentDocs(timestamp: number, policyId: string) { id: '963b081e-60d1-482c-befd-a5815fa8290f', version: '8.0.0', }, - enrolled_at: timestamp, + enrolled_at: timestamps[0], local_metadata: { elastic: { agent: { @@ -53,9 +53,9 @@ export function generateAgentDocs(timestamp: number, policyId: string) { default_api_key_id: 'x4zJBHwBfQcM6aSYYxiY', policy_revision_idx: 1, policy_coordinator_idx: 1, - updated_at: timestamp, + updated_at: timestamps[0], last_checkin_status: 'online', - last_checkin: timestamp, + last_checkin: timestamps[0], }, { access_api_key_id: 'w4zJBHwBfQcM6aSYIRjO', @@ -65,7 +65,7 @@ export function generateAgentDocs(timestamp: number, policyId: string) { id: '3838df35-a095-4af4-8fce-0b6d78793f2e', version: '8.0.0', }, - enrolled_at: timestamp, + enrolled_at: timestamps[1], local_metadata: { elastic: { agent: { @@ -103,9 +103,9 @@ export function generateAgentDocs(timestamp: number, policyId: string) { default_api_key_id: 'x4zJBHwBfQcM6aSYYxiY', policy_revision_idx: 1, policy_coordinator_idx: 1, - updated_at: timestamp, + updated_at: timestamps[1], last_checkin_status: 'online', - last_checkin: timestamp, + last_checkin: timestamps[1], }, ]; } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 6e1fb2bcd5c131..b92a26e7851273 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -15,12 +15,18 @@ import { METADATA_UNITED_TRANSFORM, METADATA_TRANSFORMS_STATUS_ROUTE, metadataTransformPrefix, + ENDPOINT_DEFAULT_SORT_FIELD, + ENDPOINT_DEFAULT_SORT_DIRECTION, } from '@kbn/security-solution-plugin/common/endpoint/constants'; import { AGENTS_INDEX } from '@kbn/fleet-plugin/common'; import { indexFleetEndpointPolicy } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { TRANSFORM_STATES } from '@kbn/security-solution-plugin/common/constants'; import type { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; +import { + EndpointSortableField, + MetadataListResponse, +} from '@kbn/security-solution-plugin/common/endpoint/types'; import { generateAgentDocs, generateMetadataDocs } from './metadata.fixtures'; import { deleteAllDocsFromMetadataCurrentIndex, @@ -40,6 +46,9 @@ export default function ({ getService }: FtrProviderContext) { describe('test metadata apis', () => { describe('list endpoints GET route', () => { const numberOfHostsInFixture = 2; + let agent1Timestamp: number; + let agent2Timestamp: number; + let metadataTimestamp: number; before(async () => { await deleteAllDocsFromFleetAgents(getService); @@ -56,10 +65,12 @@ export default function ({ getService }: FtrProviderContext) { '1.1.1' ); const policyId = policy.integrationPolicies[0].policy_id; - const currentTime = new Date().getTime(); + agent1Timestamp = new Date().getTime(); + agent2Timestamp = agent1Timestamp + 33; + metadataTimestamp = agent1Timestamp + 666; - const agentDocs = generateAgentDocs(currentTime, policyId); - const metadataDocs = generateMetadataDocs(currentTime); + const agentDocs = generateAgentDocs([agent1Timestamp, agent2Timestamp], policyId); + const metadataDocs = generateMetadataDocs(metadataTimestamp); await Promise.all([ bulkIndex(getService, AGENTS_INDEX, agentDocs), @@ -294,6 +305,92 @@ export default function ({ getService }: FtrProviderContext) { expect(body.page).to.eql(0); expect(body.pageSize).to.eql(10); }); + + describe('`last_checkin` runtime field', () => { + it('should sort based on `last_checkin` - because it is a runtime field', async () => { + const { body: bodyAsc }: { body: MetadataListResponse } = await supertest + .get(HOST_METADATA_LIST_ROUTE) + .set('kbn-xsrf', 'xxx') + .set('Elastic-Api-Version', '2023-10-31') + .query({ + sortField: 'last_checkin', + sortDirection: 'asc', + }) + .expect(200); + + expect(bodyAsc.data[0].last_checkin).to.eql(new Date(agent1Timestamp).toISOString()); + expect(bodyAsc.data[1].last_checkin).to.eql(new Date(agent2Timestamp).toISOString()); + + const { body: bodyDesc }: { body: MetadataListResponse } = await supertest + .get(HOST_METADATA_LIST_ROUTE) + .set('kbn-xsrf', 'xxx') + .set('Elastic-Api-Version', '2023-10-31') + .query({ + sortField: 'last_checkin', + sortDirection: 'desc', + }) + .expect(200); + + expect(bodyDesc.data[0].last_checkin).to.eql(new Date(agent2Timestamp).toISOString()); + expect(bodyDesc.data[1].last_checkin).to.eql(new Date(agent1Timestamp).toISOString()); + }); + }); + + describe('sorting', () => { + it('metadata api should return 400 with not supported sorting field', async () => { + await supertest + .get(HOST_METADATA_LIST_ROUTE) + .set('kbn-xsrf', 'xxx') + .set('Elastic-Api-Version', '2023-10-31') + .query({ + sortField: 'abc', + }) + .expect(400); + }); + + it('metadata api should sort by enrollment date by default', async () => { + const { body }: { body: MetadataListResponse } = await supertest + .get(HOST_METADATA_LIST_ROUTE) + .set('kbn-xsrf', 'xxx') + .set('Elastic-Api-Version', '2023-10-31') + .expect(200); + + expect(body.sortDirection).to.eql(ENDPOINT_DEFAULT_SORT_DIRECTION); + expect(body.sortField).to.eql(ENDPOINT_DEFAULT_SORT_FIELD); + }); + + for (const field of Object.values(EndpointSortableField)) { + it(`metadata api should be able to sort by ${field}`, async () => { + let body: MetadataListResponse; + + ({ body } = await supertest + .get(HOST_METADATA_LIST_ROUTE) + .set('kbn-xsrf', 'xxx') + .set('Elastic-Api-Version', '2023-10-31') + .query({ + sortField: field, + sortDirection: 'asc', + }) + .expect(200)); + + expect(body.sortDirection).to.eql('asc'); + expect(body.sortField).to.eql(field); + + ({ body } = await supertest + .get(HOST_METADATA_LIST_ROUTE) + .set('kbn-xsrf', 'xxx') + .set('Elastic-Api-Version', '2023-10-31') + .query({ + sortField: field, + sortDirection: 'desc', + }) + .expect(200)); + + expect(body.sortDirection).to.eql('desc'); + expect(body.sortField).to.eql(field); + }); + } + }); }); describe('get metadata transforms', () => { diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 437ff50201b881..7992cf9ba25049 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -138,6 +138,7 @@ "@kbn/ml-category-validator", "@kbn/observability-ai-assistant-plugin", "@kbn/stack-connectors-plugin", + "@kbn/stack-alerts-plugin", "@kbn/aiops-utils" ] } diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index 6159b765f4a778..8a292d4d2dedef 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -77,7 +77,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'enterpriseSearchVectorSearch', 'appSearch', 'workplaceSearch', - 'guidedOnboardingFeature' + 'guidedOnboardingFeature', + 'securitySolutionAssistant' ) ); break; diff --git a/x-pack/test_serverless/api_integration/config.base.ts b/x-pack/test_serverless/api_integration/config.base.ts index a60591c4008cf7..447950aff50ccd 100644 --- a/x-pack/test_serverless/api_integration/config.base.ts +++ b/x-pack/test_serverless/api_integration/config.base.ts @@ -26,7 +26,6 @@ export function createTestConfig(options: CreateTestConfigOptions) { ...svlSharedConfig.get('kbnTestServer.serverArgs'), `--serverless=${options.serverlessProject}`, `--xpack.alerting.enableFrameworkAlerts=true`, - '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', '--xpack.observability.unsafe.thresholdRule.enabled=true', '--server.publicBaseUrl=https://localhost:5601', ], diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index.ts index de30854beccfc8..3ca6b715102d99 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./spaces')); loadTestFile(require.resolve('./security_response_headers')); loadTestFile(require.resolve('./rollups')); + loadTestFile(require.resolve('./index_management')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index.ts new file mode 100644 index 00000000000000..dd7d8bc20e624c --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Index Management APIs', function () { + loadTestFile(require.resolve('./index_templates')); + }); +} 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 new file mode 100644 index 00000000000000..a4e082387ab4a8 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts @@ -0,0 +1,94 @@ +/* + * 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 'expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const API_BASE_PATH = '/api/index_management'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const log = getService('log'); + + describe('Index templates', function () { + const templateName = `template-${Math.random()}`; + const indexTemplate = { + name: templateName, + body: { + index_patterns: ['test*'], + }, + }; + + before(async () => { + // Create a new index template to test against + try { + await es.indices.putIndexTemplate(indexTemplate); + } catch (err) { + log.debug('[Setup error] Error creating index template'); + throw err; + } + }); + + after(async () => { + // Cleanup template created for testing purposes + try { + await es.indices.deleteIndexTemplate({ + name: templateName, + }); + } catch (err) { + log.debug('[Cleanup error] Error deleting index template'); + throw err; + } + }); + + describe('get all', () => { + it('should list all the index templates with the expected parameters', async () => { + const { body: allTemplates } = await supertest + .get(`${API_BASE_PATH}/index_templates`) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .expect(200); + + // Legacy templates are not applicable on serverless + expect(allTemplates.legacyTemplates.length).toEqual(0); + + const indexTemplateFound = allTemplates.templates.find( + (template: { name: string }) => template.name === indexTemplate.name + ); + + expect(indexTemplateFound).toBeTruthy(); + + const expectedKeys = [ + 'name', + 'indexPatterns', + 'hasSettings', + 'hasAliases', + 'hasMappings', + '_kbnMeta', + ].sort(); + + expect(Object.keys(indexTemplateFound).sort()).toEqual(expectedKeys); + }); + }); + + describe('get one', () => { + it('should return an index template with the expected parameters', async () => { + const { body } = await supertest + .get(`${API_BASE_PATH}/index_templates/${templateName}`) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .expect(200); + + const expectedKeys = ['name', 'indexPatterns', 'template', '_kbnMeta'].sort(); + + expect(body.name).toEqual(templateName); + expect(Object.keys(body).sort()).toEqual(expectedKeys); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/config.base.ts b/x-pack/test_serverless/functional/config.base.ts index 23739a9615e696..640ae2402b5447 100644 --- a/x-pack/test_serverless/functional/config.base.ts +++ b/x-pack/test_serverless/functional/config.base.ts @@ -55,6 +55,9 @@ export function createTestConfig(options: CreateTestConfigOptions) { management: { pathname: '/app/management', }, + indexManagement: { + pathname: '/app/management/data/index_management', + }, }, // choose where screenshots should be saved screenshots: { diff --git a/x-pack/test_serverless/functional/test_suites/common/index.ts b/x-pack/test_serverless/functional/test_suites/common/index.ts index 31497afb8c7d8a..7150589527b042 100644 --- a/x-pack/test_serverless/functional/test_suites/common/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/index.ts @@ -14,5 +14,8 @@ export default function ({ loadTestFile }: FtrProviderContext) { // platform security loadTestFile(require.resolve('./security/navigation/avatar_menu')); + + // Management + loadTestFile(require.resolve('./index_management')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/common/index_management/index.ts b/x-pack/test_serverless/functional/test_suites/common/index_management/index.ts new file mode 100644 index 00000000000000..52472972a1faa9 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/index_management/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext) => { + describe('Index Management', function () { + loadTestFile(require.resolve('./index_templates')); + }); +}; diff --git a/x-pack/test_serverless/functional/test_suites/common/index_management/index_templates.ts b/x-pack/test_serverless/functional/test_suites/common/index_management/index_templates.ts new file mode 100644 index 00000000000000..26feb519a39a89 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/index_management/index_templates.ts @@ -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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'indexManagement', 'header']); + const browser = getService('browser'); + const security = getService('security'); + const retry = getService('retry'); + + describe('Index Templates', function () { + before(async () => { + await security.testUser.setRoles(['index_management_user']); + await pageObjects.common.navigateToApp('indexManagement'); + // Navigate to the index templates tab + await pageObjects.indexManagement.changeTabs('templatesTab'); + }); + + it('renders the index templates tab', async () => { + await retry.waitFor('index templates list to be visible', async () => { + return await testSubjects.exists('templateList'); + }); + + const url = await browser.getCurrentUrl(); + expect(url).to.contain(`/templates`); + }); + }); +}; diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/attachment_framework.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/attachment_framework.ts new file mode 100644 index 00000000000000..e8be4ff1cf4d38 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/attachment_framework.ts @@ -0,0 +1,117 @@ +/* + * 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 'expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getPageObject, getService }: FtrProviderContext) => { + const dashboard = getPageObject('dashboard'); + const lens = getPageObject('lens'); + const svlCommonNavigation = getPageObject('svlCommonNavigation'); + const svlObltNavigation = getService('svlObltNavigation'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const cases = getService('cases'); + const find = getService('find'); + + describe('persistable attachment', () => { + describe('lens visualization', () => { + before(async () => { + 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 svlObltNavigation.navigateToLandingPage(); + + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'dashboards' }); + + await dashboard.clickNewDashboard(); + + await lens.createAndAddLensFromDashboard({}); + + await dashboard.waitForRenderComplete(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' + ); + }); + + it('adds lens visualization to a new case', async () => { + const caseTitle = 'case created in observability from my dashboard with lens visualization'; + + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-embeddable_addToNewCase'); + + await testSubjects.existOrFail('create-case-flyout'); + + await testSubjects.setValue('input', caseTitle); + + await testSubjects.setValue('euiMarkdownEditorTextArea', 'test description'); + + // verify that solution picker is not visible + await testSubjects.missingOrFail('caseOwnerSelector'); + + await testSubjects.click('create-case-submit'); + + await cases.common.expectToasterToContain(`${caseTitle} has been updated`); + + await testSubjects.click('toaster-content-case-view-link'); + + if (await testSubjects.exists('appLeaveConfirmModal')) { + await testSubjects.exists('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton'); + } + + const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); + expect(await title.getVisibleText()).toEqual(caseTitle); + + await testSubjects.existOrFail('comment-persistableState-.lens'); + }); + + it('adds lens visualization to an existing case from dashboard', async () => { + const theCaseTitle = 'case already exists in observability!!'; + const theCase = await cases.api.createCase({ + title: theCaseTitle, + description: 'This is a test case to verify existing action scenario!!', + owner: 'observability', + }); + + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'dashboards' }); + + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-embeddable_addToExistingCase'); + + // verify that solution filter is not visible + await testSubjects.missingOrFail('solution-filter-popover-button'); + + await testSubjects.click(`cases-table-row-select-${theCase.id}`); + + await cases.common.expectToasterToContain(`${theCaseTitle} has been updated`); + await testSubjects.click('toaster-content-case-view-link'); + + if (await testSubjects.exists('appLeaveConfirmModal')) { + await testSubjects.exists('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton'); + } + + const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); + expect(await title.getVisibleText()).toEqual(theCaseTitle); + + await testSubjects.existOrFail('comment-persistableState-.lens'); + }); + }); + }); +}; diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/columns_selection.ts b/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/columns_selection.ts new file mode 100644 index 00000000000000..dfba8f72a699d6 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/columns_selection.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const defaultLogColumns = ['@timestamp', 'message']; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'discover']); + + describe('Columns selection initialization and update', () => { + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + ); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + ); + }); + + describe('when the log explorer profile loads', () => { + it("should initialize the table columns to logs' default selection", async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + + await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage(); + + await retry.try(async () => { + expect(await PageObjects.discover.getColumnHeaders()).to.eql(defaultLogColumns); + }); + }); + + it('should restore the table columns from the URL state if exists', async () => { + await PageObjects.common.navigateToApp('discover', { + hash: '/p/log-explorer?_a=(columns:!(message,data_stream.namespace))', + }); + + await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage(); + + await retry.try(async () => { + expect(await PageObjects.discover.getColumnHeaders()).to.eql([ + ...defaultLogColumns, + 'data_stream.namespace', + ]); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/customization.ts b/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/customization.ts index 579f4e4b8f5c52..a647293a73145f 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/customization.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/customization.ts @@ -24,11 +24,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('DatasetSelector should replace the DataViewPicker', async () => { // Assert does not render on discover app await PageObjects.common.navigateToApp('discover'); - await testSubjects.missingOrFail('dataset-selector-popover'); + await testSubjects.missingOrFail('datasetSelectorPopover'); // Assert it renders on log-explorer profile await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); - await testSubjects.existOrFail('dataset-selector-popover'); + await testSubjects.existOrFail('datasetSelectorPopover'); }); it('the TopNav bar should hide New, Open and Save options', async () => { @@ -50,6 +50,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('openInspectorButton'); await testSubjects.missingOrFail('discoverSaveButton'); }); + + it('should render a filter controls section as part of the unified search bar', async () => { + // Assert does not render on discover app + await PageObjects.common.navigateToApp('discover'); + await testSubjects.missingOrFail('datasetFiltersCustomization'); + + // Assert it renders on log-explorer profile + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + await testSubjects.existOrFail('datasetFiltersCustomization', { allowHidden: true }); + }); }); }); } diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/dataset_selector.ts b/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/dataset_selector.ts new file mode 100644 index 00000000000000..cbb3ea9d95de52 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/dataset_selector.ts @@ -0,0 +1,664 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +const initialPackageMap = { + apache: 'Apache HTTP Server', + aws: 'AWS', + system: 'System', +}; +const initialPackagesTexts = Object.values(initialPackageMap); + +const expectedUncategorized = ['logs-gaming-*', 'logs-manufacturing-*', 'logs-retail-*']; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'discoverLogExplorer']); + + describe('Dataset Selector', () => { + before(async () => { + await PageObjects.discoverLogExplorer.removeInstalledPackages(); + }); + + describe('without installed integrations or uncategorized data streams', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + beforeEach(async () => { + await browser.refresh(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + }); + + describe('when open on the first navigation level', () => { + it('should always display the "All log datasets" entry as the first item', async () => { + const allLogDatasetButton = + await PageObjects.discoverLogExplorer.getAllLogDatasetsButton(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + const allLogDatasetTitle = await allLogDatasetButton.getVisibleText(); + const firstEntryTitle = await menuEntries[0].getVisibleText(); + + expect(allLogDatasetTitle).to.be('All log datasets'); + expect(allLogDatasetTitle).to.be(firstEntryTitle); + }); + + it('should always display the unmanaged datasets entry as the second item', async () => { + const unamanagedDatasetButton = + await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + const unmanagedDatasetTitle = await unamanagedDatasetButton.getVisibleText(); + const secondEntryTitle = await menuEntries[1].getVisibleText(); + + expect(unmanagedDatasetTitle).to.be('Uncategorized'); + expect(unmanagedDatasetTitle).to.be(secondEntryTitle); + }); + + it('should display an error prompt if could not retrieve the integrations', async function () { + // Skip the test in case network condition utils are not available + try { + await retry.try(async () => { + await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + }); + + await PageObjects.common.sleep(5000); + await browser.setNetworkConditions('OFFLINE'); + await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + + await retry.try(async () => { + await PageObjects.discoverLogExplorer.assertNoIntegrationsErrorExists(); + }); + + await browser.restoreNetworkConditions(); + } catch (error) { + this.skip(); + } + }); + + it('should display an empty prompt for no integrations', async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations.length).to.be(0); + + await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + }); + }); + + describe('when navigating into Uncategorized data streams', () => { + it('should display a loading skeleton while loading', async function () { + // Skip the test in case network condition utils are not available + try { + await browser.setNetworkConditions('SLOW_3G'); // Almost stuck network conditions + const unamanagedDatasetButton = + await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await unamanagedDatasetButton.click(); + + await PageObjects.discoverLogExplorer.assertLoadingSkeletonExists(); + + await browser.restoreNetworkConditions(); + } catch (error) { + this.skip(); + } + }); + + it('should display an error prompt if could not retrieve the data streams', async function () { + // Skip the test in case network condition utils are not available + try { + const unamanagedDatasetButton = + await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await unamanagedDatasetButton.click(); + + await retry.try(async () => { + await PageObjects.discoverLogExplorer.assertNoDataStreamsPromptExists(); + }); + + await browser.setNetworkConditions('OFFLINE'); + await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + + await retry.try(async () => { + await PageObjects.discoverLogExplorer.assertNoDataStreamsErrorExists(); + }); + + await browser.restoreNetworkConditions(); + } catch (error) { + this.skip(); + } + }); + + it('should display an empty prompt for no data streams', async () => { + const unamanagedDatasetButton = + await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await unamanagedDatasetButton.click(); + + const unamanagedDatasetEntries = + await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(unamanagedDatasetEntries.length).to.be(0); + + await PageObjects.discoverLogExplorer.assertNoDataStreamsPromptExists(); + }); + }); + }); + + describe('with installed integrations and uncategorized data streams', () => { + let cleanupIntegrationsSetup: () => Promise; + + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + ); + cleanupIntegrationsSetup = await PageObjects.discoverLogExplorer.setupInitialIntegrations(); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/discover_log_explorer/data_streams' + ); + await cleanupIntegrationsSetup(); + }); + + describe('when open on the first navigation level', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + beforeEach(async () => { + await browser.refresh(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + }); + + it('should always display the "All log datasets" entry as the first item', async () => { + const allLogDatasetButton = + await PageObjects.discoverLogExplorer.getAllLogDatasetsButton(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + const allLogDatasetTitle = await allLogDatasetButton.getVisibleText(); + const firstEntryTitle = await menuEntries[0].getVisibleText(); + + expect(allLogDatasetTitle).to.be('All log datasets'); + expect(allLogDatasetTitle).to.be(firstEntryTitle); + }); + + it('should always display the unmanaged datasets entry as the second item', async () => { + const unamanagedDatasetButton = + await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + const unmanagedDatasetTitle = await unamanagedDatasetButton.getVisibleText(); + const secondEntryTitle = await menuEntries[1].getVisibleText(); + + expect(unmanagedDatasetTitle).to.be('Uncategorized'); + expect(unmanagedDatasetTitle).to.be(secondEntryTitle); + }); + + it('should display a list of installed integrations', async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + + expect(integrations.length).to.be(3); + expect(integrations).to.eql(initialPackagesTexts); + }); + + it('should sort the integrations list by the clicked sorting option', async () => { + // Test ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql(initialPackagesTexts); + }); + + // Test descending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql(initialPackagesTexts.slice().reverse()); + }); + + // Test back ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql(initialPackagesTexts); + }); + }); + + it('should filter the integrations list by the typed integration name', async () => { + await PageObjects.discoverLogExplorer.typeSearchFieldWith('system'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.system]); + }); + + await PageObjects.discoverLogExplorer.typeSearchFieldWith('a'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.apache, initialPackageMap.aws]); + }); + }); + + it('should display an empty prompt when the search does not match any result', async () => { + await PageObjects.discoverLogExplorer.typeSearchFieldWith('no result search text'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations.length).to.be(0); + }); + + await PageObjects.discoverLogExplorer.assertNoIntegrationsPromptExists(); + }); + + it('should load more integrations by scrolling to the end of the list', async () => { + // Install more integrations and reload the page + const cleanupAdditionalSetup = + await PageObjects.discoverLogExplorer.setupAdditionalIntegrations(); + await browser.refresh(); + + await PageObjects.discoverLogExplorer.openDatasetSelector(); + + // Initially fetched integrations + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(nodes.length).to.be(15); + await nodes.at(-1)?.scrollIntoViewIfNecessary(); + }); + + // Load more integrations + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(nodes.length).to.be(20); + await nodes.at(-1)?.scrollIntoViewIfNecessary(); + }); + + // No other integrations to load after scrolling to last integration + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(nodes.length).to.be(20); + }); + + cleanupAdditionalSetup(); + }); + }); + + describe('when clicking on integration and moving into the second navigation level', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + beforeEach(async () => { + await browser.refresh(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + }); + + it('should display a list of available datasets', async () => { + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + await nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + }); + + it('should sort the datasets list by the clicked sorting option', async () => { + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + await nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + }); + + // Test ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + + // Test descending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('error'); + expect(await menuEntries[1].getVisibleText()).to.be('access'); + }); + + // Test back ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + }); + + it('should filter the datasets list by the typed dataset name', async () => { + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + await nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + }); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + + await PageObjects.discoverLogExplorer.typeSearchFieldWith('err'); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(menuEntries.length).to.be(1); + expect(await menuEntries[0].getVisibleText()).to.be('error'); + }); + }); + + it('should update the current selection with the clicked dataset', async () => { + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + await nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + }); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('access'); + menuEntries[0].click(); + }); + + await retry.try(async () => { + const selectorButton = await PageObjects.discoverLogExplorer.getDatasetSelectorButton(); + + expect(await selectorButton.getVisibleText()).to.be('[Apache HTTP Server] access'); + }); + }); + }); + + describe('when navigating into Uncategorized data streams', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + beforeEach(async () => { + await browser.refresh(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + }); + + it('should display a list of available datasets', async () => { + const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await button.click(); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); + expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); + expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); + expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[2]); + }); + }); + + it('should sort the datasets list by the clicked sorting option', async () => { + const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await button.click(); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); + }); + + // Test ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); + expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); + expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[2]); + }); + + // Test descending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('desc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[2]); + expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); + expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[0]); + }); + + // Test back ascending order + await PageObjects.discoverLogExplorer.clickSortButtonBy('asc'); + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); + expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); + expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[2]); + }); + }); + + it('should filter the datasets list by the typed dataset name', async () => { + const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await button.click(); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); + }); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be(expectedUncategorized[0]); + expect(await menuEntries[1].getVisibleText()).to.be(expectedUncategorized[1]); + expect(await menuEntries[2].getVisibleText()).to.be(expectedUncategorized[2]); + }); + + await PageObjects.discoverLogExplorer.typeSearchFieldWith('retail'); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(menuEntries.length).to.be(1); + expect(await menuEntries[0].getVisibleText()).to.be('logs-retail-*'); + }); + }); + + it('should update the current selection with the clicked dataset', async () => { + const button = await PageObjects.discoverLogExplorer.getUnmanagedDatasetsButton(); + await button.click(); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + + expect(await panelTitleNode.getVisibleText()).to.be('Uncategorized'); + }); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await menuEntries[0].getVisibleText()).to.be('logs-gaming-*'); + menuEntries[0].click(); + }); + + await retry.try(async () => { + const selectorButton = await PageObjects.discoverLogExplorer.getDatasetSelectorButton(); + + expect(await selectorButton.getVisibleText()).to.be('logs-gaming-*'); + }); + }); + }); + + describe('when open/close the selector', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + beforeEach(async () => { + await browser.refresh(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + }); + + it('should restore the latest navigation panel', async () => { + await retry.try(async () => { + const { nodes } = await PageObjects.discoverLogExplorer.getIntegrations(); + await nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + + await PageObjects.discoverLogExplorer.closeDatasetSelector(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + }); + + it('should restore the latest search results', async () => { + await PageObjects.discoverLogExplorer.typeSearchFieldWith('system'); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.system]); + }); + + await PageObjects.discoverLogExplorer.closeDatasetSelector(); + await PageObjects.discoverLogExplorer.openDatasetSelector(); + + await retry.try(async () => { + const { integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.system]); + }); + }); + }); + + describe('when switching between integration panels', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { hash: '/p/log-explorer' }); + }); + + it('should remember the latest search and restore its results for each integration', async () => { + await PageObjects.discoverLogExplorer.openDatasetSelector(); + await PageObjects.discoverLogExplorer.clearSearchField(); + + await PageObjects.discoverLogExplorer.typeSearchFieldWith('apache'); + + await retry.try(async () => { + const { nodes, integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.apache]); + nodes[0].click(); + }); + + await retry.try(async () => { + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(await panelTitleNode.getVisibleText()).to.be('Apache HTTP Server'); + expect(await menuEntries[0].getVisibleText()).to.be('access'); + expect(await menuEntries[1].getVisibleText()).to.be('error'); + }); + + await PageObjects.discoverLogExplorer.typeSearchFieldWith('err'); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + expect(menuEntries.length).to.be(1); + expect(await menuEntries[0].getVisibleText()).to.be('error'); + }); + + // Navigate back to integrations + const panelTitleNode = + await PageObjects.discoverLogExplorer.getDatasetSelectorContextMenuPanelTitle(); + panelTitleNode.click(); + + await retry.try(async () => { + const { nodes, integrations } = await PageObjects.discoverLogExplorer.getIntegrations(); + expect(integrations).to.eql([initialPackageMap.apache]); + + const searchValue = await PageObjects.discoverLogExplorer.getSearchFieldValue(); + expect(searchValue).to.eql('apache'); + + nodes[0].click(); + }); + + await retry.try(async () => { + const menuEntries = await PageObjects.discoverLogExplorer.getCurrentPanelEntries(); + + const searchValue = await PageObjects.discoverLogExplorer.getSearchFieldValue(); + expect(searchValue).to.eql('err'); + + expect(menuEntries.length).to.be(1); + expect(await menuEntries[0].getVisibleText()).to.be('error'); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/index.ts b/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/index.ts index e334c028cbaca0..8e9843fc02815f 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/discover_log_explorer/index.ts @@ -9,7 +9,9 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function (loadTestFile: FtrProviderContext['loadTestFile']) { describe('Discover Log-Explorer profile', function () { + loadTestFile(require.resolve('./columns_selection')); loadTestFile(require.resolve('./customization')); loadTestFile(require.resolve('./dataset_selection_state')); + loadTestFile(require.resolve('./dataset_selector')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/observability/index.ts b/x-pack/test_serverless/functional/test_suites/observability/index.ts index b0d394341dfe88..70e76dbf3955f9 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./landing_page')); loadTestFile(require.resolve('./navigation')); loadDiscoverLogExplorerSuite(loadTestFile); + loadTestFile(require.resolve('./cases/attachment_framework')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/search/cases/attachment_framework.ts b/x-pack/test_serverless/functional/test_suites/search/cases/attachment_framework.ts new file mode 100644 index 00000000000000..4f086118098866 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/cases/attachment_framework.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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getPageObject, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const dashboard = getPageObject('dashboard'); + const lens = getPageObject('lens'); + const svlSearchNavigation = getService('svlSearchNavigation'); + const svlCommonNavigation = getPageObject('svlCommonNavigation'); + + describe('persistable attachment', () => { + describe('lens visualization', () => { + before(async () => { + 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 svlSearchNavigation.navigateToLandingPage(); + + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'dashboards' }); + + await dashboard.clickNewDashboard(); + + await lens.createAndAddLensFromDashboard({}); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' + ); + }); + + it('does not show actions to add lens visualization to case', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.missingOrFail('embeddablePanelAction-embeddable_addToNewCase'); + await testSubjects.missingOrFail('embeddablePanelAction-embeddable_addToExistingCase'); + }); + }); + }); +}; diff --git a/x-pack/test_serverless/functional/test_suites/search/index.ts b/x-pack/test_serverless/functional/test_suites/search/index.ts index 8dce70b4f15e93..9a3f5de27f16ca 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('serverless search UI', function () { loadTestFile(require.resolve('./landing_page')); loadTestFile(require.resolve('./navigation')); + loadTestFile(require.resolve('./cases/attachment_framework')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/roles/complete_with_endpoint_roles.cy.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/roles/complete_with_endpoint_roles.cy.ts index 6b54dab968577f..30247aae8201df 100644 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/roles/complete_with_endpoint_roles.cy.ts +++ b/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/roles/complete_with_endpoint_roles.cy.ts @@ -13,6 +13,7 @@ import { EndpointArtifactPageId, ensureArtifactPageAuthzAccess, ensureEndpointListPageAuthzAccess, + ensurePolicyListPageAuthzAccess, getArtifactListEmptyStateAddButton, getEndpointManagementPageList, getEndpointManagementPageMap, @@ -31,6 +32,7 @@ import { getConsoleHelpPanelResponseActionTestSubj, openConsoleHelpPanel, } from '../../../screens/endpoint_management/response_console'; +import { ensurePolicyDetailsPageAuthzAccess } from '../../../screens/endpoint_management/policy_details'; describe( 'User Roles for Security Complete PLI with Endpoint Complete addon', @@ -132,6 +134,11 @@ describe( ensureEndpointListPageAuthzAccess('all', true); }); + it('should have read access to Endpoint Policy Management', () => { + ensurePolicyListPageAuthzAccess('read', true); + ensurePolicyDetailsPageAuthzAccess(loadedEndpoints.integrationPolicies[0].id, 'read', true); + }); + for (const { title, id } of artifactPagesFullAccess) { it(`should have CRUD access to: ${title}`, () => { ensureArtifactPageAuthzAccess('all', id as EndpointArtifactPageId); diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/roles/essentials_with_endpoint.roles.cy.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/roles/essentials_with_endpoint.roles.cy.ts index 0f46da5baa721e..95f30254e7fb22 100644 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/roles/essentials_with_endpoint.roles.cy.ts +++ b/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management/roles/essentials_with_endpoint.roles.cy.ts @@ -23,6 +23,7 @@ import { visitFleetAgentList, } from '../../../screens'; import { ServerlessRoleName } from '../../../../../../../shared/lib'; +import { ensurePolicyDetailsPageAuthzAccess } from '../../../screens/endpoint_management/policy_details'; describe( 'Roles for Security Essential PLI with Endpoint Essentials addon', @@ -98,6 +99,7 @@ describe( it('should have read access to Endpoint Policy Management', () => { ensurePolicyListPageAuthzAccess('read', true); + ensurePolicyDetailsPageAuthzAccess(loadedEndpoints.integrationPolicies[0].id, 'read', true); }); for (const { title, id } of artifactPagesFullAccess) { @@ -173,6 +175,7 @@ describe( it('should have access to policy management', () => { ensurePolicyListPageAuthzAccess('all', true); + ensurePolicyDetailsPageAuthzAccess(loadedEndpoints.integrationPolicies[0].id, 'all', true); }); it(`should NOT have access to Host Isolation Exceptions`, () => { diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/screens/endpoint_management/policy_details.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/screens/endpoint_management/policy_details.ts index 2ba5de32cbab6c..fd8fb40f2ac196 100644 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/screens/endpoint_management/policy_details.ts +++ b/x-pack/test_serverless/functional/test_suites/security/cypress/screens/endpoint_management/policy_details.ts @@ -6,7 +6,29 @@ */ import { APP_POLICIES_PATH } from '@kbn/security-solution-plugin/common/constants'; +import { UserAuthzAccessLevel } from './types'; +import { getNoPrivilegesPage } from './common'; export const visitPolicyDetails = (policyId: string): Cypress.Chainable => { return cy.visit(`${APP_POLICIES_PATH}/${policyId}`); }; + +export const ensurePolicyDetailsPageAuthzAccess = ( + policyId: string, + accessLevel: UserAuthzAccessLevel, + visitPage: boolean = false +): Cypress.Chainable => { + if (visitPage) { + visitPolicyDetails(policyId); + } + + if (accessLevel === 'none') { + return getNoPrivilegesPage().should('exist'); + } + + if (accessLevel === 'read') { + return cy.getByTestSubj('policyDetailsSaveButton').should('not.exist'); + } + + return cy.getByTestSubj('policyDetailsSaveButton').should('exist'); +}; diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts new file mode 100644 index 00000000000000..a35787cff6aad9 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { expect } from 'expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default ({ getPageObject, getService }: FtrProviderContext) => { + const dashboard = getPageObject('dashboard'); + const lens = getPageObject('lens'); + const svlSecNavigation = getService('svlSecNavigation'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const cases = getService('cases'); + const find = getService('find'); + + describe('persistable attachment', () => { + describe('lens visualization', () => { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/dashboard/feature_controls/security/security.json' + ); + + await svlSecNavigation.navigateToLandingPage(); + + await testSubjects.click('solutionSideNavItemLink-dashboards'); + + await dashboard.clickNewDashboard(); + + await lens.createAndAddLensFromDashboard({}); + + await dashboard.waitForRenderComplete(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' + ); + }); + + it('adds lens visualization to a new case', async () => { + const caseTitle = + 'case created in security solution from my dashboard with lens visualization'; + + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-embeddable_addToNewCase'); + + await testSubjects.existOrFail('create-case-flyout'); + + await testSubjects.setValue('input', caseTitle); + + await testSubjects.setValue('euiMarkdownEditorTextArea', 'test description'); + + // verify that solution picker is not visible + await testSubjects.missingOrFail('caseOwnerSelector'); + + await testSubjects.click('create-case-submit'); + + await cases.common.expectToasterToContain(`${caseTitle} has been updated`); + + await testSubjects.click('toaster-content-case-view-link'); + + if (await testSubjects.exists('appLeaveConfirmModal')) { + await testSubjects.exists('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton'); + } + + const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); + expect(await title.getVisibleText()).toEqual(caseTitle); + + await testSubjects.existOrFail('comment-persistableState-.lens'); + }); + + it('adds lens visualization to an existing case from dashboard', async () => { + const theCaseTitle = 'case already exists in security solution!!'; + const theCase = await cases.api.createCase({ + title: theCaseTitle, + description: 'This is a test case to verify existing action scenario!!', + owner: 'securitySolution', + }); + + await testSubjects.click('solutionSideNavItemLink-dashboards'); + + if (await testSubjects.exists('edit-unsaved-New-Dashboard')) { + await testSubjects.click('edit-unsaved-New-Dashboard'); + } + + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-embeddable_addToExistingCase'); + + // verify that solution filter is not visible + await testSubjects.missingOrFail('solution-filter-popover-button'); + + await testSubjects.click(`cases-table-row-select-${theCase.id}`); + + await cases.common.expectToasterToContain(`${theCaseTitle} has been updated`); + await testSubjects.click('toaster-content-case-view-link'); + + if (await testSubjects.exists('appLeaveConfirmModal')) { + await testSubjects.exists('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton'); + } + + const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); + expect(await title.getVisibleText()).toEqual(theCaseTitle); + + await testSubjects.existOrFail('comment-persistableState-.lens'); + }); + }); + }); +}; diff --git a/x-pack/test_serverless/functional/test_suites/security/index.ts b/x-pack/test_serverless/functional/test_suites/security/index.ts index 722a5d7aa3d4c6..cd762bbee7d5d7 100644 --- a/x-pack/test_serverless/functional/test_suites/security/index.ts +++ b/x-pack/test_serverless/functional/test_suites/security/index.ts @@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./ftr/landing_page')); loadTestFile(require.resolve('./ftr/navigation')); loadTestFile(require.resolve('./ftr/management')); + loadTestFile(require.resolve('./ftr/cases/attachment_framework')); }); } diff --git a/x-pack/test_serverless/shared/config.base.ts b/x-pack/test_serverless/shared/config.base.ts index 5ee130b96525d7..35c4320a7f17f5 100644 --- a/x-pack/test_serverless/shared/config.base.ts +++ b/x-pack/test_serverless/shared/config.base.ts @@ -60,6 +60,7 @@ export default async () => { appenders: ['deprecation'], }, ])}`, + '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', ], }, diff --git a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml index eff113dee5ac93..8c866d0a5a7b76 100644 --- a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml +++ b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml @@ -165,7 +165,7 @@ t3_analyst: - event_filters_all - host_isolation_exceptions_all - blocklist_all - - policy_management_all # Elastic Defend Policy Management + - policy_management_read # Elastic Defend Policy Management - host_isolation_all - process_operations_all - actions_log_management_all # Response actions history diff --git a/yarn.lock b/yarn.lock index 945c81f6d566d8..5a72d2a9a2534c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5228,6 +5228,10 @@ version "0.0.0" uid "" +"@kbn/search-api-panels@link:packages/kbn-search-api-panels": + version "0.0.0" + uid "" + "@kbn/search-examples-plugin@link:examples/search_examples": version "0.0.0" uid "" @@ -5884,6 +5888,10 @@ version "0.0.0" uid "" +"@kbn/use-tracked-promise@link:packages/kbn-use-tracked-promise": + version "0.0.0" + uid "" + "@kbn/user-profile-components@link:packages/kbn-user-profile-components": version "0.0.0" uid "" @@ -5976,6 +5984,10 @@ version "0.0.0" uid "" +"@kbn/lens-embeddable-utils@link:packages/kbn-lens-embeddable-utils": + version "0.0.0" + uid "" + "@kbn/visualizations-plugin@link:src/plugins/visualizations": version "0.0.0" uid "" @@ -10659,14 +10671,14 @@ ansi-regex@^2.0.0: integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1"