; }; pageSize: number; }"
+ ],
+ "path": "packages/shared-ux/table_persist/src/table_persist_hoc.tsx",
+ "deprecated": false,
+ "trackAdoption": false
+ }
+ ],
+ "initialIsOpen": false
}
],
- "interfaces": [],
"enums": [],
"misc": [
{
@@ -79,6 +336,57 @@
"deprecated": false,
"trackAdoption": false,
"initialIsOpen": false
+ },
+ {
+ "parentPluginId": "@kbn/shared-ux-table-persist",
+ "id": "def-common.EuiTablePersistPropsGetter",
+ "type": "Type",
+ "tags": [],
+ "label": "EuiTablePersistPropsGetter",
+ "description": [],
+ "signature": [
+ "(props: Omit) => ",
+ "EuiTablePersistProps",
+ ""
+ ],
+ "path": "packages/shared-ux/table_persist/src/table_persist_hoc.tsx",
+ "deprecated": false,
+ "trackAdoption": false,
+ "returnComment": [],
+ "children": [
+ {
+ "parentPluginId": "@kbn/shared-ux-table-persist",
+ "id": "def-common.EuiTablePersistPropsGetter.$1",
+ "type": "Object",
+ "tags": [],
+ "label": "props",
+ "description": [],
+ "signature": [
+ "{ [P in Exclude]: P[P]; }"
+ ],
+ "path": "packages/shared-ux/table_persist/src/table_persist_hoc.tsx",
+ "deprecated": false,
+ "trackAdoption": false
+ }
+ ],
+ "initialIsOpen": false
+ },
+ {
+ "parentPluginId": "@kbn/shared-ux-table-persist",
+ "id": "def-common.HOCProps",
+ "type": "Type",
+ "tags": [],
+ "label": "HOCProps",
+ "description": [],
+ "signature": [
+ "P & { euiTablePersistProps?: Partial<",
+ "EuiTablePersistProps",
+ "> | undefined; }"
+ ],
+ "path": "packages/shared-ux/table_persist/src/table_persist_hoc.tsx",
+ "deprecated": false,
+ "trackAdoption": false,
+ "initialIsOpen": false
}
],
"objects": []
diff --git a/api_docs/kbn_shared_ux_table_persist.mdx b/api_docs/kbn_shared_ux_table_persist.mdx
index 0522c15beb2ab..06ed280a7243c 100644
--- a/api_docs/kbn_shared_ux_table_persist.mdx
+++ b/api_docs/kbn_shared_ux_table_persist.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-table-persist
title: "@kbn/shared-ux-table-persist"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/shared-ux-table-persist plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-table-persist']
---
import kbnSharedUxTablePersistObj from './kbn_shared_ux_table_persist.devdocs.json';
@@ -21,13 +21,16 @@ Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sh
| Public API count | Any count | Items lacking comments | Missing exports |
|-------------------|-----------|------------------------|-----------------|
-| 3 | 0 | 2 | 2 |
+| 17 | 0 | 16 | 2 |
## Common
### Functions
+### Interfaces
+
+
### Consts, variables and types
diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx
index b551a950c4253..1cf9823de35f1 100644
--- a/api_docs/kbn_shared_ux_utility.mdx
+++ b/api_docs/kbn_shared_ux_utility.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility
title: "@kbn/shared-ux-utility"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/shared-ux-utility plugin
-date: 2024-11-18
+date: 2024-11-19
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 32453973c879e..5849842e95def 100644
--- a/api_docs/kbn_slo_schema.mdx
+++ b/api_docs/kbn_slo_schema.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-slo-schema
title: "@kbn/slo-schema"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/slo-schema plugin
-date: 2024-11-18
+date: 2024-11-19
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 2b943b2710c2a..86c0a5b60bb34 100644
--- a/api_docs/kbn_some_dev_log.mdx
+++ b/api_docs/kbn_some_dev_log.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-some-dev-log
title: "@kbn/some-dev-log"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/some-dev-log plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log']
---
import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json';
diff --git a/api_docs/kbn_sort_predicates.mdx b/api_docs/kbn_sort_predicates.mdx
index d7254a9438d21..58808bafb8c26 100644
--- a/api_docs/kbn_sort_predicates.mdx
+++ b/api_docs/kbn_sort_predicates.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sort-predicates
title: "@kbn/sort-predicates"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/sort-predicates plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sort-predicates']
---
import kbnSortPredicatesObj from './kbn_sort_predicates.devdocs.json';
diff --git a/api_docs/kbn_sse_utils.mdx b/api_docs/kbn_sse_utils.mdx
index 082c2e61243c4..97f887d6916e9 100644
--- a/api_docs/kbn_sse_utils.mdx
+++ b/api_docs/kbn_sse_utils.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sse-utils
title: "@kbn/sse-utils"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/sse-utils plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sse-utils']
---
import kbnSseUtilsObj from './kbn_sse_utils.devdocs.json';
diff --git a/api_docs/kbn_sse_utils_client.mdx b/api_docs/kbn_sse_utils_client.mdx
index 1e1fa5b24e048..01be6b681684d 100644
--- a/api_docs/kbn_sse_utils_client.mdx
+++ b/api_docs/kbn_sse_utils_client.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sse-utils-client
title: "@kbn/sse-utils-client"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/sse-utils-client plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sse-utils-client']
---
import kbnSseUtilsClientObj from './kbn_sse_utils_client.devdocs.json';
diff --git a/api_docs/kbn_sse_utils_server.mdx b/api_docs/kbn_sse_utils_server.mdx
index 1ab926f56fd45..224cc31048ac4 100644
--- a/api_docs/kbn_sse_utils_server.mdx
+++ b/api_docs/kbn_sse_utils_server.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sse-utils-server
title: "@kbn/sse-utils-server"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/sse-utils-server plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sse-utils-server']
---
import kbnSseUtilsServerObj from './kbn_sse_utils_server.devdocs.json';
diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx
index 8c9b9d36989a7..febd634486838 100644
--- a/api_docs/kbn_std.mdx
+++ b/api_docs/kbn_std.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-std
title: "@kbn/std"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/std plugin
-date: 2024-11-18
+date: 2024-11-19
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 c4efb27932985..58880300d5bde 100644
--- a/api_docs/kbn_stdio_dev_helpers.mdx
+++ b/api_docs/kbn_stdio_dev_helpers.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers
title: "@kbn/stdio-dev-helpers"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/stdio-dev-helpers plugin
-date: 2024-11-18
+date: 2024-11-19
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 09b4b3e349f55..8ebca570d046e 100644
--- a/api_docs/kbn_storybook.mdx
+++ b/api_docs/kbn_storybook.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-storybook
title: "@kbn/storybook"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/storybook plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook']
---
import kbnStorybookObj from './kbn_storybook.devdocs.json';
diff --git a/api_docs/kbn_synthetics_e2e.mdx b/api_docs/kbn_synthetics_e2e.mdx
index 02e8b982aa0e0..010d69a1b8aa7 100644
--- a/api_docs/kbn_synthetics_e2e.mdx
+++ b/api_docs/kbn_synthetics_e2e.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-synthetics-e2e
title: "@kbn/synthetics-e2e"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/synthetics-e2e plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/synthetics-e2e']
---
import kbnSyntheticsE2eObj from './kbn_synthetics_e2e.devdocs.json';
diff --git a/api_docs/kbn_synthetics_private_location.mdx b/api_docs/kbn_synthetics_private_location.mdx
index bdaa2514a88bd..b7851081801d0 100644
--- a/api_docs/kbn_synthetics_private_location.mdx
+++ b/api_docs/kbn_synthetics_private_location.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-synthetics-private-location
title: "@kbn/synthetics-private-location"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/synthetics-private-location plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/synthetics-private-location']
---
import kbnSyntheticsPrivateLocationObj from './kbn_synthetics_private_location.devdocs.json';
diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx
index 5231cd4e3c458..36b2444bb48a1 100644
--- a/api_docs/kbn_telemetry_tools.mdx
+++ b/api_docs/kbn_telemetry_tools.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools
title: "@kbn/telemetry-tools"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/telemetry-tools plugin
-date: 2024-11-18
+date: 2024-11-19
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 492a87573f63d..40461c7c08ff8 100644
--- a/api_docs/kbn_test.mdx
+++ b/api_docs/kbn_test.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test
title: "@kbn/test"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/test plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test']
---
import kbnTestObj from './kbn_test.devdocs.json';
diff --git a/api_docs/kbn_test_eui_helpers.mdx b/api_docs/kbn_test_eui_helpers.mdx
index 130509125034f..1a69ab75edb2e 100644
--- a/api_docs/kbn_test_eui_helpers.mdx
+++ b/api_docs/kbn_test_eui_helpers.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-eui-helpers
title: "@kbn/test-eui-helpers"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/test-eui-helpers plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-eui-helpers']
---
import kbnTestEuiHelpersObj from './kbn_test_eui_helpers.devdocs.json';
diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx
index 8b32345859485..6796757558f19 100644
--- a/api_docs/kbn_test_jest_helpers.mdx
+++ b/api_docs/kbn_test_jest_helpers.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers
title: "@kbn/test-jest-helpers"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/test-jest-helpers plugin
-date: 2024-11-18
+date: 2024-11-19
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 079d2d63a454d..1522caa831c31 100644
--- a/api_docs/kbn_test_subj_selector.mdx
+++ b/api_docs/kbn_test_subj_selector.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-subj-selector
title: "@kbn/test-subj-selector"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/test-subj-selector plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-subj-selector']
---
import kbnTestSubjSelectorObj from './kbn_test_subj_selector.devdocs.json';
diff --git a/api_docs/kbn_timerange.mdx b/api_docs/kbn_timerange.mdx
index 5a3daf0b3a51d..ea5afd5d95344 100644
--- a/api_docs/kbn_timerange.mdx
+++ b/api_docs/kbn_timerange.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-timerange
title: "@kbn/timerange"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/timerange plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/timerange']
---
import kbnTimerangeObj from './kbn_timerange.devdocs.json';
diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx
index 30405402bbba9..beb39dd02b954 100644
--- a/api_docs/kbn_tooling_log.mdx
+++ b/api_docs/kbn_tooling_log.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log
title: "@kbn/tooling-log"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/tooling-log plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log']
---
import kbnToolingLogObj from './kbn_tooling_log.devdocs.json';
diff --git a/api_docs/kbn_transpose_utils.mdx b/api_docs/kbn_transpose_utils.mdx
index 5fe0d46e8f6bb..f7c50e163efdd 100644
--- a/api_docs/kbn_transpose_utils.mdx
+++ b/api_docs/kbn_transpose_utils.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-transpose-utils
title: "@kbn/transpose-utils"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/transpose-utils plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/transpose-utils']
---
import kbnTransposeUtilsObj from './kbn_transpose_utils.devdocs.json';
diff --git a/api_docs/kbn_triggers_actions_ui_types.mdx b/api_docs/kbn_triggers_actions_ui_types.mdx
index 58a81829af635..7f18eac98ec52 100644
--- a/api_docs/kbn_triggers_actions_ui_types.mdx
+++ b/api_docs/kbn_triggers_actions_ui_types.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-triggers-actions-ui-types
title: "@kbn/triggers-actions-ui-types"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/triggers-actions-ui-types plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/triggers-actions-ui-types']
---
import kbnTriggersActionsUiTypesObj from './kbn_triggers_actions_ui_types.devdocs.json';
diff --git a/api_docs/kbn_try_in_console.mdx b/api_docs/kbn_try_in_console.mdx
index d40d2431e7053..a6dd8c821d8ab 100644
--- a/api_docs/kbn_try_in_console.mdx
+++ b/api_docs/kbn_try_in_console.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-try-in-console
title: "@kbn/try-in-console"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/try-in-console plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/try-in-console']
---
import kbnTryInConsoleObj from './kbn_try_in_console.devdocs.json';
diff --git a/api_docs/kbn_ts_projects.mdx b/api_docs/kbn_ts_projects.mdx
index 6f313c8e8b059..ecfda351e476a 100644
--- a/api_docs/kbn_ts_projects.mdx
+++ b/api_docs/kbn_ts_projects.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ts-projects
title: "@kbn/ts-projects"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/ts-projects plugin
-date: 2024-11-18
+date: 2024-11-19
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 21d0e0b9959b8..87d01260b4357 100644
--- a/api_docs/kbn_typed_react_router_config.mdx
+++ b/api_docs/kbn_typed_react_router_config.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config
title: "@kbn/typed-react-router-config"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/typed-react-router-config plugin
-date: 2024-11-18
+date: 2024-11-19
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 aa691e7e43d71..6b26b2ad9952f 100644
--- a/api_docs/kbn_ui_actions_browser.mdx
+++ b/api_docs/kbn_ui_actions_browser.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-actions-browser
title: "@kbn/ui-actions-browser"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/ui-actions-browser plugin
-date: 2024-11-18
+date: 2024-11-19
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.devdocs.json b/api_docs/kbn_ui_shared_deps_src.devdocs.json
index ed0f25116b66d..2a61ae29701c1 100644
--- a/api_docs/kbn_ui_shared_deps_src.devdocs.json
+++ b/api_docs/kbn_ui_shared_deps_src.devdocs.json
@@ -699,6 +699,17 @@
"path": "packages/kbn-ui-shared-deps-src/src/definitions.js",
"deprecated": false,
"trackAdoption": false
+ },
+ {
+ "parentPluginId": "@kbn/ui-shared-deps-src",
+ "id": "def-common.externals.kbnreactkibanacontexttheme",
+ "type": "string",
+ "tags": [],
+ "label": "'@kbn/react-kibana-context-theme'",
+ "description": [],
+ "path": "packages/kbn-ui-shared-deps-src/src/definitions.js",
+ "deprecated": false,
+ "trackAdoption": false
}
],
"initialIsOpen": false
diff --git a/api_docs/kbn_ui_shared_deps_src.mdx b/api_docs/kbn_ui_shared_deps_src.mdx
index a856843a787f7..c9249e50d8971 100644
--- a/api_docs/kbn_ui_shared_deps_src.mdx
+++ b/api_docs/kbn_ui_shared_deps_src.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-shared-deps-src
title: "@kbn/ui-shared-deps-src"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/ui-shared-deps-src plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-shared-deps-src']
---
import kbnUiSharedDepsSrcObj from './kbn_ui_shared_deps_src.devdocs.json';
@@ -21,7 +21,7 @@ Contact [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kiban
| Public API count | Any count | Items lacking comments | Missing exports |
|-------------------|-----------|------------------------|-----------------|
-| 59 | 0 | 50 | 0 |
+| 60 | 0 | 51 | 0 |
## Common
diff --git a/api_docs/kbn_ui_theme.devdocs.json b/api_docs/kbn_ui_theme.devdocs.json
index b7c9d0a6f2831..79750b38df53b 100644
--- a/api_docs/kbn_ui_theme.devdocs.json
+++ b/api_docs/kbn_ui_theme.devdocs.json
@@ -69,14 +69,6 @@
"deprecated": true,
"trackAdoption": false,
"references": [
- {
- "plugin": "@kbn/monaco",
- "path": "packages/kbn-monaco/src/esql/lib/esql_theme.ts"
- },
- {
- "plugin": "@kbn/monaco",
- "path": "packages/kbn-monaco/src/esql/lib/esql_theme.ts"
- },
{
"plugin": "@kbn/monaco",
"path": "packages/kbn-monaco/src/console/theme.ts"
diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx
index d17a315c77e03..c9a665af3aacf 100644
--- a/api_docs/kbn_ui_theme.mdx
+++ b/api_docs/kbn_ui_theme.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme
title: "@kbn/ui-theme"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/ui-theme plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme']
---
import kbnUiThemeObj from './kbn_ui_theme.devdocs.json';
diff --git a/api_docs/kbn_unified_data_table.devdocs.json b/api_docs/kbn_unified_data_table.devdocs.json
index 3c035dc025739..1398b8b7116ac 100644
--- a/api_docs/kbn_unified_data_table.devdocs.json
+++ b/api_docs/kbn_unified_data_table.devdocs.json
@@ -2056,9 +2056,9 @@
"{ theme: ",
{
"pluginId": "@kbn/react-kibana-context-common",
- "scope": "public",
+ "scope": "common",
"docId": "kibKbnReactKibanaContextCommonPluginApi",
- "section": "def-public.ThemeServiceStart",
+ "section": "def-common.ThemeServiceStart",
"text": "ThemeServiceStart"
},
"; fieldFormats: ",
diff --git a/api_docs/kbn_unified_data_table.mdx b/api_docs/kbn_unified_data_table.mdx
index 7ed91c3de9c8c..6e92450416f2c 100644
--- a/api_docs/kbn_unified_data_table.mdx
+++ b/api_docs/kbn_unified_data_table.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unified-data-table
title: "@kbn/unified-data-table"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/unified-data-table plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-data-table']
---
import kbnUnifiedDataTableObj from './kbn_unified_data_table.devdocs.json';
diff --git a/api_docs/kbn_unified_doc_viewer.mdx b/api_docs/kbn_unified_doc_viewer.mdx
index e625f00480d16..e73502404b937 100644
--- a/api_docs/kbn_unified_doc_viewer.mdx
+++ b/api_docs/kbn_unified_doc_viewer.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unified-doc-viewer
title: "@kbn/unified-doc-viewer"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/unified-doc-viewer plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-doc-viewer']
---
import kbnUnifiedDocViewerObj from './kbn_unified_doc_viewer.devdocs.json';
diff --git a/api_docs/kbn_unified_field_list.mdx b/api_docs/kbn_unified_field_list.mdx
index b0036b160ebae..679c8a1a627bf 100644
--- a/api_docs/kbn_unified_field_list.mdx
+++ b/api_docs/kbn_unified_field_list.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unified-field-list
title: "@kbn/unified-field-list"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/unified-field-list plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unified-field-list']
---
import kbnUnifiedFieldListObj from './kbn_unified_field_list.devdocs.json';
diff --git a/api_docs/kbn_unsaved_changes_badge.mdx b/api_docs/kbn_unsaved_changes_badge.mdx
index 1131276f7e27d..69b103c013ea3 100644
--- a/api_docs/kbn_unsaved_changes_badge.mdx
+++ b/api_docs/kbn_unsaved_changes_badge.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unsaved-changes-badge
title: "@kbn/unsaved-changes-badge"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/unsaved-changes-badge plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unsaved-changes-badge']
---
import kbnUnsavedChangesBadgeObj from './kbn_unsaved_changes_badge.devdocs.json';
diff --git a/api_docs/kbn_unsaved_changes_prompt.mdx b/api_docs/kbn_unsaved_changes_prompt.mdx
index 478c5f526b6e9..743ab3d4ff54a 100644
--- a/api_docs/kbn_unsaved_changes_prompt.mdx
+++ b/api_docs/kbn_unsaved_changes_prompt.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-unsaved-changes-prompt
title: "@kbn/unsaved-changes-prompt"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/unsaved-changes-prompt plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/unsaved-changes-prompt']
---
import kbnUnsavedChangesPromptObj from './kbn_unsaved_changes_prompt.devdocs.json';
diff --git a/api_docs/kbn_use_tracked_promise.mdx b/api_docs/kbn_use_tracked_promise.mdx
index ecfa81ba34e7b..08e527d88a14c 100644
--- a/api_docs/kbn_use_tracked_promise.mdx
+++ b/api_docs/kbn_use_tracked_promise.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-use-tracked-promise
title: "@kbn/use-tracked-promise"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/use-tracked-promise plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/use-tracked-promise']
---
import kbnUseTrackedPromiseObj from './kbn_use_tracked_promise.devdocs.json';
diff --git a/api_docs/kbn_user_profile_components.devdocs.json b/api_docs/kbn_user_profile_components.devdocs.json
index c5a703d4e9cc2..34214d8791833 100644
--- a/api_docs/kbn_user_profile_components.devdocs.json
+++ b/api_docs/kbn_user_profile_components.devdocs.json
@@ -976,9 +976,9 @@
", \"reportEvent\"> | undefined; theme: ",
{
"pluginId": "@kbn/react-kibana-context-common",
- "scope": "public",
+ "scope": "common",
"docId": "kibKbnReactKibanaContextCommonPluginApi",
- "section": "def-public.ThemeServiceStart",
+ "section": "def-common.ThemeServiceStart",
"text": "ThemeServiceStart"
},
"; }"
diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx
index e02e491845484..c60ec0e607d48 100644
--- a/api_docs/kbn_user_profile_components.mdx
+++ b/api_docs/kbn_user_profile_components.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-user-profile-components
title: "@kbn/user-profile-components"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/user-profile-components plugin
-date: 2024-11-18
+date: 2024-11-19
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 09b257552bcd8..ae809e17f6738 100644
--- a/api_docs/kbn_utility_types.mdx
+++ b/api_docs/kbn_utility_types.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types
title: "@kbn/utility-types"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/utility-types plugin
-date: 2024-11-18
+date: 2024-11-19
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 ec4ba8727bb39..fd12297b5b8c9 100644
--- a/api_docs/kbn_utility_types_jest.mdx
+++ b/api_docs/kbn_utility_types_jest.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest
title: "@kbn/utility-types-jest"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/utility-types-jest plugin
-date: 2024-11-18
+date: 2024-11-19
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 bd042e4e6b8f2..3d49b2cf72612 100644
--- a/api_docs/kbn_utils.mdx
+++ b/api_docs/kbn_utils.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utils
title: "@kbn/utils"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/utils plugin
-date: 2024-11-18
+date: 2024-11-19
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 81e865b3d66ba..0710e42dc8eba 100644
--- a/api_docs/kbn_visualization_ui_components.mdx
+++ b/api_docs/kbn_visualization_ui_components.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-visualization-ui-components
title: "@kbn/visualization-ui-components"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/visualization-ui-components plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/visualization-ui-components']
---
import kbnVisualizationUiComponentsObj from './kbn_visualization_ui_components.devdocs.json';
diff --git a/api_docs/kbn_visualization_utils.mdx b/api_docs/kbn_visualization_utils.mdx
index b9d07c5b6d7f7..5f8fa23f162c4 100644
--- a/api_docs/kbn_visualization_utils.mdx
+++ b/api_docs/kbn_visualization_utils.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-visualization-utils
title: "@kbn/visualization-utils"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/visualization-utils plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/visualization-utils']
---
import kbnVisualizationUtilsObj from './kbn_visualization_utils.devdocs.json';
diff --git a/api_docs/kbn_xstate_utils.mdx b/api_docs/kbn_xstate_utils.mdx
index ec2f50b60f4d3..1aaae7295b988 100644
--- a/api_docs/kbn_xstate_utils.mdx
+++ b/api_docs/kbn_xstate_utils.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-xstate-utils
title: "@kbn/xstate-utils"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/xstate-utils plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/xstate-utils']
---
import kbnXstateUtilsObj from './kbn_xstate_utils.devdocs.json';
diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx
index 45003d0ffa2af..6aa5f611d5ce0 100644
--- a/api_docs/kbn_yarn_lock_validator.mdx
+++ b/api_docs/kbn_yarn_lock_validator.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-yarn-lock-validator
title: "@kbn/yarn-lock-validator"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/yarn-lock-validator plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator']
---
import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json';
diff --git a/api_docs/kbn_zod.mdx b/api_docs/kbn_zod.mdx
index 3e199bcf7f782..a53a340bc5e8e 100644
--- a/api_docs/kbn_zod.mdx
+++ b/api_docs/kbn_zod.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-zod
title: "@kbn/zod"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/zod plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/zod']
---
import kbnZodObj from './kbn_zod.devdocs.json';
diff --git a/api_docs/kbn_zod_helpers.mdx b/api_docs/kbn_zod_helpers.mdx
index afb2b519ce418..5c179412886ef 100644
--- a/api_docs/kbn_zod_helpers.mdx
+++ b/api_docs/kbn_zod_helpers.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-zod-helpers
title: "@kbn/zod-helpers"
image: https://source.unsplash.com/400x175/?github
description: API docs for the @kbn/zod-helpers plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/zod-helpers']
---
import kbnZodHelpersObj from './kbn_zod_helpers.devdocs.json';
diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx
index c54abc7675927..fefc3fbc8e142 100644
--- a/api_docs/kibana_overview.mdx
+++ b/api_docs/kibana_overview.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaOverview
title: "kibanaOverview"
image: https://source.unsplash.com/400x175/?github
description: API docs for the kibanaOverview plugin
-date: 2024-11-18
+date: 2024-11-19
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 faca5278b8eba..99e4d8fe29755 100644
--- a/api_docs/kibana_react.devdocs.json
+++ b/api_docs/kibana_react.devdocs.json
@@ -1455,9 +1455,9 @@
"<",
{
"pluginId": "@kbn/react-kibana-context-common",
- "scope": "public",
+ "scope": "common",
"docId": "kibKbnReactKibanaContextCommonPluginApi",
- "section": "def-public.KibanaTheme",
+ "section": "def-common.KibanaTheme",
"text": "KibanaTheme"
},
">) => React.JSX.Element"
@@ -1493,9 +1493,9 @@
"<",
{
"pluginId": "@kbn/react-kibana-context-common",
- "scope": "public",
+ "scope": "common",
"docId": "kibKbnReactKibanaContextCommonPluginApi",
- "section": "def-public.KibanaTheme",
+ "section": "def-common.KibanaTheme",
"text": "KibanaTheme"
},
">"
@@ -2653,17 +2653,17 @@
"Pick<",
{
"pluginId": "@kbn/react-kibana-context-theme",
- "scope": "public",
+ "scope": "common",
"docId": "kibKbnReactKibanaContextThemePluginApi",
- "section": "def-public.KibanaThemeProviderProps",
+ "section": "def-common.KibanaThemeProviderProps",
"text": "KibanaThemeProviderProps"
},
", \"children\" | \"modify\"> & ",
{
"pluginId": "@kbn/react-kibana-context-common",
- "scope": "public",
+ "scope": "common",
"docId": "kibKbnReactKibanaContextCommonPluginApi",
- "section": "def-public.ThemeServiceStart",
+ "section": "def-common.ThemeServiceStart",
"text": "ThemeServiceStart"
}
],
diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx
index 1ad3660508d8e..bf766ea8f113f 100644
--- a/api_docs/kibana_react.mdx
+++ b/api_docs/kibana_react.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaReact
title: "kibanaReact"
image: https://source.unsplash.com/400x175/?github
description: API docs for the kibanaReact plugin
-date: 2024-11-18
+date: 2024-11-19
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 0cce259cdd89c..f2caa5e23d7b3 100644
--- a/api_docs/kibana_utils.mdx
+++ b/api_docs/kibana_utils.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaUtils
title: "kibanaUtils"
image: https://source.unsplash.com/400x175/?github
description: API docs for the kibanaUtils plugin
-date: 2024-11-18
+date: 2024-11-19
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 506c852537a97..a9db4da2dc551 100644
--- a/api_docs/kubernetes_security.mdx
+++ b/api_docs/kubernetes_security.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity
title: "kubernetesSecurity"
image: https://source.unsplash.com/400x175/?github
description: API docs for the kubernetesSecurity plugin
-date: 2024-11-18
+date: 2024-11-19
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 e56ed576b7bca..373bbe2f38835 100644
--- a/api_docs/lens.mdx
+++ b/api_docs/lens.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lens
title: "lens"
image: https://source.unsplash.com/400x175/?github
description: API docs for the lens plugin
-date: 2024-11-18
+date: 2024-11-19
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 a4265684dbd16..384de74cedc5c 100644
--- a/api_docs/license_api_guard.mdx
+++ b/api_docs/license_api_guard.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard
title: "licenseApiGuard"
image: https://source.unsplash.com/400x175/?github
description: API docs for the licenseApiGuard plugin
-date: 2024-11-18
+date: 2024-11-19
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 fe2780566ae03..d7749a9229353 100644
--- a/api_docs/license_management.mdx
+++ b/api_docs/license_management.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseManagement
title: "licenseManagement"
image: https://source.unsplash.com/400x175/?github
description: API docs for the licenseManagement plugin
-date: 2024-11-18
+date: 2024-11-19
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 30e23c7819503..5344bf70f5584 100644
--- a/api_docs/licensing.mdx
+++ b/api_docs/licensing.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licensing
title: "licensing"
image: https://source.unsplash.com/400x175/?github
description: API docs for the licensing plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing']
---
import licensingObj from './licensing.devdocs.json';
diff --git a/api_docs/links.mdx b/api_docs/links.mdx
index 92a7c7b0fd092..311c805a8d2ed 100644
--- a/api_docs/links.mdx
+++ b/api_docs/links.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/links
title: "links"
image: https://source.unsplash.com/400x175/?github
description: API docs for the links plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'links']
---
import linksObj from './links.devdocs.json';
diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx
index 5cd3e710ae0a3..08c95634e190a 100644
--- a/api_docs/lists.mdx
+++ b/api_docs/lists.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lists
title: "lists"
image: https://source.unsplash.com/400x175/?github
description: API docs for the lists plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists']
---
import listsObj from './lists.devdocs.json';
diff --git a/api_docs/logs_data_access.mdx b/api_docs/logs_data_access.mdx
index 8c6add22b15eb..78b3a7e9b4bc5 100644
--- a/api_docs/logs_data_access.mdx
+++ b/api_docs/logs_data_access.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/logsDataAccess
title: "logsDataAccess"
image: https://source.unsplash.com/400x175/?github
description: API docs for the logsDataAccess plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'logsDataAccess']
---
import logsDataAccessObj from './logs_data_access.devdocs.json';
diff --git a/api_docs/logs_explorer.mdx b/api_docs/logs_explorer.mdx
index beb564b9117ab..af8344bcb022e 100644
--- a/api_docs/logs_explorer.mdx
+++ b/api_docs/logs_explorer.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/logsExplorer
title: "logsExplorer"
image: https://source.unsplash.com/400x175/?github
description: API docs for the logsExplorer plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'logsExplorer']
---
import logsExplorerObj from './logs_explorer.devdocs.json';
diff --git a/api_docs/logs_shared.mdx b/api_docs/logs_shared.mdx
index 8cc82045d002f..7894519385f1e 100644
--- a/api_docs/logs_shared.mdx
+++ b/api_docs/logs_shared.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/logsShared
title: "logsShared"
image: https://source.unsplash.com/400x175/?github
description: API docs for the logsShared plugin
-date: 2024-11-18
+date: 2024-11-19
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 be42f94512a3b..158469eae5cd5 100644
--- a/api_docs/management.mdx
+++ b/api_docs/management.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/management
title: "management"
image: https://source.unsplash.com/400x175/?github
description: API docs for the management plugin
-date: 2024-11-18
+date: 2024-11-19
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 b376211b961a6..8a2518435d53d 100644
--- a/api_docs/maps.mdx
+++ b/api_docs/maps.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/maps
title: "maps"
image: https://source.unsplash.com/400x175/?github
description: API docs for the maps plugin
-date: 2024-11-18
+date: 2024-11-19
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 7ab0692dbbd43..fde7bdc426948 100644
--- a/api_docs/maps_ems.mdx
+++ b/api_docs/maps_ems.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mapsEms
title: "mapsEms"
image: https://source.unsplash.com/400x175/?github
description: API docs for the mapsEms plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms']
---
import mapsEmsObj from './maps_ems.devdocs.json';
diff --git a/api_docs/metrics_data_access.mdx b/api_docs/metrics_data_access.mdx
index 21baf958548b0..5a252eaae4b4d 100644
--- a/api_docs/metrics_data_access.mdx
+++ b/api_docs/metrics_data_access.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/metricsDataAccess
title: "metricsDataAccess"
image: https://source.unsplash.com/400x175/?github
description: API docs for the metricsDataAccess plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'metricsDataAccess']
---
import metricsDataAccessObj from './metrics_data_access.devdocs.json';
diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx
index 98625d6fb49d6..ea44e1ec9a966 100644
--- a/api_docs/ml.mdx
+++ b/api_docs/ml.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ml
title: "ml"
image: https://source.unsplash.com/400x175/?github
description: API docs for the ml plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml']
---
import mlObj from './ml.devdocs.json';
diff --git a/api_docs/mock_idp_plugin.mdx b/api_docs/mock_idp_plugin.mdx
index 23919b5a11c67..f944972b77892 100644
--- a/api_docs/mock_idp_plugin.mdx
+++ b/api_docs/mock_idp_plugin.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mockIdpPlugin
title: "mockIdpPlugin"
image: https://source.unsplash.com/400x175/?github
description: API docs for the mockIdpPlugin plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mockIdpPlugin']
---
import mockIdpPluginObj from './mock_idp_plugin.devdocs.json';
diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx
index e83bc4ee0d86e..7ae60fd1f5356 100644
--- a/api_docs/monitoring.mdx
+++ b/api_docs/monitoring.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoring
title: "monitoring"
image: https://source.unsplash.com/400x175/?github
description: API docs for the monitoring plugin
-date: 2024-11-18
+date: 2024-11-19
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 709e49ac0ff87..5b62e6e0d1283 100644
--- a/api_docs/monitoring_collection.mdx
+++ b/api_docs/monitoring_collection.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoringCollection
title: "monitoringCollection"
image: https://source.unsplash.com/400x175/?github
description: API docs for the monitoringCollection plugin
-date: 2024-11-18
+date: 2024-11-19
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 60ff383c817e9..496072f43a492 100644
--- a/api_docs/navigation.mdx
+++ b/api_docs/navigation.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/navigation
title: "navigation"
image: https://source.unsplash.com/400x175/?github
description: API docs for the navigation plugin
-date: 2024-11-18
+date: 2024-11-19
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 256a7f3d73776..c7f9280661b62 100644
--- a/api_docs/newsfeed.mdx
+++ b/api_docs/newsfeed.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/newsfeed
title: "newsfeed"
image: https://source.unsplash.com/400x175/?github
description: API docs for the newsfeed plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed']
---
import newsfeedObj from './newsfeed.devdocs.json';
diff --git a/api_docs/no_data_page.mdx b/api_docs/no_data_page.mdx
index 37ae968514a0f..119862d079f69 100644
--- a/api_docs/no_data_page.mdx
+++ b/api_docs/no_data_page.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/noDataPage
title: "noDataPage"
image: https://source.unsplash.com/400x175/?github
description: API docs for the noDataPage plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'noDataPage']
---
import noDataPageObj from './no_data_page.devdocs.json';
diff --git a/api_docs/notifications.mdx b/api_docs/notifications.mdx
index c49241d716fa5..605975c60b42b 100644
--- a/api_docs/notifications.mdx
+++ b/api_docs/notifications.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/notifications
title: "notifications"
image: https://source.unsplash.com/400x175/?github
description: API docs for the notifications plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'notifications']
---
import notificationsObj from './notifications.devdocs.json';
diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx
index 43d81b9fe5ae6..f728ea4fa7e9b 100644
--- a/api_docs/observability.mdx
+++ b/api_docs/observability.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observability
title: "observability"
image: https://source.unsplash.com/400x175/?github
description: API docs for the observability plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability']
---
import observabilityObj from './observability.devdocs.json';
diff --git a/api_docs/observability_a_i_assistant.mdx b/api_docs/observability_a_i_assistant.mdx
index 0ad44a6888fb2..d9cce36dffe60 100644
--- a/api_docs/observability_a_i_assistant.mdx
+++ b/api_docs/observability_a_i_assistant.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityAIAssistant
title: "observabilityAIAssistant"
image: https://source.unsplash.com/400x175/?github
description: API docs for the observabilityAIAssistant plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityAIAssistant']
---
import observabilityAIAssistantObj from './observability_a_i_assistant.devdocs.json';
diff --git a/api_docs/observability_a_i_assistant_app.mdx b/api_docs/observability_a_i_assistant_app.mdx
index 3fdb21b1ed3ce..3536df0a49b49 100644
--- a/api_docs/observability_a_i_assistant_app.mdx
+++ b/api_docs/observability_a_i_assistant_app.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityAIAssistantApp
title: "observabilityAIAssistantApp"
image: https://source.unsplash.com/400x175/?github
description: API docs for the observabilityAIAssistantApp plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityAIAssistantApp']
---
import observabilityAIAssistantAppObj from './observability_a_i_assistant_app.devdocs.json';
diff --git a/api_docs/observability_ai_assistant_management.mdx b/api_docs/observability_ai_assistant_management.mdx
index 7dab03b59c44e..d4567385c8703 100644
--- a/api_docs/observability_ai_assistant_management.mdx
+++ b/api_docs/observability_ai_assistant_management.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityAiAssistantManagement
title: "observabilityAiAssistantManagement"
image: https://source.unsplash.com/400x175/?github
description: API docs for the observabilityAiAssistantManagement plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityAiAssistantManagement']
---
import observabilityAiAssistantManagementObj from './observability_ai_assistant_management.devdocs.json';
diff --git a/api_docs/observability_logs_explorer.mdx b/api_docs/observability_logs_explorer.mdx
index 045420af299ae..4afb5190018cf 100644
--- a/api_docs/observability_logs_explorer.mdx
+++ b/api_docs/observability_logs_explorer.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityLogsExplorer
title: "observabilityLogsExplorer"
image: https://source.unsplash.com/400x175/?github
description: API docs for the observabilityLogsExplorer plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityLogsExplorer']
---
import observabilityLogsExplorerObj from './observability_logs_explorer.devdocs.json';
diff --git a/api_docs/observability_onboarding.mdx b/api_docs/observability_onboarding.mdx
index dbe4026fff641..306031fa78045 100644
--- a/api_docs/observability_onboarding.mdx
+++ b/api_docs/observability_onboarding.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityOnboarding
title: "observabilityOnboarding"
image: https://source.unsplash.com/400x175/?github
description: API docs for the observabilityOnboarding plugin
-date: 2024-11-18
+date: 2024-11-19
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 197de9a7572fc..8dd043f350144 100644
--- a/api_docs/observability_shared.mdx
+++ b/api_docs/observability_shared.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityShared
title: "observabilityShared"
image: https://source.unsplash.com/400x175/?github
description: API docs for the observabilityShared plugin
-date: 2024-11-18
+date: 2024-11-19
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 3a87bea6e1b75..46cd632977208 100644
--- a/api_docs/osquery.mdx
+++ b/api_docs/osquery.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/osquery
title: "osquery"
image: https://source.unsplash.com/400x175/?github
description: API docs for the osquery plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery']
---
import osqueryObj from './osquery.devdocs.json';
diff --git a/api_docs/painless_lab.mdx b/api_docs/painless_lab.mdx
index c698e85725780..f4bccf19eb2aa 100644
--- a/api_docs/painless_lab.mdx
+++ b/api_docs/painless_lab.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/painlessLab
title: "painlessLab"
image: https://source.unsplash.com/400x175/?github
description: API docs for the painlessLab plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'painlessLab']
---
import painlessLabObj from './painless_lab.devdocs.json';
diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx
index 245663b0c8643..b250a83cfafaa 100644
--- a/api_docs/plugin_directory.mdx
+++ b/api_docs/plugin_directory.mdx
@@ -7,7 +7,7 @@ id: kibDevDocsPluginDirectory
slug: /kibana-dev-docs/api-meta/plugin-api-directory
title: Directory
description: Directory of public APIs available through plugins or packages.
-date: 2024-11-18
+date: 2024-11-19
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 |
|--------------|----------|------------------------|
-| 883 | 753 | 47 |
+| 884 | 754 | 47 |
### Public API health stats
| API Count | Any Count | Missing comments | Missing exports |
|--------------|----------|-----------------|--------|
-| 54324 | 247 | 40789 | 2009 |
+| 54380 | 247 | 40840 | 2011 |
## Plugin Directory
@@ -48,7 +48,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| cloudLinks | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | Adds the links to the Elastic Cloud console | 0 | 0 | 0 | 0 |
| | [@elastic/kibana-cloud-security-posture](https://github.com/orgs/elastic/teams/kibana-cloud-security-posture) | The cloud security posture plugin | 13 | 0 | 2 | 2 |
| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 39 | 0 | 30 | 0 |
-| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Content management app | 149 | 0 | 125 | 6 |
+| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Content management app | 150 | 0 | 126 | 6 |
| | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 135 | 0 | 131 | 15 |
| crossClusterReplication | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 0 | 0 | 0 | 0 |
| customBranding | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Enables customization of Kibana | 0 | 0 | 0 | 0 |
@@ -103,7 +103,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The file upload plugin contains components and services for uploading a file, analyzing its data, and then importing the data into an Elasticsearch index. Supported file types include CSV, TSV, newline-delimited JSON and GeoJSON. | 89 | 0 | 89 | 8 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | File upload, download, sharing, and serving over HTTP implementation in Kibana. | 240 | 0 | 24 | 9 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Simple UI for managing files in Kibana | 3 | 0 | 3 | 0 |
-| | [@elastic/fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1422 | 5 | 1297 | 81 |
+| | [@elastic/fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1427 | 5 | 1302 | 81 |
| ftrApis | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 72 | 0 | 14 | 5 |
| globalSearchBar | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 0 | 0 | 0 | 0 |
@@ -114,7 +114,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 149 | 0 | 111 | 1 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Image embeddable | 1 | 0 | 1 | 0 |
| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 4 | 0 | 4 | 0 |
-| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 243 | 0 | 238 | 1 |
+| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 244 | 0 | 239 | 1 |
| | [@elastic/appex-ai-infra](https://github.com/orgs/elastic/teams/appex-ai-infra) | - | 33 | 0 | 28 | 4 |
| | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | This plugin visualizes data from Filebeat and Metricbeat, and integrates with other Observability solutions | 24 | 0 | 24 | 5 |
| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 4 | 0 | 4 | 0 |
@@ -171,7 +171,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 263 | 0 | 226 | 10 |
| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 24 | 0 | 19 | 2 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 114 | 2 | 109 | 5 |
-| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 25 | 0 | 25 | 0 |
+| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 25 | 0 | 25 | 1 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 148 | 0 | 139 | 2 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 36 | 0 | 30 | 3 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 105 | 0 | 58 | 0 |
@@ -187,7 +187,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/search-kibana](https://github.com/orgs/elastic/teams/search-kibana) | - | 22 | 0 | 16 | 1 |
| searchprofiler | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-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. | 455 | 0 | 238 | 0 |
-| | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | - | 188 | 0 | 120 | 33 |
+| | [@elastic/security-solution](https://github.com/orgs/elastic/teams/security-solution) | - | 187 | 0 | 119 | 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. | 7 | 0 | 7 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | The core Serverless plugin, providing APIs to Serverless Project plugins. | 25 | 0 | 24 | 0 |
@@ -291,8 +291,9 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 10 | 0 | 8 | 4 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 32 | 0 | 28 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 9 | 0 | 6 | 2 |
-| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 45 | 0 | 44 | 0 |
-| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 7 | 0 | 7 | 0 |
+| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 1 | 0 | 1 | 0 |
+| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 43 | 0 | 42 | 1 |
+| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 13 | 0 | 13 | 1 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 8 | 0 | 8 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 3 | 0 | 3 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 10 | 0 | 10 | 0 |
@@ -417,6 +418,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 7 | 0 | 7 | 0 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 5 | 0 | 0 | 0 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 6 | 0 | 6 | 0 |
+| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2 | 0 | 2 | 0 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2 | 0 | 2 | 0 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2 | 0 | 2 | 1 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 4 | 0 | 4 | 1 |
@@ -445,8 +447,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 146 | 1 | 63 | 0 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 16 | 0 | 16 | 0 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 15 | 0 | 15 | 2 |
-| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 12 | 0 | 2 | 0 |
-| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 21 | 0 | 20 | 0 |
+| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 33 | 0 | 22 | 0 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 20 | 0 | 3 | 0 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 24 | 0 | 24 | 3 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 4 | 0 | 4 | 0 |
@@ -456,12 +457,12 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 29 | 0 | 4 | 0 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 15 | 0 | 14 | 1 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 9 | 0 | 9 | 0 |
-| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 6 | 0 | 2 | 0 |
+| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 7 | 0 | 2 | 0 |
| | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 9 | 0 | 9 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 33 | 2 | 20 | 1 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 11 | 1 | 11 | 3 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 8 | 0 | 8 | 0 |
-| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 34 | 0 | 8 | 0 |
+| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 48 | 0 | 20 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 42 | 1 | 24 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 20 | 1 | 19 | 3 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 6 | 0 | 6 | 0 |
@@ -513,7 +514,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 16 | 0 | 8 | 0 |
| | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | - | 42 | 0 | 41 | 0 |
| | [@elastic/security-generative-ai](https://github.com/orgs/elastic/teams/security-generative-ai) | - | 169 | 0 | 140 | 10 |
-| | [@elastic/security-generative-ai](https://github.com/orgs/elastic/teams/security-generative-ai) | - | 401 | 0 | 370 | 0 |
+| | [@elastic/security-generative-ai](https://github.com/orgs/elastic/teams/security-generative-ai) | - | 400 | 0 | 369 | 0 |
| | [@elastic/obs-entities](https://github.com/orgs/elastic/teams/obs-entities) | - | 45 | 0 | 45 | 0 |
| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 55 | 0 | 40 | 7 |
| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 32 | 0 | 19 | 1 |
@@ -550,7 +551,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 47 | 0 | 40 | 0 |
| | [@elastic/security-threat-hunting](https://github.com/orgs/elastic/teams/security-threat-hunting) | - | 85 | 0 | 80 | 2 |
| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 75 | 0 | 73 | 0 |
-| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 124 | 3 | 124 | 0 |
+| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 126 | 3 | 126 | 0 |
| | [@elastic/appex-ai-infra](https://github.com/orgs/elastic/teams/appex-ai-infra) | - | 124 | 0 | 41 | 1 |
| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 7 | 1 | 7 | 1 |
| | [@elastic/obs-ux-management-team](https://github.com/orgs/elastic/teams/obs-ux-management-team) | - | 9 | 0 | 9 | 0 |
@@ -578,7 +579,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 23 | 0 | 7 | 0 |
| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 8 | 0 | 2 | 3 |
| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 45 | 0 | 0 | 0 |
-| | [@elastic/appex-sharedux @elastic/kibana-management](https://github.com/orgs/elastic/teams/appex-sharedux ) | - | 139 | 0 | 138 | 0 |
+| | [@elastic/appex-sharedux @elastic/kibana-management](https://github.com/orgs/elastic/teams/appex-sharedux ) | - | 140 | 0 | 139 | 0 |
| | [@elastic/appex-sharedux @elastic/kibana-management](https://github.com/orgs/elastic/teams/appex-sharedux ) | - | 20 | 0 | 11 | 0 |
| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 88 | 0 | 10 | 0 |
| | [@elastic/kibana-management](https://github.com/orgs/elastic/teams/kibana-management) | - | 56 | 0 | 6 | 0 |
@@ -616,7 +617,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 42 | 1 | 35 | 1 |
| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 32 | 0 | 0 | 0 |
| | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 22 | 0 | 16 | 0 |
-| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 123 | 0 | 123 | 3 |
+| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 124 | 0 | 124 | 3 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 55 | 1 | 50 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 2 | 0 | 2 | 0 |
| | [@elastic/obs-ux-management-team](https://github.com/orgs/elastic/teams/obs-ux-management-team) | - | 10 | 0 | 10 | 2 |
@@ -627,7 +628,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/obs-ux-management-team](https://github.com/orgs/elastic/teams/obs-ux-management-team) | - | 4 | 0 | 4 | 1 |
| | [@elastic/security-detection-rule-management](https://github.com/orgs/elastic/teams/security-detection-rule-management) | - | 12 | 0 | 12 | 0 |
| | [@elastic/security-detection-rule-management](https://github.com/orgs/elastic/teams/security-detection-rule-management) | - | 15 | 0 | 15 | 0 |
-| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 45 | 0 | 45 | 10 |
+| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 45 | 0 | 45 | 9 |
| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 51 | 5 | 34 | 0 |
| | [@elastic/security-asset-management](https://github.com/orgs/elastic/teams/security-asset-management) | - | 66 | 0 | 66 | 0 |
| | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | - | 5 | 0 | 5 | 0 |
@@ -641,12 +642,12 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/obs-ux-infra_services-team](https://github.com/orgs/elastic/teams/obs-ux-infra_services-team) | - | 168 | 0 | 55 | 0 |
| | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 13 | 0 | 7 | 0 |
| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 22 | 0 | 9 | 0 |
-| | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | - | 8 | 0 | 7 | 0 |
-| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 8 | 0 | 2 | 0 |
+| | [@elastic/obs-ux-logs-team](https://github.com/orgs/elastic/teams/obs-ux-logs-team) | - | 9 | 0 | 8 | 0 |
+| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 15 | 0 | 8 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 3 | 0 | 1 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 10 | 0 | 4 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 18 | 0 | 3 | 0 |
-| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 11 | 0 | 2 | 0 |
+| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 13 | 0 | 3 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 11 | 0 | 8 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 14 | 0 | 7 | 0 |
| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 18 | 0 | 18 | 0 |
@@ -765,7 +766,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 2 | 0 | 0 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 15 | 0 | 4 | 0 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 8 | 0 | 8 | 4 |
-| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 3 | 0 | 2 | 2 |
+| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 17 | 0 | 16 | 2 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 16 | 0 | 6 | 0 |
| | [@elastic/obs-ux-management-team](https://github.com/orgs/elastic/teams/obs-ux-management-team) | - | 182 | 0 | 182 | 0 |
| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 20 | 0 | 12 | 0 |
@@ -791,7 +792,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana']
| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 39 | 0 | 25 | 1 |
| | [@elastic/obs-knowledge-team](https://github.com/orgs/elastic/teams/obs-knowledge-team) | - | 86 | 0 | 86 | 1 |
| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 42 | 0 | 28 | 0 |
-| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 59 | 0 | 50 | 0 |
+| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 60 | 0 | 51 | 0 |
| | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 9 | 0 | 8 | 0 |
| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the unified data table which can be integrated into apps | 184 | 0 | 108 | 1 |
| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 18 | 0 | 17 | 5 |
diff --git a/api_docs/presentation_panel.mdx b/api_docs/presentation_panel.mdx
index d51877a9b2bc3..fcdb0202034e3 100644
--- a/api_docs/presentation_panel.mdx
+++ b/api_docs/presentation_panel.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationPanel
title: "presentationPanel"
image: https://source.unsplash.com/400x175/?github
description: API docs for the presentationPanel plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationPanel']
---
import presentationPanelObj from './presentation_panel.devdocs.json';
diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx
index 5745d55a9b379..d31ed581bc93b 100644
--- a/api_docs/presentation_util.mdx
+++ b/api_docs/presentation_util.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationUtil
title: "presentationUtil"
image: https://source.unsplash.com/400x175/?github
description: API docs for the presentationUtil plugin
-date: 2024-11-18
+date: 2024-11-19
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 38efb7720b14c..182321a4503db 100644
--- a/api_docs/profiling.mdx
+++ b/api_docs/profiling.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profiling
title: "profiling"
image: https://source.unsplash.com/400x175/?github
description: API docs for the profiling plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling']
---
import profilingObj from './profiling.devdocs.json';
diff --git a/api_docs/profiling_data_access.mdx b/api_docs/profiling_data_access.mdx
index 7cc1f64e2755c..fe9da1d9da48c 100644
--- a/api_docs/profiling_data_access.mdx
+++ b/api_docs/profiling_data_access.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profilingDataAccess
title: "profilingDataAccess"
image: https://source.unsplash.com/400x175/?github
description: API docs for the profilingDataAccess plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profilingDataAccess']
---
import profilingDataAccessObj from './profiling_data_access.devdocs.json';
diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx
index ff432fcd2a533..85dd281521417 100644
--- a/api_docs/remote_clusters.mdx
+++ b/api_docs/remote_clusters.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/remoteClusters
title: "remoteClusters"
image: https://source.unsplash.com/400x175/?github
description: API docs for the remoteClusters plugin
-date: 2024-11-18
+date: 2024-11-19
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 908f8b9b0e194..db7489c8e0f67 100644
--- a/api_docs/reporting.mdx
+++ b/api_docs/reporting.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/reporting
title: "reporting"
image: https://source.unsplash.com/400x175/?github
description: API docs for the reporting plugin
-date: 2024-11-18
+date: 2024-11-19
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 acfe380ba50f3..82eb5d37a29f3 100644
--- a/api_docs/rollup.mdx
+++ b/api_docs/rollup.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/rollup
title: "rollup"
image: https://source.unsplash.com/400x175/?github
description: API docs for the rollup plugin
-date: 2024-11-18
+date: 2024-11-19
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 221610e06519f..7472749738569 100644
--- a/api_docs/rule_registry.mdx
+++ b/api_docs/rule_registry.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ruleRegistry
title: "ruleRegistry"
image: https://source.unsplash.com/400x175/?github
description: API docs for the ruleRegistry plugin
-date: 2024-11-18
+date: 2024-11-19
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 3f0297e5ab6a7..4122d9f383e73 100644
--- a/api_docs/runtime_fields.mdx
+++ b/api_docs/runtime_fields.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/runtimeFields
title: "runtimeFields"
image: https://source.unsplash.com/400x175/?github
description: API docs for the runtimeFields plugin
-date: 2024-11-18
+date: 2024-11-19
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 dcb2a4cc3702b..1de39b0c710aa 100644
--- a/api_docs/saved_objects.mdx
+++ b/api_docs/saved_objects.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjects
title: "savedObjects"
image: https://source.unsplash.com/400x175/?github
description: API docs for the savedObjects plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects']
---
import savedObjectsObj from './saved_objects.devdocs.json';
diff --git a/api_docs/saved_objects_finder.devdocs.json b/api_docs/saved_objects_finder.devdocs.json
index d33b50de2badc..866969ae30c37 100644
--- a/api_docs/saved_objects_finder.devdocs.json
+++ b/api_docs/saved_objects_finder.devdocs.json
@@ -36,6 +36,16 @@
"text": "SavedObjectsTaggingApi"
},
" | undefined) => (props: ",
+ {
+ "pluginId": "@kbn/shared-ux-table-persist",
+ "scope": "common",
+ "docId": "kibKbnSharedUxTablePersistPluginApi",
+ "section": "def-common.HOCProps",
+ "text": "HOCProps"
+ },
+ "<",
+ "SavedObjectFinderItem",
+ ", ",
{
"pluginId": "savedObjectsFinder",
"scope": "public",
@@ -43,7 +53,7 @@
"section": "def-public.SavedObjectFinderProps",
"text": "SavedObjectFinderProps"
},
- ") => React.JSX.Element"
+ ">) => React.JSX.Element"
],
"path": "src/plugins/saved_objects_finder/public/finder/index.tsx",
"deprecated": false,
@@ -126,6 +136,16 @@
"description": [],
"signature": [
"(props: ",
+ {
+ "pluginId": "@kbn/shared-ux-table-persist",
+ "scope": "common",
+ "docId": "kibKbnSharedUxTablePersistPluginApi",
+ "section": "def-common.HOCProps",
+ "text": "HOCProps"
+ },
+ "<",
+ "SavedObjectFinderItem",
+ ", ",
{
"pluginId": "savedObjectsFinder",
"scope": "public",
@@ -133,7 +153,7 @@
"section": "def-public.SavedObjectFinderProps",
"text": "SavedObjectFinderProps"
},
- ") => React.JSX.Element"
+ ">) => React.JSX.Element"
],
"path": "src/plugins/saved_objects_finder/public/finder/index.tsx",
"deprecated": false,
@@ -147,13 +167,24 @@
"label": "props",
"description": [],
"signature": [
+ {
+ "pluginId": "@kbn/shared-ux-table-persist",
+ "scope": "common",
+ "docId": "kibKbnSharedUxTablePersistPluginApi",
+ "section": "def-common.HOCProps",
+ "text": "HOCProps"
+ },
+ "<",
+ "SavedObjectFinderItem",
+ ", ",
{
"pluginId": "savedObjectsFinder",
"scope": "public",
"docId": "kibSavedObjectsFinderPluginApi",
"section": "def-public.SavedObjectFinderProps",
"text": "SavedObjectFinderProps"
- }
+ },
+ ">"
],
"path": "src/plugins/saved_objects_finder/public/finder/index.tsx",
"deprecated": false,
diff --git a/api_docs/saved_objects_finder.mdx b/api_docs/saved_objects_finder.mdx
index afa8c637e6c6f..8cf1ce9957e37 100644
--- a/api_docs/saved_objects_finder.mdx
+++ b/api_docs/saved_objects_finder.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsFinder
title: "savedObjectsFinder"
image: https://source.unsplash.com/400x175/?github
description: API docs for the savedObjectsFinder plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder']
---
import savedObjectsFinderObj from './saved_objects_finder.devdocs.json';
@@ -21,7 +21,7 @@ Contact [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/k
| Public API count | Any count | Items lacking comments | Missing exports |
|-------------------|-----------|------------------------|-----------------|
-| 25 | 0 | 25 | 0 |
+| 25 | 0 | 25 | 1 |
## Client
diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx
index c06046f2617c9..925106bd8d08c 100644
--- a/api_docs/saved_objects_management.mdx
+++ b/api_docs/saved_objects_management.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement
title: "savedObjectsManagement"
image: https://source.unsplash.com/400x175/?github
description: API docs for the savedObjectsManagement plugin
-date: 2024-11-18
+date: 2024-11-19
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 86960a5703c9e..ca588d2b735f8 100644
--- a/api_docs/saved_objects_tagging.mdx
+++ b/api_docs/saved_objects_tagging.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging
title: "savedObjectsTagging"
image: https://source.unsplash.com/400x175/?github
description: API docs for the savedObjectsTagging plugin
-date: 2024-11-18
+date: 2024-11-19
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 9c2c3e272efc7..74da07b00bf00 100644
--- a/api_docs/saved_objects_tagging_oss.mdx
+++ b/api_docs/saved_objects_tagging_oss.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss
title: "savedObjectsTaggingOss"
image: https://source.unsplash.com/400x175/?github
description: API docs for the savedObjectsTaggingOss plugin
-date: 2024-11-18
+date: 2024-11-19
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 84589f3c20cb2..8e33b2a2c1702 100644
--- a/api_docs/saved_search.mdx
+++ b/api_docs/saved_search.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedSearch
title: "savedSearch"
image: https://source.unsplash.com/400x175/?github
description: API docs for the savedSearch plugin
-date: 2024-11-18
+date: 2024-11-19
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 39fd37382dd99..461ebbc12aa3b 100644
--- a/api_docs/screenshot_mode.mdx
+++ b/api_docs/screenshot_mode.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotMode
title: "screenshotMode"
image: https://source.unsplash.com/400x175/?github
description: API docs for the screenshotMode plugin
-date: 2024-11-18
+date: 2024-11-19
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 0de6d6950cf11..e80ec2240e25d 100644
--- a/api_docs/screenshotting.mdx
+++ b/api_docs/screenshotting.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotting
title: "screenshotting"
image: https://source.unsplash.com/400x175/?github
description: API docs for the screenshotting plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting']
---
import screenshottingObj from './screenshotting.devdocs.json';
diff --git a/api_docs/search_assistant.mdx b/api_docs/search_assistant.mdx
index b009eba970833..8dd97cd61ed81 100644
--- a/api_docs/search_assistant.mdx
+++ b/api_docs/search_assistant.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchAssistant
title: "searchAssistant"
image: https://source.unsplash.com/400x175/?github
description: API docs for the searchAssistant plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchAssistant']
---
import searchAssistantObj from './search_assistant.devdocs.json';
diff --git a/api_docs/search_connectors.mdx b/api_docs/search_connectors.mdx
index 45419b1da492b..6cb52a719810d 100644
--- a/api_docs/search_connectors.mdx
+++ b/api_docs/search_connectors.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchConnectors
title: "searchConnectors"
image: https://source.unsplash.com/400x175/?github
description: API docs for the searchConnectors plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchConnectors']
---
import searchConnectorsObj from './search_connectors.devdocs.json';
diff --git a/api_docs/search_homepage.mdx b/api_docs/search_homepage.mdx
index 6df692b002699..399d01272324e 100644
--- a/api_docs/search_homepage.mdx
+++ b/api_docs/search_homepage.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchHomepage
title: "searchHomepage"
image: https://source.unsplash.com/400x175/?github
description: API docs for the searchHomepage plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchHomepage']
---
import searchHomepageObj from './search_homepage.devdocs.json';
diff --git a/api_docs/search_indices.devdocs.json b/api_docs/search_indices.devdocs.json
index 11be05829ab55..a50dd817dbf4d 100644
--- a/api_docs/search_indices.devdocs.json
+++ b/api_docs/search_indices.devdocs.json
@@ -194,7 +194,7 @@
"label": "privileges",
"description": [],
"signature": [
- "{ canCreateApiKeys: boolean; canCreateIndex: boolean; }"
+ "{ canCreateApiKeys: boolean; canManageIndex: boolean; canDeleteDocuments: boolean; }"
],
"path": "x-pack/plugins/search_indices/common/types.ts",
"deprecated": false,
diff --git a/api_docs/search_indices.mdx b/api_docs/search_indices.mdx
index 00126b6ee6544..df245f5f03571 100644
--- a/api_docs/search_indices.mdx
+++ b/api_docs/search_indices.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchIndices
title: "searchIndices"
image: https://source.unsplash.com/400x175/?github
description: API docs for the searchIndices plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchIndices']
---
import searchIndicesObj from './search_indices.devdocs.json';
diff --git a/api_docs/search_inference_endpoints.mdx b/api_docs/search_inference_endpoints.mdx
index b8c6196aeb200..157d2776a67e7 100644
--- a/api_docs/search_inference_endpoints.mdx
+++ b/api_docs/search_inference_endpoints.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchInferenceEndpoints
title: "searchInferenceEndpoints"
image: https://source.unsplash.com/400x175/?github
description: API docs for the searchInferenceEndpoints plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchInferenceEndpoints']
---
import searchInferenceEndpointsObj from './search_inference_endpoints.devdocs.json';
diff --git a/api_docs/search_notebooks.mdx b/api_docs/search_notebooks.mdx
index 0d1aac7785924..ddb5240a9c75c 100644
--- a/api_docs/search_notebooks.mdx
+++ b/api_docs/search_notebooks.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchNotebooks
title: "searchNotebooks"
image: https://source.unsplash.com/400x175/?github
description: API docs for the searchNotebooks plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchNotebooks']
---
import searchNotebooksObj from './search_notebooks.devdocs.json';
diff --git a/api_docs/search_playground.mdx b/api_docs/search_playground.mdx
index 61a6f99f5999d..6b8495406fab3 100644
--- a/api_docs/search_playground.mdx
+++ b/api_docs/search_playground.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/searchPlayground
title: "searchPlayground"
image: https://source.unsplash.com/400x175/?github
description: API docs for the searchPlayground plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'searchPlayground']
---
import searchPlaygroundObj from './search_playground.devdocs.json';
diff --git a/api_docs/security.mdx b/api_docs/security.mdx
index 2e9342a74b11c..f3ebd9cff5f9e 100644
--- a/api_docs/security.mdx
+++ b/api_docs/security.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/security
title: "security"
image: https://source.unsplash.com/400x175/?github
description: API docs for the security plugin
-date: 2024-11-18
+date: 2024-11-19
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 34c21c3ec482d..be3a215235f75 100644
--- a/api_docs/security_solution.devdocs.json
+++ b/api_docs/security_solution.devdocs.json
@@ -1622,17 +1622,6 @@
"deprecated": false,
"trackAdoption": false
},
- {
- "parentPluginId": "securitySolution",
- "id": "def-public.TimelineModel.isLoading",
- "type": "boolean",
- "tags": [],
- "label": "isLoading",
- "description": [],
- "path": "x-pack/plugins/security_solution/public/timelines/store/model.ts",
- "deprecated": false,
- "trackAdoption": false
- },
{
"parentPluginId": "securitySolution",
"id": "def-public.TimelineModel.selectAll",
diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx
index 373338937bc0e..eabc79c042d67 100644
--- a/api_docs/security_solution.mdx
+++ b/api_docs/security_solution.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolution
title: "securitySolution"
image: https://source.unsplash.com/400x175/?github
description: API docs for the securitySolution plugin
-date: 2024-11-18
+date: 2024-11-19
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 |
|-------------------|-----------|------------------------|-----------------|
-| 188 | 0 | 120 | 33 |
+| 187 | 0 | 119 | 33 |
## Client
diff --git a/api_docs/security_solution_ess.mdx b/api_docs/security_solution_ess.mdx
index 559c09dbf06e2..94fdf277f8368 100644
--- a/api_docs/security_solution_ess.mdx
+++ b/api_docs/security_solution_ess.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolutionEss
title: "securitySolutionEss"
image: https://source.unsplash.com/400x175/?github
description: API docs for the securitySolutionEss plugin
-date: 2024-11-18
+date: 2024-11-19
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 e0065df4ecacc..e70dcc4730fc3 100644
--- a/api_docs/security_solution_serverless.mdx
+++ b/api_docs/security_solution_serverless.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolutionServerless
title: "securitySolutionServerless"
image: https://source.unsplash.com/400x175/?github
description: API docs for the securitySolutionServerless plugin
-date: 2024-11-18
+date: 2024-11-19
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 8749fe4d92660..d2d0f195fa048 100644
--- a/api_docs/serverless.mdx
+++ b/api_docs/serverless.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverless
title: "serverless"
image: https://source.unsplash.com/400x175/?github
description: API docs for the serverless plugin
-date: 2024-11-18
+date: 2024-11-19
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 00ee21ea02b09..6210a8772fd0e 100644
--- a/api_docs/serverless_observability.mdx
+++ b/api_docs/serverless_observability.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverlessObservability
title: "serverlessObservability"
image: https://source.unsplash.com/400x175/?github
description: API docs for the serverlessObservability plugin
-date: 2024-11-18
+date: 2024-11-19
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 2db46e3c68490..2d7e7a0e7d7c4 100644
--- a/api_docs/serverless_search.mdx
+++ b/api_docs/serverless_search.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/serverlessSearch
title: "serverlessSearch"
image: https://source.unsplash.com/400x175/?github
description: API docs for the serverlessSearch plugin
-date: 2024-11-18
+date: 2024-11-19
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 7b62a224989d0..8b40bb2dd322e 100644
--- a/api_docs/session_view.mdx
+++ b/api_docs/session_view.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/sessionView
title: "sessionView"
image: https://source.unsplash.com/400x175/?github
description: API docs for the sessionView plugin
-date: 2024-11-18
+date: 2024-11-19
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 45774f7e2d842..2139cb04491bb 100644
--- a/api_docs/share.mdx
+++ b/api_docs/share.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/share
title: "share"
image: https://source.unsplash.com/400x175/?github
description: API docs for the share plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share']
---
import shareObj from './share.devdocs.json';
diff --git a/api_docs/slo.mdx b/api_docs/slo.mdx
index 7b3f527a49f9d..b22401ebe551a 100644
--- a/api_docs/slo.mdx
+++ b/api_docs/slo.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/slo
title: "slo"
image: https://source.unsplash.com/400x175/?github
description: API docs for the slo plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'slo']
---
import sloObj from './slo.devdocs.json';
diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx
index a9e05d6cdfd98..46db8d0a9994e 100644
--- a/api_docs/snapshot_restore.mdx
+++ b/api_docs/snapshot_restore.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/snapshotRestore
title: "snapshotRestore"
image: https://source.unsplash.com/400x175/?github
description: API docs for the snapshotRestore plugin
-date: 2024-11-18
+date: 2024-11-19
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 55be4bf34542e..1d7f1b0c7bfc6 100644
--- a/api_docs/spaces.mdx
+++ b/api_docs/spaces.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/spaces
title: "spaces"
image: https://source.unsplash.com/400x175/?github
description: API docs for the spaces plugin
-date: 2024-11-18
+date: 2024-11-19
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 9c71d4cda67db..15d17a8701e2a 100644
--- a/api_docs/stack_alerts.mdx
+++ b/api_docs/stack_alerts.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackAlerts
title: "stackAlerts"
image: https://source.unsplash.com/400x175/?github
description: API docs for the stackAlerts plugin
-date: 2024-11-18
+date: 2024-11-19
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 94587d12e043b..a77013bb1792d 100644
--- a/api_docs/stack_connectors.mdx
+++ b/api_docs/stack_connectors.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackConnectors
title: "stackConnectors"
image: https://source.unsplash.com/400x175/?github
description: API docs for the stackConnectors plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackConnectors']
---
import stackConnectorsObj from './stack_connectors.devdocs.json';
diff --git a/api_docs/streams.mdx b/api_docs/streams.mdx
index 7a5b3130362ce..51481a76fb2b0 100644
--- a/api_docs/streams.mdx
+++ b/api_docs/streams.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/streams
title: "streams"
image: https://source.unsplash.com/400x175/?github
description: API docs for the streams plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'streams']
---
import streamsObj from './streams.devdocs.json';
diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx
index a3357e3e444b1..7a13f204d27d5 100644
--- a/api_docs/task_manager.mdx
+++ b/api_docs/task_manager.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/taskManager
title: "taskManager"
image: https://source.unsplash.com/400x175/?github
description: API docs for the taskManager plugin
-date: 2024-11-18
+date: 2024-11-19
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 6ab5a230f505f..bfb78a2f949e4 100644
--- a/api_docs/telemetry.mdx
+++ b/api_docs/telemetry.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetry
title: "telemetry"
image: https://source.unsplash.com/400x175/?github
description: API docs for the telemetry plugin
-date: 2024-11-18
+date: 2024-11-19
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 76d235e1138da..5861a8251a767 100644
--- a/api_docs/telemetry_collection_manager.mdx
+++ b/api_docs/telemetry_collection_manager.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager
title: "telemetryCollectionManager"
image: https://source.unsplash.com/400x175/?github
description: API docs for the telemetryCollectionManager plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager']
---
import telemetryCollectionManagerObj from './telemetry_collection_manager.devdocs.json';
diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx
index 87cfd065698b6..b92670a8084b8 100644
--- a/api_docs/telemetry_management_section.mdx
+++ b/api_docs/telemetry_management_section.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection
title: "telemetryManagementSection"
image: https://source.unsplash.com/400x175/?github
description: API docs for the telemetryManagementSection plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection']
---
import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json';
diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx
index b10fbeff24ed9..ed0ce98e5da82 100644
--- a/api_docs/threat_intelligence.mdx
+++ b/api_docs/threat_intelligence.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/threatIntelligence
title: "threatIntelligence"
image: https://source.unsplash.com/400x175/?github
description: API docs for the threatIntelligence plugin
-date: 2024-11-18
+date: 2024-11-19
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 6502ee5852668..50635b3ce597b 100644
--- a/api_docs/timelines.mdx
+++ b/api_docs/timelines.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/timelines
title: "timelines"
image: https://source.unsplash.com/400x175/?github
description: API docs for the timelines plugin
-date: 2024-11-18
+date: 2024-11-19
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 3399b36b2eb68..d1d34de64765d 100644
--- a/api_docs/transform.mdx
+++ b/api_docs/transform.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/transform
title: "transform"
image: https://source.unsplash.com/400x175/?github
description: API docs for the transform plugin
-date: 2024-11-18
+date: 2024-11-19
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 61879a7949cb9..2ab50bd744c93 100644
--- a/api_docs/triggers_actions_ui.mdx
+++ b/api_docs/triggers_actions_ui.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi
title: "triggersActionsUi"
image: https://source.unsplash.com/400x175/?github
description: API docs for the triggersActionsUi plugin
-date: 2024-11-18
+date: 2024-11-19
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 dcc35afa83f5c..33176238a3d4c 100644
--- a/api_docs/ui_actions.mdx
+++ b/api_docs/ui_actions.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActions
title: "uiActions"
image: https://source.unsplash.com/400x175/?github
description: API docs for the uiActions plugin
-date: 2024-11-18
+date: 2024-11-19
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 ef8269a852d2d..2bd93d9aac558 100644
--- a/api_docs/ui_actions_enhanced.mdx
+++ b/api_docs/ui_actions_enhanced.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced
title: "uiActionsEnhanced"
image: https://source.unsplash.com/400x175/?github
description: API docs for the uiActionsEnhanced plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced']
---
import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json';
diff --git a/api_docs/unified_doc_viewer.mdx b/api_docs/unified_doc_viewer.mdx
index 9930ef3ff36ee..a44fb45c590f5 100644
--- a/api_docs/unified_doc_viewer.mdx
+++ b/api_docs/unified_doc_viewer.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedDocViewer
title: "unifiedDocViewer"
image: https://source.unsplash.com/400x175/?github
description: API docs for the unifiedDocViewer plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedDocViewer']
---
import unifiedDocViewerObj from './unified_doc_viewer.devdocs.json';
diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx
index 5b7119ff1e357..ca05464f6756d 100644
--- a/api_docs/unified_histogram.mdx
+++ b/api_docs/unified_histogram.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedHistogram
title: "unifiedHistogram"
image: https://source.unsplash.com/400x175/?github
description: API docs for the unifiedHistogram plugin
-date: 2024-11-18
+date: 2024-11-19
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 e72d80dde3808..18690587f94c9 100644
--- a/api_docs/unified_search.mdx
+++ b/api_docs/unified_search.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch
title: "unifiedSearch"
image: https://source.unsplash.com/400x175/?github
description: API docs for the unifiedSearch plugin
-date: 2024-11-18
+date: 2024-11-19
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 36376743a819c..a3918a98aeaf0 100644
--- a/api_docs/unified_search_autocomplete.mdx
+++ b/api_docs/unified_search_autocomplete.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete
title: "unifiedSearch.autocomplete"
image: https://source.unsplash.com/400x175/?github
description: API docs for the unifiedSearch.autocomplete plugin
-date: 2024-11-18
+date: 2024-11-19
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 1f29ed363a8ef..1f30689e4767a 100644
--- a/api_docs/uptime.mdx
+++ b/api_docs/uptime.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uptime
title: "uptime"
image: https://source.unsplash.com/400x175/?github
description: API docs for the uptime plugin
-date: 2024-11-18
+date: 2024-11-19
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 98bdd8f04d112..1da3998e7d737 100644
--- a/api_docs/url_forwarding.mdx
+++ b/api_docs/url_forwarding.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/urlForwarding
title: "urlForwarding"
image: https://source.unsplash.com/400x175/?github
description: API docs for the urlForwarding plugin
-date: 2024-11-18
+date: 2024-11-19
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 e1e458adc9a5d..8b5f81cd130c9 100644
--- a/api_docs/usage_collection.mdx
+++ b/api_docs/usage_collection.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/usageCollection
title: "usageCollection"
image: https://source.unsplash.com/400x175/?github
description: API docs for the usageCollection plugin
-date: 2024-11-18
+date: 2024-11-19
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 ec6e8a643f54a..d2c6eb2eac9fb 100644
--- a/api_docs/ux.mdx
+++ b/api_docs/ux.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ux
title: "ux"
image: https://source.unsplash.com/400x175/?github
description: API docs for the ux plugin
-date: 2024-11-18
+date: 2024-11-19
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 44ee4694e5f4b..28d6baa8e07b6 100644
--- a/api_docs/vis_default_editor.mdx
+++ b/api_docs/vis_default_editor.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor
title: "visDefaultEditor"
image: https://source.unsplash.com/400x175/?github
description: API docs for the visDefaultEditor plugin
-date: 2024-11-18
+date: 2024-11-19
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 0919f05d411c8..82630aad9287a 100644
--- a/api_docs/vis_type_gauge.mdx
+++ b/api_docs/vis_type_gauge.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeGauge
title: "visTypeGauge"
image: https://source.unsplash.com/400x175/?github
description: API docs for the visTypeGauge plugin
-date: 2024-11-18
+date: 2024-11-19
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 162be3f2d6e9f..8ecee7156cd84 100644
--- a/api_docs/vis_type_heatmap.mdx
+++ b/api_docs/vis_type_heatmap.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap
title: "visTypeHeatmap"
image: https://source.unsplash.com/400x175/?github
description: API docs for the visTypeHeatmap plugin
-date: 2024-11-18
+date: 2024-11-19
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 d6cafc8e57a8e..2fed52ab7e710 100644
--- a/api_docs/vis_type_pie.mdx
+++ b/api_docs/vis_type_pie.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypePie
title: "visTypePie"
image: https://source.unsplash.com/400x175/?github
description: API docs for the visTypePie plugin
-date: 2024-11-18
+date: 2024-11-19
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 b25c97eac561f..f9106d16c532f 100644
--- a/api_docs/vis_type_table.mdx
+++ b/api_docs/vis_type_table.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTable
title: "visTypeTable"
image: https://source.unsplash.com/400x175/?github
description: API docs for the visTypeTable plugin
-date: 2024-11-18
+date: 2024-11-19
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 4e5d25eff1a7b..599a402469c0f 100644
--- a/api_docs/vis_type_timelion.mdx
+++ b/api_docs/vis_type_timelion.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion
title: "visTypeTimelion"
image: https://source.unsplash.com/400x175/?github
description: API docs for the visTypeTimelion plugin
-date: 2024-11-18
+date: 2024-11-19
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 32d6bae3fb8ee..b87b3c9886a1f 100644
--- a/api_docs/vis_type_timeseries.mdx
+++ b/api_docs/vis_type_timeseries.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries
title: "visTypeTimeseries"
image: https://source.unsplash.com/400x175/?github
description: API docs for the visTypeTimeseries plugin
-date: 2024-11-18
+date: 2024-11-19
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 a57d4eda79831..8aaa38ef75b15 100644
--- a/api_docs/vis_type_vega.mdx
+++ b/api_docs/vis_type_vega.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVega
title: "visTypeVega"
image: https://source.unsplash.com/400x175/?github
description: API docs for the visTypeVega plugin
-date: 2024-11-18
+date: 2024-11-19
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 f8bebd50bbaae..f4cde283ff9d8 100644
--- a/api_docs/vis_type_vislib.mdx
+++ b/api_docs/vis_type_vislib.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVislib
title: "visTypeVislib"
image: https://source.unsplash.com/400x175/?github
description: API docs for the visTypeVislib plugin
-date: 2024-11-18
+date: 2024-11-19
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 e4ef617635c0e..55367e6537d98 100644
--- a/api_docs/vis_type_xy.mdx
+++ b/api_docs/vis_type_xy.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeXy
title: "visTypeXy"
image: https://source.unsplash.com/400x175/?github
description: API docs for the visTypeXy plugin
-date: 2024-11-18
+date: 2024-11-19
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 cd03c879191db..c11feff9f4f74 100644
--- a/api_docs/visualizations.mdx
+++ b/api_docs/visualizations.mdx
@@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visualizations
title: "visualizations"
image: https://source.unsplash.com/400x175/?github
description: API docs for the visualizations plugin
-date: 2024-11-18
+date: 2024-11-19
tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations']
---
import visualizationsObj from './visualizations.devdocs.json';
diff --git a/config/serverless.security.yml b/config/serverless.security.yml
index 5057fa193bef4..d7c1a13822ccf 100644
--- a/config/serverless.security.yml
+++ b/config/serverless.security.yml
@@ -92,6 +92,9 @@ xpack.fleet.internal.registry.excludePackages: [
# ML integrations
'dga',
+
+ # Unsupported in serverless
+ 'cloud-defend',
]
# fleet_server package installed to publish agent metrics
xpack.fleet.packages:
diff --git a/config/serverless.yml b/config/serverless.yml
index 75be6151e3bb2..0967df966f61a 100644
--- a/config/serverless.yml
+++ b/config/serverless.yml
@@ -7,6 +7,7 @@ xpack.fleet.internal.disableILMPolicies: true
xpack.fleet.internal.activeAgentsSoftLimit: 25000
xpack.fleet.internal.onlyAllowAgentUpgradeToKnownVersions: true
xpack.fleet.internal.retrySetupOnBoot: true
+xpack.fleet.internal.useMeteringApi: true
## Fine-tune the feature privileges.
xpack.features.overrides:
diff --git a/docs/upgrade-notes.asciidoc b/docs/upgrade-notes.asciidoc
index c2d866f90eed3..4d4208b2253f7 100644
--- a/docs/upgrade-notes.asciidoc
+++ b/docs/upgrade-notes.asciidoc
@@ -49,6 +49,32 @@ For Elastic Security release information, refer to {security-guide}/release-note
[float]
==== Kibana APIs
+[discrete]
+[[breaking-199656]]
+.Removed all security v1 endpoints (9.0.0)
+[%collapsible]
+====
+*Details* +
+All `v1` Kibana security HTTP endpoints have been removed.
+
+`GET /api/security/v1/logout` has been replaced by `GET /api/security/logout`
+`GET /api/security/v1/oidc/implicit` has been replaced by `GET /api/security/oidc/implicit`
+`GET /api/security/v1/oidc` has been replaced by GET `/api/security/oidc/callback`
+`POST /api/security/v1/oidc` has been replaced by POST `/api/security/oidc/initiate_login`
+`POST /api/security/v1/saml` has been replaced by POST `/api/security/saml/callback`
+`GET /api/security/v1/me` has been removed with no replacement.
+
+For more information, refer to {kibana-pull}199656[#199656].
+
+*Impact* +
+Any HTTP API calls to the `v1` Kibana security endpoints will fail with a 404 status code starting from version 9.0.0.
+Third party OIDC and SAML identity providers configured with `v1` endpoints will no longer work.
+
+*Action* +
+Update any OIDC and SAML identity providers to reference the corresponding replacement endpoint listed above.
+Remove references to the `/api/security/v1/me` endpoint from any automations, applications, tooling, and scripts.
+====
+
[discrete]
[[breaking-193792]]
.Access to all internal APIs is blocked (9.0.0)
@@ -814,18 +840,6 @@ The legacy audit logger has been removed. For more information, refer to {kibana
Audit logs will be written to the default location in the new ECS format. To change the output file, filter events, and more, use the <>.
====
-[discrete]
-[[breaking-47929]]
-.[Security] Removed `/api/security/v1/saml` route. (8.0)
-[%collapsible]
-====
-*Details* +
-The `/api/security/v1/saml` route has been removed and is reflected in the kibana.yml `server.xsrf.whitelist` setting, {es}, and the Identity Provider SAML settings. For more information, refer to {kibana-pull}47929[#47929]
-
-*Impact* +
-Use the `/api/security/saml/callback` route, or wait to upgrade to 8.0.0-alpha2 when the `/api/security/saml/callback` route breaking change is reverted.
-====
-
[discrete]
[[breaking-41700]]
.[Security] Legacy browsers rejected by default. (8.0)
diff --git a/package.json b/package.json
index 1114f3a94ca6e..a8c60004e6040 100644
--- a/package.json
+++ b/package.json
@@ -232,6 +232,7 @@
"@kbn/content-management-content-insights-public": "link:packages/content-management/content_insights/content_insights_public",
"@kbn/content-management-content-insights-server": "link:packages/content-management/content_insights/content_insights_server",
"@kbn/content-management-examples-plugin": "link:examples/content_management_examples",
+ "@kbn/content-management-favorites-common": "link:packages/content-management/favorites/favorites_common",
"@kbn/content-management-favorites-public": "link:packages/content-management/favorites/favorites_public",
"@kbn/content-management-favorites-server": "link:packages/content-management/favorites/favorites_server",
"@kbn/content-management-plugin": "link:src/plugins/content_management",
@@ -360,6 +361,7 @@
"@kbn/core-preboot-server": "link:packages/core/preboot/core-preboot-server",
"@kbn/core-preboot-server-internal": "link:packages/core/preboot/core-preboot-server-internal",
"@kbn/core-provider-plugin": "link:test/plugin_functional/plugins/core_provider_plugin",
+ "@kbn/core-rendering-browser": "link:packages/core/rendering/core-rendering-browser",
"@kbn/core-rendering-browser-internal": "link:packages/core/rendering/core-rendering-browser-internal",
"@kbn/core-rendering-server-internal": "link:packages/core/rendering/core-rendering-server-internal",
"@kbn/core-root-browser-internal": "link:packages/core/root/core-root-browser-internal",
diff --git a/packages/content-management/favorites/favorites_common/README.md b/packages/content-management/favorites/favorites_common/README.md
new file mode 100644
index 0000000000000..61608fa380e20
--- /dev/null
+++ b/packages/content-management/favorites/favorites_common/README.md
@@ -0,0 +1,3 @@
+# @kbn/content-management-favorites-common
+
+Shared client & server code for the favorites packages.
diff --git a/packages/content-management/favorites/favorites_common/index.ts b/packages/content-management/favorites/favorites_common/index.ts
new file mode 100644
index 0000000000000..05ad1fa0b9cef
--- /dev/null
+++ b/packages/content-management/favorites/favorites_common/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+// Limit the number of favorites to prevent too large objects due to metadata
+export const FAVORITES_LIMIT = 100;
diff --git a/packages/content-management/favorites/favorites_common/jest.config.js b/packages/content-management/favorites/favorites_common/jest.config.js
new file mode 100644
index 0000000000000..c8b618b4f4ac6
--- /dev/null
+++ b/packages/content-management/favorites/favorites_common/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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+module.exports = {
+ preset: '@kbn/test/jest_node',
+ rootDir: '../../../..',
+ roots: ['/packages/content-management/favorites/favorites_common'],
+};
diff --git a/packages/content-management/favorites/favorites_common/kibana.jsonc b/packages/content-management/favorites/favorites_common/kibana.jsonc
new file mode 100644
index 0000000000000..69e13e656639b
--- /dev/null
+++ b/packages/content-management/favorites/favorites_common/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-common",
+ "id": "@kbn/content-management-favorites-common",
+ "owner": "@elastic/appex-sharedux"
+}
diff --git a/packages/content-management/favorites/favorites_common/package.json b/packages/content-management/favorites/favorites_common/package.json
new file mode 100644
index 0000000000000..cb3a685ebc064
--- /dev/null
+++ b/packages/content-management/favorites/favorites_common/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/content-management-favorites-common",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
+}
\ No newline at end of file
diff --git a/packages/content-management/favorites/favorites_common/tsconfig.json b/packages/content-management/favorites/favorites_common/tsconfig.json
new file mode 100644
index 0000000000000..0d78dace105e1
--- /dev/null
+++ b/packages/content-management/favorites/favorites_common/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/content-management/favorites/favorites_public/src/favorites_client.ts b/packages/content-management/favorites/favorites_public/src/favorites_client.ts
index 3b3d439caecda..84c44db5fd64c 100644
--- a/packages/content-management/favorites/favorites_public/src/favorites_client.ts
+++ b/packages/content-management/favorites/favorites_public/src/favorites_client.ts
@@ -9,36 +9,52 @@
import type { HttpStart } from '@kbn/core-http-browser';
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
-import type { GetFavoritesResponse } from '@kbn/content-management-favorites-server';
+import type {
+ GetFavoritesResponse as GetFavoritesResponseServer,
+ AddFavoriteResponse,
+ RemoveFavoriteResponse,
+} from '@kbn/content-management-favorites-server';
-export interface FavoritesClientPublic {
- getFavorites(): Promise;
- addFavorite({ id }: { id: string }): Promise;
- removeFavorite({ id }: { id: string }): Promise;
+export interface GetFavoritesResponse
+ extends GetFavoritesResponseServer {
+ favoriteMetadata: Metadata extends object ? Record : never;
+}
+
+type AddFavoriteRequest = Metadata extends object
+ ? { id: string; metadata: Metadata }
+ : { id: string };
+
+export interface FavoritesClientPublic {
+ getFavorites(): Promise>;
+ addFavorite(params: AddFavoriteRequest): Promise;
+ removeFavorite(params: { id: string }): Promise;
getFavoriteType(): string;
reportAddFavoriteClick(): void;
reportRemoveFavoriteClick(): void;
}
-export class FavoritesClient implements FavoritesClientPublic {
+export class FavoritesClient
+ implements FavoritesClientPublic
+{
constructor(
private readonly appName: string,
private readonly favoriteObjectType: string,
private readonly deps: { http: HttpStart; usageCollection?: UsageCollectionStart }
) {}
- public async getFavorites(): Promise {
+ public async getFavorites(): Promise> {
return this.deps.http.get(`/internal/content_management/favorites/${this.favoriteObjectType}`);
}
- public async addFavorite({ id }: { id: string }): Promise {
+ public async addFavorite(params: AddFavoriteRequest): Promise {
return this.deps.http.post(
- `/internal/content_management/favorites/${this.favoriteObjectType}/${id}/favorite`
+ `/internal/content_management/favorites/${this.favoriteObjectType}/${params.id}/favorite`,
+ { body: 'metadata' in params ? JSON.stringify({ metadata: params.metadata }) : undefined }
);
}
- public async removeFavorite({ id }: { id: string }): Promise {
+ public async removeFavorite({ id }: { id: string }): Promise {
return this.deps.http.post(
`/internal/content_management/favorites/${this.favoriteObjectType}/${id}/unfavorite`
);
diff --git a/packages/content-management/favorites/favorites_public/src/favorites_query.tsx b/packages/content-management/favorites/favorites_public/src/favorites_query.tsx
index e3ca1e4ed202d..63e8ad3a7ef75 100644
--- a/packages/content-management/favorites/favorites_public/src/favorites_query.tsx
+++ b/packages/content-management/favorites/favorites_public/src/favorites_query.tsx
@@ -11,6 +11,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import React from 'react';
+import type { IHttpFetchError } from '@kbn/core-http-browser';
import { useFavoritesClient, useFavoritesContext } from './favorites_context';
const favoritesKeys = {
@@ -54,14 +55,14 @@ export const useAddFavorite = () => {
onSuccess: (data) => {
queryClient.setQueryData(favoritesKeys.byType(favoritesClient!.getFavoriteType()), data);
},
- onError: (error: Error) => {
+ onError: (error: IHttpFetchError<{ message?: string }>) => {
notifyError?.(
<>
{i18n.translate('contentManagement.favorites.addFavoriteError', {
defaultMessage: 'Error adding to Starred',
})}
>,
- error?.message
+ error?.body?.message ?? error.message
);
},
}
diff --git a/packages/content-management/favorites/favorites_server/index.ts b/packages/content-management/favorites/favorites_server/index.ts
index bcb8d0bffba8c..2810102d9165c 100644
--- a/packages/content-management/favorites/favorites_server/index.ts
+++ b/packages/content-management/favorites/favorites_server/index.ts
@@ -7,4 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-export { registerFavorites, type GetFavoritesResponse } from './src';
+export {
+ registerFavorites,
+ type GetFavoritesResponse,
+ type FavoritesSetup,
+ type AddFavoriteResponse,
+ type RemoveFavoriteResponse,
+} from './src';
diff --git a/packages/content-management/favorites/favorites_server/src/favorites_registry.ts b/packages/content-management/favorites/favorites_server/src/favorites_registry.ts
new file mode 100644
index 0000000000000..53fc6dc4b5260
--- /dev/null
+++ b/packages/content-management/favorites/favorites_server/src/favorites_registry.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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+import { ObjectType } from '@kbn/config-schema';
+
+interface FavoriteTypeConfig {
+ typeMetadataSchema?: ObjectType;
+}
+
+export type FavoritesRegistrySetup = Pick;
+
+export class FavoritesRegistry {
+ private favoriteTypes = new Map();
+
+ registerFavoriteType(type: string, config: FavoriteTypeConfig = {}) {
+ if (this.favoriteTypes.has(type)) {
+ throw new Error(`Favorite type ${type} is already registered`);
+ }
+
+ this.favoriteTypes.set(type, config);
+ }
+
+ hasType(type: string) {
+ return this.favoriteTypes.has(type);
+ }
+
+ validateMetadata(type: string, metadata?: object) {
+ if (!this.hasType(type)) {
+ throw new Error(`Favorite type ${type} is not registered`);
+ }
+
+ const typeConfig = this.favoriteTypes.get(type)!;
+ const typeMetadataSchema = typeConfig.typeMetadataSchema;
+
+ if (typeMetadataSchema) {
+ typeMetadataSchema.validate(metadata);
+ } else {
+ if (metadata === undefined) {
+ return; /* ok */
+ } else {
+ throw new Error(`Favorite type ${type} does not support metadata`);
+ }
+ }
+ }
+}
diff --git a/packages/content-management/favorites/favorites_server/src/favorites_routes.ts b/packages/content-management/favorites/favorites_server/src/favorites_routes.ts
index 663d0181f3806..512b2cbe1260e 100644
--- a/packages/content-management/favorites/favorites_server/src/favorites_routes.ts
+++ b/packages/content-management/favorites/favorites_server/src/favorites_routes.ts
@@ -14,12 +14,9 @@ import {
SECURITY_EXTENSION_ID,
} from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
-import { FavoritesService } from './favorites_service';
+import { FavoritesService, FavoritesLimitExceededError } from './favorites_service';
import { favoritesSavedObjectType } from './favorites_saved_object';
-
-// only dashboard is supported for now
-// TODO: make configurable or allow any string
-const typeSchema = schema.oneOf([schema.literal('dashboard')]);
+import { FavoritesRegistry } from './favorites_registry';
/**
* @public
@@ -27,9 +24,45 @@ const typeSchema = schema.oneOf([schema.literal('dashboard')]);
*/
export interface GetFavoritesResponse {
favoriteIds: string[];
+ favoriteMetadata?: Record;
+}
+
+export interface AddFavoriteResponse {
+ favoriteIds: string[];
}
-export function registerFavoritesRoutes({ core, logger }: { core: CoreSetup; logger: Logger }) {
+export interface RemoveFavoriteResponse {
+ favoriteIds: string[];
+}
+
+export function registerFavoritesRoutes({
+ core,
+ logger,
+ favoritesRegistry,
+}: {
+ core: CoreSetup;
+ logger: Logger;
+ favoritesRegistry: FavoritesRegistry;
+}) {
+ const typeSchema = schema.string({
+ validate: (type) => {
+ if (!favoritesRegistry.hasType(type)) {
+ return `Unknown favorite type: ${type}`;
+ }
+ },
+ });
+
+ const metadataSchema = schema.maybe(
+ schema.object(
+ {
+ // validated later by the registry depending on the type
+ },
+ {
+ unknowns: 'allow',
+ }
+ )
+ );
+
const router = core.http.createRouter();
const getSavedObjectClient = (coreRequestHandlerContext: CoreRequestHandlerContext) => {
@@ -49,6 +82,13 @@ export function registerFavoritesRoutes({ core, logger }: { core: CoreSetup; log
id: schema.string(),
type: typeSchema,
}),
+ body: schema.maybe(
+ schema.nullable(
+ schema.object({
+ metadata: metadataSchema,
+ })
+ )
+ ),
},
// we don't protect the route with any access tags as
// we only give access to the current user's favorites ids
@@ -67,13 +107,35 @@ export function registerFavoritesRoutes({ core, logger }: { core: CoreSetup; log
const favorites = new FavoritesService(type, userId, {
savedObjectClient: getSavedObjectClient(coreRequestHandlerContext),
logger,
+ favoritesRegistry,
});
- const favoriteIds: GetFavoritesResponse = await favorites.addFavorite({
- id: request.params.id,
- });
+ const id = request.params.id;
+ const metadata = request.body?.metadata;
- return response.ok({ body: favoriteIds });
+ try {
+ favoritesRegistry.validateMetadata(type, metadata);
+ } catch (e) {
+ return response.badRequest({ body: { message: e.message } });
+ }
+
+ try {
+ const favoritesResult = await favorites.addFavorite({
+ id,
+ metadata,
+ });
+ const addFavoritesResponse: AddFavoriteResponse = {
+ favoriteIds: favoritesResult.favoriteIds,
+ };
+
+ return response.ok({ body: addFavoritesResponse });
+ } catch (e) {
+ if (e instanceof FavoritesLimitExceededError) {
+ return response.forbidden({ body: { message: e.message } });
+ }
+
+ throw e; // unexpected error, let the global error handler deal with it
+ }
}
);
@@ -102,12 +164,18 @@ export function registerFavoritesRoutes({ core, logger }: { core: CoreSetup; log
const favorites = new FavoritesService(type, userId, {
savedObjectClient: getSavedObjectClient(coreRequestHandlerContext),
logger,
+ favoritesRegistry,
});
- const favoriteIds: GetFavoritesResponse = await favorites.removeFavorite({
+ const favoritesResult: GetFavoritesResponse = await favorites.removeFavorite({
id: request.params.id,
});
- return response.ok({ body: favoriteIds });
+
+ const removeFavoriteResponse: RemoveFavoriteResponse = {
+ favoriteIds: favoritesResult.favoriteIds,
+ };
+
+ return response.ok({ body: removeFavoriteResponse });
}
);
@@ -135,12 +203,18 @@ export function registerFavoritesRoutes({ core, logger }: { core: CoreSetup; log
const favorites = new FavoritesService(type, userId, {
savedObjectClient: getSavedObjectClient(coreRequestHandlerContext),
logger,
+ favoritesRegistry,
});
- const getFavoritesResponse: GetFavoritesResponse = await favorites.getFavorites();
+ const favoritesResult = await favorites.getFavorites();
+
+ const favoritesResponse: GetFavoritesResponse = {
+ favoriteIds: favoritesResult.favoriteIds,
+ favoriteMetadata: favoritesResult.favoriteMetadata,
+ };
return response.ok({
- body: getFavoritesResponse,
+ body: favoritesResponse,
});
}
);
diff --git a/packages/content-management/favorites/favorites_server/src/favorites_saved_object.ts b/packages/content-management/favorites/favorites_server/src/favorites_saved_object.ts
index 73cd3b3ca185f..776133f408975 100644
--- a/packages/content-management/favorites/favorites_server/src/favorites_saved_object.ts
+++ b/packages/content-management/favorites/favorites_server/src/favorites_saved_object.ts
@@ -14,6 +14,7 @@ export interface FavoritesSavedObjectAttributes {
userId: string;
type: string;
favoriteIds: string[];
+ favoriteMetadata?: Record;
}
const schemaV1 = schema.object({
@@ -22,6 +23,10 @@ const schemaV1 = schema.object({
favoriteIds: schema.arrayOf(schema.string()),
});
+const schemaV3 = schemaV1.extends({
+ favoriteMetadata: schema.maybe(schema.object({}, { unknowns: 'allow' })),
+});
+
export const favoritesSavedObjectName = 'favorites';
export const favoritesSavedObjectType: SavedObjectsType = {
@@ -34,6 +39,7 @@ export const favoritesSavedObjectType: SavedObjectsType = {
userId: { type: 'keyword' },
type: { type: 'keyword' },
favoriteIds: { type: 'keyword' },
+ favoriteMetadata: { type: 'object', dynamic: false },
},
},
modelVersions: {
@@ -65,5 +71,19 @@ export const favoritesSavedObjectType: SavedObjectsType = {
create: schemaV1,
},
},
+ 3: {
+ changes: [
+ {
+ type: 'mappings_addition',
+ addedMappings: {
+ favoriteMetadata: { type: 'object', dynamic: false },
+ },
+ },
+ ],
+ schemas: {
+ forwardCompatibility: schemaV3.extends({}, { unknowns: 'ignore' }),
+ create: schemaV3,
+ },
+ },
},
};
diff --git a/packages/content-management/favorites/favorites_server/src/favorites_service.ts b/packages/content-management/favorites/favorites_server/src/favorites_service.ts
index 41c9b10f05507..6258e66897fa3 100644
--- a/packages/content-management/favorites/favorites_server/src/favorites_service.ts
+++ b/packages/content-management/favorites/favorites_server/src/favorites_service.ts
@@ -7,9 +7,17 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+// eslint-disable-next-line max-classes-per-file
import type { SavedObject, SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
+import { FAVORITES_LIMIT } from '@kbn/content-management-favorites-common';
import { Logger, SavedObjectsErrorHelpers } from '@kbn/core/server';
import { favoritesSavedObjectType, FavoritesSavedObjectAttributes } from './favorites_saved_object';
+import { FavoritesRegistry } from './favorites_registry';
+
+export interface FavoritesState {
+ favoriteIds: string[];
+ favoriteMetadata?: Record;
+}
export class FavoritesService {
constructor(
@@ -18,23 +26,38 @@ export class FavoritesService {
private readonly deps: {
savedObjectClient: SavedObjectsClientContract;
logger: Logger;
+ favoritesRegistry: FavoritesRegistry;
}
) {
if (!this.userId || !this.type) {
// This should never happen, but just in case let's do a runtime check
throw new Error('userId and object type are required to use a favorite service');
}
+
+ if (!this.deps.favoritesRegistry.hasType(this.type)) {
+ throw new Error(`Favorite type ${this.type} is not registered`);
+ }
}
- public async getFavorites(): Promise<{ favoriteIds: string[] }> {
+ public async getFavorites(): Promise {
const favoritesSavedObject = await this.getFavoritesSavedObject();
const favoriteIds = favoritesSavedObject?.attributes?.favoriteIds ?? [];
+ const favoriteMetadata = favoritesSavedObject?.attributes?.favoriteMetadata;
- return { favoriteIds };
+ return { favoriteIds, favoriteMetadata };
}
- public async addFavorite({ id }: { id: string }): Promise<{ favoriteIds: string[] }> {
+ /**
+ * @throws {FavoritesLimitExceededError}
+ */
+ public async addFavorite({
+ id,
+ metadata,
+ }: {
+ id: string;
+ metadata?: object;
+ }): Promise {
let favoritesSavedObject = await this.getFavoritesSavedObject();
if (!favoritesSavedObject) {
@@ -44,14 +67,28 @@ export class FavoritesService {
userId: this.userId,
type: this.type,
favoriteIds: [id],
+ ...(metadata
+ ? {
+ favoriteMetadata: {
+ [id]: metadata,
+ },
+ }
+ : {}),
},
{
id: this.getFavoriteSavedObjectId(),
}
);
- return { favoriteIds: favoritesSavedObject.attributes.favoriteIds };
+ return {
+ favoriteIds: favoritesSavedObject.attributes.favoriteIds,
+ favoriteMetadata: favoritesSavedObject.attributes.favoriteMetadata,
+ };
} else {
+ if ((favoritesSavedObject.attributes.favoriteIds ?? []).length >= FAVORITES_LIMIT) {
+ throw new FavoritesLimitExceededError();
+ }
+
const newFavoriteIds = [
...(favoritesSavedObject.attributes.favoriteIds ?? []).filter(
(favoriteId) => favoriteId !== id
@@ -59,22 +96,34 @@ export class FavoritesService {
id,
];
+ const newFavoriteMetadata = metadata
+ ? {
+ ...favoritesSavedObject.attributes.favoriteMetadata,
+ [id]: metadata,
+ }
+ : undefined;
+
await this.deps.savedObjectClient.update(
favoritesSavedObjectType.name,
favoritesSavedObject.id,
{
favoriteIds: newFavoriteIds,
+ ...(newFavoriteMetadata
+ ? {
+ favoriteMetadata: newFavoriteMetadata,
+ }
+ : {}),
},
{
version: favoritesSavedObject.version,
}
);
- return { favoriteIds: newFavoriteIds };
+ return { favoriteIds: newFavoriteIds, favoriteMetadata: newFavoriteMetadata };
}
}
- public async removeFavorite({ id }: { id: string }): Promise<{ favoriteIds: string[] }> {
+ public async removeFavorite({ id }: { id: string }): Promise {
const favoritesSavedObject = await this.getFavoritesSavedObject();
if (!favoritesSavedObject) {
@@ -85,19 +134,36 @@ export class FavoritesService {
(favoriteId) => favoriteId !== id
);
+ const newFavoriteMetadata = favoritesSavedObject.attributes.favoriteMetadata
+ ? { ...favoritesSavedObject.attributes.favoriteMetadata }
+ : undefined;
+
+ if (newFavoriteMetadata) {
+ delete newFavoriteMetadata[id];
+ }
+
await this.deps.savedObjectClient.update(
favoritesSavedObjectType.name,
favoritesSavedObject.id,
{
+ ...favoritesSavedObject.attributes,
favoriteIds: newFavoriteIds,
+ ...(newFavoriteMetadata
+ ? {
+ favoriteMetadata: newFavoriteMetadata,
+ }
+ : {}),
},
{
version: favoritesSavedObject.version,
+ // We don't want to merge the attributes here because we want to remove the keys from the metadata
+ mergeAttributes: false,
}
);
return {
favoriteIds: newFavoriteIds,
+ favoriteMetadata: newFavoriteMetadata,
};
}
@@ -123,3 +189,14 @@ export class FavoritesService {
return `${this.type}:${this.userId}`;
}
}
+
+export class FavoritesLimitExceededError extends Error {
+ constructor() {
+ super(
+ `Limit reached: This list can contain a maximum of ${FAVORITES_LIMIT} items. Please remove an item before adding a new one.`
+ );
+
+ this.name = 'FavoritesLimitExceededError';
+ Object.setPrototypeOf(this, FavoritesLimitExceededError.prototype); // For TypeScript compatibility
+ }
+}
diff --git a/packages/content-management/favorites/favorites_server/src/index.ts b/packages/content-management/favorites/favorites_server/src/index.ts
index d6cdd51285b38..44e3b9f259a33 100644
--- a/packages/content-management/favorites/favorites_server/src/index.ts
+++ b/packages/content-management/favorites/favorites_server/src/index.ts
@@ -12,8 +12,19 @@ import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { registerFavoritesRoutes } from './favorites_routes';
import { favoritesSavedObjectType } from './favorites_saved_object';
import { registerFavoritesUsageCollection } from './favorites_usage_collection';
+import { FavoritesRegistry, FavoritesRegistrySetup } from './favorites_registry';
-export type { GetFavoritesResponse } from './favorites_routes';
+export type {
+ GetFavoritesResponse,
+ AddFavoriteResponse,
+ RemoveFavoriteResponse,
+} from './favorites_routes';
+
+/**
+ * @public
+ * Setup contract for the favorites feature.
+ */
+export type FavoritesSetup = FavoritesRegistrySetup;
/**
* @public
@@ -31,11 +42,14 @@ export function registerFavorites({
core: CoreSetup;
logger: Logger;
usageCollection?: UsageCollectionSetup;
-}) {
+}): FavoritesSetup {
+ const favoritesRegistry = new FavoritesRegistry();
core.savedObjects.registerType(favoritesSavedObjectType);
- registerFavoritesRoutes({ core, logger });
+ registerFavoritesRoutes({ core, logger, favoritesRegistry });
if (usageCollection) {
registerFavoritesUsageCollection({ core, usageCollection });
}
+
+ return favoritesRegistry;
}
diff --git a/packages/content-management/favorites/favorites_server/tsconfig.json b/packages/content-management/favorites/favorites_server/tsconfig.json
index 5a9ae392c875b..bbab19ade978b 100644
--- a/packages/content-management/favorites/favorites_server/tsconfig.json
+++ b/packages/content-management/favorites/favorites_server/tsconfig.json
@@ -19,5 +19,6 @@
"@kbn/core-saved-objects-api-server",
"@kbn/core-lifecycle-server",
"@kbn/usage-collection-plugin",
+ "@kbn/content-management-favorites-common",
]
}
diff --git a/packages/core/rendering/core-rendering-browser-internal/src/rendering_service.tsx b/packages/core/rendering/core-rendering-browser-internal/src/rendering_service.tsx
index 700dad544cd2b..12a597ba9318f 100644
--- a/packages/core/rendering/core-rendering-browser-internal/src/rendering_service.tsx
+++ b/packages/core/rendering/core-rendering-browser-internal/src/rendering_service.tsx
@@ -18,6 +18,7 @@ import type { I18nStart } from '@kbn/core-i18n-browser';
import type { OverlayStart } from '@kbn/core-overlays-browser';
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
+import { APP_FIXED_VIEWPORT_ID } from '@kbn/core-rendering-browser';
import { AppWrapper } from './app_containers';
interface StartServices {
@@ -68,7 +69,7 @@ export class RenderingService {
{/* The App Wrapper outside of the fixed headers that accepts custom class names from apps */}
{/* Affixes a div to restrict the position of charts tooltip to the visible viewport minus the header */}
-
+
{/* The actual plugin/app */}
{appComponent}
diff --git a/packages/core/rendering/core-rendering-browser-internal/tsconfig.json b/packages/core/rendering/core-rendering-browser-internal/tsconfig.json
index 42c59f96b2471..4b0c009a0a033 100644
--- a/packages/core/rendering/core-rendering-browser-internal/tsconfig.json
+++ b/packages/core/rendering/core-rendering-browser-internal/tsconfig.json
@@ -26,7 +26,8 @@
"@kbn/core-analytics-browser-mocks",
"@kbn/core-analytics-browser",
"@kbn/core-i18n-browser",
- "@kbn/core-theme-browser"
+ "@kbn/core-theme-browser",
+ "@kbn/core-rendering-browser"
],
"exclude": [
"target/**/*",
diff --git a/packages/core/rendering/core-rendering-browser/README.md b/packages/core/rendering/core-rendering-browser/README.md
new file mode 100644
index 0000000000000..40141d7611e72
--- /dev/null
+++ b/packages/core/rendering/core-rendering-browser/README.md
@@ -0,0 +1,4 @@
+# @kbn/core-rendering-browser
+
+This package contains the types and implementation for Core's browser-side rendering service.
+
diff --git a/packages/core/rendering/core-rendering-browser/index.ts b/packages/core/rendering/core-rendering-browser/index.ts
new file mode 100644
index 0000000000000..d8ccea264df05
--- /dev/null
+++ b/packages/core/rendering/core-rendering-browser/index.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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+export { APP_FIXED_VIEWPORT_ID, useAppFixedViewport } from './src';
diff --git a/packages/core/rendering/core-rendering-browser/jest.config.js b/packages/core/rendering/core-rendering-browser/jest.config.js
new file mode 100644
index 0000000000000..13f1819553812
--- /dev/null
+++ b/packages/core/rendering/core-rendering-browser/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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../..',
+ roots: ['/packages/core/rendering/core-rendering-browser'],
+};
diff --git a/packages/core/rendering/core-rendering-browser/kibana.jsonc b/packages/core/rendering/core-rendering-browser/kibana.jsonc
new file mode 100644
index 0000000000000..4b43c11865134
--- /dev/null
+++ b/packages/core/rendering/core-rendering-browser/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-browser",
+ "id": "@kbn/core-rendering-browser",
+ "owner": "@elastic/kibana-core"
+}
diff --git a/packages/core/rendering/core-rendering-browser/package.json b/packages/core/rendering/core-rendering-browser/package.json
new file mode 100644
index 0000000000000..4f1fa6f68ef01
--- /dev/null
+++ b/packages/core/rendering/core-rendering-browser/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@kbn/core-rendering-browser",
+ "private": true,
+ "version": "1.0.0",
+ "author": "Kibana Core",
+ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
+}
\ No newline at end of file
diff --git a/packages/core/rendering/core-rendering-browser/src/index.ts b/packages/core/rendering/core-rendering-browser/src/index.ts
new file mode 100644
index 0000000000000..aad756d296561
--- /dev/null
+++ b/packages/core/rendering/core-rendering-browser/src/index.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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+export { APP_FIXED_VIEWPORT_ID, useAppFixedViewport } from './use_app_fixed_viewport';
diff --git a/packages/core/rendering/core-rendering-browser/src/use_app_fixed_viewport.ts b/packages/core/rendering/core-rendering-browser/src/use_app_fixed_viewport.ts
new file mode 100644
index 0000000000000..ecf44a0018b49
--- /dev/null
+++ b/packages/core/rendering/core-rendering-browser/src/use_app_fixed_viewport.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { useRef } from 'react';
+
+export const APP_FIXED_VIEWPORT_ID = 'app-fixed-viewport';
+
+export function useAppFixedViewport() {
+ const ref = useRef(document.getElementById(APP_FIXED_VIEWPORT_ID) ?? undefined);
+ return ref.current;
+}
diff --git a/packages/core/rendering/core-rendering-browser/tsconfig.json b/packages/core/rendering/core-rendering-browser/tsconfig.json
new file mode 100644
index 0000000000000..3a932605dfa75
--- /dev/null
+++ b/packages/core/rendering/core-rendering-browser/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node",
+ "react"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ],
+ "kbn_references": [],
+ "exclude": [
+ "target/**/*",
+ ]
+}
diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json
index 5493b8dc3bbdb..a5642cee10958 100644
--- a/packages/kbn-check-mappings-update-cli/current_fields.json
+++ b/packages/kbn-check-mappings-update-cli/current_fields.json
@@ -443,6 +443,7 @@
],
"favorites": [
"favoriteIds",
+ "favoriteMetadata",
"type",
"userId"
],
diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json
index 726b6e9e1d4c5..61f680509c133 100644
--- a/packages/kbn-check-mappings-update-cli/current_mappings.json
+++ b/packages/kbn-check-mappings-update-cli/current_mappings.json
@@ -1509,6 +1509,10 @@
"favoriteIds": {
"type": "keyword"
},
+ "favoriteMetadata": {
+ "dynamic": false,
+ "type": "object"
+ },
"type": {
"type": "keyword"
},
diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts
index 251d08dde715a..c7d714ebfbbb7 100644
--- a/packages/kbn-doc-links/src/get_doc_links.ts
+++ b/packages/kbn-doc-links/src/get_doc_links.ts
@@ -470,6 +470,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
securitySolution: {
artifactControl: `${SECURITY_SOLUTION_DOCS}artifact-control.html`,
avcResults: `${ELASTIC_WEBSITE_URL}blog/elastic-av-comparatives-business-security-test`,
+ bidirectionalIntegrations: `${SECURITY_SOLUTION_DOCS}third-party-actions.html`,
trustedApps: `${SECURITY_SOLUTION_DOCS}trusted-apps-ov.html`,
eventFilters: `${SECURITY_SOLUTION_DOCS}event-filters.html`,
blocklist: `${SECURITY_SOLUTION_DOCS}blocklist.html`,
@@ -716,6 +717,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
mappingRoles: `${ELASTICSEARCH_DOCS}mapping-roles.html`,
mappingRolesFieldRules: `${ELASTICSEARCH_DOCS}role-mapping-resources.html#mapping-roles-rule-field`,
runAsPrivilege: `${ELASTICSEARCH_DOCS}security-privileges.html#_run_as_privilege`,
+ deprecatedV1Endpoints: `${KIBANA_DOCS}breaking-changes-summary.html#breaking-199656`,
},
spaces: {
kibanaLegacyUrlAliases: `${KIBANA_DOCS}legacy-url-aliases.html`,
diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts
index f1a6a8d4b578d..ac0f66d83b705 100644
--- a/packages/kbn-doc-links/src/types.ts
+++ b/packages/kbn-doc-links/src/types.ts
@@ -340,6 +340,7 @@ export interface DocLinks {
readonly aiAssistant: string;
readonly artifactControl: string;
readonly avcResults: string;
+ readonly bidirectionalIntegrations: string;
readonly trustedApps: string;
readonly eventFilters: string;
readonly eventMerging: string;
@@ -504,6 +505,7 @@ export interface DocLinks {
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
+ deprecatedV1Endpoints: string;
}>;
readonly spaces: Readonly<{
kibanaLegacyUrlAliases: string;
diff --git a/packages/kbn-esql-editor/src/editor_footer/discard_starred_query/discard_starred_query_modal.tsx b/packages/kbn-esql-editor/src/editor_footer/discard_starred_query/discard_starred_query_modal.tsx
new file mode 100644
index 0000000000000..5efa3a1469354
--- /dev/null
+++ b/packages/kbn-esql-editor/src/editor_footer/discard_starred_query/discard_starred_query_modal.tsx
@@ -0,0 +1,109 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import React, { useState, useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiText,
+ EuiCheckbox,
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiHorizontalRule,
+} from '@elastic/eui';
+
+export interface DiscardStarredQueryModalProps {
+ onClose: (dismissFlag?: boolean, removeQuery?: boolean) => Promise;
+}
+// Needed for React.lazy
+// eslint-disable-next-line import/no-default-export
+export default function DiscardStarredQueryModal({ onClose }: DiscardStarredQueryModalProps) {
+ const [dismissModalChecked, setDismissModalChecked] = useState(false);
+ const onTransitionModalDismiss = useCallback((e: React.ChangeEvent) => {
+ setDismissModalChecked(e.target.checked);
+ }, []);
+
+ return (
+ onClose()}
+ style={{ width: 700 }}
+ data-test-subj="discard-starred-query-modal"
+ >
+
+
+ {i18n.translate('esqlEditor.discardStarredQueryModal.title', {
+ defaultMessage: 'Discard starred query',
+ })}
+
+
+
+
+
+ {i18n.translate('esqlEditor.discardStarredQueryModal.body', {
+ defaultMessage:
+ 'Removing a starred query will remove it from the list. This has no impact on the recent query history.',
+ })}
+
+
+
+
+
+
+
+
+
+
+
+ {
+ await onClose(dismissModalChecked, false);
+ }}
+ color="primary"
+ data-test-subj="esqlEditor-discard-starred-query-cancel-btn"
+ >
+ {i18n.translate('esqlEditor.discardStarredQueryModal.cancelLabel', {
+ defaultMessage: 'Cancel',
+ })}
+
+
+
+ {
+ await onClose(dismissModalChecked, true);
+ }}
+ color="danger"
+ iconType="trash"
+ data-test-subj="esqlEditor-discard-starred-query-discard-btn"
+ >
+ {i18n.translate('esqlEditor.discardStarredQueryModal.discardQueryLabel', {
+ defaultMessage: 'Discard query',
+ })}
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/kbn-esql-editor/src/editor_footer/discard_starred_query/index.tsx b/packages/kbn-esql-editor/src/editor_footer/discard_starred_query/index.tsx
new file mode 100644
index 0000000000000..544b251c76754
--- /dev/null
+++ b/packages/kbn-esql-editor/src/editor_footer/discard_starred_query/index.tsx
@@ -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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import React from 'react';
+import type { DiscardStarredQueryModalProps } from './discard_starred_query_modal';
+
+const Fallback = () => ;
+
+const LazyDiscardStarredQueryModal = React.lazy(() => import('./discard_starred_query_modal'));
+export const DiscardStarredQueryModal = (props: DiscardStarredQueryModalProps) => (
+ }>
+
+
+);
diff --git a/packages/kbn-esql-editor/src/editor_footer/esql_starred_queries_service.test.tsx b/packages/kbn-esql-editor/src/editor_footer/esql_starred_queries_service.test.tsx
new file mode 100644
index 0000000000000..fca4d95c6f6cb
--- /dev/null
+++ b/packages/kbn-esql-editor/src/editor_footer/esql_starred_queries_service.test.tsx
@@ -0,0 +1,203 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { EsqlStarredQueriesService } from './esql_starred_queries_service';
+import { coreMock } from '@kbn/core/public/mocks';
+import type { Storage } from '@kbn/kibana-utils-plugin/public';
+
+class LocalStorageMock {
+ public store: Record;
+ constructor(defaultStore: Record) {
+ this.store = defaultStore;
+ }
+ clear() {
+ this.store = {};
+ }
+ get(key: string) {
+ return this.store[key] || null;
+ }
+ set(key: string, value: unknown) {
+ this.store[key] = String(value);
+ }
+ remove(key: string) {
+ delete this.store[key];
+ }
+}
+
+describe('EsqlStarredQueriesService', () => {
+ const core = coreMock.createStart();
+ const storage = new LocalStorageMock({}) as unknown as Storage;
+
+ it('should initialize', async () => {
+ const service = await EsqlStarredQueriesService.initialize({
+ http: core.http,
+ storage,
+ });
+ expect(service).toBeDefined();
+ expect(service.queries$.value).toEqual([]);
+ });
+
+ it('should add a new starred query', async () => {
+ const service = await EsqlStarredQueriesService.initialize({
+ http: core.http,
+ storage,
+ });
+ const query = {
+ queryString: 'SELECT * FROM test',
+ timeRan: '2021-09-01T00:00:00Z',
+ status: 'success' as const,
+ };
+
+ await service.addStarredQuery(query);
+ expect(service.queries$.value).toEqual([
+ {
+ id: expect.any(String),
+ ...query,
+ // stores now()
+ timeRan: expect.any(String),
+ },
+ ]);
+ });
+
+ it('should not add the same query twice', async () => {
+ const service = await EsqlStarredQueriesService.initialize({
+ http: core.http,
+ storage,
+ });
+ const query = {
+ queryString: 'SELECT * FROM test',
+ timeRan: '2021-09-01T00:00:00Z',
+ status: 'success' as const,
+ };
+
+ const expected = {
+ id: expect.any(String),
+ ...query,
+ // stores now()
+ timeRan: expect.any(String),
+ // trimmed query
+ queryString: 'SELECT * FROM test',
+ };
+
+ await service.addStarredQuery(query);
+ expect(service.queries$.value).toEqual([expected]);
+
+ // second time
+ await service.addStarredQuery(query);
+ expect(service.queries$.value).toEqual([expected]);
+ });
+
+ it('should add the query trimmed', async () => {
+ const service = await EsqlStarredQueriesService.initialize({
+ http: core.http,
+ storage,
+ });
+ const query = {
+ queryString: `SELECT * FROM test |
+ WHERE field != 'value'`,
+ timeRan: '2021-09-01T00:00:00Z',
+ status: 'error' as const,
+ };
+
+ await service.addStarredQuery(query);
+ expect(service.queries$.value).toEqual([
+ {
+ id: expect.any(String),
+ ...query,
+ timeRan: expect.any(String),
+ // trimmed query
+ queryString: `SELECT * FROM test | WHERE field != 'value'`,
+ },
+ ]);
+ });
+
+ it('should remove a query', async () => {
+ const service = await EsqlStarredQueriesService.initialize({
+ http: core.http,
+ storage,
+ });
+ const query = {
+ queryString: `SELECT * FROM test | WHERE field != 'value'`,
+ timeRan: '2021-09-01T00:00:00Z',
+ status: 'error' as const,
+ };
+
+ await service.addStarredQuery(query);
+ expect(service.queries$.value).toEqual([
+ {
+ id: expect.any(String),
+ ...query,
+ timeRan: expect.any(String),
+ // trimmed query
+ queryString: `SELECT * FROM test | WHERE field != 'value'`,
+ },
+ ]);
+
+ await service.removeStarredQuery(query.queryString);
+ expect(service.queries$.value).toEqual([]);
+ });
+
+ it('should return the button correctly', async () => {
+ const service = await EsqlStarredQueriesService.initialize({
+ http: core.http,
+ storage,
+ });
+ const query = {
+ queryString: 'SELECT * FROM test',
+ timeRan: '2021-09-01T00:00:00Z',
+ status: 'success' as const,
+ };
+
+ await service.addStarredQuery(query);
+ const buttonWithTooltip = service.renderStarredButton(query);
+ const button = buttonWithTooltip.props.children;
+ expect(button.props.title).toEqual('Remove ES|QL query from Starred');
+ expect(button.props.iconType).toEqual('starFilled');
+ });
+
+ it('should display the modal when the Remove button is clicked', async () => {
+ const service = await EsqlStarredQueriesService.initialize({
+ http: core.http,
+ storage,
+ });
+ const query = {
+ queryString: 'SELECT * FROM test',
+ timeRan: '2021-09-01T00:00:00Z',
+ status: 'success' as const,
+ };
+
+ await service.addStarredQuery(query);
+ const buttonWithTooltip = service.renderStarredButton(query);
+ const button = buttonWithTooltip.props.children;
+ expect(button.props.title).toEqual('Remove ES|QL query from Starred');
+ button.props.onClick();
+
+ expect(service.discardModalVisibility$.value).toEqual(true);
+ });
+
+ it('should NOT display the modal when Remove the button is clicked but the user has dismissed the modal permanently', async () => {
+ storage.set('esqlEditor.starredQueriesDiscard', true);
+ const service = await EsqlStarredQueriesService.initialize({
+ http: core.http,
+ storage,
+ });
+ const query = {
+ queryString: 'SELECT * FROM test',
+ timeRan: '2021-09-01T00:00:00Z',
+ status: 'success' as const,
+ };
+
+ await service.addStarredQuery(query);
+ const buttonWithTooltip = service.renderStarredButton(query);
+ const button = buttonWithTooltip.props.children;
+ button.props.onClick();
+
+ expect(service.discardModalVisibility$.value).toEqual(false);
+ });
+});
diff --git a/packages/kbn-esql-editor/src/editor_footer/esql_starred_queries_service.tsx b/packages/kbn-esql-editor/src/editor_footer/esql_starred_queries_service.tsx
new file mode 100644
index 0000000000000..80ef716cfd4b0
--- /dev/null
+++ b/packages/kbn-esql-editor/src/editor_footer/esql_starred_queries_service.tsx
@@ -0,0 +1,241 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+import React from 'react';
+import { BehaviorSubject } from 'rxjs';
+import { i18n } from '@kbn/i18n';
+import { v4 as uuidv4 } from 'uuid';
+import type { CoreStart } from '@kbn/core/public';
+import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
+import type { Storage } from '@kbn/kibana-utils-plugin/public';
+import { EuiButtonIcon } from '@elastic/eui';
+import { FavoritesClient } from '@kbn/content-management-favorites-public';
+import { FAVORITES_LIMIT as ESQL_STARRED_QUERIES_LIMIT } from '@kbn/content-management-favorites-common';
+import { type QueryHistoryItem, getTrimmedQuery } from '../history_local_storage';
+import { TooltipWrapper } from './tooltip_wrapper';
+
+const STARRED_QUERIES_DISCARD_KEY = 'esqlEditor.starredQueriesDiscard';
+
+/**
+ * EsqlStarredQueriesService is a service that manages the starred queries in the ES|QL editor.
+ * It provides methods to add and remove queries from the starred list.
+ * It also provides a method to render the starred button in the editor list table.
+ *
+ * @param client - The FavoritesClient instance.
+ * @param starredQueries - The list of starred queries.
+ * @param queries$ - The BehaviorSubject that emits the starred queries list.
+ * @method initialize - Initializes the service and retrieves the starred queries from the favoriteService.
+ * @method checkIfQueryIsStarred - Checks if a query is already starred.
+ * @method addStarredQuery - Adds a query to the starred list.
+ * @method removeStarredQuery - Removes a query from the starred list.
+ * @method renderStarredButton - Renders the starred button in the editor list table.
+ * @returns EsqlStarredQueriesService instance.
+ *
+ */
+export interface StarredQueryItem extends QueryHistoryItem {
+ id: string;
+}
+
+interface EsqlStarredQueriesServices {
+ http: CoreStart['http'];
+ storage: Storage;
+ usageCollection?: UsageCollectionStart;
+}
+
+interface EsqlStarredQueriesParams {
+ client: FavoritesClient;
+ starredQueries: StarredQueryItem[];
+ storage: Storage;
+}
+
+function generateId() {
+ return uuidv4();
+}
+
+interface StarredQueryMetadata {
+ queryString: string;
+ createdAt: string;
+ status: 'success' | 'warning' | 'error';
+}
+
+export class EsqlStarredQueriesService {
+ private client: FavoritesClient;
+ private starredQueries: StarredQueryItem[] = [];
+ private queryToEdit: string = '';
+ private storage: Storage;
+ queries$: BehaviorSubject;
+ discardModalVisibility$: BehaviorSubject = new BehaviorSubject(false);
+
+ constructor({ client, starredQueries, storage }: EsqlStarredQueriesParams) {
+ this.client = client;
+ this.starredQueries = starredQueries;
+ this.queries$ = new BehaviorSubject(starredQueries);
+ this.storage = storage;
+ }
+
+ static async initialize(services: EsqlStarredQueriesServices) {
+ const client = new FavoritesClient('esql_editor', 'esql_query', {
+ http: services.http,
+ usageCollection: services.usageCollection,
+ });
+
+ const { favoriteMetadata } = (await client?.getFavorites()) || {};
+ const retrievedQueries: StarredQueryItem[] = [];
+
+ if (!favoriteMetadata) {
+ return new EsqlStarredQueriesService({
+ client,
+ starredQueries: [],
+ storage: services.storage,
+ });
+ }
+ Object.keys(favoriteMetadata).forEach((id) => {
+ const item = favoriteMetadata[id];
+ const { queryString, createdAt, status } = item;
+ retrievedQueries.push({ id, queryString, timeRan: createdAt, status });
+ });
+
+ return new EsqlStarredQueriesService({
+ client,
+ starredQueries: retrievedQueries,
+ storage: services.storage,
+ });
+ }
+
+ private checkIfQueryIsStarred(queryString: string) {
+ return this.starredQueries.some((item) => item.queryString === queryString);
+ }
+
+ private checkIfStarredQueriesLimitReached() {
+ return this.starredQueries.length >= ESQL_STARRED_QUERIES_LIMIT;
+ }
+
+ async addStarredQuery(item: Pick) {
+ const favoriteItem: { id: string; metadata: StarredQueryMetadata } = {
+ id: generateId(),
+ metadata: {
+ queryString: getTrimmedQuery(item.queryString),
+ createdAt: new Date().toISOString(),
+ status: item.status ?? 'success',
+ },
+ };
+
+ // do not add the query if it's already starred or has reached the limit
+ if (
+ this.checkIfQueryIsStarred(favoriteItem.metadata.queryString) ||
+ this.checkIfStarredQueriesLimitReached()
+ ) {
+ return;
+ }
+
+ const starredQueries = [...this.starredQueries];
+
+ starredQueries.push({
+ queryString: favoriteItem.metadata.queryString,
+ timeRan: favoriteItem.metadata.createdAt,
+ status: favoriteItem.metadata.status,
+ id: favoriteItem.id,
+ });
+ this.queries$.next(starredQueries);
+ this.starredQueries = starredQueries;
+ await this.client.addFavorite(favoriteItem);
+
+ // telemetry, add favorite click event
+ this.client.reportAddFavoriteClick();
+ }
+
+ async removeStarredQuery(queryString: string) {
+ const trimmedQueryString = getTrimmedQuery(queryString);
+ const favoriteItem = this.starredQueries.find(
+ (item) => item.queryString === trimmedQueryString
+ );
+
+ if (!favoriteItem) {
+ return;
+ }
+
+ this.starredQueries = this.starredQueries.filter(
+ (item) => item.queryString !== trimmedQueryString
+ );
+ this.queries$.next(this.starredQueries);
+
+ await this.client.removeFavorite({ id: favoriteItem.id });
+
+ // telemetry, remove favorite click event
+ this.client.reportRemoveFavoriteClick();
+ }
+
+ async onDiscardModalClose(shouldDismissModal?: boolean, removeQuery?: boolean) {
+ if (shouldDismissModal) {
+ // set the local storage flag to not show the modal again
+ this.storage.set(STARRED_QUERIES_DISCARD_KEY, true);
+ }
+ this.discardModalVisibility$.next(false);
+
+ if (removeQuery) {
+ // remove the query
+ await this.removeStarredQuery(this.queryToEdit);
+ }
+ }
+
+ renderStarredButton(item: QueryHistoryItem) {
+ const trimmedQueryString = getTrimmedQuery(item.queryString);
+ const isStarred = this.checkIfQueryIsStarred(trimmedQueryString);
+ return (
+
+ {
+ this.queryToEdit = trimmedQueryString;
+ if (isStarred) {
+ // show the discard modal only if the user has not dismissed it
+ if (!this.storage.get(STARRED_QUERIES_DISCARD_KEY)) {
+ this.discardModalVisibility$.next(true);
+ } else {
+ await this.removeStarredQuery(item.queryString);
+ }
+ } else {
+ await this.addStarredQuery(item);
+ }
+ }}
+ data-test-subj="ESQLFavoriteButton"
+ />
+
+ );
+ }
+}
diff --git a/packages/kbn-esql-editor/src/editor_footer/query_history.test.tsx b/packages/kbn-esql-editor/src/editor_footer/history_starred_queries.test.tsx
similarity index 52%
rename from packages/kbn-esql-editor/src/editor_footer/query_history.test.tsx
rename to packages/kbn-esql-editor/src/editor_footer/history_starred_queries.test.tsx
index df41e2a2d3b91..9e0d586622c31 100644
--- a/packages/kbn-esql-editor/src/editor_footer/query_history.test.tsx
+++ b/packages/kbn-esql-editor/src/editor_footer/history_starred_queries.test.tsx
@@ -8,8 +8,15 @@
*/
import React from 'react';
-import { QueryHistoryAction, getTableColumns, QueryColumn } from './query_history';
+import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
+import { coreMock } from '@kbn/core/public/mocks';
import { render, screen } from '@testing-library/react';
+import {
+ QueryHistoryAction,
+ getTableColumns,
+ QueryColumn,
+ HistoryAndStarredQueriesTabs,
+} from './history_starred_queries';
jest.mock('../history_local_storage', () => {
const module = jest.requireActual('../history_local_storage');
@@ -18,7 +25,6 @@ jest.mock('../history_local_storage', () => {
getHistoryItems: () => [
{
queryString: 'from kibana_sample_data_flights | limit 10',
- timeZone: 'Browser',
timeRan: 'Mar. 25, 24 08:45:27',
queryRunning: false,
status: 'success',
@@ -27,7 +33,7 @@ jest.mock('../history_local_storage', () => {
};
});
-describe('QueryHistory', () => {
+describe('Starred and History queries components', () => {
describe('QueryHistoryAction', () => {
it('should render the history action component as a button if is spaceReduced is undefined', () => {
render();
@@ -47,9 +53,14 @@ describe('QueryHistory', () => {
});
describe('getTableColumns', () => {
- it('should get the history table columns correctly', async () => {
+ it('should get the table columns correctly', async () => {
const columns = getTableColumns(50, false, []);
expect(columns).toEqual([
+ {
+ 'data-test-subj': 'favoriteBtn',
+ render: expect.anything(),
+ width: '40px',
+ },
{
css: {
height: '100%',
@@ -64,7 +75,7 @@ describe('QueryHistory', () => {
{
'data-test-subj': 'queryString',
field: 'queryString',
- name: 'Recent queries',
+ name: 'Query',
render: expect.anything(),
},
{
@@ -83,11 +94,58 @@ describe('QueryHistory', () => {
},
]);
});
+
+ it('should get the table columns correctly for the starred list', async () => {
+ const columns = getTableColumns(50, false, [], true);
+ expect(columns).toEqual([
+ {
+ 'data-test-subj': 'favoriteBtn',
+ render: expect.anything(),
+ width: '40px',
+ },
+ {
+ css: {
+ height: '100%',
+ },
+ 'data-test-subj': 'status',
+ field: 'status',
+ name: '',
+ render: expect.anything(),
+ sortable: false,
+ width: '40px',
+ },
+ {
+ 'data-test-subj': 'queryString',
+ field: 'queryString',
+ name: 'Query',
+ render: expect.anything(),
+ },
+ {
+ 'data-test-subj': 'timeRan',
+ field: 'timeRan',
+ name: 'Date Added',
+ render: expect.anything(),
+ sortable: true,
+ width: '240px',
+ },
+ {
+ actions: [],
+ 'data-test-subj': 'actions',
+ name: '',
+ width: '60px',
+ },
+ ]);
+ });
});
it('should get the history table columns correctly for reduced space', async () => {
const columns = getTableColumns(50, true, []);
expect(columns).toEqual([
+ {
+ 'data-test-subj': 'favoriteBtn',
+ render: expect.anything(),
+ width: 'auto',
+ },
{
css: {
height: '100%',
@@ -110,7 +168,7 @@ describe('QueryHistory', () => {
{
'data-test-subj': 'queryString',
field: 'queryString',
- name: 'Recent queries',
+ name: 'Query',
render: expect.anything(),
},
{
@@ -132,7 +190,7 @@ describe('QueryHistory', () => {
/>
);
expect(
- screen.queryByTestId('ESQLEditor-queryHistory-queryString-expanded')
+ screen.queryByTestId('ESQLEditor-queryList-queryString-expanded')
).not.toBeInTheDocument();
});
@@ -152,9 +210,66 @@ describe('QueryHistory', () => {
isOnReducedSpaceLayout={true}
/>
);
- expect(
- screen.getByTestId('ESQLEditor-queryHistory-queryString-expanded')
- ).toBeInTheDocument();
+ expect(screen.getByTestId('ESQLEditor-queryList-queryString-expanded')).toBeInTheDocument();
+ });
+ });
+
+ describe('HistoryAndStarredQueriesTabs', () => {
+ const services = {
+ core: coreMock.createStart(),
+ };
+ it('should render two tabs', () => {
+ render(
+
+
+
+ );
+ expect(screen.getByTestId('history-queries-tab')).toBeInTheDocument();
+ expect(screen.getByTestId('history-queries-tab')).toHaveTextContent('Recent');
+ expect(screen.getByTestId('starred-queries-tab')).toBeInTheDocument();
+ expect(screen.getByTestId('starred-queries-tab')).toHaveTextContent('Starred');
+ });
+
+ it('should render the history queries tab by default', () => {
+ render(
+
+
+
+ );
+ expect(screen.getByTestId('ESQLEditor-queryHistory')).toBeInTheDocument();
+ expect(screen.getByTestId('ESQLEditor-history-starred-queries-helpText')).toHaveTextContent(
+ 'Showing last 20 queries'
+ );
+ });
+
+ it('should render the starred queries if the corresponding btn is clicked', () => {
+ render(
+
+
+
+ );
+ // click the starred queries tab
+ screen.getByTestId('starred-queries-tab').click();
+
+ expect(screen.getByTestId('ESQLEditor-starredQueries')).toBeInTheDocument();
+ expect(screen.getByTestId('ESQLEditor-history-starred-queries-helpText')).toHaveTextContent(
+ 'Showing 0 queries (max 100)'
+ );
});
});
});
diff --git a/packages/kbn-esql-editor/src/editor_footer/query_history.tsx b/packages/kbn-esql-editor/src/editor_footer/history_starred_queries.tsx
similarity index 54%
rename from packages/kbn-esql-editor/src/editor_footer/query_history.tsx
rename to packages/kbn-esql-editor/src/editor_footer/history_starred_queries.tsx
index 7316a5b49ddea..c24d0a0b1817b 100644
--- a/packages/kbn-esql-editor/src/editor_footer/query_history.tsx
+++ b/packages/kbn-esql-editor/src/editor_footer/history_starred_queries.tsx
@@ -6,8 +6,8 @@
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-
-import React, { useState, useRef, useEffect, useMemo } from 'react';
+import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
+import moment from 'moment';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
@@ -22,11 +22,26 @@ import {
EuiCopy,
EuiToolTip,
euiScrollBarStyles,
+ EuiTab,
+ EuiTabs,
+ EuiNotificationBadge,
+ EuiText,
} from '@elastic/eui';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { cssFavoriteHoverWithinEuiTableRow } from '@kbn/content-management-favorites-public';
+import { FAVORITES_LIMIT as ESQL_STARRED_QUERIES_LIMIT } from '@kbn/content-management-favorites-common';
import { css, Interpolation, Theme } from '@emotion/react';
import { useEuiTablePersist } from '@kbn/shared-ux-table-persist';
-import { type QueryHistoryItem, getHistoryItems } from '../history_local_storage';
-import { getReducedSpaceStyling, swapArrayElements } from './query_history_helpers';
+import {
+ type QueryHistoryItem,
+ getHistoryItems,
+ MAX_HISTORY_QUERIES_NUMBER,
+ dateFormat,
+} from '../history_local_storage';
+import type { ESQLEditorDeps } from '../types';
+import { getReducedSpaceStyling, swapArrayElements } from './history_starred_queries_helpers';
+import { EsqlStarredQueriesService, StarredQueryItem } from './esql_starred_queries_service';
+import { DiscardStarredQueryModal } from './discard_starred_query';
export function QueryHistoryAction({
toggleHistory,
@@ -99,9 +114,22 @@ export function QueryHistoryAction({
export const getTableColumns = (
width: number,
isOnReducedSpaceLayout: boolean,
- actions: Array>
+ actions: Array>,
+ isStarredTab = false,
+ starredQueriesService?: EsqlStarredQueriesService
): Array> => {
const columnsArray = [
+ {
+ 'data-test-subj': 'favoriteBtn',
+ render: (item: QueryHistoryItem) => {
+ const StarredQueryButton = starredQueriesService?.renderStarredButton(item);
+ if (!StarredQueryButton) {
+ return null;
+ }
+ return StarredQueryButton;
+ },
+ width: isOnReducedSpaceLayout ? 'auto' : '40px',
+ },
{
field: 'status',
name: '',
@@ -167,7 +195,7 @@ export const getTableColumns = (
field: 'queryString',
'data-test-subj': 'queryString',
name: i18n.translate('esqlEditor.query.recentQueriesColumnLabel', {
- defaultMessage: 'Recent queries',
+ defaultMessage: 'Query',
}),
render: (queryString: QueryHistoryItem['queryString']) => (
timeRan,
+ render: (timeRan: QueryHistoryItem['timeRan']) => moment(timeRan).format(dateFormat),
width: isOnReducedSpaceLayout ? 'auto' : '240px',
},
{
@@ -196,22 +228,33 @@ export const getTableColumns = (
];
// I need to swap the elements here to get the desired design
- return isOnReducedSpaceLayout ? swapArrayElements(columnsArray, 1, 2) : columnsArray;
+ return isOnReducedSpaceLayout ? swapArrayElements(columnsArray, 2, 3) : columnsArray;
};
-export function QueryHistory({
+export function QueryList({
containerCSS,
containerWidth,
onUpdateAndSubmit,
height,
+ listItems,
+ starredQueriesService,
+ tableCaption,
+ dataTestSubj,
+ isStarredTab = false,
}: {
+ listItems: QueryHistoryItem[];
containerCSS: Interpolation;
containerWidth: number;
onUpdateAndSubmit: (qs: string) => void;
height: number;
+ starredQueriesService?: EsqlStarredQueriesService;
+ tableCaption?: string;
+ dataTestSubj?: string;
+ isStarredTab?: boolean;
}) {
const theme = useEuiTheme();
const scrollBarStyles = euiScrollBarStyles(theme);
+ const [isDiscardQueryModalVisible, setIsDiscardQueryModalVisible] = useState(false);
const { sorting, onTableChange } = useEuiTablePersist({
tableId: 'esqlQueryHistory',
@@ -221,8 +264,6 @@ export function QueryHistory({
},
});
- const historyItems: QueryHistoryItem[] = getHistoryItems(sorting.sort.direction);
-
const actions: Array> = useMemo(() => {
return [
{
@@ -232,16 +273,16 @@ export function QueryHistory({
onUpdateAndSubmit(item.queryString)}
@@ -254,7 +295,7 @@ export function QueryHistory({
@@ -266,7 +307,7 @@ export function QueryHistory({
css={css`
cursor: pointer;
`}
- aria-label={i18n.translate('esqlEditor.query.querieshistoryCopy', {
+ aria-label={i18n.translate('esqlEditor.query.esqlQueriesCopy', {
defaultMessage: 'Copy query to clipboard',
})}
/>
@@ -279,14 +320,23 @@ export function QueryHistory({
},
];
}, [onUpdateAndSubmit]);
+
const isOnReducedSpaceLayout = containerWidth < 560;
const columns = useMemo(() => {
- return getTableColumns(containerWidth, isOnReducedSpaceLayout, actions);
- }, [actions, containerWidth, isOnReducedSpaceLayout]);
+ return getTableColumns(
+ containerWidth,
+ isOnReducedSpaceLayout,
+ actions,
+ isStarredTab,
+ starredQueriesService
+ );
+ }, [containerWidth, isOnReducedSpaceLayout, actions, isStarredTab, starredQueriesService]);
const { euiTheme } = theme;
const extraStyling = isOnReducedSpaceLayout ? getReducedSpaceStyling() : '';
+ const starredQueriesCellStyling = cssFavoriteHoverWithinEuiTableRow(theme.euiTheme);
+
const tableStyling = css`
.euiTable {
background-color: ${euiTheme.colors.lightestShade};
@@ -305,22 +355,40 @@ export function QueryHistory({
overflow-y: auto;
${scrollBarStyles}
${extraStyling}
+ ${starredQueriesCellStyling}
`;
+ starredQueriesService?.discardModalVisibility$.subscribe((nextVisibility) => {
+ if (isDiscardQueryModalVisible !== nextVisibility) {
+ setIsDiscardQueryModalVisible(nextVisibility);
+ }
+ });
+
return (
-
+
+ {isDiscardQueryModalVisible && (
+
+ (await starredQueriesService?.onDiscardModalClose(dismissFlag, removeQuery)) ??
+ Promise.resolve()
+ }
+ />
+ )}
);
}
@@ -354,7 +422,7 @@ export function QueryColumn({
onClick={() => {
setIsRowExpanded(!isRowExpanded);
}}
- data-test-subj="ESQLEditor-queryHistory-queryString-expanded"
+ data-test-subj="ESQLEditor-queryList-queryString-expanded"
aria-label={
isRowExpanded
? i18n.translate('esqlEditor.query.collapseLabel', {
@@ -387,3 +455,171 @@ export function QueryColumn({
>
);
}
+
+export function HistoryAndStarredQueriesTabs({
+ containerCSS,
+ containerWidth,
+ onUpdateAndSubmit,
+ height,
+}: {
+ containerCSS: Interpolation
;
+ containerWidth: number;
+ onUpdateAndSubmit: (qs: string) => void;
+ height: number;
+}) {
+ const kibana = useKibana();
+ const { core, usageCollection, storage } = kibana.services;
+
+ const [starredQueriesService, setStarredQueriesService] = useState();
+ const [starredQueries, setStarredQueries] = useState([]);
+
+ useEffect(() => {
+ const initializeService = async () => {
+ const starredService = await EsqlStarredQueriesService.initialize({
+ http: core.http,
+ usageCollection,
+ storage,
+ });
+ setStarredQueriesService(starredService);
+ };
+ if (!starredQueriesService) {
+ initializeService();
+ }
+ }, [core.http, starredQueriesService, storage, usageCollection]);
+
+ starredQueriesService?.queries$.subscribe((nextQueries) => {
+ if (nextQueries.length !== starredQueries.length) {
+ setStarredQueries(nextQueries);
+ }
+ });
+
+ const { euiTheme } = useEuiTheme();
+ const tabs = useMemo(() => {
+ return [
+ {
+ id: 'history-queries-tab',
+ name: i18n.translate('esqlEditor.query.historyQueriesTabLabel', {
+ defaultMessage: 'Recent',
+ }),
+ dataTestSubj: 'history-queries-tab',
+ content: (
+
+ ),
+ },
+ {
+ id: 'starred-queries-tab',
+ dataTestSubj: 'starred-queries-tab',
+ name: i18n.translate('esqlEditor.query.starredQueriesTabLabel', {
+ defaultMessage: 'Starred',
+ }),
+ append: (
+
+ {starredQueries?.length}
+
+ ),
+ content: (
+
+ ),
+ },
+ ];
+ }, [
+ containerCSS,
+ containerWidth,
+ height,
+ onUpdateAndSubmit,
+ starredQueries,
+ starredQueriesService,
+ ]);
+
+ const [selectedTabId, setSelectedTabId] = useState('history-queries-tab');
+ const selectedTabContent = useMemo(() => {
+ return tabs.find((obj) => obj.id === selectedTabId)?.content;
+ }, [selectedTabId, tabs]);
+
+ const onSelectedTabChanged = (id: string) => {
+ setSelectedTabId(id);
+ };
+
+ const renderTabs = useCallback(() => {
+ return tabs.map((tab, index) => (
+ onSelectedTabChanged(tab.id)}
+ isSelected={tab.id === selectedTabId}
+ append={tab.append}
+ data-test-subj={tab.dataTestSubj}
+ >
+ {tab.name}
+
+ ));
+ }, [selectedTabId, tabs]);
+
+ return (
+ <>
+
+
+ {renderTabs()}
+
+
+
+
+
+ {selectedTabId === 'history-queries-tab'
+ ? i18n.translate('esqlEditor.history.historyItemslimit', {
+ defaultMessage: 'Showing last {historyItemsLimit} queries',
+ values: { historyItemsLimit: MAX_HISTORY_QUERIES_NUMBER },
+ })
+ : i18n.translate('esqlEditor.history.starredItemslimit', {
+ defaultMessage:
+ 'Showing {starredItemsCount} queries (max {starredItemsLimit})',
+ values: {
+ starredItemsLimit: ESQL_STARRED_QUERIES_LIMIT,
+ starredItemsCount: starredQueries.length ?? 0,
+ },
+ })}
+
+
+
+
+
+ {selectedTabContent}
+ >
+ );
+}
diff --git a/packages/kbn-esql-editor/src/editor_footer/query_history_helpers.test.ts b/packages/kbn-esql-editor/src/editor_footer/history_starred_queries_helpers.test.ts
similarity index 93%
rename from packages/kbn-esql-editor/src/editor_footer/query_history_helpers.test.ts
rename to packages/kbn-esql-editor/src/editor_footer/history_starred_queries_helpers.test.ts
index 43a676aba56b5..ad33cd3687fae 100644
--- a/packages/kbn-esql-editor/src/editor_footer/query_history_helpers.test.ts
+++ b/packages/kbn-esql-editor/src/editor_footer/history_starred_queries_helpers.test.ts
@@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { swapArrayElements } from './query_history_helpers';
+import { swapArrayElements } from './history_starred_queries_helpers';
describe('query history helpers', function () {
it('should swap 2 elements in an array', function () {
diff --git a/packages/kbn-esql-editor/src/editor_footer/query_history_helpers.ts b/packages/kbn-esql-editor/src/editor_footer/history_starred_queries_helpers.ts
similarity index 86%
rename from packages/kbn-esql-editor/src/editor_footer/query_history_helpers.ts
rename to packages/kbn-esql-editor/src/editor_footer/history_starred_queries_helpers.ts
index c55bc0801ec66..2f7d4419d13c9 100644
--- a/packages/kbn-esql-editor/src/editor_footer/query_history_helpers.ts
+++ b/packages/kbn-esql-editor/src/editor_footer/history_starred_queries_helpers.ts
@@ -19,16 +19,19 @@ export const getReducedSpaceStyling = () => {
}
.euiTable thead tr {
display: grid;
- grid-template-columns: 40px 1fr 0 auto;
+ grid-template-columns: 40px 40px 1fr 0 auto;
}
.euiTable tbody tr {
display: grid;
- grid-template-columns: 40px 1fr auto;
+ grid-template-columns: 40px 40px 1fr auto;
grid-template-areas:
- 'status timeRan lastDuration actions'
- '. queryString queryString queryString';
+ 'favoriteBtn status timeRan lastDuration actions'
+ '. . queryString queryString queryString';
}
/* Set grid template areas */
+ .euiTable td[data-test-subj='favoriteBtn'] {
+ grid-area: favoriteBtn;
+ }
.euiTable td[data-test-subj='status'] {
grid-area: status;
}
diff --git a/packages/kbn-esql-editor/src/editor_footer/index.tsx b/packages/kbn-esql-editor/src/editor_footer/index.tsx
index d898d2c52c9c7..4e60e65f19ca4 100644
--- a/packages/kbn-esql-editor/src/editor_footer/index.tsx
+++ b/packages/kbn-esql-editor/src/editor_footer/index.tsx
@@ -8,7 +8,6 @@
*/
import React, { memo, useState, useCallback, useMemo } from 'react';
-
import { i18n } from '@kbn/i18n';
import {
EuiText,
@@ -27,7 +26,7 @@ import {
import { getLimitFromESQLQuery } from '@kbn/esql-utils';
import { type MonacoMessage } from '../helpers';
import { ErrorsWarningsFooterPopover } from './errors_warnings_popover';
-import { QueryHistoryAction, QueryHistory } from './query_history';
+import { QueryHistoryAction, HistoryAndStarredQueriesTabs } from './history_starred_queries';
import { SubmitFeedbackComponent } from './feedback_component';
import { QueryWrapComponent } from './query_wrap_component';
import type { ESQLEditorDeps } from '../types';
@@ -60,7 +59,6 @@ interface EditorFooterProps {
isSpaceReduced?: boolean;
hideTimeFilterInfo?: boolean;
hideQueryHistory?: boolean;
- isInCompactMode?: boolean;
displayDocumentationAsFlyout?: boolean;
}
@@ -84,7 +82,6 @@ export const EditorFooter = memo(function EditorFooter({
isLanguageComponentOpen,
setIsLanguageComponentOpen,
hideQueryHistory,
- isInCompactMode,
displayDocumentationAsFlyout,
measuredContainerWidth,
code,
@@ -310,7 +307,7 @@ export const EditorFooter = memo(function EditorFooter({
{isHistoryOpen && (
- > & {
+ tooltipContent: string;
+ /** When the condition is truthy, the tooltip will be shown */
+ condition: boolean;
+};
+
+export const TooltipWrapper: React.FunctionComponent = ({
+ children,
+ condition,
+ tooltipContent,
+ ...tooltipProps
+}) => {
+ return (
+ <>
+ {condition ? (
+
+ <>{children}>
+
+ ) : (
+ children
+ )}
+ >
+ );
+};
diff --git a/packages/kbn-esql-editor/src/esql_editor.tsx b/packages/kbn-esql-editor/src/esql_editor.tsx
index 636bb0b13ff17..767bc9026348c 100644
--- a/packages/kbn-esql-editor/src/esql_editor.tsx
+++ b/packages/kbn-esql-editor/src/esql_editor.tsx
@@ -99,7 +99,7 @@ export const ESQLEditor = memo(function ESQLEditor({
uiSettings,
} = kibana.services;
const darkMode = core.theme?.getTheme().darkMode;
- const timeZone = uiSettings?.get('dateFormat:tz');
+
const histogramBarTarget = uiSettings?.get('histogram:barTarget') ?? 50;
const [code, setCode] = useState(query.esql ?? '');
// To make server side errors less "sticky", register the state of the code when submitting
@@ -464,11 +464,10 @@ export const ESQLEditor = memo(function ESQLEditor({
validateQuery();
addQueriesToCache({
queryString: code,
- timeZone,
status: clientParserStatus,
});
}
- }, [clientParserStatus, isLoading, isQueryLoading, parseMessages, code, timeZone]);
+ }, [clientParserStatus, isLoading, isQueryLoading, parseMessages, code]);
const queryValidation = useCallback(
async ({ active }: { active: boolean }) => {
diff --git a/packages/kbn-esql-editor/src/history_local_storage.test.ts b/packages/kbn-esql-editor/src/history_local_storage.test.ts
index c149dada84894..4632bd124f80d 100644
--- a/packages/kbn-esql-editor/src/history_local_storage.test.ts
+++ b/packages/kbn-esql-editor/src/history_local_storage.test.ts
@@ -20,7 +20,6 @@ describe('history local storage', function () {
it('should add queries to cache correctly ', function () {
addQueriesToCache({
queryString: 'from kibana_sample_data_flights | limit 10',
- timeZone: 'Browser',
});
const historyItems = getCachedQueries();
expect(historyItems.length).toBe(1);
@@ -31,7 +30,6 @@ describe('history local storage', function () {
it('should update queries to cache correctly ', function () {
addQueriesToCache({
queryString: 'from kibana_sample_data_flights \n | limit 10 \n | stats meow = avg(woof)',
- timeZone: 'Browser',
status: 'success',
});
@@ -49,7 +47,6 @@ describe('history local storage', function () {
it('should update queries to cache correctly if they are the same with different format', function () {
addQueriesToCache({
queryString: 'from kibana_sample_data_flights | limit 10 | stats meow = avg(woof) ',
- timeZone: 'Browser',
status: 'success',
});
@@ -68,7 +65,6 @@ describe('history local storage', function () {
addQueriesToCache(
{
queryString: 'row 1',
- timeZone: 'Browser',
status: 'success',
},
2
diff --git a/packages/kbn-esql-editor/src/history_local_storage.ts b/packages/kbn-esql-editor/src/history_local_storage.ts
index c79561d5d3875..46dd770d8d897 100644
--- a/packages/kbn-esql-editor/src/history_local_storage.ts
+++ b/packages/kbn-esql-editor/src/history_local_storage.ts
@@ -7,10 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import moment from 'moment';
import 'moment-timezone';
const QUERY_HISTORY_ITEM_KEY = 'QUERY_HISTORY_ITEM_KEY';
-const dateFormat = 'MMM. D, YY HH:mm:ss.SSS';
+export const dateFormat = 'MMM. D, YY HH:mm:ss';
/**
* We show maximum 20 ES|QL queries in the Query history component
@@ -19,32 +18,35 @@ const dateFormat = 'MMM. D, YY HH:mm:ss.SSS';
export interface QueryHistoryItem {
status?: 'success' | 'error' | 'warning';
queryString: string;
- startDateMilliseconds?: number;
timeRan?: string;
- timeZone?: string;
}
-const MAX_QUERIES_NUMBER = 20;
+export const MAX_HISTORY_QUERIES_NUMBER = 20;
-const getKey = (queryString: string) => {
+export const getTrimmedQuery = (queryString: string) => {
return queryString.replaceAll('\n', '').trim().replace(/\s\s+/g, ' ');
};
-const getMomentTimeZone = (timeZone?: string) => {
- return !timeZone || timeZone === 'Browser' ? moment.tz.guess() : timeZone;
-};
-
-const sortDates = (date1?: number, date2?: number) => {
- return moment(date1)?.valueOf() - moment(date2)?.valueOf();
+const sortDates = (date1?: string, date2?: string) => {
+ if (!date1 || !date2) return 0;
+ return date1 < date2 ? 1 : date1 > date2 ? -1 : 0;
};
export const getHistoryItems = (sortDirection: 'desc' | 'asc'): QueryHistoryItem[] => {
const localStorageString = localStorage.getItem(QUERY_HISTORY_ITEM_KEY) ?? '[]';
- const historyItems: QueryHistoryItem[] = JSON.parse(localStorageString);
+ const localStorageItems: QueryHistoryItem[] = JSON.parse(localStorageString);
+ const historyItems: QueryHistoryItem[] = localStorageItems.map((item) => {
+ return {
+ status: item.status,
+ queryString: item.queryString,
+ timeRan: item.timeRan ? new Date(item.timeRan).toISOString() : undefined,
+ };
+ });
+
const sortedByDate = historyItems.sort((a, b) => {
return sortDirection === 'desc'
- ? sortDates(b.startDateMilliseconds, a.startDateMilliseconds)
- : sortDates(a.startDateMilliseconds, b.startDateMilliseconds);
+ ? sortDates(b.timeRan, a.timeRan)
+ : sortDates(a.timeRan, b.timeRan);
});
return sortedByDate;
};
@@ -58,24 +60,22 @@ export const getCachedQueries = (): QueryHistoryItem[] => {
// Adding the maxQueriesAllowed here for testing purposes
export const addQueriesToCache = (
item: QueryHistoryItem,
- maxQueriesAllowed = MAX_QUERIES_NUMBER
+ maxQueriesAllowed = MAX_HISTORY_QUERIES_NUMBER
) => {
// if the user is working on multiple tabs
// the cachedQueries Map might not contain all
// the localStorage queries
const queries = getHistoryItems('desc');
queries.forEach((queryItem) => {
- const trimmedQueryString = getKey(queryItem.queryString);
+ const trimmedQueryString = getTrimmedQuery(queryItem.queryString);
cachedQueries.set(trimmedQueryString, queryItem);
});
- const trimmedQueryString = getKey(item.queryString);
+ const trimmedQueryString = getTrimmedQuery(item.queryString);
if (item.queryString) {
- const tz = getMomentTimeZone(item.timeZone);
cachedQueries.set(trimmedQueryString, {
...item,
- timeRan: moment().tz(tz).format(dateFormat),
- startDateMilliseconds: moment().valueOf(),
+ timeRan: new Date().toISOString(),
status: item.status,
});
}
@@ -83,9 +83,7 @@ export const addQueriesToCache = (
let allQueries = [...getCachedQueries()];
if (allQueries.length >= maxQueriesAllowed + 1) {
- const sortedByDate = allQueries.sort((a, b) =>
- sortDates(b?.startDateMilliseconds, a?.startDateMilliseconds)
- );
+ const sortedByDate = allQueries.sort((a, b) => sortDates(b.timeRan, a.timeRan));
// queries to store in the localstorage
allQueries = sortedByDate.slice(0, maxQueriesAllowed);
diff --git a/packages/kbn-esql-editor/src/types.ts b/packages/kbn-esql-editor/src/types.ts
index 339ac7a506430..a5fcaba885b0a 100644
--- a/packages/kbn-esql-editor/src/types.ts
+++ b/packages/kbn-esql-editor/src/types.ts
@@ -13,6 +13,8 @@ import type { AggregateQuery } from '@kbn/es-query';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import type { IndexManagementPluginSetup } from '@kbn/index-management-shared-types';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
+import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
+import type { Storage } from '@kbn/kibana-utils-plugin/public';
export interface ESQLEditorProps {
/** The aggregate type query */
@@ -70,6 +72,8 @@ export interface ESQLEditorDeps {
core: CoreStart;
dataViews: DataViewsPublicPluginStart;
expressions: ExpressionsStart;
+ storage: Storage;
indexManagementApiService?: IndexManagementPluginSetup['apiService'];
fieldsMetadata?: FieldsMetadataPublicStart;
+ usageCollection?: UsageCollectionStart;
}
diff --git a/packages/kbn-esql-editor/tsconfig.json b/packages/kbn-esql-editor/tsconfig.json
index 075c5ff9ab457..5131dd90fb0a5 100644
--- a/packages/kbn-esql-editor/tsconfig.json
+++ b/packages/kbn-esql-editor/tsconfig.json
@@ -28,6 +28,10 @@
"@kbn/fields-metadata-plugin",
"@kbn/esql-validation-autocomplete",
"@kbn/esql-utils",
+ "@kbn/content-management-favorites-public",
+ "@kbn/usage-collection-plugin",
+ "@kbn/content-management-favorites-common",
+ "@kbn/kibana-utils-plugin",
"@kbn/shared-ux-table-persist",
],
"exclude": [
diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
index 02f7007b51202..28a1e8e1eb538 100644
--- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
+++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
@@ -100,7 +100,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"event_loop_delays_daily": "01b967e8e043801357503de09199dfa3853bab88",
"exception-list": "4aebc4e61fb5d608cae48eaeb0977e8db21c61a4",
"exception-list-agnostic": "6d3262d58eee28ac381ec9654f93126a58be6f5d",
- "favorites": "a68c7c8ae22eaddcca324d8b3bfc80a94e3eec3a",
+ "favorites": "e9773d802932ea85547b120e0efdd9a4f11ff4c6",
"file": "6b65ae5899b60ebe08656fd163ea532e557d3c98",
"file-upload-usage-collection-telemetry": "06e0a8c04f991e744e09d03ab2bd7f86b2088200",
"fileShare": "5be52de1747d249a221b5241af2838264e19aaa1",
diff --git a/src/dev/prs/kibana_qa_pr_list.json b/src/dev/prs/kibana_qa_pr_list.json
index a6e5b9a4f933f..5115959da9f61 100644
--- a/src/dev/prs/kibana_qa_pr_list.json
+++ b/src/dev/prs/kibana_qa_pr_list.json
@@ -1,8 +1,17 @@
{
"include": [
-"v8.13.0"
+"v8.16.0"
],
"exclude": [
+"v8.15.3",
+"v8.15.2",
+"v8.15.1",
+"v8.15.0",
+"v7.17.25",
+"v8.15.3",
+"v8.15.2",
+"v8.15.1",
+"v8.15.0",
"v8.12.0",
"v8.3.3",
"v8.3.2",
@@ -20,9 +29,10 @@
"Feature:Endpoint",
"Feature:Observability Landing - Milestone 1",
"Feature:Osquery",
-"Feature:Transforms",
+"Feature:EEM",
"Feature:Unified Integrations",
"Synthetics",
+"Feature: Observability Onboarding",
"Team: AWL: Platform",
"Team: AWP: Visualization",
"Team: Actionable Observability",
@@ -57,15 +67,19 @@
"Team:apm",
"Team:logs-metrics-ui",
"Team:uptime",
-"Team: Protections Experience",
+"Team:Protections Experience",
"Team:obs-ux-infra_services",
"Team:obs-ux-management",
"Team:Cloud Security",
+"Team:Security Generative AI",
"Team:obs-ux-logs",
"Team:Defend Workflows",
"Team:Entity Analytics",
"Team:Obs AI Assistant",
"Team:Search",
+"Team:Security-Scalability",
+"Team:Detection Engine",
+"Team:Cloud Security",
"bump",
"docs",
"failed-test",
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx
index 5baf582877a68..816a10509b425 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx
@@ -41,6 +41,7 @@ import {
} from '@kbn/expressions-plugin/public';
import type { FieldFormat } from '@kbn/field-formats-plugin/common';
import { getOverridesFor } from '@kbn/chart-expressions-common';
+import { useAppFixedViewport } from '@kbn/core-rendering-browser';
import { consolidateMetricColumns } from '../../common/utils';
import { DEFAULT_PERCENT_DECIMALS } from '../../common/constants';
import {
@@ -385,7 +386,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
[visType, visParams, containerDimensions, rescaleFactor, hasOpenedOnAggBasedEditor]
);
- const fixedViewPort = document.getElementById('app-fixed-viewport');
+ const fixedViewPort = useAppFixedViewport();
const legendPosition = visParams.legendPosition ?? Position.Right;
diff --git a/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json b/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json
index 7669646f40a6b..1d8c4c4098728 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json
+++ b/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json
@@ -30,6 +30,7 @@
"@kbn/chart-expressions-common",
"@kbn/cell-actions",
"@kbn/react-kibana-context-render",
+ "@kbn/core-rendering-browser",
],
"exclude": [
"target/**/*",
diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx
index e1c428dd15c72..349af46eb101a 100644
--- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx
+++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx
@@ -55,6 +55,7 @@ import {
} from '@kbn/visualizations-plugin/common/constants';
import { PersistedState } from '@kbn/visualizations-plugin/public';
import { getOverridesFor, ChartSizeSpec } from '@kbn/chart-expressions-common';
+import { useAppFixedViewport } from '@kbn/core-rendering-browser';
import type {
FilterEvent,
BrushEvent,
@@ -232,6 +233,7 @@ export function XYChart({
const chartRef = useRef(null);
const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
const darkMode = chartsThemeService.useDarkMode();
+ const appFixedViewport = useAppFixedViewport();
const filteredLayers = getFilteredLayers(layers);
const layersById = filteredLayers.reduce>(
(hashMap, layer) => ({ ...hashMap, [layer.layerId]: layer }),
@@ -767,7 +769,7 @@ export function XYChart({
>
, XYChartSeriesIdentifier>
- boundary={document.getElementById('app-fixed-viewport') ?? undefined}
+ boundary={appFixedViewport}
headerFormatter={
!args.detailedTooltip && xAxisColumn
? ({ value }) => (
diff --git a/src/plugins/chart_expressions/expression_xy/tsconfig.json b/src/plugins/chart_expressions/expression_xy/tsconfig.json
index efa65a7f28a7d..cd8bd4db90b89 100644
--- a/src/plugins/chart_expressions/expression_xy/tsconfig.json
+++ b/src/plugins/chart_expressions/expression_xy/tsconfig.json
@@ -35,6 +35,7 @@
"@kbn/es-query",
"@kbn/cell-actions",
"@kbn/react-kibana-context-render",
+ "@kbn/core-rendering-browser",
],
"exclude": [
"target/**/*",
diff --git a/src/plugins/console/common/constants/index.ts b/src/plugins/console/common/constants/index.ts
index a00bcebcf38cc..b4d6a594241ce 100644
--- a/src/plugins/console/common/constants/index.ts
+++ b/src/plugins/console/common/constants/index.ts
@@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-export { MAJOR_VERSION } from './plugin';
+export { MAJOR_VERSION, WELCOME_TOUR_DELAY } from './plugin';
export { API_BASE_PATH, KIBANA_API_PREFIX } from './api';
export { DEFAULT_VARIABLES } from './variables';
export {
diff --git a/src/plugins/console/common/constants/plugin.ts b/src/plugins/console/common/constants/plugin.ts
index 27ddb7d5dff1d..bb87e300c138d 100644
--- a/src/plugins/console/common/constants/plugin.ts
+++ b/src/plugins/console/common/constants/plugin.ts
@@ -8,3 +8,5 @@
*/
export const MAJOR_VERSION = '8.0.0';
+
+export const WELCOME_TOUR_DELAY = 250;
diff --git a/src/plugins/console/public/application/components/console_tour_step.tsx b/src/plugins/console/public/application/components/console_tour_step.tsx
index 578d590bfff4a..97e999b0090aa 100644
--- a/src/plugins/console/public/application/components/console_tour_step.tsx
+++ b/src/plugins/console/public/application/components/console_tour_step.tsx
@@ -7,8 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import React, { ReactNode, ReactElement } from 'react';
+import React, { ReactNode, ReactElement, useState, useEffect } from 'react';
import { EuiTourStep, PopoverAnchorPosition } from '@elastic/eui';
+import { WELCOME_TOUR_DELAY } from '../../../common/constants';
export interface ConsoleTourStepProps {
step: number;
@@ -44,11 +45,31 @@ export const ConsoleTourStep = ({ tourStepProps, children }: Props) => {
css,
} = tourStepProps;
+ const [popoverVisible, setPopoverVisible] = useState(false);
+
+ useEffect(() => {
+ let timeoutId: any;
+
+ if (isStepOpen) {
+ timeoutId = setTimeout(() => {
+ setPopoverVisible(true);
+ }, WELCOME_TOUR_DELAY);
+ } else {
+ setPopoverVisible(false);
+ }
+
+ return () => {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ };
+ }, [isStepOpen]);
+
return (
{
+ const debouncedResize = debounce(() => {
+ window.dispatchEvent(new Event('resize'));
+ }, WELCOME_TOUR_DELAY);
+
+ debouncedResize();
+
+ // Cleanup the debounce instance on unmount or dependency change
+ return () => {
+ debouncedResize.cancel();
+ };
+ }, [consoleHeight]);
+
useEffect(() => {
function handleResize() {
const newMaxConsoleHeight = getCurrentConsoleMaxSize(euiTheme);
diff --git a/src/plugins/content_management/server/plugin.test.ts b/src/plugins/content_management/server/plugin.test.ts
index 30bbc57ee0159..de077a3f6d4de 100644
--- a/src/plugins/content_management/server/plugin.test.ts
+++ b/src/plugins/content_management/server/plugin.test.ts
@@ -91,7 +91,7 @@ describe('ContentManagementPlugin', () => {
const { plugin, coreSetup, pluginsSetup } = setup();
const api = plugin.setup(coreSetup, pluginsSetup);
- expect(Object.keys(api).sort()).toEqual(['crud', 'eventBus', 'register']);
+ expect(Object.keys(api).sort()).toEqual(['crud', 'eventBus', 'favorites', 'register']);
expect(api.crud('')).toBe('mockedCrud');
expect(api.register({} as any)).toBe('mockedRegister');
expect(api.eventBus.emit({} as any)).toBe('mockedEventBusEmit');
diff --git a/src/plugins/content_management/server/plugin.ts b/src/plugins/content_management/server/plugin.ts
index c82ed1c66fee2..0215f3d36771b 100755
--- a/src/plugins/content_management/server/plugin.ts
+++ b/src/plugins/content_management/server/plugin.ts
@@ -76,10 +76,15 @@ export class ContentManagementPlugin
contentRegistry,
});
- registerFavorites({ core, logger: this.logger, usageCollection: plugins.usageCollection });
+ const favoritesSetup = registerFavorites({
+ core,
+ logger: this.logger,
+ usageCollection: plugins.usageCollection,
+ });
return {
...coreApi,
+ favorites: favoritesSetup,
};
}
diff --git a/src/plugins/content_management/server/types.ts b/src/plugins/content_management/server/types.ts
index 22d1d57e38ba8..020f135a7d080 100644
--- a/src/plugins/content_management/server/types.ts
+++ b/src/plugins/content_management/server/types.ts
@@ -9,6 +9,7 @@
import type { Version } from '@kbn/object-versioning';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
+import type { FavoritesSetup } from '@kbn/content-management-favorites-server';
import type { CoreApi, StorageContextGetTransformFn } from './core';
export interface ContentManagementServerSetupDependencies {
@@ -18,8 +19,9 @@ export interface ContentManagementServerSetupDependencies {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ContentManagementServerStartDependencies {}
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface ContentManagementServerSetup extends CoreApi {}
+export interface ContentManagementServerSetup extends CoreApi {
+ favorites: FavoritesSetup;
+}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ContentManagementServerStart {}
diff --git a/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx b/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx
index 5f23b21dc9155..5433646e3db8e 100644
--- a/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx
@@ -62,8 +62,7 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc
}
}, [api, setDisableEditButton]);
- const [hasLockedHoverActions, dataViews, parentViewMode] = useBatchedOptionalPublishingSubjects(
- api.hasLockedHoverActions$,
+ const [dataViews, parentViewMode] = useBatchedOptionalPublishingSubjects(
api.parentApi?.dataViews,
getViewModeSubject(api ?? undefined)
);
@@ -77,7 +76,7 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc
onClick={() => {
setIsPopoverOpen(!isPopoverOpen);
if (apiCanLockHoverActions(api)) {
- api?.lockHoverActions(!hasLockedHoverActions);
+ api?.lockHoverActions(!api.hasLockedHoverActions$.value);
}
}}
data-test-subj={`embeddablePanelNotification-${api.uuid}`}
diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx
index 0ef976af51eb6..76a545d1ea9fc 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx
@@ -18,6 +18,7 @@ import { Layout, Responsive as ResponsiveReactGridLayout } from 'react-grid-layo
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
+import { useAppFixedViewport } from '@kbn/core-rendering-browser';
import { DashboardPanelState } from '../../../../common';
import { DashboardGridItem } from './dashboard_grid_item';
import { useDashboardGridSettings } from './use_dashboard_grid_settings';
@@ -25,7 +26,13 @@ import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
import { getPanelLayoutsAreEqual } from '../../state/diffing/dashboard_diffing_utils';
import { DASHBOARD_GRID_HEIGHT, DASHBOARD_MARGIN_SIZE } from '../../../dashboard_constants';
-export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
+export const DashboardGrid = ({
+ dashboardContainer,
+ viewportWidth,
+}: {
+ dashboardContainer?: HTMLElement;
+ viewportWidth: number;
+}) => {
const dashboardApi = useDashboardApi();
const [animatePanelTransforms, expandedPanelId, focusedPanelId, panels, useMargins, viewMode] =
@@ -51,6 +58,8 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
}
}, [expandedPanelId]);
+ const appFixedViewport = useAppFixedViewport();
+
const panelsInOrder: string[] = useMemo(() => {
return Object.keys(panels).sort((embeddableIdA, embeddableIdB) => {
const panelA = panels[embeddableIdA];
@@ -72,6 +81,8 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
const type = panels[embeddableId].type;
return (
{
/>
);
});
- }, [expandedPanelId, panels, panelsInOrder, focusedPanelId]);
+ }, [
+ appFixedViewport,
+ dashboardContainer,
+ expandedPanelId,
+ panels,
+ panelsInOrder,
+ focusedPanelId,
+ ]);
const onLayoutChange = useCallback(
(newLayout: Array) => {
diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx
index 9b5a00c628608..5ad1363e6f8af 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx
@@ -23,6 +23,8 @@ import { embeddableService, presentationUtilService } from '../../../services/ki
type DivProps = Pick, 'className' | 'style' | 'children'>;
export interface Props extends DivProps {
+ appFixedViewport?: HTMLElement;
+ dashboardContainer?: HTMLElement;
id: DashboardPanelState['explicitInput']['id'];
index?: number;
type: DashboardPanelState['type'];
@@ -35,6 +37,8 @@ export interface Props extends DivProps {
export const Item = React.forwardRef(
(
{
+ appFixedViewport,
+ dashboardContainer,
expandedPanelId,
focusedPanelId,
id,
@@ -92,10 +96,8 @@ export const Item = React.forwardRef(
}
}, [id, dashboardApi, scrollToPanelId, highlightPanelId, ref, blurPanel]);
- const dashboardContainerTopOffset =
- (document.querySelector('.dashboardContainer') as HTMLDivElement)?.offsetTop || 0;
- const globalNavTopOffset =
- (document.querySelector('#app-fixed-viewport') as HTMLDivElement)?.offsetTop || 0;
+ const dashboardContainerTopOffset = dashboardContainer?.offsetTop || 0;
+ const globalNavTopOffset = appFixedViewport?.offsetTop || 0;
const focusStyles = blurPanel
? css`
diff --git a/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx
index 027d2aee62b15..51f414bfcc298 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx
@@ -41,7 +41,7 @@ export const useDebouncedWidthObserver = (skipDebounce = false, wait = 100) => {
return { ref, width };
};
-export const DashboardViewport = () => {
+export const DashboardViewport = ({ dashboardContainer }: { dashboardContainer?: HTMLElement }) => {
const dashboardApi = useDashboardApi();
const [hasControls, setHasControls] = useState(false);
const [
@@ -160,7 +160,9 @@ export const DashboardViewport = () => {
otherwise, there is a race condition where the panels can end up being squashed
TODO only render when dashboardInitialized
*/}
- {viewportWidth !== 0 && }
+ {viewportWidth !== 0 && (
+
+ )}
);
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
index e21a2f94bfc51..99f4fb7c2fa90 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
@@ -470,7 +470,7 @@ export class DashboardContainer
coreStart={{ chrome: coreServices.chrome, customBranding: coreServices.customBranding }}
>
-
+
,
diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts
index e3d67ca10716b..7762e7da0da96 100644
--- a/src/plugins/dashboard/server/plugin.ts
+++ b/src/plugins/dashboard/server/plugin.ts
@@ -75,6 +75,8 @@ export class DashboardPlugin
},
});
+ plugins.contentManagement.favorites.registerFavoriteType('dashboard');
+
if (plugins.taskManager) {
initializeDashboardTelemetryTask(this.logger, core, plugins.taskManager, plugins.embeddable);
}
diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json
index 3e95675ea64c3..1bf6827433b66 100644
--- a/src/plugins/dashboard/tsconfig.json
+++ b/src/plugins/dashboard/tsconfig.json
@@ -81,6 +81,7 @@
"@kbn/core-custom-branding-browser-mocks",
"@kbn/core-mount-utils-browser",
"@kbn/visualization-utils",
+ "@kbn/core-rendering-browser",
],
"exclude": ["target/**/*"]
}
diff --git a/src/plugins/esql/kibana.jsonc b/src/plugins/esql/kibana.jsonc
index 6ee732ef79f5a..2f2e765f0b774 100644
--- a/src/plugins/esql/kibana.jsonc
+++ b/src/plugins/esql/kibana.jsonc
@@ -10,16 +10,19 @@
"browser": true,
"optionalPlugins": [
"indexManagement",
- "fieldsMetadata"
+ "fieldsMetadata",
+ "usageCollection"
],
"requiredPlugins": [
"data",
"expressions",
"dataViews",
"uiActions",
+ "contentManagement"
],
"requiredBundles": [
"kibanaReact",
+ "kibanaUtils",
]
}
}
diff --git a/src/plugins/esql/public/kibana_services.ts b/src/plugins/esql/public/kibana_services.ts
index ae6eca13715f5..3ada58d7c2aec 100644
--- a/src/plugins/esql/public/kibana_services.ts
+++ b/src/plugins/esql/public/kibana_services.ts
@@ -11,8 +11,10 @@ import { BehaviorSubject } from 'rxjs';
import type { CoreStart } from '@kbn/core/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
+import type { Storage } from '@kbn/kibana-utils-plugin/public';
import type { IndexManagementPluginSetup } from '@kbn/index-management-shared-types';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
+import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
export let core: CoreStart;
@@ -20,8 +22,10 @@ interface ServiceDeps {
core: CoreStart;
dataViews: DataViewsPublicPluginStart;
expressions: ExpressionsStart;
+ storage: Storage;
indexManagementApiService?: IndexManagementPluginSetup['apiService'];
fieldsMetadata?: FieldsMetadataPublicStart;
+ usageCollection?: UsageCollectionStart;
}
const servicesReady$ = new BehaviorSubject(undefined);
@@ -41,15 +45,19 @@ export const setKibanaServices = (
kibanaCore: CoreStart,
dataViews: DataViewsPublicPluginStart,
expressions: ExpressionsStart,
+ storage: Storage,
indexManagement?: IndexManagementPluginSetup,
- fieldsMetadata?: FieldsMetadataPublicStart
+ fieldsMetadata?: FieldsMetadataPublicStart,
+ usageCollection?: UsageCollectionStart
) => {
core = kibanaCore;
servicesReady$.next({
core,
dataViews,
expressions,
+ storage,
indexManagementApiService: indexManagement?.apiService,
fieldsMetadata,
+ usageCollection,
});
};
diff --git a/src/plugins/esql/public/plugin.ts b/src/plugins/esql/public/plugin.ts
index ca75c27eccdca..99199d21c1ef8 100755
--- a/src/plugins/esql/public/plugin.ts
+++ b/src/plugins/esql/public/plugin.ts
@@ -14,6 +14,8 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { IndexManagementPluginSetup } from '@kbn/index-management-shared-types';
import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
+import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
+import { Storage } from '@kbn/kibana-utils-plugin/public';
import {
updateESQLQueryTrigger,
UpdateESQLQueryAction,
@@ -27,6 +29,7 @@ interface EsqlPluginStart {
uiActions: UiActionsStart;
data: DataPublicPluginStart;
fieldsMetadata: FieldsMetadataPublicStart;
+ usageCollection?: UsageCollectionStart;
}
interface EsqlPluginSetup {
@@ -47,11 +50,20 @@ export class EsqlPlugin implements Plugin<{}, void> {
public start(
core: CoreStart,
- { dataViews, expressions, data, uiActions, fieldsMetadata }: EsqlPluginStart
+ { dataViews, expressions, data, uiActions, fieldsMetadata, usageCollection }: EsqlPluginStart
): void {
+ const storage = new Storage(localStorage);
const appendESQLAction = new UpdateESQLQueryAction(data);
uiActions.addTriggerAction(UPDATE_ESQL_QUERY_TRIGGER, appendESQLAction);
- setKibanaServices(core, dataViews, expressions, this.indexManagement, fieldsMetadata);
+ setKibanaServices(
+ core,
+ dataViews,
+ expressions,
+ storage,
+ this.indexManagement,
+ fieldsMetadata,
+ usageCollection
+ );
}
public stop() {}
diff --git a/src/plugins/esql/server/plugin.ts b/src/plugins/esql/server/plugin.ts
index acddcb35b6ca1..a227c8e95b4af 100644
--- a/src/plugins/esql/server/plugin.ts
+++ b/src/plugins/esql/server/plugin.ts
@@ -8,11 +8,21 @@
*/
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
+import { schema } from '@kbn/config-schema';
+import { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
import { getUiSettings } from './ui_settings';
export class EsqlServerPlugin implements Plugin {
- public setup(core: CoreSetup) {
+ public setup(core: CoreSetup, plugins: { contentManagement: ContentManagementServerSetup }) {
core.uiSettings.register(getUiSettings());
+
+ plugins.contentManagement.favorites.registerFavoriteType('esql_query', {
+ typeMetadataSchema: schema.object({
+ queryString: schema.string(),
+ createdAt: schema.string(),
+ status: schema.string(),
+ }),
+ });
return {};
}
diff --git a/src/plugins/esql/tsconfig.json b/src/plugins/esql/tsconfig.json
index 85503fd846b4c..2f9bd7f0883b3 100644
--- a/src/plugins/esql/tsconfig.json
+++ b/src/plugins/esql/tsconfig.json
@@ -22,7 +22,10 @@
"@kbn/ui-actions-plugin",
"@kbn/data-plugin",
"@kbn/es-query",
- "@kbn/fields-metadata-plugin"
+ "@kbn/fields-metadata-plugin",
+ "@kbn/usage-collection-plugin",
+ "@kbn/content-management-plugin",
+ "@kbn/kibana-utils-plugin",
],
"exclude": [
"target/**/*",
diff --git a/src/plugins/ui_actions/public/actions/action_internal.test.ts b/src/plugins/ui_actions/public/actions/action_internal.test.ts
index 5029811e80523..8bb0aadcc4677 100644
--- a/src/plugins/ui_actions/public/actions/action_internal.test.ts
+++ b/src/plugins/ui_actions/public/actions/action_internal.test.ts
@@ -20,4 +20,42 @@ describe('ActionInternal', () => {
const action = new ActionInternal(defaultActionDef);
expect(action.id).toBe('test-action');
});
+
+ describe('displays toasts when execute function throws', () => {
+ const addWarningMock = jest.fn();
+ beforeAll(() => {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ require('../services').getNotifications = () => ({
+ toasts: {
+ addWarning: addWarningMock,
+ },
+ });
+ });
+
+ beforeEach(() => {
+ addWarningMock.mockReset();
+ });
+
+ test('execute function is sync', async () => {
+ const action = new ActionInternal({
+ id: 'test-action',
+ execute: () => {
+ throw new Error('');
+ },
+ });
+ await action.execute({});
+ expect(addWarningMock).toBeCalledTimes(1);
+ });
+
+ test('execute function is async', async () => {
+ const action = new ActionInternal({
+ id: 'test-action',
+ execute: async () => {
+ throw new Error('');
+ },
+ });
+ await action.execute({});
+ expect(addWarningMock).toBeCalledTimes(1);
+ });
+ });
});
diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts
index d9091551b87a1..ccef920ecc465 100644
--- a/src/plugins/ui_actions/public/actions/action_internal.ts
+++ b/src/plugins/ui_actions/public/actions/action_internal.ts
@@ -9,7 +9,9 @@
import * as React from 'react';
import type { Presentable, PresentableGrouping } from '@kbn/ui-actions-browser/src/types';
+import { i18n } from '@kbn/i18n';
import { Action, ActionDefinition, ActionMenuItemProps } from './action';
+import { getNotifications } from '../services';
/**
* @internal
@@ -45,8 +47,17 @@ export class ActionInternal
}
}
- public execute(context: Context) {
- return this.definition.execute(context);
+ public async execute(context: Context) {
+ try {
+ return await this.definition.execute(context);
+ } catch (e) {
+ getNotifications()?.toasts.addWarning(
+ i18n.translate('uiActions.execute.unhandledErrorMsg', {
+ defaultMessage: `Unable to execute action, error: {errorMessage}`,
+ values: { errorMessage: e.message },
+ })
+ );
+ }
}
public getIconType(context: Context): string | undefined {
diff --git a/src/plugins/ui_actions/public/plugin.ts b/src/plugins/ui_actions/public/plugin.ts
index 04461f15a6a69..988ef1116e715 100644
--- a/src/plugins/ui_actions/public/plugin.ts
+++ b/src/plugins/ui_actions/public/plugin.ts
@@ -16,7 +16,7 @@ import {
addPanelMenuTrigger,
} from '@kbn/ui-actions-browser/src/triggers';
import { UiActionsService } from './service';
-import { setAnalytics, setI18n, setTheme } from './services';
+import { setAnalytics, setI18n, setNotifications, setTheme } from './services';
export type UiActionsPublicSetup = Pick<
UiActionsService,
@@ -60,6 +60,7 @@ export class UiActionsPlugin
public start(core: CoreStart): UiActionsPublicStart {
setAnalytics(core.analytics);
setI18n(core.i18n);
+ setNotifications(core.notifications);
setTheme(core.theme);
return this.service;
}
diff --git a/src/plugins/ui_actions/public/services.ts b/src/plugins/ui_actions/public/services.ts
index abbfcc1feb944..ccb9520c3bcfb 100644
--- a/src/plugins/ui_actions/public/services.ts
+++ b/src/plugins/ui_actions/public/services.ts
@@ -7,9 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { AnalyticsServiceStart, I18nStart, ThemeServiceSetup } from '@kbn/core/public';
+import { AnalyticsServiceStart, CoreStart, I18nStart, ThemeServiceSetup } from '@kbn/core/public';
import { createGetterSetter } from '@kbn/kibana-utils-plugin/public';
export const [getAnalytics, setAnalytics] = createGetterSetter('Analytics');
export const [getI18n, setI18n] = createGetterSetter('I18n');
+export const [getNotifications, setNotifications] =
+ createGetterSetter('Notifications');
export const [getTheme, setTheme] = createGetterSetter('Theme');
diff --git a/test/functional/apps/console/_onboarding_tour.ts b/test/functional/apps/console/_onboarding_tour.ts
index 330498cb7b5ec..1fc47a70d14b0 100644
--- a/test/functional/apps/console/_onboarding_tour.ts
+++ b/test/functional/apps/console/_onboarding_tour.ts
@@ -10,6 +10,9 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
+// The euiTour shows with a small delay, so with 1s we should be safe
+const DELAY_FOR = 1000;
+
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const browser = getService('browser');
@@ -40,22 +43,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await isTourStepOpen('filesTourStep')).to.be(false);
};
+ const waitUntilFinishedLoading = async () => {
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.common.sleep(DELAY_FOR);
+ };
+
it('displays all five steps in the tour', async () => {
+ const andWaitFor = DELAY_FOR;
+ await waitUntilFinishedLoading();
+
log.debug('on Shell tour step');
expect(await isTourStepOpen('shellTourStep')).to.be(true);
- await PageObjects.console.clickNextTourStep();
+ await PageObjects.console.clickNextTourStep(andWaitFor);
log.debug('on Editor tour step');
expect(await isTourStepOpen('editorTourStep')).to.be(true);
- await PageObjects.console.clickNextTourStep();
+ await PageObjects.console.clickNextTourStep(andWaitFor);
log.debug('on History tour step');
expect(await isTourStepOpen('historyTourStep')).to.be(true);
- await PageObjects.console.clickNextTourStep();
+ await PageObjects.console.clickNextTourStep(andWaitFor);
log.debug('on Config tour step');
expect(await isTourStepOpen('configTourStep')).to.be(true);
- await PageObjects.console.clickNextTourStep();
+ await PageObjects.console.clickNextTourStep(andWaitFor);
log.debug('on Files tour step');
expect(await isTourStepOpen('filesTourStep')).to.be(true);
@@ -73,10 +84,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// Tour should reset after clearing local storage
await browser.clearLocalStorage();
await browser.refresh();
+
+ await waitUntilFinishedLoading();
expect(await isTourStepOpen('shellTourStep')).to.be(true);
});
it('skipping the tour hides the tour steps', async () => {
+ await waitUntilFinishedLoading();
+
expect(await isTourStepOpen('shellTourStep')).to.be(true);
expect(await testSubjects.exists('consoleSkipTourButton')).to.be(true);
await PageObjects.console.clickSkipTour();
@@ -90,6 +105,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('allows re-running the tour', async () => {
+ await waitUntilFinishedLoading();
+
await PageObjects.console.skipTourIfExists();
// Verify that tour is hiddern
@@ -100,6 +117,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.console.clickRerunTour();
// Verify that first tour step is visible
+ await waitUntilFinishedLoading();
expect(await isTourStepOpen('shellTourStep')).to.be(true);
});
});
diff --git a/test/functional/apps/discover/esql/_esql_view.ts b/test/functional/apps/discover/esql/_esql_view.ts
index 6c91899c9ebc5..b1fd957f97a6d 100644
--- a/test/functional/apps/discover/esql/_esql_view.ts
+++ b/test/functional/apps/discover/esql/_esql_view.ts
@@ -401,12 +401,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('ESQLEditor-toggle-query-history-button');
const historyItems = await esql.getHistoryItems();
- log.debug(historyItems);
- const queryAdded = historyItems.some((item) => {
- return item[1] === 'FROM logstash-* | LIMIT 10';
- });
-
- expect(queryAdded).to.be(true);
+ await esql.isQueryPresentInTable('FROM logstash-* | LIMIT 10', historyItems);
});
it('updating the query should add this to the history', async () => {
@@ -423,12 +418,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('ESQLEditor-toggle-query-history-button');
const historyItems = await esql.getHistoryItems();
- log.debug(historyItems);
- const queryAdded = historyItems.some((item) => {
- return item[1] === 'from logstash-* | limit 100 | drop @timestamp';
- });
-
- expect(queryAdded).to.be(true);
+ await esql.isQueryPresentInTable(
+ 'from logstash-* | limit 100 | drop @timestamp',
+ historyItems
+ );
});
it('should select a query from the history and submit it', async () => {
@@ -442,12 +435,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await esql.clickHistoryItem(1);
const historyItems = await esql.getHistoryItems();
- log.debug(historyItems);
- const queryAdded = historyItems.some((item) => {
- return item[1] === 'from logstash-* | limit 100 | drop @timestamp';
- });
-
- expect(queryAdded).to.be(true);
+ await esql.isQueryPresentInTable(
+ 'from logstash-* | limit 100 | drop @timestamp',
+ historyItems
+ );
});
it('should add a failed query to the history', async () => {
diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts
index 87308d24fd8c4..a80f3426e256e 100644
--- a/test/functional/page_objects/console_page.ts
+++ b/test/functional/page_objects/console_page.ts
@@ -276,8 +276,12 @@ export class ConsolePageObject extends FtrService {
await this.testSubjects.click('consoleSkipTourButton');
}
- public async clickNextTourStep() {
+ public async clickNextTourStep(andWaitFor: number = 0) {
await this.testSubjects.click('consoleNextTourStepButton');
+
+ if (andWaitFor) {
+ await this.common.sleep(andWaitFor);
+ }
}
public async clickCompleteTour() {
diff --git a/test/functional/services/esql.ts b/test/functional/services/esql.ts
index c144c6e8993be..9a2bd8149563e 100644
--- a/test/functional/services/esql.ts
+++ b/test/functional/services/esql.ts
@@ -8,6 +8,7 @@
*/
import expect from '@kbn/expect';
+import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services';
import { FtrService } from '../ftr_provider_context';
export class ESQLService extends FtrService {
@@ -20,9 +21,28 @@ export class ESQLService extends FtrService {
expect(await codeEditor.getAttribute('innerText')).to.contain(statement);
}
+ public async isQueryPresentInTable(query: string, items: string[][]) {
+ const queryAdded = items.some((item) => {
+ return item[2] === query;
+ });
+
+ expect(queryAdded).to.be(true);
+ }
+
public async getHistoryItems(): Promise {
const queryHistory = await this.testSubjects.find('ESQLEditor-queryHistory');
- const tableBody = await this.retry.try(async () => queryHistory.findByTagName('tbody'));
+ const tableItems = await this.getStarredHistoryTableItems(queryHistory);
+ return tableItems;
+ }
+
+ public async getStarredItems(): Promise {
+ const starredQueries = await this.testSubjects.find('ESQLEditor-starredQueries');
+ const tableItems = await this.getStarredHistoryTableItems(starredQueries);
+ return tableItems;
+ }
+
+ private async getStarredHistoryTableItems(element: WebElementWrapper): Promise {
+ const tableBody = await this.retry.try(async () => element.findByTagName('tbody'));
const $ = await tableBody.parseDomContent();
return $('tr')
.toArray()
@@ -44,6 +64,20 @@ export class ESQLService extends FtrService {
});
}
+ public async getStarredItem(rowIndex = 0) {
+ const queryHistory = await this.testSubjects.find('ESQLEditor-starredQueries');
+ const tableBody = await this.retry.try(async () => queryHistory.findByTagName('tbody'));
+ const rows = await this.retry.try(async () => tableBody.findAllByTagName('tr'));
+
+ return rows[rowIndex];
+ }
+
+ public async clickStarredItem(rowIndex = 0) {
+ const row = await this.getStarredItem(rowIndex);
+ const toggle = await row.findByTestSubject('ESQLEditor-history-starred-queries-run-button');
+ await toggle.click();
+ }
+
public async getHistoryItem(rowIndex = 0) {
const queryHistory = await this.testSubjects.find('ESQLEditor-queryHistory');
const tableBody = await this.retry.try(async () => queryHistory.findByTagName('tbody'));
@@ -54,7 +88,7 @@ export class ESQLService extends FtrService {
public async clickHistoryItem(rowIndex = 0) {
const row = await this.getHistoryItem(rowIndex);
- const toggle = await row.findByTestSubject('ESQLEditor-queryHistory-runQuery-button');
+ const toggle = await row.findByTestSubject('ESQLEditor-history-starred-queries-run-button');
await toggle.click();
}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index f6aaa2ee0ac7f..26fe060916a92 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -206,6 +206,8 @@
"@kbn/content-management-content-insights-server/*": ["packages/content-management/content_insights/content_insights_server/*"],
"@kbn/content-management-examples-plugin": ["examples/content_management_examples"],
"@kbn/content-management-examples-plugin/*": ["examples/content_management_examples/*"],
+ "@kbn/content-management-favorites-common": ["packages/content-management/favorites/favorites_common"],
+ "@kbn/content-management-favorites-common/*": ["packages/content-management/favorites/favorites_common/*"],
"@kbn/content-management-favorites-public": ["packages/content-management/favorites/favorites_public"],
"@kbn/content-management-favorites-public/*": ["packages/content-management/favorites/favorites_public/*"],
"@kbn/content-management-favorites-server": ["packages/content-management/favorites/favorites_server"],
@@ -542,6 +544,8 @@
"@kbn/core-preboot-server-mocks/*": ["packages/core/preboot/core-preboot-server-mocks/*"],
"@kbn/core-provider-plugin": ["test/plugin_functional/plugins/core_provider_plugin"],
"@kbn/core-provider-plugin/*": ["test/plugin_functional/plugins/core_provider_plugin/*"],
+ "@kbn/core-rendering-browser": ["packages/core/rendering/core-rendering-browser"],
+ "@kbn/core-rendering-browser/*": ["packages/core/rendering/core-rendering-browser/*"],
"@kbn/core-rendering-browser-internal": ["packages/core/rendering/core-rendering-browser-internal"],
"@kbn/core-rendering-browser-internal/*": ["packages/core/rendering/core-rendering-browser-internal/*"],
"@kbn/core-rendering-browser-mocks": ["packages/core/rendering/core-rendering-browser-mocks"],
diff --git a/x-pack/packages/kbn-elastic-assistant-common/constants.ts b/x-pack/packages/kbn-elastic-assistant-common/constants.ts
index 49db6c295a51a..7a884936d04e2 100755
--- a/x-pack/packages/kbn-elastic-assistant-common/constants.ts
+++ b/x-pack/packages/kbn-elastic-assistant-common/constants.ts
@@ -55,3 +55,8 @@ export const ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_INDICES_URL =
export const ELASTIC_AI_ASSISTANT_EVALUATE_URL =
`${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/evaluate` as const;
+
+// Defend insights
+export const DEFEND_INSIGHTS_TOOL_ID = 'defend-insights';
+export const DEFEND_INSIGHTS = `${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/defend_insights`;
+export const DEFEND_INSIGHTS_BY_ID = `${DEFEND_INSIGHTS}/{id}`;
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts
index d883dfe98d564..0e204b4b949ea 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts
@@ -20,4 +20,5 @@ export type AssistantFeatureKey = keyof AssistantFeatures;
*/
export const defaultAssistantFeatures = Object.freeze({
assistantModelEvaluation: false,
+ defendInsights: false,
});
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts
index 0f8b6235d7dc9..8777e8d728279 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts
@@ -19,4 +19,5 @@ import { z } from '@kbn/zod';
export type GetCapabilitiesResponse = z.infer;
export const GetCapabilitiesResponse = z.object({
assistantModelEvaluation: z.boolean(),
+ defendInsights: z.boolean(),
});
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml
index a042abd391796..e9b6ca9697256 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml
@@ -22,8 +22,11 @@ paths:
properties:
assistantModelEvaluation:
type: boolean
+ defendInsights:
+ type: boolean
required:
- assistantModelEvaluation
+ - defendInsights
'400':
description: Generic Error
content:
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/common_attributes.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/common_attributes.gen.ts
new file mode 100644
index 0000000000000..e070c3129e192
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/common_attributes.gen.ts
@@ -0,0 +1,217 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ *
+ * info:
+ * title: Common Defend Insights Attributes
+ * version: not applicable
+ */
+
+import { z } from '@kbn/zod';
+
+import { NonEmptyString, User } from '../common_attributes.gen';
+import { Replacements, ApiConfig } from '../conversations/common_attributes.gen';
+
+/**
+ * A Defend insight event
+ */
+export type DefendInsightEvent = z.infer;
+export const DefendInsightEvent = z.object({
+ /**
+ * The event's ID
+ */
+ id: z.string(),
+ /**
+ * The endpoint's ID
+ */
+ endpointId: z.string(),
+ /**
+ * The value of the event
+ */
+ value: z.string(),
+});
+
+/**
+ * The insight type (ie. incompatible_antivirus)
+ */
+export type DefendInsightType = z.infer;
+export const DefendInsightType = z.enum(['incompatible_antivirus', 'noisy_process_tree']);
+export type DefendInsightTypeEnum = typeof DefendInsightType.enum;
+export const DefendInsightTypeEnum = DefendInsightType.enum;
+
+/**
+ * A Defend insight generated from endpoint events
+ */
+export type DefendInsight = z.infer;
+export const DefendInsight = z.object({
+ /**
+ * The group category of the events (ie. Windows Defender)
+ */
+ group: z.string(),
+ /**
+ * An array of event objects
+ */
+ events: z.array(DefendInsightEvent).optional(),
+});
+
+/**
+ * Array of Defend insights
+ */
+export type DefendInsights = z.infer;
+export const DefendInsights = z.array(DefendInsight);
+
+/**
+ * The status of the Defend insight.
+ */
+export type DefendInsightStatus = z.infer;
+export const DefendInsightStatus = z.enum(['running', 'succeeded', 'failed', 'canceled']);
+export type DefendInsightStatusEnum = typeof DefendInsightStatus.enum;
+export const DefendInsightStatusEnum = DefendInsightStatus.enum;
+
+/**
+ * Run durations for the Defend insight
+ */
+export type DefendInsightGenerationInterval = z.infer;
+export const DefendInsightGenerationInterval = z.object({
+ /**
+ * The time the Defend insight was generated
+ */
+ date: z.string(),
+ /**
+ * The duration of the Defend insight generation
+ */
+ durationMs: z.number().int(),
+});
+
+export type DefendInsightsResponse = z.infer;
+export const DefendInsightsResponse = z.object({
+ id: NonEmptyString,
+ timestamp: NonEmptyString.optional(),
+ /**
+ * The last time the Defend insight was updated.
+ */
+ updatedAt: z.string(),
+ /**
+ * The last time the Defend insight was viewed in the browser.
+ */
+ lastViewedAt: z.string(),
+ /**
+ * The number of events in the context.
+ */
+ eventsContextCount: z.number().int().optional(),
+ /**
+ * The time the Defend insight was created.
+ */
+ createdAt: z.string(),
+ replacements: Replacements.optional(),
+ users: z.array(User),
+ /**
+ * The status of the Defend insight.
+ */
+ status: DefendInsightStatus,
+ endpointIds: z.array(NonEmptyString),
+ insightType: DefendInsightType,
+ /**
+ * The Defend insights.
+ */
+ insights: DefendInsights,
+ /**
+ * LLM API configuration.
+ */
+ apiConfig: ApiConfig,
+ /**
+ * Kibana space
+ */
+ namespace: z.string(),
+ /**
+ * The backing index required for update requests.
+ */
+ backingIndex: z.string(),
+ /**
+ * The most 5 recent generation intervals
+ */
+ generationIntervals: z.array(DefendInsightGenerationInterval),
+ /**
+ * The average generation interval in milliseconds
+ */
+ averageIntervalMs: z.number().int(),
+ /**
+ * The reason for a status of failed.
+ */
+ failureReason: z.string().optional(),
+});
+
+export type DefendInsightUpdateProps = z.infer;
+export const DefendInsightUpdateProps = z.object({
+ id: NonEmptyString,
+ /**
+ * LLM API configuration.
+ */
+ apiConfig: ApiConfig.optional(),
+ /**
+ * The number of events in the context.
+ */
+ eventsContextCount: z.number().int().optional(),
+ /**
+ * The Defend insights.
+ */
+ insights: DefendInsights.optional(),
+ /**
+ * The status of the Defend insight.
+ */
+ status: DefendInsightStatus.optional(),
+ replacements: Replacements.optional(),
+ /**
+ * The most 5 recent generation intervals
+ */
+ generationIntervals: z.array(DefendInsightGenerationInterval).optional(),
+ /**
+ * The backing index required for update requests.
+ */
+ backingIndex: z.string(),
+ /**
+ * The reason for a status of failed.
+ */
+ failureReason: z.string().optional(),
+ /**
+ * The last time the Defend insight was viewed in the browser.
+ */
+ lastViewedAt: z.string().optional(),
+});
+
+export type DefendInsightsUpdateProps = z.infer;
+export const DefendInsightsUpdateProps = z.array(DefendInsightUpdateProps);
+
+export type DefendInsightCreateProps = z.infer;
+export const DefendInsightCreateProps = z.object({
+ /**
+ * The Defend insight id.
+ */
+ id: z.string().optional(),
+ /**
+ * The status of the Defend insight.
+ */
+ status: DefendInsightStatus,
+ /**
+ * The number of events in the context.
+ */
+ eventsContextCount: z.number().int().optional(),
+ endpointIds: z.array(NonEmptyString),
+ insightType: DefendInsightType,
+ /**
+ * The Defend insights.
+ */
+ insights: DefendInsights,
+ /**
+ * LLM API configuration.
+ */
+ apiConfig: ApiConfig,
+ replacements: Replacements.optional(),
+});
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/common_attributes.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/common_attributes.schema.yaml
new file mode 100644
index 0000000000000..5c27449c7d346
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/common_attributes.schema.yaml
@@ -0,0 +1,224 @@
+openapi: 3.0.0
+info:
+ title: Common Defend Insights Attributes
+ version: 'not applicable'
+paths: {}
+components:
+ x-codegen-enabled: true
+ schemas:
+ DefendInsightEvent:
+ type: object
+ description: A Defend insight event
+ required:
+ - 'id'
+ - 'endpointId'
+ - 'value'
+ properties:
+ id:
+ description: The event's ID
+ type: string
+ endpointId:
+ description: The endpoint's ID
+ type: string
+ value:
+ description: The value of the event
+ type: string
+
+ DefendInsightType:
+ description: The insight type (ie. incompatible_antivirus)
+ type: string
+ enum:
+ - incompatible_antivirus
+ - noisy_process_tree
+
+ DefendInsight:
+ type: object
+ description: A Defend insight generated from endpoint events
+ required:
+ - 'group'
+ properties:
+ group:
+ description: The group category of the events (ie. Windows Defender)
+ type: string
+ events:
+ description: An array of event objects
+ type: array
+ items:
+ $ref: '#/components/schemas/DefendInsightEvent'
+
+ DefendInsights:
+ type: array
+ description: Array of Defend insights
+ items:
+ $ref: '#/components/schemas/DefendInsight'
+
+ DefendInsightsResponse:
+ type: object
+ required:
+ - apiConfig
+ - id
+ - createdAt
+ - updatedAt
+ - lastViewedAt
+ - users
+ - namespace
+ - endpointIds
+ - insightType
+ - insights
+ - status
+ - backingIndex
+ - generationIntervals
+ - averageIntervalMs
+ properties:
+ id:
+ $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
+ 'timestamp':
+ $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
+ updatedAt:
+ description: The last time the Defend insight was updated.
+ type: string
+ lastViewedAt:
+ description: The last time the Defend insight was viewed in the browser.
+ type: string
+ eventsContextCount:
+ type: integer
+ description: The number of events in the context.
+ createdAt:
+ description: The time the Defend insight was created.
+ type: string
+ replacements:
+ $ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
+ users:
+ type: array
+ items:
+ $ref: '../common_attributes.schema.yaml#/components/schemas/User'
+ status:
+ $ref: '#/components/schemas/DefendInsightStatus'
+ description: The status of the Defend insight.
+ endpointIds:
+ type: array
+ items:
+ $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
+ insightType:
+ $ref: '#/components/schemas/DefendInsightType'
+ insights:
+ $ref: '#/components/schemas/DefendInsights'
+ description: The Defend insights.
+ apiConfig:
+ $ref: '../conversations/common_attributes.schema.yaml#/components/schemas/ApiConfig'
+ description: LLM API configuration.
+ namespace:
+ type: string
+ description: Kibana space
+ backingIndex:
+ type: string
+ description: The backing index required for update requests.
+ generationIntervals:
+ type: array
+ description: The most 5 recent generation intervals
+ items:
+ $ref: '#/components/schemas/DefendInsightGenerationInterval'
+ averageIntervalMs:
+ type: integer
+ description: The average generation interval in milliseconds
+ failureReason:
+ type: string
+ description: The reason for a status of failed.
+
+ DefendInsightGenerationInterval:
+ type: object
+ description: Run durations for the Defend insight
+ required:
+ - 'date'
+ - 'durationMs'
+ properties:
+ date:
+ description: The time the Defend insight was generated
+ type: string
+ durationMs:
+ description: The duration of the Defend insight generation
+ type: integer
+
+ DefendInsightStatus:
+ type: string
+ description: The status of the Defend insight.
+ enum:
+ - running
+ - succeeded
+ - failed
+ - canceled
+
+ DefendInsightUpdateProps:
+ type: object
+ required:
+ - id
+ - backingIndex
+ properties:
+ id:
+ $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
+ apiConfig:
+ $ref: '../conversations/common_attributes.schema.yaml#/components/schemas/ApiConfig'
+ description: LLM API configuration.
+ eventsContextCount:
+ type: integer
+ description: The number of events in the context.
+ insights:
+ $ref: '#/components/schemas/DefendInsights'
+ description: The Defend insights.
+ status:
+ $ref: '#/components/schemas/DefendInsightStatus'
+ description: The status of the Defend insight.
+ replacements:
+ $ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
+ generationIntervals:
+ type: array
+ description: The most 5 recent generation intervals
+ items:
+ $ref: '#/components/schemas/DefendInsightGenerationInterval'
+ backingIndex:
+ type: string
+ description: The backing index required for update requests.
+ failureReason:
+ type: string
+ description: The reason for a status of failed.
+ lastViewedAt:
+ description: The last time the Defend insight was viewed in the browser.
+ type: string
+
+ DefendInsightsUpdateProps:
+ type: array
+ items:
+ $ref: '#/components/schemas/DefendInsightUpdateProps'
+
+ DefendInsightCreateProps:
+ type: object
+ required:
+ - endpointIds
+ - insightType
+ - insights
+ - apiConfig
+ - status
+ properties:
+ id:
+ type: string
+ description: The Defend insight id.
+ status:
+ $ref: '#/components/schemas/DefendInsightStatus'
+ description: The status of the Defend insight.
+ eventsContextCount:
+ type: integer
+ description: The number of events in the context.
+ endpointIds:
+ type: array
+ items:
+ $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
+ insightType:
+ $ref: '#/components/schemas/DefendInsightType'
+ insights:
+ $ref: '#/components/schemas/DefendInsights'
+ description: The Defend insights.
+ apiConfig:
+ $ref: '../conversations/common_attributes.schema.yaml#/components/schemas/ApiConfig'
+ description: LLM API configuration.
+ replacements:
+ $ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insight_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insight_route.gen.ts
new file mode 100644
index 0000000000000..fafaca8f48ead
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insight_route.gen.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.
+ */
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ *
+ * info:
+ * title: Get Defend Insight API endpoint
+ * version: 1
+ */
+
+import { z } from '@kbn/zod';
+
+import { NonEmptyString } from '../common_attributes.gen';
+import { DefendInsightsResponse } from './common_attributes.gen';
+
+export type DefendInsightGetRequestParams = z.infer;
+export const DefendInsightGetRequestParams = z.object({
+ /**
+ * The Defend insight id
+ */
+ id: NonEmptyString,
+});
+export type DefendInsightGetRequestParamsInput = z.input;
+
+export type DefendInsightGetResponse = z.infer;
+export const DefendInsightGetResponse = z.object({
+ data: DefendInsightsResponse.nullable().optional(),
+});
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insight_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insight_route.schema.yaml
new file mode 100644
index 0000000000000..2684bf53cf87b
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insight_route.schema.yaml
@@ -0,0 +1,45 @@
+openapi: 3.0.3
+info:
+ title: Get Defend Insight API endpoint
+ version: '1'
+paths:
+ /internal/elastic_assistant/defend_insights/{id}:
+ get:
+ x-codegen-enabled: true
+ x-labels: [ess, serverless]
+ operationId: DefendInsightGet
+ description: Get Defend insight by id
+ summary: Get Defend insight data
+ tags:
+ - defend_insights
+ parameters:
+ - name: 'id'
+ in: path
+ required: true
+ description: The Defend insight id
+ schema:
+ $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
+ responses:
+ '200':
+ description: Successful response
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: './common_attributes.schema.yaml#/components/schemas/DefendInsightsResponse'
+ nullable: true
+ '400':
+ description: Generic Error
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ statusCode:
+ type: number
+ error:
+ type: string
+ message:
+ type: string
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insights_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insights_route.gen.ts
new file mode 100644
index 0000000000000..0a2f3d618a869
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insights_route.gen.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.
+ */
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ *
+ * info:
+ * title: Get Defend Insights API endpoint
+ * version: 1
+ */
+
+import { z } from '@kbn/zod';
+import { ArrayFromString } from '@kbn/zod-helpers';
+
+import { NonEmptyString } from '../common_attributes.gen';
+import {
+ DefendInsightType,
+ DefendInsightStatus,
+ DefendInsightsResponse,
+} from './common_attributes.gen';
+
+export type DefendInsightsGetRequestQuery = z.infer;
+export const DefendInsightsGetRequestQuery = z.object({
+ /**
+ * The insight ids for which to get Defend insights
+ */
+ ids: ArrayFromString(NonEmptyString).optional(),
+ /**
+ * The connector id for which to get Defend insights
+ */
+ connector_id: NonEmptyString.optional(),
+ /**
+ * The insight type for which to get Defend insights
+ */
+ type: DefendInsightType.optional(),
+ /**
+ * The status for which to get Defend insights
+ */
+ status: DefendInsightStatus.optional(),
+ /**
+ * The endpoint ids for which to get Defend insights
+ */
+ endpoint_ids: ArrayFromString(NonEmptyString).optional(),
+ /**
+ * The number of Defend insights to return
+ */
+ size: z.coerce.number().optional(),
+});
+export type DefendInsightsGetRequestQueryInput = z.input;
+
+export type DefendInsightsGetResponse = z.infer;
+export const DefendInsightsGetResponse = z.object({
+ data: z.array(DefendInsightsResponse),
+});
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insights_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insights_route.schema.yaml
new file mode 100644
index 0000000000000..5d7e0b5358f81
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/get_defend_insights_route.schema.yaml
@@ -0,0 +1,82 @@
+openapi: 3.0.0
+info:
+ title: Get Defend Insights API endpoint
+ version: '1'
+paths:
+ /internal/elastic_assistant/defend_insights:
+ get:
+ x-codegen-enabled: true
+ x-labels: [ess, serverless]
+ operationId: DefendInsightsGet
+ description: Get relevant data for Defend insights
+ summary: Get relevant data for Defend insights
+ tags:
+ - defend_insights
+ parameters:
+ - name: 'ids'
+ in: query
+ required: false
+ description: The insight ids for which to get Defend insights
+ schema:
+ type: array
+ items:
+ $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
+ - name: 'connector_id'
+ in: query
+ required: false
+ description: The connector id for which to get Defend insights
+ schema:
+ $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
+ - name: 'type'
+ in: query
+ required: false
+ description: The insight type for which to get Defend insights
+ schema:
+ $ref: './common_attributes.schema.yaml#/components/schemas/DefendInsightType'
+ - name: 'status'
+ in: query
+ required: false
+ description: The status for which to get Defend insights
+ schema:
+ $ref: './common_attributes.schema.yaml#/components/schemas/DefendInsightStatus'
+ - name: 'endpoint_ids'
+ in: query
+ required: false
+ description: The endpoint ids for which to get Defend insights
+ schema:
+ type: array
+ items:
+ $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
+ - name: 'size'
+ in: query
+ required: false
+ description: The number of Defend insights to return
+ schema:
+ type: number
+ responses:
+ '200':
+ description: Successful response
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - data
+ properties:
+ data:
+ type: array
+ items:
+ $ref: './common_attributes.schema.yaml#/components/schemas/DefendInsightsResponse'
+ '400':
+ description: Generic Error
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ statusCode:
+ type: number
+ error:
+ type: string
+ message:
+ type: string
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/index.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/index.ts
new file mode 100644
index 0000000000000..0518abdf6dcb7
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './common_attributes.gen';
+export * from './get_defend_insight_route.gen';
+export * from './get_defend_insights_route.gen';
+export * from './post_defend_insights_route.gen';
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/post_defend_insights_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/post_defend_insights_route.gen.ts
new file mode 100644
index 0000000000000..cc0ccfeea1980
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/post_defend_insights_route.gen.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.
+ */
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ *
+ * info:
+ * title: Post Defend Insights API endpoint
+ * version: 1
+ */
+
+import { z } from '@kbn/zod';
+
+import { NonEmptyString } from '../common_attributes.gen';
+import { DefendInsightType, DefendInsightsResponse } from './common_attributes.gen';
+import { AnonymizationFieldResponse } from '../anonymization_fields/bulk_crud_anonymization_fields_route.gen';
+import { ApiConfig, Replacements } from '../conversations/common_attributes.gen';
+
+export type DefendInsightsPostRequestBody = z.infer;
+export const DefendInsightsPostRequestBody = z.object({
+ endpointIds: z.array(NonEmptyString),
+ insightType: DefendInsightType,
+ anonymizationFields: z.array(AnonymizationFieldResponse),
+ /**
+ * LLM API configuration.
+ */
+ apiConfig: ApiConfig,
+ langSmithProject: z.string().optional(),
+ langSmithApiKey: z.string().optional(),
+ model: z.string().optional(),
+ replacements: Replacements.optional(),
+ subAction: z.enum(['invokeAI', 'invokeStream']),
+});
+export type DefendInsightsPostRequestBodyInput = z.input;
+
+export type DefendInsightsPostResponse = z.infer;
+export const DefendInsightsPostResponse = DefendInsightsResponse;
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/post_defend_insights_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/post_defend_insights_route.schema.yaml
new file mode 100644
index 0000000000000..87c7cdbb81a8e
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/defend_insights/post_defend_insights_route.schema.yaml
@@ -0,0 +1,77 @@
+openapi: 3.0.0
+info:
+ title: Post Defend Insights API endpoint
+ version: '1'
+components:
+ x-codegen-enabled: true
+
+paths:
+ /internal/elastic_assistant/defend_insights:
+ post:
+ x-codegen-enabled: true
+ x-labels: [ess, serverless]
+ operationId: DefendInsightsPost
+ description: Generate Elastic Defend configuration insights
+ summary: Generate Elastic Defend configuration insights from endpoint events via the Elastic Assistant
+ tags:
+ - defend_insights
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - endpointIds
+ - insightType
+ - apiConfig
+ - anonymizationFields
+ - subAction
+ properties:
+ endpointIds:
+ type: array
+ items:
+ $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
+ insightType:
+ $ref: './common_attributes.schema.yaml#/components/schemas/DefendInsightType'
+ anonymizationFields:
+ type: array
+ items:
+ $ref: '../anonymization_fields/bulk_crud_anonymization_fields_route.schema.yaml#/components/schemas/AnonymizationFieldResponse'
+ apiConfig:
+ $ref: '../conversations/common_attributes.schema.yaml#/components/schemas/ApiConfig'
+ description: LLM API configuration.
+ langSmithProject:
+ type: string
+ langSmithApiKey:
+ type: string
+ model:
+ type: string
+ replacements:
+ $ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
+ subAction:
+ type: string
+ enum:
+ - invokeAI
+ - invokeStream
+ responses:
+ '200':
+ description: Successful response
+ content:
+ application/json:
+ schema:
+ $ref: './common_attributes.schema.yaml#/components/schemas/DefendInsightsResponse'
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ statusCode:
+ type: number
+ error:
+ type: string
+ message:
+ type: string
+
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/index.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/index.ts
index 9233791a870c3..02ac9b7b1ba90 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/index.ts
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/index.ts
@@ -27,6 +27,9 @@ export * from './attack_discovery/get_attack_discovery_route.gen';
export * from './attack_discovery/post_attack_discovery_route.gen';
export * from './attack_discovery/cancel_attack_discovery_route.gen';
+// Defend insight Schemas
+export * from './defend_insights';
+
// Chat Schemas
export * from './chat/post_chat_complete_route.gen';
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/mocks.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/mocks.ts
deleted file mode 100644
index 24a43bd3182df..0000000000000
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/mocks.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { IndexEntryCreateFields } from './common_attributes.gen';
-
-export const indexEntryMock: IndexEntryCreateFields = {
- type: 'index',
- name: 'SpongBotSlackConnector',
- namespace: 'default',
- index: 'spongbot',
- field: 'semantic_text',
- description: "Use this index to search for the user's Slack messages.",
- queryDescription:
- 'The free text search that the user wants to perform over this dataset. So if asking "what are my slack messages from last week about failed tests", the query would be "A test has failed! failing test failed test".',
-};
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/utils/bedrock.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/utils/bedrock.ts
index ab3756d43dc0e..6d503d675796b 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/utils/bedrock.ts
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/utils/bedrock.ts
@@ -15,14 +15,14 @@ import { fromUtf8, toUtf8 } from '@smithy/util-utf8';
* @param {Uint8Array[]} chunks - Array of Uint8Array chunks to be parsed.
* @returns {string} - Parsed string from the Bedrock buffer.
*/
-export const parseBedrockBuffer = (chunks: Uint8Array[], logger: Logger): string => {
+export const parseBedrockBuffer = (chunks: Uint8Array[]): string => {
// Initialize an empty Uint8Array to store the concatenated buffer.
let bedrockBuffer: Uint8Array = new Uint8Array(0);
// Map through each chunk to process the Bedrock buffer.
return chunks
.map((chunk) => {
- const processedChunk = handleBedrockChunk({ chunk, bedrockBuffer, logger });
+ const processedChunk = handleBedrockChunk({ chunk, bedrockBuffer });
bedrockBuffer = processedChunk.bedrockBuffer;
return processedChunk.decodedChunk;
})
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_create_knowledge_base_entry.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_create_knowledge_base_entry.test.tsx
new file mode 100644
index 0000000000000..73d0ddb976861
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_create_knowledge_base_entry.test.tsx
@@ -0,0 +1,109 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { act, renderHook } from '@testing-library/react-hooks';
+import {
+ useCreateKnowledgeBaseEntry,
+ UseCreateKnowledgeBaseEntryParams,
+} from './use_create_knowledge_base_entry';
+import { useInvalidateKnowledgeBaseEntries } from './use_knowledge_base_entries';
+
+jest.mock('./use_knowledge_base_entries', () => ({
+ useInvalidateKnowledgeBaseEntries: jest.fn(),
+}));
+
+jest.mock('@tanstack/react-query', () => ({
+ useMutation: jest.fn().mockImplementation((queryKey, fn, opts) => {
+ return {
+ mutate: async (variables: unknown) => {
+ try {
+ const res = await fn(variables);
+ opts.onSuccess(res);
+ opts.onSettled();
+ return Promise.resolve(res);
+ } catch (e) {
+ opts.onError(e);
+ opts.onSettled();
+ }
+ },
+ };
+ }),
+}));
+
+const http = {
+ post: jest.fn(),
+};
+const toasts = {
+ addError: jest.fn(),
+ addSuccess: jest.fn(),
+};
+const defaultProps = { http, toasts } as unknown as UseCreateKnowledgeBaseEntryParams;
+const defaultArgs = { title: 'Test Entry' };
+describe('useCreateKnowledgeBaseEntry', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should call the mutation function on success', async () => {
+ const invalidateKnowledgeBaseEntries = jest.fn();
+ (useInvalidateKnowledgeBaseEntries as jest.Mock).mockReturnValue(
+ invalidateKnowledgeBaseEntries
+ );
+ http.post.mockResolvedValue({});
+
+ const { result } = renderHook(() => useCreateKnowledgeBaseEntry(defaultProps));
+
+ await act(async () => {
+ // @ts-ignore
+ await result.current.mutate(defaultArgs);
+ });
+
+ expect(http.post).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ body: JSON.stringify(defaultArgs),
+ })
+ );
+ expect(toasts.addSuccess).toHaveBeenCalledWith({
+ title: expect.any(String),
+ });
+ expect(invalidateKnowledgeBaseEntries).toHaveBeenCalled();
+ });
+
+ it('should call the onError function on error', async () => {
+ const error = new Error('Test Error');
+ http.post.mockRejectedValue(error);
+
+ const { result } = renderHook(() => useCreateKnowledgeBaseEntry(defaultProps));
+
+ await act(async () => {
+ // @ts-ignore
+ await result.current.mutate(defaultArgs);
+ });
+
+ expect(toasts.addError).toHaveBeenCalledWith(error, {
+ title: expect.any(String),
+ });
+ });
+
+ it('should call the onSettled function after mutation', async () => {
+ const invalidateKnowledgeBaseEntries = jest.fn();
+ (useInvalidateKnowledgeBaseEntries as jest.Mock).mockReturnValue(
+ invalidateKnowledgeBaseEntries
+ );
+ http.post.mockResolvedValue({});
+
+ const { result } = renderHook(() => useCreateKnowledgeBaseEntry(defaultProps));
+
+ await act(async () => {
+ // @ts-ignore
+ await result.current.mutate(defaultArgs);
+ });
+
+ expect(invalidateKnowledgeBaseEntries).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_delete_knowledge_base_entries.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_delete_knowledge_base_entries.test.tsx
new file mode 100644
index 0000000000000..6003b1f81f435
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_delete_knowledge_base_entries.test.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 { act, renderHook } from '@testing-library/react-hooks';
+import {
+ useDeleteKnowledgeBaseEntries,
+ UseDeleteKnowledgeEntriesParams,
+} from './use_delete_knowledge_base_entries';
+import { useInvalidateKnowledgeBaseEntries } from './use_knowledge_base_entries';
+
+jest.mock('./use_knowledge_base_entries', () => ({
+ useInvalidateKnowledgeBaseEntries: jest.fn(),
+}));
+
+jest.mock('@tanstack/react-query', () => ({
+ useMutation: jest.fn().mockImplementation((queryKey, fn, opts) => {
+ return {
+ mutate: async (variables: unknown) => {
+ try {
+ const res = await fn(variables);
+ opts.onSuccess(res);
+ opts.onSettled();
+ return Promise.resolve(res);
+ } catch (e) {
+ opts.onError(e);
+ opts.onSettled();
+ }
+ },
+ };
+ }),
+}));
+
+const http = {
+ post: jest.fn(),
+};
+const toasts = {
+ addError: jest.fn(),
+};
+const defaultProps = { http, toasts } as unknown as UseDeleteKnowledgeEntriesParams;
+const defaultArgs = { ids: ['1'], query: '' };
+
+describe('useDeleteKnowledgeBaseEntries', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should call the mutation function on success', async () => {
+ const invalidateKnowledgeBaseEntries = jest.fn();
+ (useInvalidateKnowledgeBaseEntries as jest.Mock).mockReturnValue(
+ invalidateKnowledgeBaseEntries
+ );
+ http.post.mockResolvedValue({});
+
+ const { result } = renderHook(() => useDeleteKnowledgeBaseEntries(defaultProps));
+
+ await act(async () => {
+ // @ts-ignore
+ await result.current.mutate(defaultArgs);
+ });
+
+ expect(http.post).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ body: JSON.stringify({ delete: { query: '', ids: ['1'] } }),
+ version: '1',
+ })
+ );
+ expect(invalidateKnowledgeBaseEntries).toHaveBeenCalled();
+ });
+
+ it('should call the onError function on error', async () => {
+ const error = new Error('Test Error');
+ http.post.mockRejectedValue(error);
+
+ const { result } = renderHook(() => useDeleteKnowledgeBaseEntries(defaultProps));
+
+ await act(async () => {
+ // @ts-ignore
+ await result.current.mutate(defaultArgs);
+ });
+
+ expect(toasts.addError).toHaveBeenCalledWith(error, {
+ title: expect.any(String),
+ });
+ });
+
+ it('should call the onSettled function after mutation', async () => {
+ const invalidateKnowledgeBaseEntries = jest.fn();
+ (useInvalidateKnowledgeBaseEntries as jest.Mock).mockReturnValue(
+ invalidateKnowledgeBaseEntries
+ );
+ http.post.mockResolvedValue({});
+
+ const { result } = renderHook(() => useDeleteKnowledgeBaseEntries(defaultProps));
+
+ await act(async () => {
+ // @ts-ignore
+ await result.current.mutate(defaultArgs);
+ });
+
+ expect(invalidateKnowledgeBaseEntries).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_knowledge_base_entries.test.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_knowledge_base_entries.test.ts
new file mode 100644
index 0000000000000..f298258800d35
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_knowledge_base_entries.test.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 { renderHook } from '@testing-library/react-hooks';
+import { useKnowledgeBaseEntries } from './use_knowledge_base_entries';
+import { HttpSetup } from '@kbn/core/public';
+import { IToasts } from '@kbn/core-notifications-browser';
+import { TestProviders } from '../../../../mock/test_providers/test_providers';
+
+describe('useKnowledgeBaseEntries', () => {
+ const httpMock: HttpSetup = {
+ fetch: jest.fn(),
+ } as unknown as HttpSetup;
+ const toastsMock: IToasts = {
+ addError: jest.fn(),
+ } as unknown as IToasts;
+
+ it('fetches knowledge base entries successfully', async () => {
+ (httpMock.fetch as jest.Mock).mockResolvedValue({
+ page: 1,
+ perPage: 100,
+ total: 1,
+ data: [{ id: '1', title: 'Entry 1' }],
+ });
+
+ const { result, waitForNextUpdate } = renderHook(
+ () => useKnowledgeBaseEntries({ http: httpMock, enabled: true }),
+ {
+ wrapper: TestProviders,
+ }
+ );
+ expect(result.current.fetchStatus).toEqual('fetching');
+
+ await waitForNextUpdate();
+
+ expect(result.current.data).toEqual({
+ page: 1,
+ perPage: 100,
+ total: 1,
+ data: [{ id: '1', title: 'Entry 1' }],
+ });
+ });
+
+ it('handles fetch error', async () => {
+ const error = new Error('Fetch error');
+ (httpMock.fetch as jest.Mock).mockRejectedValue(error);
+
+ const { waitForNextUpdate } = renderHook(
+ () => useKnowledgeBaseEntries({ http: httpMock, toasts: toastsMock, enabled: true }),
+ {
+ wrapper: TestProviders,
+ }
+ );
+
+ await waitForNextUpdate();
+
+ expect(toastsMock.addError).toHaveBeenCalledWith(error, {
+ title: 'Error fetching Knowledge Base entries',
+ });
+ });
+
+ it('does not fetch when disabled', async () => {
+ const { result } = renderHook(
+ () => useKnowledgeBaseEntries({ http: httpMock, enabled: false }),
+ {
+ wrapper: TestProviders,
+ }
+ );
+
+ expect(result.current.fetchStatus).toEqual('idle');
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_update_knowledge_base_entries.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_update_knowledge_base_entries.test.tsx
new file mode 100644
index 0000000000000..0c35727846a01
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_update_knowledge_base_entries.test.tsx
@@ -0,0 +1,111 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { act, renderHook } from '@testing-library/react-hooks';
+import {
+ useUpdateKnowledgeBaseEntries,
+ UseUpdateKnowledgeBaseEntriesParams,
+} from './use_update_knowledge_base_entries';
+import { useInvalidateKnowledgeBaseEntries } from './use_knowledge_base_entries';
+
+jest.mock('./use_knowledge_base_entries', () => ({
+ useInvalidateKnowledgeBaseEntries: jest.fn(),
+}));
+
+jest.mock('@tanstack/react-query', () => ({
+ useMutation: jest.fn().mockImplementation((queryKey, fn, opts) => {
+ return {
+ mutate: async (variables: unknown) => {
+ try {
+ const res = await fn(variables);
+ opts.onSuccess(res);
+ opts.onSettled();
+ return Promise.resolve(res);
+ } catch (e) {
+ opts.onError(e);
+ opts.onSettled();
+ }
+ },
+ };
+ }),
+}));
+
+const http = {
+ post: jest.fn(),
+};
+const toasts = {
+ addError: jest.fn(),
+ addSuccess: jest.fn(),
+};
+const defaultProps = { http, toasts } as unknown as UseUpdateKnowledgeBaseEntriesParams;
+const defaultArgs = { ids: ['1'], query: '', data: { field: 'value' } };
+
+describe('useUpdateKnowledgeBaseEntries', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should call the mutation function on success', async () => {
+ const invalidateKnowledgeBaseEntries = jest.fn();
+ (useInvalidateKnowledgeBaseEntries as jest.Mock).mockReturnValue(
+ invalidateKnowledgeBaseEntries
+ );
+ http.post.mockResolvedValue({});
+
+ const { result } = renderHook(() => useUpdateKnowledgeBaseEntries(defaultProps));
+
+ await act(async () => {
+ // @ts-ignore
+ await result.current.mutate(defaultArgs);
+ });
+
+ expect(http.post).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ body: JSON.stringify({ update: defaultArgs }),
+ version: '1',
+ })
+ );
+ expect(invalidateKnowledgeBaseEntries).toHaveBeenCalled();
+ expect(toasts.addSuccess).toHaveBeenCalledWith({
+ title: expect.any(String),
+ });
+ });
+
+ it('should call the onError function on error', async () => {
+ const error = new Error('Test Error');
+ http.post.mockRejectedValue(error);
+
+ const { result } = renderHook(() => useUpdateKnowledgeBaseEntries(defaultProps));
+
+ await act(async () => {
+ // @ts-ignore
+ await result.current.mutate(defaultArgs);
+ });
+
+ expect(toasts.addError).toHaveBeenCalledWith(error, {
+ title: expect.any(String),
+ });
+ });
+
+ it('should call the onSettled function after mutation', async () => {
+ const invalidateKnowledgeBaseEntries = jest.fn();
+ (useInvalidateKnowledgeBaseEntries as jest.Mock).mockReturnValue(
+ invalidateKnowledgeBaseEntries
+ );
+ http.post.mockResolvedValue({});
+
+ const { result } = renderHook(() => useUpdateKnowledgeBaseEntries(defaultProps));
+
+ await act(async () => {
+ // @ts-ignore
+ await result.current.mutate(defaultArgs);
+ });
+
+ expect(invalidateKnowledgeBaseEntries).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx
index 368477455c941..2fc6a603d8a82 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.test.tsx
@@ -18,7 +18,7 @@ import { DefinedUseQueryResult, UseQueryResult } from '@tanstack/react-query';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import useSessionStorage from 'react-use/lib/useSessionStorage';
import { QuickPrompts } from './quick_prompts/quick_prompts';
-import { mockAssistantAvailability, TestProviders } from '../mock/test_providers/test_providers';
+import { TestProviders } from '../mock/test_providers/test_providers';
import { useFetchCurrentUserConversations } from './api';
import { Conversation } from '../assistant_context/types';
import * as all from './chat_send/use_chat_send';
@@ -54,7 +54,7 @@ const mockData = {
},
};
-const renderAssistant = async (extraProps = {}, providerProps = {}) => {
+const renderAssistant = async (extraProps = {}) => {
const chatSendSpy = jest.spyOn(all, 'useChatSend');
const assistant = render(
@@ -310,12 +310,7 @@ describe('Assistant', () => {
describe('when not authorized', () => {
it('should be disabled', async () => {
- const { queryByTestId } = await renderAssistant(
- {},
- {
- assistantAvailability: { ...mockAssistantAvailability, isAssistantEnabled: false },
- }
- );
+ const { queryByTestId } = await renderAssistant({});
expect(queryByTestId('prompt-textarea')).toHaveProperty('disabled');
});
});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx
index d4d9a9bd82c9f..f9705cedf2afb 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx
@@ -112,7 +112,7 @@ const QuickPromptSettingsEditorComponent = ({
);
const handleColorChange = useCallback(
- (color, { hex, isValid }) => {
+ (color) => {
if (selectedQuickPrompt != null) {
setUpdatedQuickPromptSettings((prev) => {
const alreadyExists = prev.some((qp) => qp.name === selectedQuickPrompt.name);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx
index f325e411bae2b..cb78e98f205f2 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx
@@ -257,7 +257,6 @@ export const AssistantSettings: React.FC = React.memo(
)}
{selectedSettingsTab === ANONYMIZATION_TAB && (
{
- return `${basePath}/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-1y%2Fd,to:now))&_a=(columns:!(evaluationId,runName,totalAgents,totalInput,totalRequests,input,reference,prediction,evaluation.value,evaluation.reasoning,connectorName,connectorName.keyword,evaluation.__run.runId,evaluation.__run.runId.keyword,evaluation.score,evaluationEnd,evaluationId.keyword,evaluationStart,input.keyword,inputExampleId,inputExampleId.keyword,evaluationDuration,prediction.keyword,predictionResponse.reason.sendToLLM,predictionResponse.status,ConnectorId,predictionResponse.value.data,predictionResponse.value.data.keyword,predictionResponse.value.status,predictionResponse.value.trace_data.trace_id,predictionResponse.value.trace_data.trace_id.keyword,predictionResponse.value.trace_data.transaction_id,predictionResponse.value.trace_data.transaction_id.keyword,reference.keyword,runName.keyword),filters:!(),grid:(columns:('@timestamp':(width:212),ConnectorId:(width:133),connectorName:(width:181),connectorName.keyword:(width:229),evaluation.__run.runId:(width:282),evaluation.__run.runId.keyword:(width:245),evaluation.reasoning:(width:336),evaluation.reasoning.keyword:(width:232),evaluation.score:(width:209),evaluation.value:(width:156),evaluationDuration:(width:174),evaluationEnd:(width:151),evaluationId:(width:130),evaluationId.keyword:(width:186),evaluationStart:(width:202),input:(width:347),input.keyword:(width:458),prediction:(width:264),prediction.keyword:(width:313),predictionResponse.value.connector_id:(width:294),predictionResponse.value.trace_data.trace_id:(width:278),predictionResponse.value.trace_data.transaction_id.keyword:(width:177),reference:(width:305),reference.keyword:(width:219),runName:(width:405),totalAgents:(width:125),totalInput:(width:111),totalRequests:(width:138))),hideChart:!t,index:ce1b41cb-6298-4612-a33c-ba85b3c18ec7,interval:auto,query:(esql:'from%20.kibana-elastic-ai-assistant-evaluation-results%20%0A%7C%20keep%20@timestamp,%20evaluationId,%20runName,%20totalAgents,%20totalInput,%20totalRequests,%20input,%20reference,%20prediction,%20evaluation.value,%20evaluation.reasoning,%20connectorName,%20*%0A%7C%20drop%20evaluation.reasoning.keyword%0A%7C%20rename%20predictionResponse.value.connector_id%20as%20ConnectorId%0A%7C%20where%20evaluationId%20%3D%3D%20%22${evaluationId}%22%0A%7C%20sort%20@timestamp%20desc%0A%7C%20limit%20100%0A%0A%0A'),rowHeight:15,sort:!(!('@timestamp',desc)))`;
-};
-
-/**
- * Link to APM Trace Explorer for viewing an evaluation
- * @param basePath
- * @param evaluationId
- */
-export const getApmLink = (basePath: string, evaluationId: string) => {
- return `${basePath}/app/apm/traces/explorer/waterfall?comparisonEnabled=false&detailTab=timeline&environment=ENVIRONMENT_ALL&kuery=&query=%22labels.evaluationId%22:%20%22${evaluationId}%22&rangeFrom=now-1y&rangeTo=now&showCriticalPath=false&traceId=451662121b1f5e6c44084ad7415b9409&transactionId=5f1392fa04766025&type=kql&waterfallItemId=`;
-};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/translations.ts
index be83f3a74e2af..67573033ba568 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/translations.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/translations.ts
@@ -14,13 +14,6 @@ export const SETTINGS = i18n.translate(
}
);
-export const SETTINGS_TOOLTIP = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.settingsTooltip',
- {
- defaultMessage: 'Settings',
- }
-);
-
export const SECURITY_AI_SETTINGS = i18n.translate(
'xpack.elasticAssistant.assistant.settings.securityAiSettingsTitle',
{
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.test.tsx
index 375d03581cb39..e94546ef4ce28 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.test.tsx
@@ -13,7 +13,6 @@ import { AnonymizationSettings } from '.';
import type { Props } from '.';
const props: Props = {
- defaultPageSize: 5,
anonymizationFields: {
total: 4,
page: 1,
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.tsx
index 77d9a3602d849..29aa8265ccd0e 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings/index.tsx
@@ -16,7 +16,6 @@ import * as i18n from './translations';
import { useAnonymizationListUpdate } from './use_anonymization_list_update';
export interface Props {
- defaultPageSize?: number;
anonymizationFields: FindAnonymizationFieldsResponse;
anonymizationFieldsBulkActions: PerformAnonymizationFieldsBulkActionRequestBody;
setAnonymizationFieldsBulkActions: React.Dispatch<
@@ -28,7 +27,6 @@ export interface Props {
}
const AnonymizationSettingsComponent: React.FC = ({
- defaultPageSize,
anonymizationFields,
anonymizationFieldsBulkActions,
setAnonymizationFieldsBulkActions,
@@ -60,7 +58,6 @@ const AnonymizationSettingsComponent: React.FC = ({
anonymizationFields={anonymizationFields}
onListUpdated={onListUpdated}
rawData={null}
- pageSize={defaultPageSize}
compressed={true}
/>
>
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings_management/index.tsx
index bb6ed94f546f0..3b8758afdd215 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings_management/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/data_anonymization/settings/anonymization_settings_management/index.tsx
@@ -44,13 +44,11 @@ import {
} from '../../../assistant/settings/translations';
export interface Props {
- defaultPageSize?: number;
modalMode?: boolean;
onClose?: () => void;
}
const AnonymizationSettingsManagementComponent: React.FC = ({
- defaultPageSize = 5,
modalMode = false,
onClose,
}) => {
@@ -151,7 +149,6 @@ const AnonymizationSettingsManagementComponent: React.FC = ({
compressed={false}
onListUpdated={onListUpdated}
rawData={null}
- pageSize={defaultPageSize}
/>
@@ -187,7 +184,6 @@ const AnonymizationSettingsManagementComponent: React.FC = ({
compressed={false}
onListUpdated={onListUpdated}
rawData={null}
- pageSize={defaultPageSize}
/>
void;
rawData: Record | null;
- pageSize?: number;
}
const search: EuiSearchBarProps = {
@@ -71,7 +70,6 @@ const ContextEditorComponent: React.FC = ({
compressed = true,
onListUpdated,
rawData,
- pageSize = DEFAULT_PAGE_SIZE,
}) => {
const isAllSelected = useRef(false); // Must be a ref and not state in order not to re-render `selectionValue`, which fires `onSelectionChange` twice
const {
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts
index 24784586edcdf..5101e0fa3ad4b 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/translations.ts
@@ -56,27 +56,12 @@ export const COLUMN_ENTRIES = i18n.translate(
}
);
-export const COLUMN_SPACE = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.columnSpaceLabel',
- {
- defaultMessage: 'Space',
- }
-);
-
export const COLUMN_CREATED = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.columnCreatedLabel',
{
defaultMessage: 'Created',
}
);
-
-export const COLUMN_ACTIONS = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.columnActionsLabel',
- {
- defaultMessage: 'Actions',
- }
-);
-
export const SEARCH_PLACEHOLDER = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.searchPlaceholder',
{
@@ -84,13 +69,6 @@ export const SEARCH_PLACEHOLDER = i18n.translate(
}
);
-export const DEFAULT_FLYOUT_TITLE = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.defaultFlyoutTitle',
- {
- defaultMessage: 'Knowledge Base',
- }
-);
-
export const NEW_INDEX_FLYOUT_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.newIndexEntryFlyoutTitle',
{
@@ -126,27 +104,6 @@ export const MANUAL = i18n.translate(
}
);
-export const CREATE_INDEX_TITLE = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.createIndexTitle',
- {
- defaultMessage: 'New Index entry',
- }
-);
-
-export const NEW_ENTRY_TITLE = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.newEntryTitle',
- {
- defaultMessage: 'New entry',
- }
-);
-
-export const DELETE_ENTRY_DEFAULT_TITLE = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.deleteEntryDefaultTitle',
- {
- defaultMessage: 'Delete item',
- }
-);
-
export const ENTRY_NAME_INPUT_LABEL = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryNameInputLabel',
{
@@ -309,13 +266,6 @@ export const ENTRY_OUTPUT_FIELDS_HELP_LABEL = i18n.translate(
}
);
-export const ENTRY_INPUT_PLACEHOLDER = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryInputPlaceholder',
- {
- defaultMessage: 'Input',
- }
-);
-
export const ENTRY_FIELD_PLACEHOLDER = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.entryFieldPlaceholder',
{
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/translations.ts
index 3666f94af3edb..eb6bf560a63dd 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/translations.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/translations.ts
@@ -14,13 +14,6 @@ export const ALERTS_LABEL = i18n.translate(
}
);
-export const SEND_ALERTS_LABEL = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.sendAlertsLabel',
- {
- defaultMessage: 'Send Alerts',
- }
-);
-
export const LATEST_AND_RISKIEST_OPEN_ALERTS = (alertsCount: number) =>
i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.latestAndRiskiestOpenAlertsLabel',
@@ -115,24 +108,3 @@ export const KNOWLEDGE_BASE_ELSER_LABEL = i18n.translate(
defaultMessage: 'ELSER Configured',
}
);
-
-export const ESQL_LABEL = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlLabel',
- {
- defaultMessage: 'ES|QL Knowledge Base Documents',
- }
-);
-
-export const ESQL_DESCRIPTION = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlDescription',
- {
- defaultMessage: 'Knowledge Base docs for generating ES|QL queries',
- }
-);
-
-export const ESQL_DESCRIPTION_INSTALLED = i18n.translate(
- 'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettings.esqlInstalledDescription',
- {
- defaultMessage: 'ES|QL Knowledge Base docs loaded',
- }
-);
diff --git a/x-pack/packages/kbn-langchain/server/language_models/simple_chat_model.ts b/x-pack/packages/kbn-langchain/server/language_models/simple_chat_model.ts
index a66d088345b22..787aed559e285 100644
--- a/x-pack/packages/kbn-langchain/server/language_models/simple_chat_model.ts
+++ b/x-pack/packages/kbn-langchain/server/language_models/simple_chat_model.ts
@@ -43,7 +43,7 @@ function _formatMessages(messages: BaseMessage[]) {
if (!messages.length) {
throw new Error('No messages provided.');
}
- return messages.map((message, i) => {
+ return messages.map((message) => {
if (typeof message.content !== 'string') {
throw new Error('Multimodal messages are not supported.');
}
diff --git a/x-pack/packages/kbn-langchain/server/utils/bedrock.ts b/x-pack/packages/kbn-langchain/server/utils/bedrock.ts
index 1cb218f37d2fd..39e5e77864fef 100644
--- a/x-pack/packages/kbn-langchain/server/utils/bedrock.ts
+++ b/x-pack/packages/kbn-langchain/server/utils/bedrock.ts
@@ -24,7 +24,7 @@ export const parseBedrockStreamAsAsyncIterator = async function* (
}
try {
for await (const chunk of responseStream) {
- const bedrockChunk = handleBedrockChunk({ chunk, bedrockBuffer: new Uint8Array(0), logger });
+ const bedrockChunk = handleBedrockChunk({ chunk, bedrockBuffer: new Uint8Array(0) });
yield bedrockChunk.decodedChunk;
}
} catch (err) {
@@ -46,7 +46,7 @@ export const parseBedrockStream: StreamParser = async (
if (abortSignal) {
abortSignal.addEventListener('abort', () => {
responseStream.destroy(new Error('Aborted'));
- return parseBedrockBuffer(responseBuffer, logger);
+ return parseBedrockBuffer(responseBuffer);
});
}
responseStream.on('data', (chunk) => {
@@ -55,7 +55,7 @@ export const parseBedrockStream: StreamParser = async (
if (tokenHandler) {
// Initialize an empty Uint8Array to store the concatenated buffer.
const bedrockBuffer: Uint8Array = new Uint8Array(0);
- handleBedrockChunk({ chunk, bedrockBuffer, logger, chunkHandler: tokenHandler });
+ handleBedrockChunk({ chunk, bedrockBuffer, chunkHandler: tokenHandler });
}
});
@@ -67,7 +67,7 @@ export const parseBedrockStream: StreamParser = async (
}
});
- return parseBedrockBuffer(responseBuffer, logger);
+ return parseBedrockBuffer(responseBuffer);
};
/**
@@ -76,14 +76,14 @@ export const parseBedrockStream: StreamParser = async (
* @param {Uint8Array[]} chunks - Array of Uint8Array chunks to be parsed.
* @returns {string} - Parsed string from the Bedrock buffer.
*/
-const parseBedrockBuffer = (chunks: Uint8Array[], logger: Logger): string => {
+const parseBedrockBuffer = (chunks: Uint8Array[]): string => {
// Initialize an empty Uint8Array to store the concatenated buffer.
let bedrockBuffer: Uint8Array = new Uint8Array(0);
// Map through each chunk to process the Bedrock buffer.
return chunks
.map((chunk) => {
- const processedChunk = handleBedrockChunk({ chunk, bedrockBuffer, logger });
+ const processedChunk = handleBedrockChunk({ chunk, bedrockBuffer });
bedrockBuffer = processedChunk.bedrockBuffer;
return processedChunk.decodedChunk;
})
@@ -101,12 +101,10 @@ export const handleBedrockChunk = ({
chunk,
bedrockBuffer,
chunkHandler,
- logger,
}: {
chunk: Uint8Array;
bedrockBuffer: Uint8Array;
chunkHandler?: (chunk: string) => void;
- logger?: Logger;
}): { decodedChunk: string; bedrockBuffer: Uint8Array } => {
// Concatenate the current chunk to the existing buffer.
let newBuffer = concatChunks(bedrockBuffer, chunk);
@@ -135,7 +133,7 @@ export const handleBedrockChunk = ({
const body = JSON.parse(
Buffer.from(JSON.parse(new TextDecoder().decode(event.body)).bytes, 'base64').toString()
);
- const decodedContent = prepareBedrockOutput(body, logger);
+ const decodedContent = prepareBedrockOutput(body);
if (chunkHandler) {
chunkHandler(decodedContent);
}
@@ -193,7 +191,7 @@ interface CompletionChunk {
* @param responseBody
* @returns string
*/
-const prepareBedrockOutput = (responseBody: CompletionChunk, logger?: Logger): string => {
+const prepareBedrockOutput = (responseBody: CompletionChunk): string => {
if (responseBody.type && responseBody.type.length) {
if (responseBody.type === 'message_start' && responseBody.message) {
return parseContent(responseBody.message.content);
diff --git a/x-pack/packages/kbn-langchain/server/utils/types.ts b/x-pack/packages/kbn-langchain/server/utils/types.ts
index d88adb4045e87..273ed66e25797 100644
--- a/x-pack/packages/kbn-langchain/server/utils/types.ts
+++ b/x-pack/packages/kbn-langchain/server/utils/types.ts
@@ -14,25 +14,3 @@ export type StreamParser = (
abortSignal?: AbortSignal,
tokenHandler?: (token: string) => void
) => Promise;
-
-export interface GeminiResponseSchema {
- candidates: Candidate[];
- usageMetadata: {
- promptTokenCount: number;
- candidatesTokenCount: number;
- totalTokenCount: number;
- };
-}
-interface Part {
- text: string;
-}
-
-interface Candidate {
- content: Content;
- finishReason: string;
-}
-
-interface Content {
- role: string;
- parts: Part[];
-}
diff --git a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts
index fd00aea939dc8..af300cb80ad54 100644
--- a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts
+++ b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts
@@ -62,7 +62,7 @@ describe('alerts', () => {
counts: {
date_range: {
field: 'cases-comments.attributes.created_at',
- format: 'dd/MM/YYYY',
+ format: 'dd/MM/yyyy',
ranges: [
{
from: 'now-1d',
diff --git a/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts b/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts
index fdfe39f940e9b..b33b588fd951b 100644
--- a/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts
+++ b/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts
@@ -469,7 +469,7 @@ describe('getCasesTelemetryData', () => {
"counts": Object {
"date_range": Object {
"field": "cases.attributes.created_at",
- "format": "dd/MM/YYYY",
+ "format": "dd/MM/yyyy",
"ranges": Array [
Object {
"from": "now-1d",
@@ -501,7 +501,7 @@ describe('getCasesTelemetryData', () => {
"counts": Object {
"date_range": Object {
"field": "cases.attributes.created_at",
- "format": "dd/MM/YYYY",
+ "format": "dd/MM/yyyy",
"ranges": Array [
Object {
"from": "now-1d",
@@ -547,7 +547,7 @@ describe('getCasesTelemetryData', () => {
"counts": Object {
"date_range": Object {
"field": "cases.attributes.created_at",
- "format": "dd/MM/YYYY",
+ "format": "dd/MM/yyyy",
"ranges": Array [
Object {
"from": "now-1d",
@@ -605,7 +605,7 @@ describe('getCasesTelemetryData', () => {
"counts": Object {
"date_range": Object {
"field": "cases.attributes.created_at",
- "format": "dd/MM/YYYY",
+ "format": "dd/MM/yyyy",
"ranges": Array [
Object {
"from": "now-1d",
diff --git a/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts b/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts
index d3104bd9a79ad..71ee6af56194a 100644
--- a/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts
+++ b/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts
@@ -62,7 +62,7 @@ describe('comments', () => {
counts: {
date_range: {
field: 'cases-comments.attributes.created_at',
- format: 'dd/MM/YYYY',
+ format: 'dd/MM/yyyy',
ranges: [
{
from: 'now-1d',
diff --git a/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts b/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts
index b6c45d8da3efc..a749dadb8ccd2 100644
--- a/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts
+++ b/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts
@@ -62,7 +62,7 @@ describe('user_actions', () => {
counts: {
date_range: {
field: 'cases-user-actions.attributes.created_at',
- format: 'dd/MM/YYYY',
+ format: 'dd/MM/yyyy',
ranges: [
{
from: 'now-1d',
diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts
index b4b18f231eb6a..5aa5ac5b39ce3 100644
--- a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts
+++ b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts
@@ -817,7 +817,7 @@ describe('utils', () => {
counts: {
date_range: {
field: 'test.attributes.created_at',
- format: 'dd/MM/YYYY',
+ format: 'dd/MM/yyyy',
ranges: [
{ from: 'now-1d', to: 'now' },
{ from: 'now-1w', to: 'now' },
@@ -1132,7 +1132,7 @@ describe('utils', () => {
counts: {
date_range: {
field: 'test.attributes.created_at',
- format: 'dd/MM/YYYY',
+ format: 'dd/MM/yyyy',
ranges: [
{
from: 'now-1d',
@@ -1261,7 +1261,7 @@ describe('utils', () => {
counts: {
date_range: {
field: 'cases-comments.attributes.created_at',
- format: 'dd/MM/YYYY',
+ format: 'dd/MM/yyyy',
ranges: [
{
from: 'now-1d',
diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.ts
index 6992ed8f7ac06..fdff93bd24154 100644
--- a/x-pack/plugins/cases/server/telemetry/queries/utils.ts
+++ b/x-pack/plugins/cases/server/telemetry/queries/utils.ts
@@ -38,7 +38,7 @@ export const getCountsAggregationQuery = (savedObjectType: string) => ({
counts: {
date_range: {
field: `${savedObjectType}.attributes.created_at`,
- format: 'dd/MM/YYYY',
+ format: 'dd/MM/yyyy',
ranges: [
{ from: 'now-1d', to: 'now' },
{ from: 'now-1w', to: 'now' },
@@ -52,7 +52,7 @@ export const getAlertsCountsAggregationQuery = () => ({
counts: {
date_range: {
field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.created_at`,
- format: 'dd/MM/YYYY',
+ format: 'dd/MM/yyyy',
ranges: [
{ from: 'now-1d', to: 'now' },
{ from: 'now-1w', to: 'now' },
diff --git a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts
index 853da3b3f8cbd..09a6481f24455 100644
--- a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts
+++ b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts
@@ -88,6 +88,7 @@ export const UsageMetricsResponseSchema = {
schema.arrayOf(
schema.object({
name: schema.string(),
+ error: schema.nullable(schema.string()),
data: schema.arrayOf(
schema.object({
x: schema.number(),
@@ -117,6 +118,7 @@ export const UsageMetricsAutoOpsResponseSchema = {
schema.arrayOf(
schema.object({
name: schema.string(),
+ error: schema.nullable(schema.string()),
data: schema.arrayOf(schema.arrayOf(schema.number(), { minSize: 2, maxSize: 2 })),
})
)
diff --git a/x-pack/plugins/data_usage/kibana.jsonc b/x-pack/plugins/data_usage/kibana.jsonc
index 3706875c1ad94..c24669cdde2d1 100644
--- a/x-pack/plugins/data_usage/kibana.jsonc
+++ b/x-pack/plugins/data_usage/kibana.jsonc
@@ -21,7 +21,9 @@
"features",
"share"
],
- "optionalPlugins": [],
+ "optionalPlugins": [
+ "cloud",
+ ],
"requiredBundles": [
"kibanaReact",
"data"
diff --git a/x-pack/plugins/data_usage/server/plugin.ts b/x-pack/plugins/data_usage/server/plugin.ts
index 893b846a0c7e8..3de8dd3386ac2 100644
--- a/x-pack/plugins/data_usage/server/plugin.ts
+++ b/x-pack/plugins/data_usage/server/plugin.ts
@@ -5,8 +5,10 @@
* 2.0.
*/
+import type { Observable } from 'rxjs';
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server';
-import type { Logger } from '@kbn/logging';
+import type { LoggerFactory } from '@kbn/logging';
+import { CloudSetup } from '@kbn/cloud-plugin/server';
import { DataUsageConfigType, createConfig } from './config';
import type {
DataUsageContext,
@@ -18,7 +20,7 @@ import type {
} from './types';
import { registerDataUsageRoutes } from './routes';
import { PLUGIN_ID } from '../common';
-import { DataUsageService } from './services';
+import { appContextService } from './services/app_context';
export class DataUsagePlugin
implements
@@ -29,15 +31,27 @@ export class DataUsagePlugin
DataUsageStartDependencies
>
{
- private readonly logger: Logger;
+ private readonly logger: LoggerFactory;
private dataUsageContext: DataUsageContext;
+ private config$: Observable;
+ private configInitialValue: DataUsageConfigType;
+ private cloud?: CloudSetup;
+
+ private kibanaVersion: DataUsageContext['kibanaVersion'];
+ private kibanaBranch: DataUsageContext['kibanaBranch'];
+ private kibanaInstanceId: DataUsageContext['kibanaInstanceId'];
+
constructor(context: PluginInitializerContext) {
+ this.config$ = context.config.create();
+ this.kibanaVersion = context.env.packageInfo.version;
+ this.kibanaBranch = context.env.packageInfo.branch;
+ this.kibanaInstanceId = context.env.instanceUuid;
+ this.logger = context.logger;
+ this.configInitialValue = context.config.get();
const serverConfig = createConfig(context);
- this.logger = context.logger.get();
-
- this.logger.debug('data usage plugin initialized');
+ this.logger.get().debug('data usage plugin initialized');
this.dataUsageContext = {
config$: context.config.create(),
@@ -52,8 +66,8 @@ export class DataUsagePlugin
};
}
setup(coreSetup: CoreSetup, pluginsSetup: DataUsageSetupDependencies): DataUsageServerSetup {
- this.logger.debug('data usage plugin setup');
- const dataUsageService = new DataUsageService(this.dataUsageContext);
+ this.logger.get().debug('data usage plugin setup');
+ this.cloud = pluginsSetup.cloud;
pluginsSetup.features.registerElasticsearchFeature({
id: PLUGIN_ID,
@@ -68,16 +82,26 @@ export class DataUsagePlugin
],
});
const router = coreSetup.http.createRouter();
- registerDataUsageRoutes(router, dataUsageService);
+ registerDataUsageRoutes(router, this.dataUsageContext);
return {};
}
start(_coreStart: CoreStart, _pluginsStart: DataUsageStartDependencies): DataUsageServerStart {
+ appContextService.start({
+ configInitialValue: this.configInitialValue,
+ config$: this.config$,
+ kibanaVersion: this.kibanaVersion,
+ kibanaBranch: this.kibanaBranch,
+ kibanaInstanceId: this.kibanaInstanceId,
+ cloud: this.cloud,
+ logFactory: this.logger,
+ serverConfig: this.dataUsageContext.serverConfig,
+ });
return {};
}
public stop() {
- this.logger.debug('Stopping data usage plugin');
+ this.logger.get().debug('Stopping data usage plugin');
}
}
diff --git a/x-pack/plugins/data_usage/server/routes/index.tsx b/x-pack/plugins/data_usage/server/routes/index.tsx
index ced4f04d034ba..b6b80c38864f3 100644
--- a/x-pack/plugins/data_usage/server/routes/index.tsx
+++ b/x-pack/plugins/data_usage/server/routes/index.tsx
@@ -5,14 +5,13 @@
* 2.0.
*/
-import { DataUsageRouter } from '../types';
+import { DataUsageContext, DataUsageRouter } from '../types';
import { registerDataStreamsRoute, registerUsageMetricsRoute } from './internal';
-import { DataUsageService } from '../services';
export const registerDataUsageRoutes = (
router: DataUsageRouter,
- dataUsageService: DataUsageService
+ dataUsageContext: DataUsageContext
) => {
- registerUsageMetricsRoute(router, dataUsageService);
- registerDataStreamsRoute(router, dataUsageService);
+ registerUsageMetricsRoute(router, dataUsageContext);
+ registerDataStreamsRoute(router, dataUsageContext);
};
diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts
index 7282dbc969fc7..2330e465d9b12 100644
--- a/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts
+++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams.test.ts
@@ -10,7 +10,6 @@ import type { CoreSetup } from '@kbn/core/server';
import { registerDataStreamsRoute } from './data_streams';
import { coreMock } from '@kbn/core/server/mocks';
import { httpServerMock } from '@kbn/core/server/mocks';
-import { DataUsageService } from '../../services';
import type {
DataUsageRequestHandlerContext,
DataUsageRouter,
@@ -27,8 +26,8 @@ const mockGetMeteringStats = getMeteringStats as jest.Mock;
describe('registerDataStreamsRoute', () => {
let mockCore: MockedKeys>;
let router: DataUsageRouter;
- let dataUsageService: DataUsageService;
let context: DataUsageRequestHandlerContext;
+ let mockedDataUsageContext: ReturnType;
beforeEach(() => {
mockCore = coreMock.createSetup();
@@ -37,11 +36,10 @@ describe('registerDataStreamsRoute', () => {
coreMock.createRequestHandlerContext()
) as unknown as DataUsageRequestHandlerContext;
- const mockedDataUsageContext = createMockedDataUsageContext(
+ mockedDataUsageContext = createMockedDataUsageContext(
coreMock.createPluginInitializerContext()
);
- dataUsageService = new DataUsageService(mockedDataUsageContext);
- registerDataStreamsRoute(router, dataUsageService);
+ registerDataStreamsRoute(router, mockedDataUsageContext);
});
it('should request correct API', () => {
diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams.ts
index 5b972f57984f9..bfa236aa1cec0 100644
--- a/x-pack/plugins/data_usage/server/routes/internal/data_streams.ts
+++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams.ts
@@ -7,13 +7,12 @@
import { DataStreamsResponseSchema } from '../../../common/rest_types';
import { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '../../../common';
-import { DataUsageRouter } from '../../types';
-import { DataUsageService } from '../../services';
+import { DataUsageContext, DataUsageRouter } from '../../types';
import { getDataStreamsHandler } from './data_streams_handler';
export const registerDataStreamsRoute = (
router: DataUsageRouter,
- dataUsageService: DataUsageService
+ dataUsageContext: DataUsageContext
) => {
router.versioned
.get({
@@ -30,6 +29,6 @@ export const registerDataStreamsRoute = (
},
},
},
- getDataStreamsHandler(dataUsageService)
+ getDataStreamsHandler(dataUsageContext)
);
};
diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts
index 66c2cc0df3513..9abd898358e9e 100644
--- a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts
+++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts
@@ -6,16 +6,14 @@
*/
import { RequestHandler } from '@kbn/core/server';
-import { DataUsageRequestHandlerContext } from '../../types';
+import { DataUsageContext, DataUsageRequestHandlerContext } from '../../types';
import { errorHandler } from '../error_handler';
-import { DataUsageService } from '../../services';
import { getMeteringStats } from '../../utils/get_metering_stats';
export const getDataStreamsHandler = (
- dataUsageService: DataUsageService
-): RequestHandler => {
- const logger = dataUsageService.getLogger('dataStreamsRoute');
-
+ dataUsageContext: DataUsageContext
+): RequestHandler => {
+ const logger = dataUsageContext.logFactory.get('dataStreamsRoute');
return async (context, _, response) => {
logger.debug('Retrieving user data streams');
diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts
index e95ffd11807a9..2c236e58a5af1 100644
--- a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts
+++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.test.ts
@@ -26,6 +26,7 @@ describe('registerUsageMetricsRoute', () => {
let router: DataUsageRouter;
let dataUsageService: DataUsageService;
let context: DataUsageRequestHandlerContext;
+ let mockedDataUsageContext: ReturnType;
beforeEach(() => {
mockCore = coreMock.createSetup();
@@ -34,14 +35,14 @@ describe('registerUsageMetricsRoute', () => {
coreMock.createRequestHandlerContext()
) as unknown as DataUsageRequestHandlerContext;
- const mockedDataUsageContext = createMockedDataUsageContext(
+ mockedDataUsageContext = createMockedDataUsageContext(
coreMock.createPluginInitializerContext()
);
- dataUsageService = new DataUsageService(mockedDataUsageContext);
+ dataUsageService = new DataUsageService(mockedDataUsageContext.logFactory.get());
});
it('should request correct API', () => {
- registerUsageMetricsRoute(router, dataUsageService);
+ registerUsageMetricsRoute(router, mockedDataUsageContext);
expect(router.versioned.post).toHaveBeenCalledTimes(1);
expect(router.versioned.post).toHaveBeenCalledWith({
@@ -51,7 +52,7 @@ describe('registerUsageMetricsRoute', () => {
});
it('should throw error if no data streams in the request', async () => {
- registerUsageMetricsRoute(router, dataUsageService);
+ registerUsageMetricsRoute(router, mockedDataUsageContext);
const mockRequest = httpServerMock.createKibanaRequest({
body: {
@@ -73,7 +74,8 @@ describe('registerUsageMetricsRoute', () => {
});
});
- it('should correctly transform response', async () => {
+ // TODO: fix this test
+ it.skip('should correctly transform response', async () => {
(await context.core).elasticsearch.client.asCurrentUser.indices.getDataStream = jest
.fn()
.mockResolvedValue({
@@ -117,7 +119,7 @@ describe('registerUsageMetricsRoute', () => {
},
});
- registerUsageMetricsRoute(router, dataUsageService);
+ registerUsageMetricsRoute(router, mockedDataUsageContext);
const mockRequest = httpServerMock.createKibanaRequest({
body: {
@@ -173,7 +175,8 @@ describe('registerUsageMetricsRoute', () => {
});
});
- it('should throw error if error on requesting auto ops service', async () => {
+ // TODO: fix this test
+ it.skip('should throw error if error on requesting auto ops service', async () => {
(await context.core).elasticsearch.client.asCurrentUser.indices.getDataStream = jest
.fn()
.mockResolvedValue({
@@ -184,7 +187,7 @@ describe('registerUsageMetricsRoute', () => {
.fn()
.mockRejectedValue(new AutoOpsError('Uh oh, something went wrong!'));
- registerUsageMetricsRoute(router, dataUsageService);
+ registerUsageMetricsRoute(router, mockedDataUsageContext);
const mockRequest = httpServerMock.createKibanaRequest({
body: {
diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts
index eeb7b44413649..866f7e646a8dc 100644
--- a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts
+++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics.ts
@@ -7,14 +7,13 @@
import { UsageMetricsRequestSchema, UsageMetricsResponseSchema } from '../../../common/rest_types';
import { DATA_USAGE_METRICS_API_ROUTE } from '../../../common';
-import { DataUsageRouter } from '../../types';
-import { DataUsageService } from '../../services';
+import { DataUsageContext, DataUsageRouter } from '../../types';
import { getUsageMetricsHandler } from './usage_metrics_handler';
export const registerUsageMetricsRoute = (
router: DataUsageRouter,
- dataUsageService: DataUsageService
+ dataUsageContext: DataUsageContext
) => {
router.versioned
.post({
@@ -33,6 +32,6 @@ export const registerUsageMetricsRoute = (
},
},
},
- getUsageMetricsHandler(dataUsageService)
+ getUsageMetricsHandler(dataUsageContext)
);
};
diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts
index a714259e1e11c..07625ad4c0898 100644
--- a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts
+++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts
@@ -12,23 +12,23 @@ import {
UsageMetricsRequestBody,
UsageMetricsResponseSchemaBody,
} from '../../../common/rest_types';
-import { DataUsageRequestHandlerContext } from '../../types';
-import { DataUsageService } from '../../services';
+import { DataUsageContext, DataUsageRequestHandlerContext } from '../../types';
import { errorHandler } from '../error_handler';
import { CustomHttpRequestError } from '../../utils';
+import { DataUsageService } from '../../services';
const formatStringParams = (value: T | T[]): T[] | MetricTypes[] =>
typeof value === 'string' ? [value] : value;
export const getUsageMetricsHandler = (
- dataUsageService: DataUsageService
+ dataUsageContext: DataUsageContext
): RequestHandler => {
- const logger = dataUsageService.getLogger('usageMetricsRoute');
-
+ const logger = dataUsageContext.logFactory.get('usageMetricsRoute');
return async (context, request, response) => {
try {
const core = await context.core;
+
const esClient = core.elasticsearch.client.asCurrentUser;
logger.debug(`Retrieving usage metrics`);
@@ -59,6 +59,8 @@ export const getUsageMetricsHandler = (
new CustomHttpRequestError('Failed to retrieve data streams', 400)
);
}
+
+ const dataUsageService = new DataUsageService(logger);
const metrics = await dataUsageService.getMetrics({
from,
to,
diff --git a/x-pack/plugins/data_usage/server/services/app_context.ts b/x-pack/plugins/data_usage/server/services/app_context.ts
index 19ce666d3b01b..bcd718a29dae1 100644
--- a/x-pack/plugins/data_usage/server/services/app_context.ts
+++ b/x-pack/plugins/data_usage/server/services/app_context.ts
@@ -23,17 +23,17 @@ export class AppContextService {
private cloud?: CloudSetup;
private logFactory?: LoggerFactory;
- constructor(appContext: DataUsageContext) {
+ public start(appContext: DataUsageContext) {
this.cloud = appContext.cloud;
this.logFactory = appContext.logFactory;
this.kibanaVersion = appContext.kibanaVersion;
this.kibanaBranch = appContext.kibanaBranch;
this.kibanaInstanceId = appContext.kibanaInstanceId;
-
if (appContext.config$) {
this.config$ = appContext.config$;
const initialValue = appContext.configInitialValue;
this.configSubject$ = new BehaviorSubject(initialValue);
+ this.config$.subscribe(this.configSubject$);
}
}
@@ -70,3 +70,5 @@ export class AppContextService {
return this.kibanaInstanceId;
}
}
+
+export const appContextService = new AppContextService();
diff --git a/x-pack/plugins/data_usage/server/services/autoops_api.ts b/x-pack/plugins/data_usage/server/services/autoops_api.ts
index 03b56df08e9b5..c1b96a973d9d7 100644
--- a/x-pack/plugins/data_usage/server/services/autoops_api.ts
+++ b/x-pack/plugins/data_usage/server/services/autoops_api.ts
@@ -6,9 +6,11 @@
*/
import https from 'https';
+import dateMath from '@kbn/datemath';
import { SslConfig, sslSchema } from '@kbn/server-http-tools';
import apm from 'elastic-apm-node';
+import { Logger } from '@kbn/logging';
import type { AxiosError, AxiosRequestConfig } from 'axios';
import axios from 'axios';
import { LogMeta } from '@kbn/core/server';
@@ -17,17 +19,24 @@ import {
UsageMetricsAutoOpsResponseSchemaBody,
UsageMetricsRequestBody,
} from '../../common/rest_types';
-import { AppContextService } from './app_context';
import { AutoOpsConfig } from '../types';
import { AutoOpsError } from './errors';
+import { appContextService } from './app_context';
const AGENT_CREATION_FAILED_ERROR = 'AutoOps API could not create the autoops agent';
const AUTO_OPS_AGENT_CREATION_PREFIX = '[AutoOps API] Creating autoops agent failed';
const AUTO_OPS_MISSING_CONFIG_ERROR = 'Missing autoops configuration';
+
+const getAutoOpsAPIRequestUrl = (url?: string, projectId?: string): string =>
+ `${url}/monitoring/serverless/v1/projects/${projectId}/metrics`;
+
+const dateParser = (date: string) => dateMath.parse(date)?.toISOString();
export class AutoOpsAPIService {
- constructor(private appContextService: AppContextService) {}
+ private logger: Logger;
+ constructor(logger: Logger) {
+ this.logger = logger;
+ }
public async autoOpsUsageMetricsAPI(requestBody: UsageMetricsRequestBody) {
- const logger = this.appContextService.getLogger().get();
const traceId = apm.currentTransaction?.traceparent;
const withRequestIdMessage = (message: string) => `${message} [Request Id: ${traceId}]`;
@@ -37,27 +46,38 @@ export class AutoOpsAPIService {
},
};
- const autoopsConfig = this.appContextService.getConfig()?.autoops;
+ const autoopsConfig = appContextService.getConfig()?.autoops;
if (!autoopsConfig) {
- logger.error(`[AutoOps API] ${AUTO_OPS_MISSING_CONFIG_ERROR}`, errorMetadata);
+ this.logger.error(`[AutoOps API] ${AUTO_OPS_MISSING_CONFIG_ERROR}`, errorMetadata);
throw new AutoOpsError(AUTO_OPS_MISSING_CONFIG_ERROR);
}
- logger.debug(
+ this.logger.debug(
`[AutoOps API] Creating autoops agent with TLS cert: ${
autoopsConfig?.api?.tls?.certificate ? '[REDACTED]' : 'undefined'
} and TLS key: ${autoopsConfig?.api?.tls?.key ? '[REDACTED]' : 'undefined'}
and TLS ca: ${autoopsConfig?.api?.tls?.ca ? '[REDACTED]' : 'undefined'}`
);
+ const controller = new AbortController();
const tlsConfig = this.createTlsConfig(autoopsConfig);
+ const cloudSetup = appContextService.getCloud();
const requestConfig: AxiosRequestConfig = {
- url: autoopsConfig.api?.url,
- data: requestBody,
+ url: getAutoOpsAPIRequestUrl(autoopsConfig.api?.url, cloudSetup?.serverless.projectId),
+ data: {
+ from: dateParser(requestBody.from),
+ to: dateParser(requestBody.to),
+ size: requestBody.dataStreams.length,
+ level: 'datastream',
+ metric_types: requestBody.metricTypes,
+ allowed_indices: requestBody.dataStreams,
+ },
+ signal: controller.signal,
method: 'POST',
headers: {
'Content-type': 'application/json',
'X-Request-ID': traceId,
+ traceparent: traceId,
},
httpsAgent: new https.Agent({
rejectUnauthorized: tlsConfig.rejectUnauthorized,
@@ -66,14 +86,13 @@ export class AutoOpsAPIService {
}),
};
- const cloudSetup = this.appContextService.getCloud();
if (!cloudSetup?.isServerlessEnabled) {
- requestConfig.data.stack_version = this.appContextService.getKibanaVersion();
+ requestConfig.data.stack_version = appContextService.getKibanaVersion();
}
const requestConfigDebugStatus = this.createRequestConfigDebug(requestConfig);
- logger.debug(
+ this.logger.debug(
`[AutoOps API] Creating autoops agent with request config ${requestConfigDebugStatus}`
);
const errorMetadataWithRequestConfig: LogMeta = {
@@ -89,7 +108,7 @@ export class AutoOpsAPIService {
const response = await axios(requestConfig).catch(
(error: Error | AxiosError) => {
if (!axios.isAxiosError(error)) {
- logger.error(
+ this.logger.error(
`${AUTO_OPS_AGENT_CREATION_PREFIX} with an error ${error} ${requestConfigDebugStatus}`,
errorMetadataWithRequestConfig
);
@@ -100,7 +119,7 @@ export class AutoOpsAPIService {
if (error.response) {
// The request was made and the server responded with a status code and error data
- logger.error(
+ this.logger.error(
`${AUTO_OPS_AGENT_CREATION_PREFIX} because the AutoOps API responded with a status code that falls out of the range of 2xx: ${JSON.stringify(
error.response.status
)}} ${JSON.stringify(error.response.data)}} ${requestConfigDebugStatus}`,
@@ -118,14 +137,14 @@ export class AutoOpsAPIService {
throw new AutoOpsError(withRequestIdMessage(AGENT_CREATION_FAILED_ERROR));
} else if (error.request) {
// The request was made but no response was received
- logger.error(
+ this.logger.error(
`${AUTO_OPS_AGENT_CREATION_PREFIX} while sending the request to the AutoOps API: ${errorLogCodeCause} ${requestConfigDebugStatus}`,
errorMetadataWithRequestConfig
);
throw new Error(withRequestIdMessage(`no response received from the AutoOps API`));
} else {
// Something happened in setting up the request that triggered an Error
- logger.error(
+ this.logger.error(
`${AUTO_OPS_AGENT_CREATION_PREFIX} to be created ${errorLogCodeCause} ${requestConfigDebugStatus}`,
errorMetadataWithRequestConfig
);
@@ -134,9 +153,13 @@ export class AutoOpsAPIService {
}
);
- const validatedResponse = UsageMetricsAutoOpsResponseSchema.body().validate(response.data);
+ const validatedResponse = response.data.metrics
+ ? UsageMetricsAutoOpsResponseSchema.body().validate(response.data)
+ : UsageMetricsAutoOpsResponseSchema.body().validate({
+ metrics: response.data,
+ });
- logger.debug(`[AutoOps API] Successfully created an autoops agent ${response}`);
+ this.logger.debug(`[AutoOps API] Successfully created an autoops agent ${response}`);
return validatedResponse;
}
diff --git a/x-pack/plugins/data_usage/server/services/index.ts b/x-pack/plugins/data_usage/server/services/index.ts
index 3752553e50e9f..69db6b590c6f3 100644
--- a/x-pack/plugins/data_usage/server/services/index.ts
+++ b/x-pack/plugins/data_usage/server/services/index.ts
@@ -5,23 +5,15 @@
* 2.0.
*/
import { ValidationError } from '@kbn/config-schema';
-import { AppContextService } from './app_context';
-import { AutoOpsAPIService } from './autoops_api';
-import type { DataUsageContext } from '../types';
+import { Logger } from '@kbn/logging';
import { MetricTypes } from '../../common/rest_types';
import { AutoOpsError } from './errors';
+import { AutoOpsAPIService } from './autoops_api';
export class DataUsageService {
- private appContextService: AppContextService;
- private autoOpsAPIService: AutoOpsAPIService;
-
- constructor(dataUsageContext: DataUsageContext) {
- this.appContextService = new AppContextService(dataUsageContext);
- this.autoOpsAPIService = new AutoOpsAPIService(this.appContextService);
- }
-
- getLogger(routeName: string) {
- return this.appContextService.getLogger().get(routeName);
+ private readonly logger: Logger;
+ constructor(logger: Logger) {
+ this.logger = logger;
}
async getMetrics({
from,
@@ -35,7 +27,8 @@ export class DataUsageService {
dataStreams: string[];
}) {
try {
- const response = await this.autoOpsAPIService.autoOpsUsageMetricsAPI({
+ const autoOpsAPIService = new AutoOpsAPIService(this.logger);
+ const response = await autoOpsAPIService.autoOpsUsageMetricsAPI({
from,
to,
metricTypes,
diff --git a/x-pack/plugins/data_usage/server/types/types.ts b/x-pack/plugins/data_usage/server/types/types.ts
index 0f4713832c895..7a1e1d76550de 100644
--- a/x-pack/plugins/data_usage/server/types/types.ts
+++ b/x-pack/plugins/data_usage/server/types/types.ts
@@ -20,6 +20,7 @@ import { DataUsageConfigType } from '../config';
export interface DataUsageSetupDependencies {
features: FeaturesPluginSetup;
+ cloud: CloudSetup;
}
/* eslint-disable @typescript-eslint/no-empty-interface*/
@@ -31,6 +32,7 @@ export interface DataUsageServerStart {}
interface DataUsageApiRequestHandlerContext {
core: CoreRequestHandlerContext;
+ logFactory: LoggerFactory;
}
export type DataUsageRequestHandlerContext = CustomRequestHandlerContext<{
diff --git a/x-pack/plugins/data_usage/tsconfig.json b/x-pack/plugins/data_usage/tsconfig.json
index 66c8a5247858b..309bad3e1b63c 100644
--- a/x-pack/plugins/data_usage/tsconfig.json
+++ b/x-pack/plugins/data_usage/tsconfig.json
@@ -32,6 +32,7 @@
"@kbn/cloud-plugin",
"@kbn/server-http-tools",
"@kbn/utility-types-jest",
+ "@kbn/datemath",
],
"exclude": ["target/**/*"]
}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx
index e0c83f36399e9..2982df103bded 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { useEffect } from 'react';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { useExpandedRowCss } from './use_expanded_row_css';
import { GeoPointContentWithMap } from './geo_point_content_with_map';
@@ -34,6 +34,7 @@ export const IndexBasedDataVisualizerExpandedRow = ({
totalDocuments,
timeFieldName,
typeAccessor = 'type',
+ onVisibilityChange,
}: {
item: FieldVisConfig;
dataView: DataView | undefined;
@@ -46,6 +47,7 @@ export const IndexBasedDataVisualizerExpandedRow = ({
*/
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
timeFieldName?: string;
+ onVisibilityChange?: (visible: boolean, item: FieldVisConfig) => void;
}) => {
const config = { ...item, stats: { ...item.stats, totalDocuments } };
const { loading, existsInDocs, fieldName } = config;
@@ -98,6 +100,14 @@ export const IndexBasedDataVisualizerExpandedRow = ({
}
}
+ useEffect(() => {
+ onVisibilityChange?.(true, item);
+
+ return () => {
+ onVisibilityChange?.(false, item);
+ };
+ }, [item, onVisibilityChange]);
+
return (
{loading === true ?
: getCardContent()}
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx
index 3b591a85ff472..e0e43efb694f2 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx
@@ -63,6 +63,7 @@ interface DataVisualizerTableProps
{
overallStatsRunning: boolean;
renderFieldName?: FieldStatisticTableEmbeddableProps['renderFieldName'];
error?: Error | string;
+ isEsql?: boolean;
}
const UnmemoizedDataVisualizerTable = ({
@@ -78,6 +79,7 @@ const UnmemoizedDataVisualizerTable = ({
overallStatsRunning,
renderFieldName,
error,
+ isEsql = false,
}: DataVisualizerTableProps) => {
const { euiTheme } = useEuiTheme();
@@ -87,7 +89,8 @@ const UnmemoizedDataVisualizerTable = ({
const { onTableChange, pagination, sorting } = useTableSettings(
items,
pageState,
- updatePageState
+ updatePageState,
+ isEsql
);
const [showDistributions, setShowDistributions] = useState(showPreviewByDefault ?? true);
const [dimensions, setDimensions] = useState(calculateTableColumnsDimensions());
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts
index b2292970230c0..be427a6ebccae 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts
@@ -27,7 +27,8 @@ interface UseTableSettingsReturnValue {
export function useTableSettings(
items: TypeOfItem[],
pageState: DataVisualizerTableState,
- updatePageState: (update: DataVisualizerTableState) => void
+ updatePageState: (update: DataVisualizerTableState) => void,
+ isEsql: boolean = false
): UseTableSettingsReturnValue {
const { pageIndex, pageSize, sortField, sortDirection } = pageState;
@@ -50,9 +51,9 @@ export function useTableSettings(
pageIndex,
pageSize,
totalItemCount: items.length,
- pageSizeOptions: PAGE_SIZE_OPTIONS,
+ pageSizeOptions: isEsql ? [10, 25] : PAGE_SIZE_OPTIONS,
}),
- [items, pageIndex, pageSize]
+ [items, pageIndex, pageSize, isEsql]
);
const sorting = useMemo(
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx
index c6190c87bcae5..5953144e715fb 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx
@@ -332,6 +332,7 @@ export const IndexDataVisualizerESQL: FC = (dataVi
+ isEsql={true}
items={configs}
pageState={dataVisualizerListState}
updatePageState={onTableChange}
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_data_visualizer_esql_data.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_data_visualizer_esql_data.tsx
index 2323b231d67f7..26b8ff3cf24d7 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_data_visualizer_esql_data.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_data_visualizer_esql_data.tsx
@@ -53,14 +53,14 @@ const defaultSearchQuery = {
};
const FALLBACK_ESQL_QUERY: ESQLQuery = { esql: '' };
-const DEFAULT_LIMIT_SIZE = '10000';
+const DEFAULT_LIMIT_SIZE = '5000';
const defaults = getDefaultPageState();
export const getDefaultESQLDataVisualizerListState = (
overrides?: Partial
): Required => ({
pageIndex: 0,
- pageSize: 25,
+ pageSize: 10,
sortField: 'fieldName',
sortDirection: 'asc',
visibleFieldTypes: [],
@@ -70,7 +70,7 @@ export const getDefaultESQLDataVisualizerListState = (
searchQuery: defaultSearchQuery,
searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
filters: [],
- showDistributions: true,
+ showDistributions: false,
showAllFields: false,
showEmptyFields: false,
probability: null,
@@ -229,6 +229,21 @@ export const useESQLDataVisualizerData = (
} as QueryDslQueryContainer;
}
}
+
+ // Ensure that we don't query frozen data
+ if (filter.bool === undefined) {
+ filter.bool = Object.create(null);
+ }
+
+ if (filter.bool && filter.bool.must_not === undefined) {
+ filter.bool.must_not = [];
+ }
+
+ if (filter.bool && Array.isArray(filter?.bool?.must_not)) {
+ filter.bool.must_not!.push({
+ term: { _tier: 'data_frozen' },
+ });
+ }
return {
id: input.id,
earliest,
@@ -332,9 +347,25 @@ export const useESQLDataVisualizerData = (
const visibleFieldTypes =
dataVisualizerListState.visibleFieldTypes ?? restorableDefaults.visibleFieldTypes;
+ const [expandedRows, setExpandedRows] = useState([]);
+
+ const onVisibilityChange = useCallback((visible: boolean, item: FieldVisConfig) => {
+ if (visible) {
+ setExpandedRows((prev) => [...prev, item.fieldName]);
+ } else {
+ setExpandedRows((prev) => prev.filter((fieldName) => fieldName !== item.fieldName));
+ }
+ }, []);
+
+ const hasExpandedRows = useMemo(() => expandedRows.length > 0, [expandedRows]);
useEffect(
function updateFieldStatFieldsToFetch() {
+ if (dataVisualizerListState?.showDistributions === false && !hasExpandedRows) {
+ setFieldStatFieldsToFetch(undefined);
+ return;
+ }
+
const { sortField, sortDirection } = dataVisualizerListState;
// Otherwise, sort the list of fields by the initial sort field and sort direction
@@ -376,6 +407,8 @@ export const useESQLDataVisualizerData = (
dataVisualizerListState.sortDirection,
nonMetricConfigs,
metricConfigs,
+ dataVisualizerListState?.showDistributions,
+ hasExpandedRows,
]
);
@@ -618,6 +651,7 @@ export const useESQLDataVisualizerData = (
typeAccessor="secondaryType"
timeFieldName={timeFieldName}
onAddFilter={input.onAddFilter}
+ onVisibilityChange={onVisibilityChange}
/>
);
}
@@ -625,7 +659,7 @@ export const useESQLDataVisualizerData = (
}, {} as ItemIdToExpandedRowMap);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
- [currentDataView, totalCount, query.esql, timeFieldName]
+ [currentDataView, totalCount, query.esql, timeFieldName, onVisibilityChange]
);
const combinedProgress = useMemo(
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts
index 3d023a6fc3811..7ea012d2d4b28 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts
@@ -313,7 +313,6 @@ export const useESQLOverallStatsData = (
{ strategy: ESQL_ASYNC_SEARCH_STRATEGY }
)) as ESQLResponse | undefined;
setQueryHistoryStatus(false);
-
const columnInfo = columnsResp?.rawResponse
? columnsResp.rawResponse.all_columns ?? columnsResp.rawResponse.columns
: [];
diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
index 745e03da10d09..8a5e34f58a10f 100644
--- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx
@@ -16,7 +16,6 @@ import { i18n } from '@kbn/i18n';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
-import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
import { StorageContextProvider } from '@kbn/ml-local-storage';
import type { DataView } from '@kbn/data-views-plugin/public';
import { getNestedProperty } from '@kbn/ml-nested-property';
@@ -49,8 +48,6 @@ import { DATA_VISUALIZER_INDEX_VIEWER } from './constants/index_data_visualizer_
import { INDEX_DATA_VISUALIZER_NAME } from '../common/constants';
import { DV_STORAGE_KEYS } from './types/storage';
-const XXL_BREAKPOINT = 1400;
-
const localStorage = new Storage(window.localStorage);
export interface DataVisualizerStateContextProviderProps {
@@ -341,29 +338,20 @@ export const IndexDataVisualizer: FC = ({
return (
-
-
-
-
- {!esql ? (
-
- ) : (
-
- )}
-
-
-
-
+
+
+
+ {!esql ? (
+
+ ) : (
+
+ )}
+
+
+
);
};
diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json
index 970526cdf464e..9e1c19c84067b 100644
--- a/x-pack/plugins/data_visualizer/tsconfig.json
+++ b/x-pack/plugins/data_visualizer/tsconfig.json
@@ -76,7 +76,6 @@
"@kbn/ml-time-buckets",
"@kbn/aiops-log-rate-analysis",
"@kbn/react-kibana-context-render",
- "@kbn/react-kibana-context-theme",
"@kbn/presentation-publishing",
"@kbn/shared-ux-utility",
"@kbn/search-types",
diff --git a/x-pack/plugins/elastic_assistant/common/anonymization/index.ts b/x-pack/plugins/elastic_assistant/common/anonymization/index.ts
index ebef2dff8bdef..9b8007a9129bf 100644
--- a/x-pack/plugins/elastic_assistant/common/anonymization/index.ts
+++ b/x-pack/plugins/elastic_assistant/common/anonymization/index.ts
@@ -9,6 +9,7 @@
export const DEFAULT_ALLOW = [
'_id',
'@timestamp',
+ 'agent.id',
'cloud.availability_zone',
'cloud.provider',
'cloud.region',
diff --git a/x-pack/plugins/elastic_assistant/common/constants.ts b/x-pack/plugins/elastic_assistant/common/constants.ts
index dd6e47e070591..3c3b016870d46 100755
--- a/x-pack/plugins/elastic_assistant/common/constants.ts
+++ b/x-pack/plugins/elastic_assistant/common/constants.ts
@@ -17,13 +17,8 @@ export const ATTACK_DISCOVERY = `${BASE_PATH}/attack_discovery`;
export const ATTACK_DISCOVERY_BY_CONNECTOR_ID = `${ATTACK_DISCOVERY}/{connectorId}`;
export const ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID = `${ATTACK_DISCOVERY}/cancel/{connectorId}`;
-export const MAX_CONVERSATIONS_TO_UPDATE_IN_PARALLEL = 50;
export const CONVERSATIONS_TABLE_MAX_PAGE_SIZE = 100;
-
-export const MAX_ANONYMIZATION_FIELDS_TO_UPDATE_IN_PARALLEL = 50;
export const ANONYMIZATION_FIELDS_TABLE_MAX_PAGE_SIZE = 100;
-
-export const MAX_PROMPTS_TO_UPDATE_IN_PARALLEL = 50;
export const PROMPTS_TABLE_MAX_PAGE_SIZE = 100;
// Knowledge Base
diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts
index 473965a835f14..7c4abffff6520 100644
--- a/x-pack/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts
+++ b/x-pack/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts
@@ -7,6 +7,7 @@
import type { PublicMethodsOf } from '@kbn/utility-types';
import { AIAssistantConversationsDataClient } from '../ai_assistant_data_clients/conversations';
+import { AIAssistantKnowledgeBaseDataClient } from '../ai_assistant_data_clients/knowledge_base';
import { AIAssistantDataClient } from '../ai_assistant_data_clients';
import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence';
@@ -14,6 +15,8 @@ type ConversationsDataClientContract = PublicMethodsOf;
type AttackDiscoveryDataClientContract = PublicMethodsOf;
export type AttackDiscoveryDataClientMock = jest.Mocked;
+type KnowledgeBaseDataClientContract = PublicMethodsOf;
+export type KnowledgeBaseDataClientMock = jest.Mocked;
const createConversationsDataClientMock = () => {
const mocked: ConversationsDataClientMock = {
@@ -52,6 +55,33 @@ export const attackDiscoveryDataClientMock: {
create: createAttackDiscoveryDataClientMock,
};
+const createKnowledgeBaseDataClientMock = () => {
+ const mocked: KnowledgeBaseDataClientMock = {
+ addKnowledgeBaseDocuments: jest.fn(),
+ createInferenceEndpoint: jest.fn(),
+ createKnowledgeBaseEntry: jest.fn(),
+ findDocuments: jest.fn(),
+ getAssistantTools: jest.fn(),
+ getKnowledgeBaseDocumentEntries: jest.fn(),
+ getReader: jest.fn(),
+ getRequiredKnowledgeBaseDocumentEntries: jest.fn(),
+ getWriter: jest.fn().mockResolvedValue({ bulk: jest.fn() }),
+ isInferenceEndpointExists: jest.fn(),
+ isModelInstalled: jest.fn(),
+ isSecurityLabsDocsLoaded: jest.fn(),
+ isSetupAvailable: jest.fn(),
+ isUserDataExists: jest.fn(),
+ setupKnowledgeBase: jest.fn(),
+ };
+ return mocked;
+};
+
+export const knowledgeBaseDataClientMock: {
+ create: () => KnowledgeBaseDataClientMock;
+} = {
+ create: createKnowledgeBaseDataClientMock,
+};
+
type AIAssistantDataClientContract = PublicMethodsOf;
export type AIAssistantDataClientMock = jest.Mocked;
diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/defend_insights_schema.mock.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/defend_insights_schema.mock.ts
new file mode 100644
index 0000000000000..d25b8bb09b13d
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/__mocks__/defend_insights_schema.mock.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { estypes } from '@elastic/elasticsearch';
+import { DefendInsightStatus, DefendInsightType } from '@kbn/elastic-assistant-common';
+
+import type { EsDefendInsightSchema } from '../ai_assistant_data_clients/defend_insights/types';
+
+export const getDefendInsightsSearchEsMock = () => {
+ const searchResponse: estypes.SearchResponse = {
+ took: 0,
+ timed_out: false,
+ _shards: {
+ total: 1,
+ successful: 1,
+ skipped: 0,
+ failed: 0,
+ },
+ hits: {
+ total: {
+ value: 1,
+ relation: 'eq',
+ },
+ max_score: 1,
+ hits: [
+ {
+ _index: '.kibana-elastic-ai-assistant-defend-insights-default',
+ _id: '655c52ec-49ee-4d20-87e5-7edd6d8f84e8',
+ _score: 1,
+ _source: {
+ '@timestamp': '2024-09-24T10:48:46.847Z',
+ created_at: '2024-09-24T10:48:46.847Z',
+ users: [
+ {
+ id: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0',
+ name: 'elastic',
+ },
+ ],
+ status: DefendInsightStatus.Enum.succeeded,
+ api_config: {
+ action_type_id: '.bedrock',
+ connector_id: 'ac4e19d1-e2e2-49af-bf4b-59428473101c',
+ model: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
+ },
+ endpoint_ids: ['6e09ec1c-644c-4148-a02d-be451c35400d'],
+ insight_type: DefendInsightType.Enum.incompatible_antivirus,
+ insights: [
+ {
+ group: 'windows_defenders',
+ events: [],
+ },
+ ],
+ updated_at: '2024-09-24T10:48:59.952Z',
+ last_viewed_at: '2024-09-24T10:49:53.522Z',
+ namespace: 'default',
+ id: '655c52ec-49ee-4d20-87e5-7edd6d8f84e8',
+ generation_intervals: [
+ {
+ date: '2024-09-24T10:48:59.952Z',
+ duration_ms: 13113,
+ },
+ ],
+ average_interval_ms: 13113,
+ replacements: [
+ {
+ uuid: '2009c67b-89b8-43d9-b502-2c32f71875a0',
+ value: 'root',
+ },
+ {
+ uuid: '9f7f91b6-6853-48b7-bfb8-403f5efb2364',
+ value: 'joey-dev-default-3539',
+ },
+ {
+ uuid: 'c08e4851-7234-408a-8083-7fd5740e4255',
+ value: 'syslog',
+ },
+ {
+ uuid: '826c58bd-1466-42fd-af1f-9094c155811b',
+ value: 'messagebus',
+ },
+ {
+ uuid: '1f8e3668-c7d7-4fdb-8195-3f337dfe10bf',
+ value: 'polkitd',
+ },
+ {
+ uuid: 'e101d201-c675-47f3-b488-77bd0ce71920',
+ value: 'systemd-network',
+ },
+ {
+ uuid: '0144102f-d69c-43a3-bf3b-04bde7d1b4e8',
+ value: 'systemd-resolve',
+ },
+ {
+ uuid: '00c5a919-949e-4031-956e-3eeb071e9210',
+ value: 'systemd-timesync',
+ },
+ ],
+ events_context_count: 100,
+ },
+ },
+ ],
+ },
+ };
+ return searchResponse;
+};
diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/knowledge_base_entry_schema.mock.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/knowledge_base_entry_schema.mock.ts
new file mode 100644
index 0000000000000..8171dd2b39249
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/__mocks__/knowledge_base_entry_schema.mock.ts
@@ -0,0 +1,174 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { estypes } from '@elastic/elasticsearch';
+import {
+ KnowledgeBaseEntryCreateProps,
+ KnowledgeBaseEntryResponse,
+ KnowledgeBaseEntryUpdateProps,
+} from '@kbn/elastic-assistant-common';
+import {
+ EsKnowledgeBaseEntrySchema,
+ EsDocumentEntry,
+} from '../ai_assistant_data_clients/knowledge_base/types';
+const indexEntry: EsKnowledgeBaseEntrySchema = {
+ id: '1234',
+ '@timestamp': '2020-04-20T15:25:31.830Z',
+ created_at: '2020-04-20T15:25:31.830Z',
+ created_by: 'my_profile_uid',
+ updated_at: '2020-04-20T15:25:31.830Z',
+ updated_by: 'my_profile_uid',
+ name: 'test',
+ namespace: 'default',
+ type: 'index',
+ index: 'test',
+ field: 'test',
+ description: 'test',
+ query_description: 'test',
+ input_schema: [
+ {
+ field_name: 'test',
+ field_type: 'test',
+ description: 'test',
+ },
+ ],
+ users: [
+ {
+ name: 'my_username',
+ id: 'my_profile_uid',
+ },
+ ],
+};
+export const documentEntry: EsDocumentEntry = {
+ id: '5678',
+ '@timestamp': '2020-04-20T15:25:31.830Z',
+ created_at: '2020-04-20T15:25:31.830Z',
+ created_by: 'my_profile_uid',
+ updated_at: '2020-04-20T15:25:31.830Z',
+ updated_by: 'my_profile_uid',
+ name: 'test',
+ namespace: 'default',
+ semantic_text: 'test',
+ type: 'document',
+ kb_resource: 'test',
+ required: true,
+ source: 'test',
+ text: 'test',
+ users: [
+ {
+ name: 'my_username',
+ id: 'my_profile_uid',
+ },
+ ],
+};
+
+export const getKnowledgeBaseEntrySearchEsMock = (src = 'document') => {
+ const searchResponse: estypes.SearchResponse = {
+ took: 3,
+ timed_out: false,
+ _shards: {
+ total: 2,
+ successful: 2,
+ skipped: 0,
+ failed: 0,
+ },
+ hits: {
+ total: {
+ value: 1,
+ relation: 'eq',
+ },
+ max_score: 0,
+ hits: [
+ {
+ _id: '1',
+ _index: '',
+ _score: 0,
+ _source: src === 'document' ? documentEntry : indexEntry,
+ },
+ ],
+ },
+ };
+ return searchResponse;
+};
+
+export const getCreateKnowledgeBaseEntrySchemaMock = (
+ rest?: Partial
+): KnowledgeBaseEntryCreateProps => {
+ const { type = 'document', ...restProps } = rest ?? {};
+ if (type === 'document') {
+ return {
+ type: 'document',
+ source: 'test',
+ text: 'test',
+ name: 'test',
+ kbResource: 'test',
+ ...restProps,
+ };
+ }
+ return {
+ type: 'index',
+ name: 'test',
+ index: 'test',
+ field: 'test',
+ description: 'test',
+ queryDescription: 'test',
+ inputSchema: [
+ {
+ fieldName: 'test',
+ fieldType: 'test',
+ description: 'test',
+ },
+ ],
+ ...restProps,
+ };
+};
+
+export const getUpdateKnowledgeBaseEntrySchemaMock = (
+ entryId = 'entry-1'
+): KnowledgeBaseEntryUpdateProps => ({
+ name: 'another 2',
+ namespace: 'default',
+ type: 'document',
+ source: 'test',
+ text: 'test',
+ kbResource: 'test',
+ id: entryId,
+});
+
+export const getKnowledgeBaseEntryMock = (
+ params: KnowledgeBaseEntryCreateProps | KnowledgeBaseEntryUpdateProps = {
+ name: 'test',
+ namespace: 'default',
+ type: 'document',
+ text: 'test',
+ source: 'test',
+ kbResource: 'test',
+ required: true,
+ }
+): KnowledgeBaseEntryResponse => ({
+ id: '1',
+ ...params,
+ createdBy: 'my_profile_uid',
+ updatedBy: 'my_profile_uid',
+ createdAt: '2020-04-20T15:25:31.830Z',
+ updatedAt: '2020-04-20T15:25:31.830Z',
+ namespace: 'default',
+ users: [
+ {
+ name: 'my_username',
+ id: 'my_profile_uid',
+ },
+ ],
+});
+
+export const getQueryKnowledgeBaseEntryParams = (
+ isUpdate?: boolean
+): KnowledgeBaseEntryCreateProps | KnowledgeBaseEntryUpdateProps => {
+ return isUpdate
+ ? getUpdateKnowledgeBaseEntrySchemaMock()
+ : getCreateKnowledgeBaseEntrySchemaMock();
+};
diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/query_text.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/query_text.ts
deleted file mode 100644
index 1ea69b786ad1f..0000000000000
--- a/x-pack/plugins/elastic_assistant/server/__mocks__/query_text.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-/**
- * This mock query text is an example of a prompt that might be passed to
- * the `ElasticSearchStore`'s `similaritySearch` function, as the `query`
- * parameter.
- *
- * In the real world, an LLM extracted the `mockQueryText` from the
- * following prompt, which includes a system prompt:
- *
- * ```
- * You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.
- * If you answer a question related to KQL, EQL, or ES|QL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.
- *
- * Use the following context to answer questions:
- *
- * Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called "follow_up" that contains a value of "true", otherwise, it should contain "false". The user names should also be enriched with their respective group names.
- * ```
- *
- * In the example above, the LLM omitted the system prompt, such that only `mockQueryText` is passed to the `similaritySearch` function.
- */
-export const mockQueryText =
- 'Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called follow_up that contains a value of true, otherwise, it should contain false. The user names should also be enriched with their respective group names.';
diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts
index 698645e8d3c55..26db891242884 100644
--- a/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts
+++ b/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts
@@ -11,10 +11,16 @@ import {
ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID,
CAPABILITIES,
} from '../../common/constants';
+import type {
+ DefendInsightsGetRequestQuery,
+ DefendInsightsPostRequestBody,
+} from '@kbn/elastic-assistant-common';
import {
AttackDiscoveryPostRequestBody,
ConversationCreateProps,
ConversationUpdateProps,
+ DEFEND_INSIGHTS,
+ DEFEND_INSIGHTS_BY_ID,
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION,
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_FIND,
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL,
@@ -23,10 +29,14 @@ import {
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID_MESSAGES,
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
ELASTIC_AI_ASSISTANT_EVALUATE_URL,
+ ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL,
+ ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BULK_ACTION,
+ ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_FIND,
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_INDICES_URL,
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_URL,
ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION,
ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND,
+ PerformKnowledgeBaseEntryBulkActionRequestBody,
PostEvaluateRequestBodyInput,
} from '@kbn/elastic-assistant-common';
import {
@@ -34,6 +44,7 @@ import {
getCreateConversationSchemaMock,
getUpdateConversationSchemaMock,
} from './conversations_schema.mock';
+import { getCreateKnowledgeBaseEntrySchemaMock } from './knowledge_base_entry_schema.mock';
import {
PromptCreateProps,
PromptUpdateProps,
@@ -67,6 +78,22 @@ export const getPostKnowledgeBaseRequest = (resource?: string) =>
query: { resource },
});
+export const getCreateKnowledgeBaseEntryRequest = () =>
+ requestMock.create({
+ method: 'post',
+ path: ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL,
+ body: getCreateKnowledgeBaseEntrySchemaMock(),
+ });
+
+export const getBulkActionKnowledgeBaseEntryRequest = (
+ body: PerformKnowledgeBaseEntryBulkActionRequestBody
+) =>
+ requestMock.create({
+ method: 'post',
+ path: ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BULK_ACTION,
+ body,
+ });
+
export const getGetCapabilitiesRequest = () =>
requestMock.create({
method: 'get',
@@ -80,6 +107,12 @@ export const getPostEvaluateRequest = ({ body }: { body: PostEvaluateRequestBody
path: ELASTIC_AI_ASSISTANT_EVALUATE_URL,
});
+export const getKnowledgeBaseEntryFindRequest = () =>
+ requestMock.create({
+ method: 'get',
+ path: ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_FIND,
+ });
+
export const getCurrentUserFindRequest = () =>
requestMock.create({
method: 'get',
@@ -208,3 +241,24 @@ export const postAttackDiscoveryRequest = (body: AttackDiscoveryPostRequestBody)
path: ATTACK_DISCOVERY,
body,
});
+
+export const getDefendInsightRequest = (insightId: string) =>
+ requestMock.create({
+ method: 'get',
+ path: DEFEND_INSIGHTS_BY_ID,
+ params: { id: insightId },
+ });
+
+export const getDefendInsightsRequest = (queryParams: DefendInsightsGetRequestQuery) =>
+ requestMock.create({
+ method: 'get',
+ path: DEFEND_INSIGHTS,
+ query: queryParams,
+ });
+
+export const postDefendInsightsRequest = (body: DefendInsightsPostRequestBody) =>
+ requestMock.create({
+ method: 'post',
+ path: DEFEND_INSIGHTS,
+ body,
+ });
diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts
index a065c7de42586..77bd6b00105b6 100644
--- a/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts
+++ b/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts
@@ -18,6 +18,7 @@ import {
attackDiscoveryDataClientMock,
conversationsDataClientMock,
dataClientMock,
+ knowledgeBaseDataClientMock,
} from './data_clients.mock';
import { AIAssistantConversationsDataClient } from '../ai_assistant_data_clients/conversations';
import { AIAssistantDataClient } from '../ai_assistant_data_clients';
@@ -27,6 +28,8 @@ import {
} from '../ai_assistant_data_clients/knowledge_base';
import { defaultAssistantFeatures } from '@kbn/elastic-assistant-common';
import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence';
+import { DefendInsightsDataClient } from '../ai_assistant_data_clients/defend_insights';
+import { authenticatedUser } from './user';
export const createMockClients = () => {
const core = coreMock.createRequestHandlerContext();
@@ -42,9 +45,10 @@ export const createMockClients = () => {
logger: loggingSystemMock.createLogger(),
telemetry: coreMock.createSetup().analytics,
getAIAssistantConversationsDataClient: conversationsDataClientMock.create(),
- getAIAssistantKnowledgeBaseDataClient: dataClientMock.create(),
+ getAIAssistantKnowledgeBaseDataClient: knowledgeBaseDataClientMock.create(),
getAIAssistantPromptsDataClient: dataClientMock.create(),
getAttackDiscoveryDataClient: attackDiscoveryDataClientMock.create(),
+ getDefendInsightsDataClient: dataClientMock.create(),
getAIAssistantAnonymizationFieldsDataClient: dataClientMock.create(),
getSpaceId: jest.fn(),
getCurrentUser: jest.fn(),
@@ -123,6 +127,10 @@ const createElasticAssistantRequestContextMock = (
() => clients.elasticAssistant.getAttackDiscoveryDataClient
) as unknown as jest.MockInstance, [], unknown> &
(() => Promise),
+ getDefendInsightsDataClient: jest.fn(
+ () => clients.elasticAssistant.getDefendInsightsDataClient
+ ) as unknown as jest.MockInstance, [], unknown> &
+ (() => Promise),
getAIAssistantKnowledgeBaseDataClient: jest.fn(
() => clients.elasticAssistant.getAIAssistantKnowledgeBaseDataClient
) as unknown as jest.MockInstance<
@@ -133,9 +141,9 @@ const createElasticAssistantRequestContextMock = (
((
params?: GetAIAssistantKnowledgeBaseDataClientParams
) => Promise),
- getCurrentUser: jest.fn(),
+ getCurrentUser: jest.fn().mockReturnValue(authenticatedUser),
getServerBasePath: jest.fn(),
- getSpaceId: jest.fn(),
+ getSpaceId: jest.fn().mockReturnValue('default'),
inference: { getClient: jest.fn() },
core: clients.core,
telemetry: clients.elasticAssistant.telemetry,
diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/response.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/response.ts
index ae736c77c30ef..b7ab289d0f270 100644
--- a/x-pack/plugins/elastic_assistant/server/__mocks__/response.ts
+++ b/x-pack/plugins/elastic_assistant/server/__mocks__/response.ts
@@ -15,8 +15,8 @@ import { EsPromptsSchema } from '../ai_assistant_data_clients/prompts/types';
import { getPromptsSearchEsMock } from './prompts_schema.mock';
import { EsAnonymizationFieldsSchema } from '../ai_assistant_data_clients/anonymization_fields/types';
import { getAnonymizationFieldsSearchEsMock } from './anonymization_fields_schema.mock';
-import { getAttackDiscoverySearchEsMock } from './attack_discovery_schema.mock';
-import { EsAttackDiscoverySchema } from '../lib/attack_discovery/persistence/types';
+import { getKnowledgeBaseEntrySearchEsMock } from './knowledge_base_entry_schema.mock';
+import { EsKnowledgeBaseEntrySchema } from '../ai_assistant_data_clients/knowledge_base/types';
export const responseMock = {
create: httpServerMock.createResponseFactory,
@@ -29,6 +29,14 @@ export const getEmptyFindResult = (): FindResponse => ({
data: getBasicEmptySearchResponse(),
});
+export const getFindKnowledgeBaseEntriesResultWithSingleHit =
+ (): FindResponse => ({
+ page: 1,
+ perPage: 1,
+ total: 1,
+ data: getKnowledgeBaseEntrySearchEsMock(),
+ });
+
export const getFindConversationsResultWithSingleHit = (): FindResponse => ({
page: 1,
perPage: 1,
@@ -36,14 +44,6 @@ export const getFindConversationsResultWithSingleHit = (): FindResponse => ({
- page: 1,
- perPage: 1,
- total: 1,
- data: getAttackDiscoverySearchEsMock(),
- });
-
export const getFindPromptsResultWithSingleHit = (): FindResponse => ({
page: 1,
perPage: 1,
diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/user.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/user.ts
new file mode 100644
index 0000000000000..bcd29818c4ed7
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/__mocks__/user.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { AuthenticatedUser } from '@kbn/core-security-common';
+
+export const authenticatedUser = {
+ username: 'my_username',
+ profile_uid: 'my_profile_uid',
+ authentication_realm: {
+ type: 'my_realm_type',
+ name: 'my_realm_name',
+ },
+} as AuthenticatedUser;
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.test.ts
index 6fba2f9c8b606..0546ab39db592 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.test.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.test.ts
@@ -8,23 +8,15 @@
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { createConversation } from './create_conversation';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
-import { estypes } from '@elastic/elasticsearch';
-import { EsConversationSchema } from './types';
import { getConversation } from './get_conversation';
+import { authenticatedUser } from '../../__mocks__/user';
import { ConversationCreateProps, ConversationResponse } from '@kbn/elastic-assistant-common';
-import { AuthenticatedUser } from '@kbn/core-security-common';
jest.mock('./get_conversation', () => ({
getConversation: jest.fn(),
}));
-const mockUser1 = {
- username: 'my_username',
- authentication_realm: {
- type: 'my_realm_type',
- name: 'my_realm_name',
- },
-} as AuthenticatedUser;
+const mockUser1 = authenticatedUser;
export const getCreateConversationMock = (): ConversationCreateProps => ({
title: 'test',
@@ -68,55 +60,6 @@ export const getConversationResponseMock = (): ConversationResponse => ({
],
});
-export const getSearchConversationMock = (): estypes.SearchResponse => ({
- _scroll_id: '123',
- _shards: {
- failed: 0,
- skipped: 0,
- successful: 0,
- total: 0,
- },
- hits: {
- hits: [
- {
- _id: '1',
- _index: '',
- _score: 0,
- _source: {
- '@timestamp': '2020-04-20T15:25:31.830Z',
- created_at: '2020-04-20T15:25:31.830Z',
- title: 'title-1',
- updated_at: '2020-04-20T15:25:31.830Z',
- messages: [],
- category: 'assistant',
- id: '1',
- namespace: 'default',
- is_default: true,
- exclude_from_last_conversation_storage: false,
- api_config: {
- action_type_id: '.gen-ai',
- connector_id: 'c1',
- default_system_prompt_id: 'prompt-1',
- model: 'test',
- provider: 'Azure OpenAI',
- },
- users: [
- {
- id: '1111',
- name: 'elastic',
- },
- ],
- replacements: undefined,
- },
- },
- ],
- max_score: 0,
- total: 1,
- },
- timed_out: false,
- took: 10,
-});
-
describe('createConversation', () => {
let logger: ReturnType;
beforeEach(() => {
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/get_conversation.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/get_conversation.test.ts
index a5a292c096cdc..43290c8a00293 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/get_conversation.test.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/get_conversation.test.ts
@@ -5,11 +5,12 @@
* 2.0.
*/
-import type { AuthenticatedUser, Logger } from '@kbn/core/server';
+import type { Logger } from '@kbn/core/server';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { getConversation } from './get_conversation';
import { estypes } from '@elastic/elasticsearch';
import { EsConversationSchema } from './types';
+import { authenticatedUser } from '../../__mocks__/user';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { ConversationResponse } from '@kbn/elastic-assistant-common';
@@ -43,13 +44,7 @@ export const getConversationResponseMock = (): ConversationResponse => ({
replacements: undefined,
});
-const mockUser1 = {
- username: 'my_username',
- authentication_realm: {
- type: 'my_realm_type',
- name: 'my_realm_name',
- },
-} as AuthenticatedUser;
+const mockUser1 = authenticatedUser;
export const getSearchConversationMock = (): estypes.SearchResponse => ({
_scroll_id: '123',
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/index.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/index.test.ts
index 7669c281a42da..4c57f66710f5e 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/index.test.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/index.test.ts
@@ -7,21 +7,15 @@
import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks';
import type { UpdateByQueryRequest } from '@elastic/elasticsearch/lib/api/types';
import { AIAssistantConversationsDataClient } from '.';
-import { AuthenticatedUser } from '@kbn/core-security-common';
import { getUpdateConversationSchemaMock } from '../../__mocks__/conversations_schema.mock';
+import { authenticatedUser } from '../../__mocks__/user';
import { AIAssistantDataClientParams } from '..';
const date = '2023-03-28T22:27:28.159Z';
let logger: ReturnType<(typeof loggingSystemMock)['createLogger']>;
const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
-const mockUser1 = {
- username: 'my_username',
- authentication_realm: {
- type: 'my_realm_type',
- name: 'my_realm_name',
- },
-} as AuthenticatedUser;
+const mockUser1 = authenticatedUser;
describe('AIAssistantConversationsDataClient', () => {
let assistantConversationsDataClientParams: AIAssistantDataClientParams;
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.test.ts
index c44329c28db48..baeea677b1a66 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.test.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.test.ts
@@ -13,8 +13,8 @@ import {
updateConversation,
} from './update_conversation';
import { getConversation } from './get_conversation';
+import { authenticatedUser } from '../../__mocks__/user';
import { ConversationResponse, ConversationUpdateProps } from '@kbn/elastic-assistant-common';
-import { AuthenticatedUser } from '@kbn/core-security-common';
export const getUpdateConversationOptionsMock = (): ConversationUpdateProps => ({
id: 'test',
@@ -31,13 +31,7 @@ export const getUpdateConversationOptionsMock = (): ConversationUpdateProps => (
replacements: {},
});
-const mockUser1 = {
- username: 'my_username',
- authentication_realm: {
- type: 'my_realm_type',
- name: 'my_realm_name',
- },
-} as AuthenticatedUser;
+const mockUser1 = authenticatedUser;
export const getConversationResponseMock = (): ConversationResponse => ({
id: 'test',
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/field_maps_configuration.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/field_maps_configuration.ts
new file mode 100644
index 0000000000000..5769ab4557102
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/field_maps_configuration.ts
@@ -0,0 +1,174 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { FieldMap } from '@kbn/data-stream-adapter';
+
+export const defendInsightsFieldMap: FieldMap = {
+ '@timestamp': {
+ type: 'date',
+ array: false,
+ required: false,
+ },
+ users: {
+ type: 'nested',
+ array: true,
+ required: false,
+ },
+ 'users.id': {
+ type: 'keyword',
+ array: false,
+ required: true,
+ },
+ 'users.name': {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
+ id: {
+ type: 'keyword',
+ array: false,
+ required: true,
+ },
+ last_viewed_at: {
+ type: 'date',
+ array: false,
+ required: true,
+ },
+ updated_at: {
+ type: 'date',
+ array: false,
+ required: true,
+ },
+ created_at: {
+ type: 'date',
+ array: false,
+ required: true,
+ },
+ endpoint_ids: {
+ type: 'keyword',
+ array: true,
+ required: false,
+ },
+ insight_type: {
+ type: 'keyword',
+ required: true,
+ },
+ insights: {
+ type: 'nested',
+ array: true,
+ required: false,
+ },
+ 'insights.group': {
+ type: 'keyword',
+ array: true,
+ required: true,
+ },
+ 'insights.events': {
+ type: 'nested',
+ array: true,
+ required: false,
+ },
+ 'insights.events.endpoint_id': {
+ type: 'keyword',
+ array: false,
+ required: true,
+ },
+ 'insights.events.id': {
+ type: 'keyword',
+ array: false,
+ required: true,
+ },
+ 'insights.events.value': {
+ type: 'text',
+ array: false,
+ required: true,
+ },
+ replacements: {
+ type: 'object',
+ array: false,
+ required: false,
+ },
+ 'replacements.value': {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
+ 'replacements.uuid': {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
+ api_config: {
+ type: 'object',
+ array: false,
+ required: true,
+ },
+ 'api_config.connector_id': {
+ type: 'keyword',
+ array: false,
+ required: true,
+ },
+ 'api_config.action_type_id': {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
+ 'api_config.default_system_prompt_id': {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
+ 'api_config.provider': {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
+ 'api_config.model': {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
+ events_context_count: {
+ type: 'integer',
+ array: false,
+ required: false,
+ },
+ status: {
+ type: 'keyword',
+ array: false,
+ required: true,
+ },
+ namespace: {
+ type: 'keyword',
+ array: false,
+ required: true,
+ },
+ average_interval_ms: {
+ type: 'integer',
+ array: false,
+ required: false,
+ },
+ failure_reason: {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
+ generation_intervals: {
+ type: 'nested',
+ array: true,
+ required: false,
+ },
+ 'generation_intervals.date': {
+ type: 'date',
+ array: false,
+ required: true,
+ },
+ 'generation_intervals.duration_ms': {
+ type: 'integer',
+ array: false,
+ required: true,
+ },
+} as const;
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/get_defend_insight.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/get_defend_insight.test.ts
new file mode 100644
index 0000000000000..415487534a1b6
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/get_defend_insight.test.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { AuthenticatedUser } from '@kbn/core-security-common';
+
+import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
+import { loggerMock } from '@kbn/logging-mocks';
+
+import { getDefendInsightsSearchEsMock } from '../../__mocks__/defend_insights_schema.mock';
+import { getDefendInsight } from './get_defend_insight';
+
+const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+const mockLogger = loggerMock.create();
+
+const mockResponse = getDefendInsightsSearchEsMock();
+
+const user = {
+ username: 'test_user',
+ profile_uid: '1234',
+ authentication_realm: {
+ type: 'my_realm_type',
+ name: 'my_realm_name',
+ },
+} as AuthenticatedUser;
+const mockRequest = {
+ esClient: mockEsClient,
+ index: 'defend-insights-index',
+ id: 'insight-id',
+ user,
+ logger: mockLogger,
+};
+describe('getDefendInsight', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should get defend insight by id successfully', async () => {
+ mockEsClient.search.mockResolvedValueOnce(mockResponse);
+
+ const response = await getDefendInsight(mockRequest);
+
+ expect(response).not.toBeNull();
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
+ expect(mockLogger.error).not.toHaveBeenCalled();
+ });
+
+ it('should return null if no defend insights found', async () => {
+ mockEsClient.search.mockResolvedValueOnce({ ...mockResponse, hits: { hits: [] } });
+
+ const response = await getDefendInsight(mockRequest);
+
+ expect(response).toBeNull();
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
+ expect(mockLogger.error).not.toHaveBeenCalled();
+ });
+
+ it('should throw error on elasticsearch search failure', async () => {
+ mockEsClient.search.mockRejectedValueOnce(new Error('Elasticsearch error'));
+
+ await expect(getDefendInsight(mockRequest)).rejects.toThrowError('Elasticsearch error');
+
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
+ expect(mockLogger.error).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/get_defend_insight.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/get_defend_insight.ts
new file mode 100644
index 0000000000000..4eeef2afd8738
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/get_defend_insight.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server';
+import { DefendInsightsResponse } from '@kbn/elastic-assistant-common';
+
+import { EsDefendInsightSchema } from './types';
+import { transformESSearchToDefendInsights } from './helpers';
+
+export interface GetDefendInsightParams {
+ esClient: ElasticsearchClient;
+ logger: Logger;
+ index: string;
+ id: string;
+ user: AuthenticatedUser;
+}
+
+export const getDefendInsight = async ({
+ esClient,
+ logger,
+ index,
+ id,
+ user,
+}: GetDefendInsightParams): Promise => {
+ const filterByUser = [
+ {
+ nested: {
+ path: 'users',
+ query: {
+ bool: {
+ must: [
+ {
+ match: user.profile_uid
+ ? { 'users.id': user.profile_uid }
+ : { 'users.name': user.username },
+ },
+ ],
+ },
+ },
+ },
+ },
+ ];
+ try {
+ const response = await esClient.search({
+ query: {
+ bool: {
+ must: [
+ {
+ bool: {
+ should: [
+ {
+ term: {
+ _id: id,
+ },
+ },
+ ],
+ },
+ },
+ ...filterByUser,
+ ],
+ },
+ },
+ _source: true,
+ ignore_unavailable: true,
+ index,
+ seq_no_primary_term: true,
+ });
+ const insights = transformESSearchToDefendInsights(response);
+ return insights[0] ?? null;
+ } catch (err) {
+ logger.error(`Error fetching Defend insight: ${err} with id: ${id}`);
+ throw err;
+ }
+};
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/helpers.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/helpers.test.ts
new file mode 100644
index 0000000000000..8e0793218154a
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/helpers.test.ts
@@ -0,0 +1,64 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { DefendInsightsGetRequestQuery } from '@kbn/elastic-assistant-common';
+
+import { DefendInsightType, DefendInsightStatus } from '@kbn/elastic-assistant-common';
+
+import { queryParamsToEsQuery } from './helpers';
+
+describe('defend insights data client helpers', () => {
+ describe('queryParamsToEsQuery', () => {
+ let queryParams: DefendInsightsGetRequestQuery;
+ let expectedQuery: object[];
+
+ function getDefaultQueryParams(): DefendInsightsGetRequestQuery {
+ return {
+ ids: ['insight-id1', 'insight-id2'],
+ endpoint_ids: ['endpoint-id1', 'endpoint-id2'],
+ connector_id: 'connector-id1',
+ type: DefendInsightType.Enum.incompatible_antivirus,
+ status: DefendInsightStatus.Enum.succeeded,
+ };
+ }
+
+ function getDefaultExpectedQuery(): object[] {
+ return [
+ { terms: { _id: queryParams.ids } },
+ { terms: { endpoint_ids: queryParams.endpoint_ids } },
+ { term: { 'api_config.connector_id': queryParams.connector_id } },
+ { term: { insight_type: queryParams.type } },
+ { term: { status: queryParams.status } },
+ ];
+ }
+
+ beforeEach(() => {
+ queryParams = getDefaultQueryParams();
+ expectedQuery = getDefaultExpectedQuery();
+ });
+
+ it('should correctly convert valid query parameters to Elasticsearch query format', () => {
+ const result = queryParamsToEsQuery(queryParams);
+ expect(result).toEqual(expectedQuery);
+ });
+
+ it('should ignore invalid query parameters', () => {
+ const badParams = {
+ ...queryParams,
+ invalid_param: 'invalid value',
+ };
+
+ const result = queryParamsToEsQuery(badParams);
+ expect(result).toEqual(expectedQuery);
+ });
+
+ it('should handle empty query parameters', () => {
+ const result = queryParamsToEsQuery({});
+ expect(result).toEqual([]);
+ });
+ });
+});
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/helpers.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/helpers.ts
new file mode 100644
index 0000000000000..b8164f53d9815
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/helpers.ts
@@ -0,0 +1,221 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { get as _get, isArray } from 'lodash';
+
+import type { estypes } from '@elastic/elasticsearch';
+import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import type { AuthenticatedUser } from '@kbn/core/server';
+import type {
+ DefendInsightCreateProps,
+ DefendInsightUpdateProps,
+ DefendInsightsResponse,
+ DefendInsightsGetRequestQuery,
+} from '@kbn/elastic-assistant-common';
+
+import type {
+ CreateDefendInsightSchema,
+ EsDefendInsightSchema,
+ UpdateDefendInsightSchema,
+} from './types';
+
+export const transformESSearchToDefendInsights = (
+ response: estypes.SearchResponse
+): DefendInsightsResponse[] => {
+ return response.hits.hits
+ .filter((hit) => hit._source !== undefined)
+ .map((hit) => {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const insightSchema = hit._source!;
+ const defendInsight: DefendInsightsResponse = {
+ timestamp: insightSchema['@timestamp'],
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ id: hit._id!,
+ backingIndex: hit._index,
+ createdAt: insightSchema.created_at,
+ updatedAt: insightSchema.updated_at,
+ lastViewedAt: insightSchema.last_viewed_at,
+ users:
+ insightSchema.users?.map((user) => ({
+ id: user.id,
+ name: user.name,
+ })) ?? [],
+ namespace: insightSchema.namespace,
+ status: insightSchema.status,
+ eventsContextCount: insightSchema.events_context_count,
+ apiConfig: {
+ connectorId: insightSchema.api_config.connector_id,
+ actionTypeId: insightSchema.api_config.action_type_id,
+ defaultSystemPromptId: insightSchema.api_config.default_system_prompt_id,
+ model: insightSchema.api_config.model,
+ provider: insightSchema.api_config.provider,
+ },
+ endpointIds: insightSchema.endpoint_ids,
+ insightType: insightSchema.insight_type,
+ insights: insightSchema.insights.map((insight) => ({
+ group: insight.group,
+ events: insight.events?.map((event) => ({
+ id: event.id,
+ endpointId: event.endpoint_id,
+ value: event.value,
+ })),
+ })),
+ replacements: insightSchema.replacements?.reduce((acc: Record, r) => {
+ acc[r.uuid] = r.value;
+ return acc;
+ }, {}),
+ generationIntervals:
+ insightSchema.generation_intervals?.map((interval) => ({
+ date: interval.date,
+ durationMs: interval.duration_ms,
+ })) ?? [],
+ averageIntervalMs: insightSchema.average_interval_ms ?? 0,
+ failureReason: insightSchema.failure_reason,
+ };
+
+ return defendInsight;
+ });
+};
+
+export const transformToCreateScheme = (
+ createdAt: string,
+ spaceId: string,
+ user: AuthenticatedUser,
+ {
+ endpointIds,
+ insightType,
+ insights,
+ apiConfig,
+ eventsContextCount,
+ replacements,
+ status,
+ }: DefendInsightCreateProps
+): CreateDefendInsightSchema => {
+ return {
+ '@timestamp': createdAt,
+ created_at: createdAt,
+ users: [
+ {
+ id: user.profile_uid,
+ name: user.username,
+ },
+ ],
+ status,
+ api_config: {
+ action_type_id: apiConfig.actionTypeId,
+ connector_id: apiConfig.connectorId,
+ default_system_prompt_id: apiConfig.defaultSystemPromptId,
+ model: apiConfig.model,
+ provider: apiConfig.provider,
+ },
+ events_context_count: eventsContextCount,
+ endpoint_ids: endpointIds,
+ insight_type: insightType,
+ insights: insights?.map((insight) => ({
+ group: insight.group,
+ events: insight.events?.map((event) => ({
+ id: event.id,
+ endpoint_id: event.endpointId,
+ value: event.value,
+ })),
+ })),
+ updated_at: createdAt,
+ last_viewed_at: createdAt,
+ replacements: replacements
+ ? Object.keys(replacements).map((key) => ({
+ uuid: key,
+ value: replacements[key],
+ }))
+ : undefined,
+ namespace: spaceId,
+ };
+};
+
+export const transformToUpdateScheme = (
+ updatedAt: string,
+ {
+ eventsContextCount,
+ apiConfig,
+ insights,
+ failureReason,
+ generationIntervals,
+ id,
+ replacements,
+ lastViewedAt,
+ status,
+ }: DefendInsightUpdateProps
+): UpdateDefendInsightSchema => {
+ const averageIntervalMsObj =
+ generationIntervals && generationIntervals.length > 0
+ ? {
+ average_interval_ms: Math.trunc(
+ generationIntervals.reduce((acc, interval) => acc + interval.durationMs, 0) /
+ generationIntervals.length
+ ),
+ generation_intervals: generationIntervals.map((interval) => ({
+ date: interval.date,
+ duration_ms: interval.durationMs,
+ })),
+ }
+ : {};
+ return {
+ events_context_count: eventsContextCount,
+ ...(apiConfig
+ ? {
+ api_config: {
+ action_type_id: apiConfig.actionTypeId,
+ connector_id: apiConfig.connectorId,
+ default_system_prompt_id: apiConfig.defaultSystemPromptId,
+ model: apiConfig.model,
+ provider: apiConfig.provider,
+ },
+ }
+ : {}),
+ ...(insights
+ ? {
+ insights: insights.map((insight) => ({
+ group: insight.group,
+ events: insight.events?.map((event) => ({
+ id: event.id,
+ endpoint_id: event.endpointId,
+ value: event.value,
+ })),
+ })),
+ }
+ : {}),
+ failure_reason: failureReason,
+ id,
+ replacements: replacements
+ ? Object.keys(replacements).map((key) => ({
+ uuid: key,
+ value: replacements[key],
+ }))
+ : undefined,
+ ...(status ? { status } : {}),
+ // only update updated_at time if this is not an update to last_viewed_at
+ ...(lastViewedAt ? { last_viewed_at: lastViewedAt } : { updated_at: updatedAt }),
+ ...averageIntervalMsObj,
+ };
+};
+
+const validParams = new Set(['ids', 'endpoint_ids', 'connector_id', 'type', 'status']);
+const paramKeyMap = { ids: '_id', connector_id: 'api_config.connector_id', type: 'insight_type' };
+export function queryParamsToEsQuery(
+ queryParams: DefendInsightsGetRequestQuery
+): QueryDslQueryContainer[] {
+ return Object.entries(queryParams).reduce((acc: object[], [k, v]) => {
+ if (!validParams.has(k)) {
+ return acc;
+ }
+
+ const filterKey = isArray(v) ? 'terms' : 'term';
+ const paramKey = _get(paramKeyMap, k, k);
+ const next = { [filterKey]: { [paramKey]: v } };
+
+ return [...acc, next];
+ }, []);
+}
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/index.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/index.test.ts
new file mode 100644
index 0000000000000..704ee9b962554
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/index.test.ts
@@ -0,0 +1,410 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor 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 */
+
+import type { AuthenticatedUser } from '@kbn/core-security-common';
+import type {
+ DefendInsightCreateProps,
+ DefendInsightsUpdateProps,
+ DefendInsightsGetRequestQuery,
+ DefendInsightsResponse,
+} from '@kbn/elastic-assistant-common';
+
+import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
+import { loggerMock } from '@kbn/logging-mocks';
+import { DefendInsightStatus, DefendInsightType } from '@kbn/elastic-assistant-common';
+
+import type { AIAssistantDataClientParams } from '..';
+
+import { getDefendInsightsSearchEsMock } from '../../__mocks__/defend_insights_schema.mock';
+import { getDefendInsight } from './get_defend_insight';
+import {
+ queryParamsToEsQuery,
+ transformESSearchToDefendInsights,
+ transformToUpdateScheme,
+} from './helpers';
+import { DefendInsightsDataClient } from '.';
+
+jest.mock('./get_defend_insight');
+jest.mock('./helpers', () => {
+ const original = jest.requireActual('./helpers');
+ return {
+ ...original,
+ queryParamsToEsQuery: jest.fn(),
+ };
+});
+
+describe('DefendInsightsDataClient', () => {
+ const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+ const mockLogger = loggerMock.create();
+ const mockGetDefendInsight = jest.mocked(getDefendInsight);
+ let user: AuthenticatedUser;
+ let dataClientParams: AIAssistantDataClientParams;
+ let dataClient: DefendInsightsDataClient;
+
+ function getDefaultUser(): AuthenticatedUser {
+ return {
+ username: 'test_user',
+ profile_uid: '1234',
+ authentication_realm: {
+ type: 'my_realm_type',
+ name: 'my_realm_name',
+ },
+ } as AuthenticatedUser;
+ }
+
+ function getDefaultDataClientParams(): AIAssistantDataClientParams {
+ return {
+ logger: mockLogger,
+ currentUser: user,
+ elasticsearchClientPromise: new Promise((resolve) => resolve(mockEsClient)),
+ indexPatternsResourceName: 'defend-insights-index',
+ kibanaVersion: '9.0.0',
+ spaceId: 'space-1',
+ } as AIAssistantDataClientParams;
+ }
+
+ beforeEach(() => {
+ user = getDefaultUser();
+ dataClientParams = getDefaultDataClientParams();
+ dataClient = new DefendInsightsDataClient(dataClientParams);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('getDefendInsight', () => {
+ it('should correctly get defend insight', async () => {
+ const id = 'some-id';
+ mockGetDefendInsight.mockResolvedValueOnce({ id } as DefendInsightsResponse);
+ const response = await dataClient.getDefendInsight({ id, authenticatedUser: user });
+
+ expect(mockGetDefendInsight).toHaveBeenCalledTimes(1);
+ expect(response).not.toBeNull();
+ expect(response!.id).toEqual(id);
+ });
+ });
+
+ describe('createDefendInsight', () => {
+ const defendInsightCreate: DefendInsightCreateProps = {
+ endpointIds: [],
+ insightType: DefendInsightType.Enum.incompatible_antivirus,
+ insights: [],
+ apiConfig: {
+ actionTypeId: 'action-type-id',
+ connectorId: 'connector-id',
+ defaultSystemPromptId: 'default-prompt-id',
+ model: 'model-name',
+ provider: 'OpenAI',
+ },
+ eventsContextCount: 10,
+ replacements: { key1: 'value1', key2: 'value2' },
+ status: DefendInsightStatus.Enum.running,
+ };
+
+ it('should create defend insight successfully', async () => {
+ const id = 'created-id';
+ // @ts-expect-error not full response interface
+ mockEsClient.create.mockResolvedValueOnce({ _id: id });
+ mockGetDefendInsight.mockResolvedValueOnce({ id } as DefendInsightsResponse);
+
+ const response = await dataClient.createDefendInsight({
+ defendInsightCreate,
+ authenticatedUser: user,
+ });
+ expect(mockEsClient.create).toHaveBeenCalledTimes(1);
+ expect(mockGetDefendInsight).toHaveBeenCalledTimes(1);
+ expect(response).not.toBeNull();
+ expect(response!.id).toEqual(id);
+ });
+
+ it('should throw error on elasticsearch create failure', async () => {
+ mockEsClient.create.mockRejectedValueOnce(new Error('Elasticsearch error'));
+ const responsePromise = dataClient.createDefendInsight({
+ defendInsightCreate,
+ authenticatedUser: user,
+ });
+ await expect(responsePromise).rejects.toThrowError('Elasticsearch error');
+ expect(mockEsClient.create).toHaveBeenCalledTimes(1);
+ expect(mockGetDefendInsight).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('findDefendInsightsByParams', () => {
+ let mockQueryParamsToEsQuery: Function;
+ let queryParams: DefendInsightsGetRequestQuery;
+ let expectedTermFilters: object[];
+
+ function getDefaultQueryParams() {
+ return {
+ ids: ['insight-id1', 'insight-id2'],
+ endpoint_ids: ['endpoint-id1', 'endpoint-id2'],
+ connector_id: 'connector-id1',
+ type: DefendInsightType.Enum.incompatible_antivirus,
+ status: DefendInsightStatus.Enum.succeeded,
+ };
+ }
+
+ function getDefaultExpectedTermFilters() {
+ return [
+ { terms: { _id: queryParams.ids } },
+ { terms: { endpoint_ids: queryParams.endpoint_ids } },
+ { term: { 'api_config.connector_id': queryParams.connector_id } },
+ { term: { insight_type: queryParams.type } },
+ { term: { status: queryParams.status } },
+ ];
+ }
+
+ beforeEach(() => {
+ queryParams = getDefaultQueryParams();
+ expectedTermFilters = getDefaultExpectedTermFilters();
+ mockQueryParamsToEsQuery = jest
+ .mocked(queryParamsToEsQuery)
+ .mockReturnValueOnce(expectedTermFilters);
+ });
+
+ it('should return defend insights successfully', async () => {
+ const mockResponse = getDefendInsightsSearchEsMock();
+ mockEsClient.search.mockResolvedValueOnce(mockResponse);
+
+ const result = await dataClient.findDefendInsightsByParams({
+ params: queryParams,
+ authenticatedUser: user,
+ });
+ const expectedResult = transformESSearchToDefendInsights(mockResponse);
+
+ expect(mockQueryParamsToEsQuery).toHaveBeenCalledTimes(1);
+ expect(mockQueryParamsToEsQuery).toHaveBeenCalledWith(queryParams);
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search).toHaveBeenCalledWith(
+ expect.objectContaining({
+ index: `${dataClientParams.indexPatternsResourceName}-${dataClientParams.spaceId}`,
+ size: 10,
+ sort: [
+ {
+ '@timestamp': 'desc',
+ },
+ ],
+ query: {
+ bool: {
+ must: [
+ ...expectedTermFilters,
+ {
+ nested: {
+ path: 'users',
+ query: {
+ bool: {
+ must: [
+ {
+ match: { 'users.id': user.profile_uid },
+ },
+ ],
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ })
+ );
+ expect(result).toEqual(expectedResult);
+ });
+
+ it('should log and throw an error if search fails', async () => {
+ const mockError = new Error('Search failed');
+ mockEsClient.search.mockRejectedValue(mockError);
+
+ await expect(
+ dataClient.findDefendInsightsByParams({
+ params: queryParams,
+ authenticatedUser: user,
+ })
+ ).rejects.toThrow(mockError);
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ `error fetching Defend insights: ${mockError} with params: ${JSON.stringify(queryParams)}`
+ );
+ });
+ });
+
+ describe('findAllDefendInsights', () => {
+ it('should correctly query ES', async () => {
+ const mockResponse = getDefendInsightsSearchEsMock();
+ mockEsClient.search.mockResolvedValueOnce(mockResponse);
+ const searchParams = {
+ query: {
+ bool: {
+ must: [
+ {
+ nested: {
+ path: 'users',
+ query: {
+ bool: {
+ must: [
+ {
+ match: { 'users.id': user.profile_uid },
+ },
+ ],
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ size: 10000,
+ _source: true,
+ ignore_unavailable: true,
+ index: `${dataClientParams.indexPatternsResourceName}-${dataClientParams.spaceId}`,
+ seq_no_primary_term: true,
+ };
+
+ const response = await dataClient.findAllDefendInsights({
+ authenticatedUser: user,
+ });
+ expect(response).not.toBeNull();
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.search).toHaveBeenCalledWith(searchParams);
+ });
+
+ it('should throw error on elasticsearch search failure', async () => {
+ mockEsClient.search.mockRejectedValueOnce(new Error('Elasticsearch error'));
+ await expect(
+ dataClient.findAllDefendInsights({
+ authenticatedUser: user,
+ })
+ ).rejects.toThrowError('Elasticsearch error');
+ expect(mockEsClient.search).toHaveBeenCalledTimes(1);
+ expect(mockLogger.error).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('updateDefendInsights', () => {
+ let defendInsightsUpdateProps: DefendInsightsUpdateProps;
+
+ function getDefaultProps() {
+ return [
+ {
+ id: 'insight-id1',
+ backingIndex: 'defend-insights-index',
+ status: DefendInsightStatus.Enum.succeeded,
+ insights: [
+ {
+ group: 'windows_defender',
+ events: [
+ {
+ id: 'event-id-1',
+ endpointId: 'endpoint-id-1',
+ value: '/windows/defender/scan.exe',
+ },
+ ],
+ },
+ ],
+ },
+ ];
+ }
+
+ beforeEach(async () => {
+ defendInsightsUpdateProps = getDefaultProps();
+ });
+
+ it('should update defend insights successfully', async () => {
+ // ensure startTime is before updatedAt timestamp
+ const startTime = new Date().getTime() - 1;
+ const mockResponse: DefendInsightsResponse[] = [
+ { id: defendInsightsUpdateProps[0].id } as DefendInsightsResponse,
+ ];
+
+ const findDefendInsightsByParamsSpy = jest.spyOn(dataClient, 'findDefendInsightsByParams');
+ findDefendInsightsByParamsSpy.mockResolvedValueOnce(mockResponse);
+
+ const result = await dataClient.updateDefendInsights({
+ defendInsightsUpdateProps,
+ authenticatedUser: user,
+ });
+ const expectedDoc = transformToUpdateScheme('', defendInsightsUpdateProps[0]);
+ delete expectedDoc.updated_at;
+
+ expect(mockEsClient.bulk).toHaveBeenCalledTimes(1);
+ expect(mockEsClient.bulk).toHaveBeenCalledWith({
+ body: [
+ {
+ update: {
+ _index: defendInsightsUpdateProps[0].backingIndex,
+ _id: defendInsightsUpdateProps[0].id,
+ },
+ },
+ {
+ doc: expect.objectContaining({ ...expectedDoc }),
+ },
+ ],
+ refresh: 'wait_for',
+ });
+ const updatedAt = (mockEsClient.bulk.mock.calls[0][0] as { body: any[] }).body[1].doc
+ .updated_at;
+ expect(new Date(updatedAt).getTime()).toBeGreaterThan(startTime);
+ expect(dataClient.findDefendInsightsByParams).toHaveBeenCalledTimes(1);
+ expect(dataClient.findDefendInsightsByParams).toHaveBeenCalledWith({
+ params: { ids: [defendInsightsUpdateProps[0].id] },
+ authenticatedUser: user,
+ });
+ expect(result).toEqual(mockResponse);
+ });
+
+ it('should log a warning and throw an error if update fails', async () => {
+ const mockError = new Error('Update failed');
+ mockEsClient.bulk.mockRejectedValue(mockError);
+
+ await expect(
+ dataClient.updateDefendInsights({
+ defendInsightsUpdateProps,
+ authenticatedUser: user,
+ })
+ ).rejects.toThrow(mockError);
+
+ expect(mockLogger.warn).toHaveBeenCalledWith(
+ `error updating Defend insights: ${mockError} for IDs: ${defendInsightsUpdateProps[0].id}`
+ );
+ });
+ });
+
+ describe('updateDefendInsight', () => {
+ it('correctly calls updateDefendInsights', async () => {
+ const defendInsightUpdateProps = {
+ id: 'insight-id1',
+ backingIndex: 'defend-insights-index',
+ status: DefendInsightStatus.Enum.succeeded,
+ insights: [
+ {
+ group: 'windows_defender',
+ events: [
+ {
+ id: 'event-id-1',
+ endpointId: 'endpoint-id-1',
+ value: '/windows/defender/scan.exe',
+ },
+ ],
+ },
+ ],
+ };
+ const updateDefendInsightsSpy = jest.spyOn(dataClient, 'updateDefendInsights');
+ updateDefendInsightsSpy.mockResolvedValueOnce([]);
+ await dataClient.updateDefendInsight({
+ defendInsightUpdateProps,
+ authenticatedUser: user,
+ });
+
+ expect(updateDefendInsightsSpy).toHaveBeenCalledTimes(1);
+ expect(updateDefendInsightsSpy).toHaveBeenCalledWith({
+ defendInsightsUpdateProps: [defendInsightUpdateProps],
+ authenticatedUser: user,
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/index.ts
new file mode 100644
index 0000000000000..b5cbbd6cd18a2
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/index.ts
@@ -0,0 +1,286 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license 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 as uuidv4 } from 'uuid';
+
+import type {
+ DefendInsightCreateProps,
+ DefendInsightUpdateProps,
+ DefendInsightsUpdateProps,
+ DefendInsightsResponse,
+ DefendInsightsGetRequestQuery,
+} from '@kbn/elastic-assistant-common';
+import type { AuthenticatedUser } from '@kbn/core-security-common';
+
+import type { AIAssistantDataClientParams } from '..';
+import type { EsDefendInsightSchema } from './types';
+
+import { AIAssistantDataClient } from '..';
+import { getDefendInsight } from './get_defend_insight';
+import {
+ queryParamsToEsQuery,
+ transformESSearchToDefendInsights,
+ transformToCreateScheme,
+ transformToUpdateScheme,
+} from './helpers';
+
+const DEFAULT_PAGE_SIZE = 10;
+
+export class DefendInsightsDataClient extends AIAssistantDataClient {
+ constructor(public readonly options: AIAssistantDataClientParams) {
+ super(options);
+ }
+
+ /**
+ * Fetches a Defend insight
+ * @param options
+ * @param options.id The existing Defend insight id.
+ * @param options.authenticatedUser Current authenticated user.
+ * @returns The Defend insight response
+ */
+ public getDefendInsight = async ({
+ id,
+ authenticatedUser,
+ }: {
+ id: string;
+ authenticatedUser: AuthenticatedUser;
+ }): Promise => {
+ const esClient = await this.options.elasticsearchClientPromise;
+ return getDefendInsight({
+ esClient,
+ logger: this.options.logger,
+ index: this.indexTemplateAndPattern.alias,
+ id,
+ user: authenticatedUser,
+ });
+ };
+
+ /**
+ * Creates a Defend insight, if given at least the "apiConfig"
+ * @param options
+ * @param options.defendInsightCreate
+ * @param options.authenticatedUser
+ * @returns The Defend insight created
+ */
+ public createDefendInsight = async ({
+ defendInsightCreate,
+ authenticatedUser,
+ }: {
+ defendInsightCreate: DefendInsightCreateProps;
+ authenticatedUser: AuthenticatedUser;
+ }): Promise => {
+ const esClient = await this.options.elasticsearchClientPromise;
+ const logger = this.options.logger;
+ const index = this.indexTemplateAndPattern.alias;
+ const user = authenticatedUser;
+ const id = defendInsightCreate?.id || uuidv4();
+ const createdAt = new Date().toISOString();
+
+ const body = transformToCreateScheme(createdAt, this.spaceId, user, defendInsightCreate);
+ try {
+ const response = await esClient.create({
+ body,
+ id,
+ index,
+ refresh: 'wait_for',
+ });
+
+ const createdDefendInsight = await getDefendInsight({
+ esClient,
+ index,
+ id: response._id,
+ logger,
+ user,
+ });
+ return createdDefendInsight;
+ } catch (err) {
+ logger.error(`error creating Defend insight: ${err} with id: ${id}`);
+ throw err;
+ }
+ };
+
+ /**
+ * Find Defend insights by params
+ * @param options
+ * @param options.params
+ * @param options.authenticatedUser
+ * @returns The Defend insights found
+ */
+ public findDefendInsightsByParams = async ({
+ params,
+ authenticatedUser,
+ }: {
+ params: DefendInsightsGetRequestQuery;
+ authenticatedUser: AuthenticatedUser;
+ }): Promise => {
+ const esClient = await this.options.elasticsearchClientPromise;
+ const logger = this.options.logger;
+ const index = this.indexTemplateAndPattern.alias;
+ const user = authenticatedUser;
+ const termFilters = queryParamsToEsQuery(params);
+ const filterByUser = [
+ {
+ nested: {
+ path: 'users',
+ query: {
+ bool: {
+ must: [
+ {
+ match: user.profile_uid
+ ? { 'users.id': user.profile_uid }
+ : { 'users.name': user.username },
+ },
+ ],
+ },
+ },
+ },
+ },
+ ];
+
+ try {
+ const query = {
+ bool: {
+ must: [...termFilters, ...filterByUser],
+ },
+ };
+ const response = await esClient.search({
+ query,
+ _source: true,
+ ignore_unavailable: true,
+ index,
+ seq_no_primary_term: true,
+ sort: [{ '@timestamp': 'desc' }],
+ size: params.size || DEFAULT_PAGE_SIZE,
+ });
+ return transformESSearchToDefendInsights(response);
+ } catch (err) {
+ logger.error(`error fetching Defend insights: ${err} with params: ${JSON.stringify(params)}`);
+ throw err;
+ }
+ };
+
+ /**
+ * Finds all Defend insight for authenticated user
+ * @param options
+ * @param options.authenticatedUser
+ * @returns The Defend insight
+ */
+ public findAllDefendInsights = async ({
+ authenticatedUser,
+ }: {
+ authenticatedUser: AuthenticatedUser;
+ }): Promise => {
+ const esClient = await this.options.elasticsearchClientPromise;
+ const logger = this.options.logger;
+ const index = this.indexTemplateAndPattern.alias;
+ const user = authenticatedUser;
+ const MAX_ITEMS = 10000;
+ const filterByUser = [
+ {
+ nested: {
+ path: 'users',
+ query: {
+ bool: {
+ must: [
+ {
+ match: user.profile_uid
+ ? { 'users.id': user.profile_uid }
+ : { 'users.name': user.username },
+ },
+ ],
+ },
+ },
+ },
+ },
+ ];
+
+ try {
+ const response = await esClient.search({
+ query: {
+ bool: {
+ must: [...filterByUser],
+ },
+ },
+ size: MAX_ITEMS,
+ _source: true,
+ ignore_unavailable: true,
+ index,
+ seq_no_primary_term: true,
+ });
+ const insights = transformESSearchToDefendInsights(response);
+ return insights ?? [];
+ } catch (err) {
+ logger.error(`error fetching Defend insights: ${err}`);
+ throw err;
+ }
+ };
+
+ /**
+ * Updates Defend insights
+ * @param options
+ * @param options.defendInsightsUpdateProps
+ * @param options.authenticatedUser
+ */
+ public updateDefendInsights = async ({
+ defendInsightsUpdateProps,
+ authenticatedUser,
+ }: {
+ defendInsightsUpdateProps: DefendInsightsUpdateProps;
+ authenticatedUser: AuthenticatedUser;
+ }): Promise => {
+ const esClient = await this.options.elasticsearchClientPromise;
+ const logger = this.options.logger;
+ const updatedAt = new Date().toISOString();
+
+ let ids: string[] = [];
+ const bulkParams = defendInsightsUpdateProps.flatMap((updateProp) => {
+ const index = updateProp.backingIndex;
+ const params = transformToUpdateScheme(updatedAt, updateProp);
+ ids = [...ids, params.id];
+ return [
+ {
+ update: {
+ _index: index,
+ _id: params.id,
+ },
+ },
+ {
+ doc: params,
+ },
+ ];
+ });
+
+ try {
+ await esClient.bulk({ body: bulkParams, refresh: 'wait_for' });
+ return this.findDefendInsightsByParams({ params: { ids }, authenticatedUser });
+ } catch (err) {
+ logger.warn(`error updating Defend insights: ${err} for IDs: ${ids}`);
+ throw err;
+ }
+ };
+
+ /**
+ * Updates a Defend insight
+ * @param options
+ * @param options.defendInsightUpdateProps
+ * @param options.authenticatedUser
+ */
+ public updateDefendInsight = async ({
+ defendInsightUpdateProps,
+ authenticatedUser,
+ }: {
+ defendInsightUpdateProps: DefendInsightUpdateProps;
+ authenticatedUser: AuthenticatedUser;
+ }): Promise => {
+ return (
+ await this.updateDefendInsights({
+ defendInsightsUpdateProps: [defendInsightUpdateProps],
+ authenticatedUser,
+ })
+ )[0];
+ };
+}
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/types.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/types.ts
new file mode 100644
index 0000000000000..f04c7ef505c2f
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights/types.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 type {
+ DefendInsightStatus,
+ DefendInsightType,
+ Provider,
+ UUID,
+} from '@kbn/elastic-assistant-common';
+
+import type { EsReplacementSchema } from '../conversations/types';
+
+interface DefendInsightInsightEventSchema {
+ id: string;
+ endpoint_id: string;
+ value: string;
+}
+
+interface DefendInsightInsightSchema {
+ group: string;
+ events?: DefendInsightInsightEventSchema[];
+}
+
+interface BaseDefendInsightSchema {
+ '@timestamp': string;
+ created_at: string;
+ updated_at: string;
+ last_viewed_at: string;
+ status: DefendInsightStatus;
+ events_context_count?: number;
+ endpoint_ids: string[];
+ insight_type: DefendInsightType;
+ insights: DefendInsightInsightSchema[];
+ api_config: {
+ connector_id: string;
+ action_type_id: string;
+ default_system_prompt_id?: string;
+ provider?: Provider;
+ model?: string;
+ };
+ replacements?: EsReplacementSchema[];
+}
+
+export interface EsDefendInsightSchema extends BaseDefendInsightSchema {
+ id: string;
+ namespace: string;
+ failure_reason?: string;
+ users?: Array<{
+ id?: string;
+ name?: string;
+ }>;
+ average_interval_ms?: number;
+ generation_intervals?: Array<{ date: string; duration_ms: number }>;
+}
+
+export interface CreateDefendInsightSchema extends BaseDefendInsightSchema {
+ id?: string | undefined;
+ users: Array<{
+ id?: string;
+ name?: string;
+ }>;
+ namespace: string;
+}
+
+export interface UpdateDefendInsightSchema {
+ id: UUID;
+ '@timestamp'?: string;
+ updated_at?: string;
+ last_viewed_at?: string;
+ status?: DefendInsightStatus;
+ events_context_count?: number;
+ insights?: DefendInsightInsightSchema[];
+ api_config?: {
+ action_type_id?: string;
+ connector_id?: string;
+ default_system_prompt_id?: string;
+ provider?: Provider;
+ model?: string;
+ };
+ replacements?: EsReplacementSchema[];
+ average_interval_ms?: number;
+ generation_intervals?: Array<{ date: string; duration_ms: number }>;
+ failure_reason?: string;
+}
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/index.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/index.test.ts
index 8167708431921..007e25e9af467 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/index.test.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/index.test.ts
@@ -6,19 +6,12 @@
*/
import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks';
import { AIAssistantDataClient, AIAssistantDataClientParams } from '.';
-import { AuthenticatedUser } from '@kbn/core-security-common';
-
+import { authenticatedUser } from '../__mocks__/user';
const date = '2023-03-28T22:27:28.159Z';
let logger: ReturnType<(typeof loggingSystemMock)['createLogger']>;
const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
-const mockUser1 = {
- username: 'my_username',
- authentication_realm: {
- type: 'my_realm_type',
- name: 'my_realm_name',
- },
-} as AuthenticatedUser;
+const mockUser1 = authenticatedUser;
describe('AIAssistantDataClient', () => {
let assistantDataClientParams: AIAssistantDataClientParams;
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.test.ts
new file mode 100644
index 0000000000000..df6533d5d8df2
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.test.ts
@@ -0,0 +1,172 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
+import { createKnowledgeBaseEntry } from './create_knowledge_base_entry';
+import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
+import { coreMock } from '@kbn/core/server/mocks';
+import { getKnowledgeBaseEntry } from './get_knowledge_base_entry';
+import { KnowledgeBaseEntryResponse } from '@kbn/elastic-assistant-common';
+import {
+ getKnowledgeBaseEntryMock,
+ getCreateKnowledgeBaseEntrySchemaMock,
+} from '../../__mocks__/knowledge_base_entry_schema.mock';
+import { authenticatedUser } from '../../__mocks__/user';
+
+jest.mock('./get_knowledge_base_entry', () => ({
+ getKnowledgeBaseEntry: jest.fn(),
+}));
+
+const telemetry = coreMock.createSetup().analytics;
+
+describe('createKnowledgeBaseEntry', () => {
+ let logger: ReturnType;
+ beforeEach(() => {
+ jest.clearAllMocks();
+ logger = loggingSystemMock.createLogger();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ const date = '2024-01-28T04:20:02.394Z';
+ jest.setSystemTime(new Date(date));
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ test('it creates a knowledge base document entry with create schema', async () => {
+ const knowledgeBaseEntry = getCreateKnowledgeBaseEntrySchemaMock();
+ (getKnowledgeBaseEntry as unknown as jest.Mock).mockResolvedValueOnce({
+ ...getKnowledgeBaseEntryMock(),
+ id: 'elastic-id-123',
+ });
+
+ const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
+ esClient.create.mockResponse(
+ // @ts-expect-error not full response interface
+ { _id: 'elastic-id-123' }
+ );
+ const createdEntry = await createKnowledgeBaseEntry({
+ esClient,
+ knowledgeBaseIndex: 'index-1',
+ spaceId: 'test',
+ user: authenticatedUser,
+ knowledgeBaseEntry,
+ logger,
+ telemetry,
+ });
+ expect(esClient.create).toHaveBeenCalledWith({
+ body: {
+ '@timestamp': '2024-01-28T04:20:02.394Z',
+ created_at: '2024-01-28T04:20:02.394Z',
+ created_by: 'my_profile_uid',
+ updated_at: '2024-01-28T04:20:02.394Z',
+ updated_by: 'my_profile_uid',
+ namespace: 'test',
+ users: [{ id: 'my_profile_uid', name: 'my_username' }],
+ type: 'document',
+ semantic_text: 'test',
+ source: 'test',
+ text: 'test',
+ name: 'test',
+ kb_resource: 'test',
+ required: false,
+ vector: undefined,
+ },
+ id: expect.any(String),
+ index: 'index-1',
+ refresh: 'wait_for',
+ });
+
+ const expected: KnowledgeBaseEntryResponse = {
+ ...getKnowledgeBaseEntryMock(),
+ id: 'elastic-id-123',
+ };
+
+ expect(createdEntry).toEqual(expected);
+ });
+
+ test('it creates a knowledge base index entry with create schema', async () => {
+ const knowledgeBaseEntry = getCreateKnowledgeBaseEntrySchemaMock({ type: 'index' });
+ (getKnowledgeBaseEntry as unknown as jest.Mock).mockResolvedValueOnce({
+ ...getKnowledgeBaseEntryMock(),
+ id: 'elastic-id-123',
+ });
+
+ const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
+ esClient.create.mockResponse(
+ // @ts-expect-error not full response interface
+ { _id: 'elastic-id-123' }
+ );
+ const createdEntry = await createKnowledgeBaseEntry({
+ esClient,
+ knowledgeBaseIndex: 'index-1',
+ spaceId: 'test',
+ user: authenticatedUser,
+ knowledgeBaseEntry,
+ logger,
+ telemetry,
+ });
+ expect(esClient.create).toHaveBeenCalledWith({
+ body: {
+ '@timestamp': '2024-01-28T04:20:02.394Z',
+ created_at: '2024-01-28T04:20:02.394Z',
+ created_by: 'my_profile_uid',
+ updated_at: '2024-01-28T04:20:02.394Z',
+ updated_by: 'my_profile_uid',
+ namespace: 'test',
+ users: [{ id: 'my_profile_uid', name: 'my_username' }],
+ query_description: 'test',
+ type: 'index',
+ name: 'test',
+ description: 'test',
+ field: 'test',
+ index: 'test',
+ input_schema: [
+ {
+ description: 'test',
+ field_name: 'test',
+ field_type: 'test',
+ },
+ ],
+ },
+ id: expect.any(String),
+ index: 'index-1',
+ refresh: 'wait_for',
+ });
+
+ const expected: KnowledgeBaseEntryResponse = {
+ ...getKnowledgeBaseEntryMock(),
+ id: 'elastic-id-123',
+ };
+
+ expect(createdEntry).toEqual(expected);
+ });
+
+ test('it throws an error when creating a knowledge base entry fails', async () => {
+ const knowledgeBaseEntry = getCreateKnowledgeBaseEntrySchemaMock();
+ const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
+ esClient.create.mockRejectedValue(new Error('Test error'));
+ await expect(
+ createKnowledgeBaseEntry({
+ esClient,
+ knowledgeBaseIndex: 'index-1',
+ spaceId: 'test',
+ user: authenticatedUser,
+ knowledgeBaseEntry,
+ logger,
+ telemetry,
+ })
+ ).rejects.toThrowError('Test error');
+ });
+});
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/get_knowledge_base_entry.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/get_knowledge_base_entry.test.ts
new file mode 100644
index 0000000000000..aa247ece22e9a
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/get_knowledge_base_entry.test.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 type { AuthenticatedUser, Logger } from '@kbn/core/server';
+import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
+import { getKnowledgeBaseEntry } from './get_knowledge_base_entry';
+import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
+
+import {
+ getKnowledgeBaseEntryMock,
+ getKnowledgeBaseEntrySearchEsMock,
+} from '../../__mocks__/knowledge_base_entry_schema.mock';
+export const mockUser = {
+ username: 'my_username',
+ authentication_realm: {
+ type: 'my_realm_type',
+ name: 'my_realm_name',
+ },
+} as AuthenticatedUser;
+describe('getKnowledgeBaseEntry', () => {
+ let loggerMock: Logger;
+ beforeEach(() => {
+ jest.clearAllMocks();
+ loggerMock = loggingSystemMock.createLogger();
+ });
+
+ test('it returns an entry as expected if the entry is found', async () => {
+ const data = getKnowledgeBaseEntrySearchEsMock();
+ const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
+ esClient.search.mockResponse(data);
+ const entry = await getKnowledgeBaseEntry({
+ esClient,
+ knowledgeBaseIndex: '.kibana-elastic-ai-assistant-knowledge-base',
+ id: '1',
+ logger: loggerMock,
+ user: mockUser,
+ });
+ const expected = getKnowledgeBaseEntryMock();
+ expect(entry).toEqual(expected);
+ });
+
+ test('it returns null if the search is empty', async () => {
+ const data = getKnowledgeBaseEntrySearchEsMock();
+ data.hits.hits = [];
+ const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
+ esClient.search.mockResponse(data);
+ const entry = await getKnowledgeBaseEntry({
+ esClient,
+ knowledgeBaseIndex: '.kibana-elastic-ai-assistant-knowledge-base',
+ id: '1',
+ logger: loggerMock,
+ user: mockUser,
+ });
+ expect(entry).toEqual(null);
+ });
+
+ test('it throws an error if the search fails', async () => {
+ const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
+ esClient.search.mockRejectedValue(new Error('search failed'));
+ await expect(
+ getKnowledgeBaseEntry({
+ esClient,
+ knowledgeBaseIndex: '.kibana-elastic-ai-assistant-knowledge-base',
+ id: '1',
+ logger: loggerMock,
+ user: mockUser,
+ })
+ ).rejects.toThrowError('search failed');
+ });
+});
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.test.tsx b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.test.tsx
new file mode 100644
index 0000000000000..69b142bdac6be
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.test.tsx
@@ -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 { errors } from '@elastic/elasticsearch';
+import { ElasticsearchClient, Logger } from '@kbn/core/server';
+import { DynamicStructuredTool } from '@langchain/core/tools';
+import {
+ isModelAlreadyExistsError,
+ getKBVectorSearchQuery,
+ getStructuredToolForIndexEntry,
+} from './helpers';
+import { authenticatedUser } from '../../__mocks__/user';
+import { getCreateKnowledgeBaseEntrySchemaMock } from '../../__mocks__/knowledge_base_entry_schema.mock';
+import { IndexEntry } from '@kbn/elastic-assistant-common';
+
+// Mock dependencies
+jest.mock('@elastic/elasticsearch');
+jest.mock('@kbn/zod', () => ({
+ z: {
+ string: jest.fn().mockReturnValue({ describe: (str: string) => str }),
+ number: jest.fn().mockReturnValue({ describe: (str: string) => str }),
+ boolean: jest.fn().mockReturnValue({ describe: (str: string) => str }),
+ object: jest.fn().mockReturnValue({ describe: (str: string) => str }),
+ any: jest.fn().mockReturnValue({ describe: (str: string) => str }),
+ },
+}));
+jest.mock('lodash');
+
+describe('isModelAlreadyExistsError', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ it('should return true if error is resource_not_found_exception', () => {
+ const error = new errors.ResponseError({
+ meta: {
+ name: 'error',
+ context: 'error',
+ request: {
+ params: { method: 'post', path: '/' },
+ options: {},
+ id: 'error',
+ },
+ connection: null,
+ attempts: 0,
+ aborted: false,
+ },
+ warnings: null,
+ body: { error: { type: 'resource_not_found_exception' } },
+ });
+ // @ts-ignore
+ error.body = {
+ error: {
+ type: 'resource_not_found_exception',
+ },
+ };
+ expect(isModelAlreadyExistsError(error)).toBe(true);
+ });
+
+ it('should return true if error is status_exception', () => {
+ const error = new errors.ResponseError({
+ meta: {
+ name: 'error',
+ context: 'error',
+ request: {
+ params: { method: 'post', path: '/' },
+ options: {},
+ id: 'error',
+ },
+ connection: null,
+ attempts: 0,
+ aborted: false,
+ },
+ warnings: null,
+ body: { error: { type: 'status_exception' } },
+ });
+ // @ts-ignore
+ error.body = {
+ error: {
+ type: 'status_exception',
+ },
+ };
+ expect(isModelAlreadyExistsError(error)).toBe(true);
+ });
+
+ it('should return false for other error types', () => {
+ const error = new Error('Some other error');
+ expect(isModelAlreadyExistsError(error)).toBe(false);
+ });
+});
+
+describe('getKBVectorSearchQuery', () => {
+ const mockUser = authenticatedUser;
+
+ it('should construct a query with no filters if none are provided', () => {
+ const query = getKBVectorSearchQuery({ user: mockUser });
+ expect(query).toEqual({
+ bool: {
+ must: [],
+ should: expect.any(Array),
+ filter: undefined,
+ minimum_should_match: 1,
+ },
+ });
+ });
+
+ it('should include kbResource in the query if provided', () => {
+ const query = getKBVectorSearchQuery({ user: mockUser, kbResource: 'esql' });
+ expect(query?.bool?.must).toEqual(
+ expect.arrayContaining([
+ {
+ term: { kb_resource: 'esql' },
+ },
+ ])
+ );
+ });
+
+ it('should include required filter in the query if required is true', () => {
+ const query = getKBVectorSearchQuery({ user: mockUser, required: true });
+ expect(query?.bool?.must).toEqual(
+ expect.arrayContaining([
+ {
+ term: { required: true },
+ },
+ ])
+ );
+ });
+
+ it('should add semantic text filter if query is provided', () => {
+ const query = getKBVectorSearchQuery({ user: mockUser, query: 'example' });
+ expect(query?.bool?.must).toEqual(
+ expect.arrayContaining([
+ {
+ semantic: {
+ field: 'semantic_text',
+ query: 'example',
+ },
+ },
+ ])
+ );
+ });
+});
+
+describe('getStructuredToolForIndexEntry', () => {
+ const mockLogger = {
+ debug: jest.fn(),
+ error: jest.fn(),
+ } as unknown as Logger;
+
+ const mockEsClient = {} as ElasticsearchClient;
+
+ const mockIndexEntry = getCreateKnowledgeBaseEntrySchemaMock({ type: 'index' }) as IndexEntry;
+
+ it('should return a DynamicStructuredTool with correct name and schema', () => {
+ const tool = getStructuredToolForIndexEntry({
+ indexEntry: mockIndexEntry,
+ esClient: mockEsClient,
+ logger: mockLogger,
+ elserId: 'elser123',
+ });
+
+ expect(tool).toBeInstanceOf(DynamicStructuredTool);
+ expect(tool.lc_kwargs).toEqual(
+ expect.objectContaining({
+ name: 'test',
+ description: 'test',
+ tags: ['knowledge-base'],
+ })
+ );
+ });
+
+ it('should execute func correctly and return expected results', async () => {
+ const mockSearchResult = {
+ hits: {
+ hits: [
+ {
+ _source: {
+ field1: 'value1',
+ field2: 2,
+ },
+ inner_hits: {
+ 'test.test': {
+ hits: {
+ hits: [
+ { _source: { text: 'Inner text 1' } },
+ { _source: { text: 'Inner text 2' } },
+ ],
+ },
+ },
+ },
+ },
+ ],
+ },
+ };
+
+ mockEsClient.search = jest.fn().mockResolvedValue(mockSearchResult);
+
+ const tool = getStructuredToolForIndexEntry({
+ indexEntry: mockIndexEntry,
+ esClient: mockEsClient,
+ logger: mockLogger,
+ elserId: 'elser123',
+ });
+
+ const input = { query: 'testQuery', field1: 'value1', field2: 2 };
+ const result = await tool.invoke(input, {});
+
+ expect(result).toContain('Below are all relevant documents in JSON format');
+ expect(result).toContain('"text":"Inner text 1\\n --- \\nInner text 2"');
+ });
+
+ it('should log an error and return error message on Elasticsearch error', async () => {
+ const mockError = new Error('Elasticsearch error');
+ mockEsClient.search = jest.fn().mockRejectedValue(mockError);
+
+ const tool = getStructuredToolForIndexEntry({
+ indexEntry: mockIndexEntry,
+ esClient: mockEsClient,
+ logger: mockLogger,
+ elserId: 'elser123',
+ });
+
+ const input = { query: 'testQuery', field1: 'value1', field2: 2 };
+ const result = await tool.invoke(input, {});
+
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ `Error performing IndexEntry KB Similarity Search: ${mockError.message}`
+ );
+ expect(result).toContain(`I'm sorry, but I was unable to find any information`);
+ });
+});
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts
index 88ecae26cf19f..a0d3afb355b4b 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/helpers.ts
@@ -173,13 +173,11 @@ export const getStructuredToolForIndexEntry = ({
// Generate filters for inputSchema fields
const filter =
- indexEntry.inputSchema?.reduce((prev, i) => {
- return [
- ...prev,
- // @ts-expect-error Possible to override types with dynamic input schema?
- { term: { [`${i.fieldName}`]: input?.[i.fieldName] } },
- ];
- }, [] as Array<{ term: { [key: string]: string } }>) ?? [];
+ indexEntry.inputSchema?.reduce(
+ // @ts-expect-error Possible to override types with dynamic input schema?
+ (prev, i) => [...prev, { term: { [`${i.fieldName}`]: input?.[i.fieldName] } }],
+ [] as Array<{ term: { [key: string]: string } }>
+ ) ?? [];
const params: SearchRequest = {
index: indexEntry.index,
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts
new file mode 100644
index 0000000000000..cf67d763e3d23
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts
@@ -0,0 +1,582 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import {
+ coreMock,
+ elasticsearchServiceMock,
+ loggingSystemMock,
+ savedObjectsRepositoryMock,
+} from '@kbn/core/server/mocks';
+import { AIAssistantKnowledgeBaseDataClient, KnowledgeBaseDataClientParams } from '.';
+import {
+ getCreateKnowledgeBaseEntrySchemaMock,
+ getKnowledgeBaseEntryMock,
+ getKnowledgeBaseEntrySearchEsMock,
+} from '../../__mocks__/knowledge_base_entry_schema.mock';
+import { authenticatedUser } from '../../__mocks__/user';
+import { IndexPatternsFetcher } from '@kbn/data-plugin/server';
+import type { MlPluginSetup } from '@kbn/ml-plugin/server';
+import { mlPluginMock } from '@kbn/ml-plugin/public/mocks';
+import pRetry from 'p-retry';
+
+import {
+ loadSecurityLabs,
+ getSecurityLabsDocsCount,
+} from '../../lib/langchain/content_loaders/security_labs_loader';
+import { DynamicStructuredTool } from '@langchain/core/tools';
+jest.mock('../../lib/langchain/content_loaders/security_labs_loader');
+jest.mock('p-retry');
+const date = '2023-03-28T22:27:28.159Z';
+let logger: ReturnType<(typeof loggingSystemMock)['createLogger']>;
+const esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser;
+
+const mockUser1 = authenticatedUser;
+
+const mockedPRetry = pRetry as jest.MockedFunction;
+mockedPRetry.mockResolvedValue({});
+const telemetry = coreMock.createSetup().analytics;
+
+describe('AIAssistantKnowledgeBaseDataClient', () => {
+ let mockOptions: KnowledgeBaseDataClientParams;
+ let ml: MlPluginSetup;
+ let savedObjectClient: ReturnType;
+ const getElserId = jest.fn();
+ const trainedModelsProvider = jest.fn();
+ const installElasticModel = jest.fn();
+ const mockLoadSecurityLabs = loadSecurityLabs as jest.Mock;
+ const mockGetSecurityLabsDocsCount = getSecurityLabsDocsCount as jest.Mock;
+ const mockGetIsKBSetupInProgress = jest.fn();
+ beforeEach(() => {
+ jest.clearAllMocks();
+ logger = loggingSystemMock.createLogger();
+ savedObjectClient = savedObjectsRepositoryMock.create();
+ mockLoadSecurityLabs.mockClear();
+ ml = mlPluginMock.createSetupContract() as unknown as MlPluginSetup; // Missing SharedServices mock, so manually mocking trainedModelsProvider
+ ml.trainedModelsProvider = trainedModelsProvider.mockImplementation(() => ({
+ getELSER: jest.fn().mockImplementation(() => '.elser_model_2'),
+ installElasticModel: installElasticModel.mockResolvedValue({}),
+ }));
+ mockOptions = {
+ logger,
+ elasticsearchClientPromise: Promise.resolve(esClientMock),
+ spaceId: 'default',
+ indexPatternsResourceName: '',
+ currentUser: mockUser1,
+ kibanaVersion: '8.8.0',
+ ml,
+ getElserId: getElserId.mockResolvedValue('elser-id'),
+ getIsKBSetupInProgress: mockGetIsKBSetupInProgress.mockReturnValue(false),
+ ingestPipelineResourceName: 'something',
+ setIsKBSetupInProgress: jest.fn().mockImplementation(() => {}),
+ manageGlobalKnowledgeBaseAIAssistant: true,
+ };
+ esClientMock.search.mockReturnValue(
+ // @ts-expect-error not full response interface
+ getKnowledgeBaseEntrySearchEsMock()
+ );
+ });
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date(date));
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+ describe('isSetupInProgress', () => {
+ it('should return true if setup is in progress', () => {
+ mockGetIsKBSetupInProgress.mockReturnValueOnce(true);
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+
+ const result = client.isSetupInProgress;
+
+ expect(result).toBe(true);
+ });
+
+ it('should return false if setup is not in progress', () => {
+ mockGetIsKBSetupInProgress.mockReturnValueOnce(false);
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+
+ const result = client.isSetupInProgress;
+
+ expect(result).toBe(false);
+ });
+ });
+ describe('isSetupAvailable', () => {
+ it('should return true if ML capabilities check succeeds', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ // @ts-expect-error not full response interface
+ esClientMock.ml.getMemoryStats.mockResolvedValue({});
+ const result = await client.isSetupAvailable();
+ expect(result).toBe(true);
+ expect(esClientMock.ml.getMemoryStats).toHaveBeenCalled();
+ });
+
+ it('should return false if ML capabilities check fails', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ esClientMock.ml.getMemoryStats.mockRejectedValue(new Error('Mocked Error'));
+ const result = await client.isSetupAvailable();
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('isModelInstalled', () => {
+ it('should check if ELSER model is installed and return true if fully_defined', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ esClientMock.ml.getTrainedModels.mockResolvedValue({
+ count: 1,
+ trained_model_configs: [
+ { fully_defined: true, model_id: '', tags: [], input: { field_names: ['content'] } },
+ ],
+ });
+ const result = await client.isModelInstalled();
+ expect(result).toBe(true);
+ expect(esClientMock.ml.getTrainedModels).toHaveBeenCalledWith({
+ model_id: 'elser-id',
+ include: 'definition_status',
+ });
+ });
+
+ it('should return false if model is not fully defined', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ esClientMock.ml.getTrainedModels.mockResolvedValue({
+ count: 0,
+ trained_model_configs: [
+ { fully_defined: false, model_id: '', tags: [], input: { field_names: ['content'] } },
+ ],
+ });
+ const result = await client.isModelInstalled();
+ expect(result).toBe(false);
+ });
+
+ it('should return false and log error if getting model details fails', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ esClientMock.ml.getTrainedModels.mockRejectedValue(new Error('error happened'));
+ const result = await client.isModelInstalled();
+ expect(result).toBe(false);
+ expect(logger.error).toHaveBeenCalled();
+ });
+ });
+
+ describe('isInferenceEndpointExists', () => {
+ it('returns true when the model is fully allocated and started in ESS', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ esClientMock.ml.getTrainedModelsStats.mockResolvedValueOnce({
+ trained_model_stats: [
+ {
+ deployment_stats: {
+ state: 'started',
+ // @ts-expect-error not full response interface
+ allocation_status: { state: 'fully_allocated' },
+ },
+ },
+ ],
+ });
+
+ const result = await client.isInferenceEndpointExists();
+
+ expect(result).toBe(true);
+ });
+
+ it('returns true when the model is started in serverless', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ esClientMock.ml.getTrainedModelsStats.mockResolvedValueOnce({
+ trained_model_stats: [
+ {
+ deployment_stats: {
+ // @ts-expect-error not full response interface
+ nodes: [{ routing_state: { routing_state: 'started' } }],
+ },
+ },
+ ],
+ });
+
+ const result = await client.isInferenceEndpointExists();
+
+ expect(result).toBe(true);
+ });
+
+ it('returns false when the model is not fully allocated in ESS', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ esClientMock.ml.getTrainedModelsStats.mockResolvedValueOnce({
+ trained_model_stats: [
+ {
+ deployment_stats: {
+ state: 'started',
+ // @ts-expect-error not full response interface
+ allocation_status: { state: 'partially_allocated' },
+ },
+ },
+ ],
+ });
+
+ const result = await client.isInferenceEndpointExists();
+
+ expect(result).toBe(false);
+ });
+
+ it('returns false when the model is not started in serverless', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ esClientMock.ml.getTrainedModelsStats.mockResolvedValueOnce({
+ trained_model_stats: [
+ {
+ deployment_stats: {
+ // @ts-expect-error not full response interface
+ nodes: [{ routing_state: { routing_state: 'stopped' } }],
+ },
+ },
+ ],
+ });
+
+ const result = await client.isInferenceEndpointExists();
+
+ expect(result).toBe(false);
+ });
+
+ it('returns false when an error occurs during the check', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ esClientMock.ml.getTrainedModelsStats.mockRejectedValueOnce(new Error('Mocked Error'));
+
+ const result = await client.isInferenceEndpointExists();
+
+ expect(result).toBe(false);
+ });
+
+ it('should return false if inference api returns undefined', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ // @ts-ignore
+ esClientMock.inference.get.mockResolvedValueOnce(undefined);
+ const result = await client.isInferenceEndpointExists();
+ expect(result).toBe(false);
+ });
+
+ it('should return false when inference check throws an error', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ esClientMock.inference.get.mockRejectedValueOnce(new Error('Mocked Error'));
+ const result = await client.isInferenceEndpointExists();
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('setupKnowledgeBase', () => {
+ it('should install, deploy, and load docs if not already done', async () => {
+ // @ts-expect-error not full response interface
+ esClientMock.search.mockResolvedValue({});
+
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ await client.setupKnowledgeBase({ soClient: savedObjectClient });
+
+ // install model
+ expect(trainedModelsProvider).toHaveBeenCalledWith({}, savedObjectClient);
+ expect(installElasticModel).toHaveBeenCalledWith('elser-id');
+
+ expect(loadSecurityLabs).toHaveBeenCalled();
+ });
+
+ it('should skip installation and deployment if model is already installed and deployed', async () => {
+ mockGetSecurityLabsDocsCount.mockResolvedValue(1);
+ esClientMock.ml.getTrainedModels.mockResolvedValue({
+ count: 1,
+ trained_model_configs: [
+ { fully_defined: true, model_id: '', tags: [], input: { field_names: ['content'] } },
+ ],
+ });
+ esClientMock.ml.getTrainedModelsStats.mockResolvedValue({
+ trained_model_stats: [
+ {
+ deployment_stats: {
+ state: 'started',
+ // @ts-expect-error not full response interface
+ allocation_status: {
+ state: 'fully_allocated',
+ },
+ },
+ },
+ ],
+ });
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+
+ await client.setupKnowledgeBase({ soClient: savedObjectClient });
+
+ expect(installElasticModel).not.toHaveBeenCalled();
+ expect(esClientMock.ml.startTrainedModelDeployment).not.toHaveBeenCalled();
+ expect(loadSecurityLabs).not.toHaveBeenCalled();
+ });
+
+ it('should handle errors during installation and deployment', async () => {
+ // @ts-expect-error not full response interface
+ esClientMock.search.mockResolvedValue({});
+ esClientMock.ml.getTrainedModels.mockResolvedValue({
+ count: 0,
+ trained_model_configs: [
+ { fully_defined: false, model_id: '', tags: [], input: { field_names: ['content'] } },
+ ],
+ });
+ mockLoadSecurityLabs.mockRejectedValue(new Error('Installation error'));
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+
+ await expect(client.setupKnowledgeBase({ soClient: savedObjectClient })).rejects.toThrow(
+ 'Error setting up Knowledge Base: Installation error'
+ );
+ expect(mockOptions.logger.error).toHaveBeenCalledWith(
+ 'Error setting up Knowledge Base: Installation error'
+ );
+ });
+ });
+
+ describe('addKnowledgeBaseDocuments', () => {
+ const documents = [
+ {
+ pageContent: 'Document 1',
+ metadata: { kbResource: 'user', source: 'user', required: false },
+ },
+ ];
+ it('should add documents to the knowledge base', async () => {
+ esClientMock.bulk.mockResolvedValue({
+ items: [
+ {
+ create: {
+ status: 200,
+ _id: '123',
+ _index: 'index',
+ },
+ },
+ ],
+ took: 9999,
+ errors: false,
+ });
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+
+ const result = await client.addKnowledgeBaseDocuments({ documents });
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toEqual(getKnowledgeBaseEntryMock());
+ });
+
+ it('should swallow errors during bulk write', async () => {
+ esClientMock.bulk.mockRejectedValueOnce(new Error('Bulk write error'));
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+
+ const result = await client.addKnowledgeBaseDocuments({ documents });
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('isSecurityLabsDocsLoaded', () => {
+ it('should resolve to true when docs exist', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+
+ const results = await client.isSecurityLabsDocsLoaded();
+
+ expect(results).toEqual(true);
+ });
+ it('should resolve to false when docs do not exist', async () => {
+ // @ts-expect-error not full response interface
+ esClientMock.search.mockResolvedValueOnce({ hits: { hits: [] } });
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+
+ const results = await client.isSecurityLabsDocsLoaded();
+
+ expect(results).toEqual(false);
+ });
+ it('should resolve to false when docs error', async () => {
+ esClientMock.search.mockRejectedValueOnce(new Error('Search error'));
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+
+ const results = await client.isSecurityLabsDocsLoaded();
+
+ expect(results).toEqual(false);
+ });
+ });
+
+ describe('getKnowledgeBaseDocumentEntries', () => {
+ it('should fetch documents based on query and filters', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+
+ const results = await client.getKnowledgeBaseDocumentEntries({
+ query: 'test query',
+ kbResource: 'security_labs',
+ });
+
+ expect(results).toHaveLength(1);
+ expect(results[0].pageContent).toBe('test');
+ expect(results[0].metadata.kbResource).toBe('test');
+ });
+
+ it('should swallow errors during search', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+
+ esClientMock.search.mockRejectedValueOnce(new Error('Search error'));
+
+ const results = await client.getKnowledgeBaseDocumentEntries({
+ query: 'test query',
+ });
+ expect(results).toEqual([]);
+ });
+
+ it('should return an empty array if no documents are found', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+
+ // @ts-expect-error not full response interface
+ esClientMock.search.mockResolvedValueOnce({ hits: { hits: [] } });
+
+ const results = await client.getKnowledgeBaseDocumentEntries({
+ query: 'test query',
+ });
+
+ expect(results).toEqual([]);
+ });
+ });
+
+ describe('getRequiredKnowledgeBaseDocumentEntries', () => {
+ it('should throw is user is not found', async () => {
+ const assistantKnowledgeBaseDataClient = new AIAssistantKnowledgeBaseDataClient({
+ ...mockOptions,
+ currentUser: null,
+ });
+ await expect(
+ assistantKnowledgeBaseDataClient.getRequiredKnowledgeBaseDocumentEntries()
+ ).rejects.toThrowError(
+ 'Authenticated user not found! Ensure kbDataClient was initialized from a request.'
+ );
+ });
+ it('should fetch the required knowledge base entry successfully', async () => {
+ const assistantKnowledgeBaseDataClient = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ const result =
+ await assistantKnowledgeBaseDataClient.getRequiredKnowledgeBaseDocumentEntries();
+
+ expect(esClientMock.search).toHaveBeenCalledTimes(1);
+
+ expect(result).toEqual([
+ getKnowledgeBaseEntryMock(getCreateKnowledgeBaseEntrySchemaMock({ required: true })),
+ ]);
+ });
+ it('should return empty array if unexpected response from findDocuments', async () => {
+ // @ts-expect-error not full response interface
+ esClientMock.search.mockResolvedValue({});
+
+ const assistantKnowledgeBaseDataClient = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ const result =
+ await assistantKnowledgeBaseDataClient.getRequiredKnowledgeBaseDocumentEntries();
+
+ expect(esClientMock.search).toHaveBeenCalledTimes(1);
+
+ expect(result).toEqual([]);
+ expect(logger.error).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('createKnowledgeBaseEntry', () => {
+ const knowledgeBaseEntry = getCreateKnowledgeBaseEntrySchemaMock();
+ it('should create a new Knowledge Base entry', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+
+ const result = await client.createKnowledgeBaseEntry({ telemetry, knowledgeBaseEntry });
+ expect(result).toEqual(getKnowledgeBaseEntryMock());
+ });
+
+ it('should throw error if user is not authenticated', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = null;
+
+ await expect(
+ client.createKnowledgeBaseEntry({ telemetry, knowledgeBaseEntry })
+ ).rejects.toThrow('Authenticated user not found!');
+ });
+
+ it('should throw error if user lacks privileges to create global entries', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+ mockOptions.manageGlobalKnowledgeBaseAIAssistant = false;
+
+ await expect(
+ client.createKnowledgeBaseEntry({ telemetry, knowledgeBaseEntry, global: true })
+ ).rejects.toThrow('User lacks privileges to create global knowledge base entries');
+ });
+ });
+
+ describe('getAssistantTools', () => {
+ it('should return structured tools for relevant index entries', async () => {
+ IndexPatternsFetcher.prototype.getExistingIndices = jest.fn().mockResolvedValue(['test']);
+ esClientMock.search.mockReturnValue(
+ // @ts-expect-error not full response interface
+ getKnowledgeBaseEntrySearchEsMock('index')
+ );
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+
+ const result = await client.getAssistantTools({
+ esClient: esClientMock,
+ });
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toBeInstanceOf(DynamicStructuredTool);
+ });
+
+ it('should return an empty array if no relevant index entries are found', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+ // @ts-expect-error not full response interface
+ esClientMock.search.mockResolvedValueOnce({ hits: { hits: [] } });
+
+ const result = await client.getAssistantTools({
+ esClient: esClientMock,
+ });
+
+ expect(result).toEqual([]);
+ });
+
+ it('should swallow errors during fetching index entries', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+ mockOptions.currentUser = mockUser1;
+ esClientMock.search.mockRejectedValueOnce(new Error('Error fetching index entries'));
+
+ const result = await client.getAssistantTools({
+ esClient: esClientMock,
+ });
+
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('createInferenceEndpoint', () => {
+ it('should create a new Knowledge Base entry', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+
+ esClientMock.inference.put.mockResolvedValueOnce({
+ inference_id: 'id',
+ task_type: 'completion',
+ service: 'string',
+ service_settings: {},
+ task_settings: {},
+ });
+
+ await client.createInferenceEndpoint();
+
+ await expect(client.createInferenceEndpoint()).resolves.not.toThrow();
+ expect(esClientMock.inference.put).toHaveBeenCalled();
+ });
+
+ it('should throw error if user is not authenticated', async () => {
+ const client = new AIAssistantKnowledgeBaseDataClient(mockOptions);
+
+ esClientMock.inference.put.mockRejectedValueOnce(new Error('Inference error'));
+
+ await expect(client.createInferenceEndpoint()).rejects.toThrow('Inference error');
+ expect(esClientMock.inference.put).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts
index fae987b6d5083..231aa1c319da4 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts
@@ -55,7 +55,7 @@ export interface GetAIAssistantKnowledgeBaseDataClientParams {
manageGlobalKnowledgeBaseAIAssistant?: boolean;
}
-interface KnowledgeBaseDataClientParams extends AIAssistantDataClientParams {
+export interface KnowledgeBaseDataClientParams extends AIAssistantDataClientParams {
ml: MlPluginSetup;
getElserId: GetElser;
getIsKBSetupInProgress: () => boolean;
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/transforms.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/transforms.test.ts
new file mode 100644
index 0000000000000..b0451774770b8
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/transforms.test.ts
@@ -0,0 +1,64 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { transformESSearchToKnowledgeBaseEntry, transformESToKnowledgeBase } from './transforms';
+import {
+ getKnowledgeBaseEntrySearchEsMock,
+ documentEntry,
+} from '../../__mocks__/knowledge_base_entry_schema.mock';
+
+describe('transforms', () => {
+ describe('transformESSearchToKnowledgeBaseEntry', () => {
+ it('should transform Elasticsearch search response to KnowledgeBaseEntryResponse', () => {
+ const esResponse = getKnowledgeBaseEntrySearchEsMock('document');
+
+ const result = transformESSearchToKnowledgeBaseEntry(esResponse);
+ expect(result).toEqual([
+ {
+ id: '1',
+ createdAt: documentEntry.created_at,
+ createdBy: documentEntry.created_by,
+ updatedAt: documentEntry.updated_at,
+ updatedBy: documentEntry.updated_by,
+ type: documentEntry.type,
+ name: documentEntry.name,
+ namespace: documentEntry.namespace,
+ kbResource: documentEntry.kb_resource,
+ source: documentEntry.source,
+ required: documentEntry.required,
+ text: documentEntry.text,
+ users: documentEntry.users,
+ },
+ ]);
+ });
+ });
+
+ describe('transformESToKnowledgeBase', () => {
+ it('should transform Elasticsearch response array to KnowledgeBaseEntryResponse array', () => {
+ const esResponse = [documentEntry];
+
+ const result = transformESToKnowledgeBase(esResponse);
+ expect(result).toEqual([
+ {
+ id: documentEntry.id,
+ createdAt: documentEntry.created_at,
+ createdBy: documentEntry.created_by,
+ updatedAt: documentEntry.updated_at,
+ updatedBy: documentEntry.updated_by,
+ type: documentEntry.type,
+ name: documentEntry.name,
+ namespace: documentEntry.namespace,
+ kbResource: documentEntry.kb_resource,
+ source: documentEntry.source,
+ required: documentEntry.required,
+ text: documentEntry.text,
+ users: documentEntry.users,
+ },
+ ]);
+ });
+ });
+});
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.test.ts
index 23a1a55564415..4bfd4da6cfcbf 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.test.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.test.ts
@@ -10,9 +10,9 @@ import { IndicesGetDataStreamResponse } from '@elastic/elasticsearch/lib/api/typ
import { errors as EsErrors } from '@elastic/elasticsearch';
import { ReplaySubject, Subject } from 'rxjs';
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
-import { AuthenticatedUser } from '@kbn/core-security-common';
import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
import { conversationsDataClientMock } from '../__mocks__/data_clients.mock';
+import { authenticatedUser } from '../__mocks__/user';
import { AIAssistantConversationsDataClient } from '../ai_assistant_data_clients/conversations';
import { AIAssistantService, AIAssistantServiceOpts } from '.';
import { retryUntil } from './create_resource_installation_helper.test';
@@ -93,13 +93,7 @@ const getSpaceResourcesInitialized = async (
const conversationsDataClient = conversationsDataClientMock.create();
-const mockUser1 = {
- username: 'my_username',
- authentication_realm: {
- type: 'my_realm_type',
- name: 'my_realm_name',
- },
-} as AuthenticatedUser;
+const mockUser1 = authenticatedUser;
describe('AI Assistant Service', () => {
let pluginStop$: Subject;
@@ -147,7 +141,7 @@ describe('AI Assistant Service', () => {
expect(assistantService.isInitialized()).toEqual(true);
- expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(5);
+ expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(6);
const expectedTemplates = [
'.kibana-elastic-ai-assistant-component-template-conversations',
@@ -155,6 +149,7 @@ describe('AI Assistant Service', () => {
'.kibana-elastic-ai-assistant-component-template-prompts',
'.kibana-elastic-ai-assistant-component-template-anonymization-fields',
'.kibana-elastic-ai-assistant-component-template-attack-discovery',
+ '.kibana-elastic-ai-assistant-component-template-defend-insights',
];
expectedTemplates.forEach((t, i) => {
expect(clusterClient.cluster.putComponentTemplate.mock.calls[i][0].name).toEqual(t);
@@ -656,7 +651,7 @@ describe('AI Assistant Service', () => {
'AI Assistant service initialized',
async () => assistantService.isInitialized() === true
);
- expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(7);
+ expect(clusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(8);
const expectedTemplates = [
'.kibana-elastic-ai-assistant-component-template-conversations',
@@ -666,6 +661,7 @@ describe('AI Assistant Service', () => {
'.kibana-elastic-ai-assistant-component-template-prompts',
'.kibana-elastic-ai-assistant-component-template-anonymization-fields',
'.kibana-elastic-ai-assistant-component-template-attack-discovery',
+ '.kibana-elastic-ai-assistant-component-template-defend-insights',
];
expectedTemplates.forEach((t, i) => {
expect(clusterClient.cluster.putComponentTemplate.mock.calls[i][0].name).toEqual(t);
@@ -690,7 +686,7 @@ describe('AI Assistant Service', () => {
async () => (await getSpaceResourcesInitialized(assistantService)) === true
);
- expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(7);
+ expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(8);
const expectedTemplates = [
'.kibana-elastic-ai-assistant-index-template-conversations',
'.kibana-elastic-ai-assistant-index-template-conversations',
@@ -699,6 +695,7 @@ describe('AI Assistant Service', () => {
'.kibana-elastic-ai-assistant-index-template-prompts',
'.kibana-elastic-ai-assistant-index-template-anonymization-fields',
'.kibana-elastic-ai-assistant-index-template-attack-discovery',
+ '.kibana-elastic-ai-assistant-index-template-defend-insights',
];
expectedTemplates.forEach((t, i) => {
expect(clusterClient.indices.putIndexTemplate.mock.calls[i][0].name).toEqual(t);
@@ -722,7 +719,7 @@ describe('AI Assistant Service', () => {
async () => (await getSpaceResourcesInitialized(assistantService)) === true
);
- expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(7);
+ expect(clusterClient.indices.putSettings).toHaveBeenCalledTimes(8);
});
test('should retry updating index mappings for existing indices for transient ES errors', async () => {
@@ -742,7 +739,7 @@ describe('AI Assistant Service', () => {
async () => (await getSpaceResourcesInitialized(assistantService)) === true
);
- expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(7);
+ expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(8);
});
test('should retry creating concrete index for transient ES errors', async () => {
diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts
index d7eff095b4be5..81ddd69fb67d3 100644
--- a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts
+++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts
@@ -13,6 +13,7 @@ import type { MlPluginSetup } from '@kbn/ml-plugin/server';
import { Subject } from 'rxjs';
import { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server';
import { attackDiscoveryFieldMap } from '../lib/attack_discovery/persistence/field_maps_configuration/field_maps_configuration';
+import { defendInsightsFieldMap } from '../ai_assistant_data_clients/defend_insights/field_maps_configuration';
import { getDefaultAnonymizationFields } from '../../common/anonymization';
import { AssistantResourceNames, GetElser } from '../types';
import { AIAssistantConversationsDataClient } from '../ai_assistant_data_clients/conversations';
@@ -33,6 +34,7 @@ import {
GetAIAssistantKnowledgeBaseDataClientParams,
} from '../ai_assistant_data_clients/knowledge_base';
import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence';
+import { DefendInsightsDataClient } from '../ai_assistant_data_clients/defend_insights';
import { createGetElserId, createPipeline, pipelineExists } from './helpers';
import { hasAIAssistantLicense } from '../routes/helpers';
@@ -64,7 +66,8 @@ export type CreateDataStream = (params: {
| 'conversations'
| 'knowledgeBase'
| 'prompts'
- | 'attackDiscovery';
+ | 'attackDiscovery'
+ | 'defendInsights';
fieldMap: FieldMap;
kibanaVersion: string;
spaceId?: string;
@@ -79,6 +82,7 @@ export class AIAssistantService {
private promptsDataStream: DataStreamSpacesAdapter;
private anonymizationFieldsDataStream: DataStreamSpacesAdapter;
private attackDiscoveryDataStream: DataStreamSpacesAdapter;
+ private defendInsightsDataStream: DataStreamSpacesAdapter;
private resourceInitializationHelper: ResourceInstallationHelper;
private initPromise: Promise;
private isKBSetupInProgress: boolean = false;
@@ -112,6 +116,11 @@ export class AIAssistantService {
kibanaVersion: options.kibanaVersion,
fieldMap: attackDiscoveryFieldMap,
});
+ this.defendInsightsDataStream = this.createDataStream({
+ resource: 'defendInsights',
+ kibanaVersion: options.kibanaVersion,
+ fieldMap: defendInsightsFieldMap,
+ });
this.initPromise = this.initializeResources();
@@ -222,6 +231,12 @@ export class AIAssistantService {
logger: this.options.logger,
pluginStop$: this.options.pluginStop$,
});
+
+ await this.defendInsightsDataStream.install({
+ esClient,
+ logger: this.options.logger,
+ pluginStop$: this.options.pluginStop$,
+ });
} catch (error) {
this.options.logger.warn(`Error initializing AI assistant resources: ${error.message}`);
this.initialized = false;
@@ -240,6 +255,7 @@ export class AIAssistantService {
prompts: getResourceName('component-template-prompts'),
anonymizationFields: getResourceName('component-template-anonymization-fields'),
attackDiscovery: getResourceName('component-template-attack-discovery'),
+ defendInsights: getResourceName('component-template-defend-insights'),
},
aliases: {
conversations: getResourceName('conversations'),
@@ -247,6 +263,7 @@ export class AIAssistantService {
prompts: getResourceName('prompts'),
anonymizationFields: getResourceName('anonymization-fields'),
attackDiscovery: getResourceName('attack-discovery'),
+ defendInsights: getResourceName('defend-insights'),
},
indexPatterns: {
conversations: getResourceName('conversations*'),
@@ -254,6 +271,7 @@ export class AIAssistantService {
prompts: getResourceName('prompts*'),
anonymizationFields: getResourceName('anonymization-fields*'),
attackDiscovery: getResourceName('attack-discovery*'),
+ defendInsights: getResourceName('defend-insights*'),
},
indexTemplate: {
conversations: getResourceName('index-template-conversations'),
@@ -261,6 +279,7 @@ export class AIAssistantService {
prompts: getResourceName('index-template-prompts'),
anonymizationFields: getResourceName('index-template-anonymization-fields'),
attackDiscovery: getResourceName('index-template-attack-discovery'),
+ defendInsights: getResourceName('index-template-defend-insights'),
},
pipelines: {
knowledgeBase: getResourceName('ingest-pipeline-knowledge-base'),
@@ -393,6 +412,25 @@ export class AIAssistantService {
});
}
+ public async createDefendInsightsDataClient(
+ opts: CreateAIAssistantClientParams
+ ): Promise {
+ const res = await this.checkResourcesInstallation(opts);
+
+ if (res === null) {
+ return null;
+ }
+
+ return new DefendInsightsDataClient({
+ logger: this.options.logger.get('defendInsights'),
+ currentUser: opts.currentUser,
+ elasticsearchClientPromise: this.options.elasticsearchClientPromise,
+ indexPatternsResourceName: this.resourceNames.aliases.defendInsights,
+ kibanaVersion: this.options.kibanaVersion,
+ spaceId: opts.spaceId,
+ });
+ }
+
public async createAIAssistantPromptsDataClient(
opts: CreateAIAssistantClientParams
): Promise {
diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_empty_open_and_acknowledged_alerts_qery_results.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_empty_open_and_acknowledged_alerts_qery_results.ts
deleted file mode 100644
index ed5549acc586a..0000000000000
--- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_empty_open_and_acknowledged_alerts_qery_results.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-export const mockEmptyOpenAndAcknowledgedAlertsQueryResults = {
- took: 0,
- timed_out: false,
- _shards: {
- total: 1,
- successful: 1,
- skipped: 0,
- failed: 0,
- },
- hits: {
- total: {
- value: 0,
- relation: 'eq',
- },
- max_score: null,
- hits: [],
- },
-};
diff --git a/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.test.ts b/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.test.ts
index 35653878fa2d2..9e3d9afeb4ba6 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.test.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.test.ts
@@ -5,22 +5,17 @@
* 2.0.
*/
-import type { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server';
+import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import { loggingSystemMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
import {
getCreateConversationSchemaMock,
getUpdateConversationSchemaMock,
} from '../../__mocks__/conversations_schema.mock';
+import { authenticatedUser } from '../../__mocks__/user';
import { DocumentsDataWriter } from './documents_data_writer';
describe('DocumentsDataWriter', () => {
- const mockUser1 = {
- username: 'my_username',
- authentication_realm: {
- type: 'my_realm_type',
- name: 'my_realm_name',
- },
- } as AuthenticatedUser;
+ const mockUser1 = authenticatedUser;
describe('#bulk', () => {
let writer: DocumentsDataWriter;
let esClientMock: ElasticsearchClient;
diff --git a/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.ts b/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.ts
index 08892038a58b7..f065d0a2f8424 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.ts
@@ -22,7 +22,7 @@ export interface BulkOperationError {
};
}
-interface WriterBulkResponse {
+export interface WriterBulkResponse {
errors: BulkOperationError[];
docs_created: string[];
docs_deleted: string[];
diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/executors/types.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/executors/types.ts
index da560dfae72dd..7dea19755a686 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/langchain/executors/types.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/executors/types.ts
@@ -75,19 +75,6 @@ export type AgentExecutor = (
params: AgentExecutorParams
) => Promise>;
-export type AgentExecutorEvaluator = (
- langChainMessages: BaseMessage[],
- exampleId?: string
-) => Promise;
-
-export interface AgentExecutorEvaluatorWithMetadata {
- agentEvaluator: AgentExecutorEvaluator;
- metadata: {
- connectorName: string;
- runName: string;
- };
-}
-
export interface TraceOptions {
evaluationId?: string;
exampleId?: string;
diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/index.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/index.ts
index b9e4f85a800a0..c1027b835765d 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/index.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/index.ts
@@ -21,8 +21,6 @@ export type GetAttackDiscoveryGraph = (
params: GetDefaultAttackDiscoveryGraphParams
) => DefaultAttackDiscoveryGraph;
-export type GraphType = 'assistant' | 'attack-discovery';
-
export interface AssistantGraphMetadata {
getDefaultAssistantGraph: GetAssistantGraph;
graphType: 'assistant';
diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/helpers.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/helpers.ts
index 133419f45d175..9b2d444d643e4 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/langchain/helpers.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/helpers.ts
@@ -6,7 +6,7 @@
*/
import { KibanaRequest } from '@kbn/core-http-server';
-import type { Message } from '@kbn/elastic-assistant-common';
+import type { DefendInsightsPostRequestBody, Message } from '@kbn/elastic-assistant-common';
import { AIMessage, BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
import {
AttackDiscoveryPostRequestBody,
@@ -36,7 +36,7 @@ export const requestHasRequiredAnonymizationParams = (
request: KibanaRequest<
unknown,
unknown,
- ExecuteConnectorRequestBody | AttackDiscoveryPostRequestBody
+ ExecuteConnectorRequestBody | AttackDiscoveryPostRequestBody | DefendInsightsPostRequestBody
>
): boolean => {
const { replacements } = request?.body ?? {};
diff --git a/x-pack/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts b/x-pack/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts
index 1087703ba13a4..92330b4960e76 100644
--- a/x-pack/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts
+++ b/x-pack/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts
@@ -411,6 +411,100 @@ export const CREATE_KNOWLEDGE_BASE_ENTRY_ERROR_EVENT: EventTypeOpts<{
},
};
+export const DEFEND_INSIGHT_SUCCESS_EVENT: EventTypeOpts<{
+ actionTypeId: string;
+ eventsContextCount: number;
+ insightsGenerated: number;
+ durationMs: number;
+ model?: string;
+ provider?: string;
+}> = {
+ eventType: 'defend_insight_success',
+ schema: {
+ actionTypeId: {
+ type: 'keyword',
+ _meta: {
+ description: 'Kibana connector type',
+ optional: false,
+ },
+ },
+ eventsContextCount: {
+ type: 'integer',
+ _meta: {
+ description: 'Number of events sent as context to the LLM',
+ optional: false,
+ },
+ },
+ insightsGenerated: {
+ type: 'integer',
+ _meta: {
+ description: 'Quantity of Defend insights generated',
+ optional: false,
+ },
+ },
+ durationMs: {
+ type: 'integer',
+ _meta: {
+ description: 'Duration of request in ms',
+ optional: false,
+ },
+ },
+ model: {
+ type: 'keyword',
+ _meta: {
+ description: 'LLM model',
+ optional: true,
+ },
+ },
+ provider: {
+ type: 'keyword',
+ _meta: {
+ description: 'OpenAI provider',
+ optional: true,
+ },
+ },
+ },
+};
+
+export const DEFEND_INSIGHT_ERROR_EVENT: EventTypeOpts<{
+ actionTypeId: string;
+ errorMessage: string;
+ model?: string;
+ provider?: string;
+}> = {
+ eventType: 'defend_insight_error',
+ schema: {
+ actionTypeId: {
+ type: 'keyword',
+ _meta: {
+ description: 'Kibana connector type',
+ optional: false,
+ },
+ },
+ errorMessage: {
+ type: 'keyword',
+ _meta: {
+ description: 'Error message from Elasticsearch',
+ },
+ },
+
+ model: {
+ type: 'keyword',
+ _meta: {
+ description: 'LLM model',
+ optional: true,
+ },
+ },
+ provider: {
+ type: 'keyword',
+ _meta: {
+ description: 'OpenAI provider',
+ optional: true,
+ },
+ },
+ },
+};
+
export const events: Array> = [
KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT,
KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT,
@@ -420,4 +514,6 @@ export const events: Array> = [
INVOKE_ASSISTANT_ERROR_EVENT,
ATTACK_DISCOVERY_SUCCESS_EVENT,
ATTACK_DISCOVERY_ERROR_EVENT,
+ DEFEND_INSIGHT_SUCCESS_EVENT,
+ DEFEND_INSIGHT_ERROR_EVENT,
];
diff --git a/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.test.ts b/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.test.ts
index e8055de3b12b9..d3d1302247052 100644
--- a/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.test.ts
+++ b/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.test.ts
@@ -14,7 +14,7 @@ import {
getEmptyFindResult,
getFindAnonymizationFieldsResultWithSingleHit,
} from '../../__mocks__/response';
-import { AuthenticatedUser } from '@kbn/core-security-common';
+import { authenticatedUser } from '../../__mocks__/user';
import { bulkActionAnonymizationFieldsRoute } from './bulk_actions_route';
import {
getAnonymizationFieldMock,
@@ -28,14 +28,7 @@ describe('Perform bulk action route', () => {
let { clients, context } = requestContextMock.createTools();
let logger: ReturnType;
const mockAnonymizationField = getAnonymizationFieldMock(getUpdateAnonymizationFieldSchemaMock());
- const mockUser1 = {
- profile_uid: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0',
- username: 'my_username',
- authentication_realm: {
- type: 'my_realm_type',
- name: 'my_realm_name',
- },
- } as AuthenticatedUser;
+ const mockUser1 = authenticatedUser;
beforeEach(async () => {
server = serverMock.create();
diff --git a/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts b/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts
index 9aedffae5cfb5..170d0599de171 100644
--- a/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts
+++ b/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts
@@ -48,8 +48,6 @@ export interface BulkOperationError {
};
}
-export type BulkActionError = BulkOperationError | unknown;
-
const buildBulkResponse = (
response: KibanaResponseFactory,
{
diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.ts
index 188976f0b3f5c..65d3cee1662c5 100644
--- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.ts
+++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.ts
@@ -15,9 +15,7 @@ import {
GenerationInterval,
Replacements,
} from '@kbn/elastic-assistant-common';
-import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
import type { Document } from '@langchain/core/documents';
-import { v4 as uuidv4 } from 'uuid';
import { Moment } from 'moment';
import { transformError } from '@kbn/securitysolution-es-utils';
import moment from 'moment/moment';
@@ -29,21 +27,6 @@ import {
} from '../../../lib/telemetry/event_based_telemetry';
import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence';
-export const REQUIRED_FOR_ATTACK_DISCOVERY: AnonymizationFieldResponse[] = [
- {
- id: uuidv4(),
- field: '_id',
- allowed: true,
- anonymized: true,
- },
- {
- id: uuidv4(),
- field: 'kibana.alert.original_time',
- allowed: true,
- anonymized: false,
- },
-];
-
export const attackDiscoveryStatus: { [k: string]: AttackDiscoveryStatus } = {
canceled: 'canceled',
failed: 'failed',
diff --git a/x-pack/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.test.ts b/x-pack/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.test.ts
new file mode 100644
index 0000000000000..fa3ff15027e23
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.test.ts
@@ -0,0 +1,149 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import type { AuthenticatedUser } from '@kbn/core-security-common';
+
+import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
+
+import type { DefendInsightsDataClient } from '../../ai_assistant_data_clients/defend_insights';
+
+import { transformESSearchToDefendInsights } from '../../ai_assistant_data_clients/defend_insights/helpers';
+import { getDefendInsightsSearchEsMock } from '../../__mocks__/defend_insights_schema.mock';
+import { getDefendInsightRequest } from '../../__mocks__/request';
+import {
+ ElasticAssistantRequestHandlerContextMock,
+ requestContextMock,
+} from '../../__mocks__/request_context';
+import { serverMock } from '../../__mocks__/server';
+import { isDefendInsightsEnabled, updateDefendInsightLastViewedAt } from './helpers';
+import { getDefendInsightRoute } from './get_defend_insight';
+
+jest.mock('./helpers');
+
+describe('getDefendInsightRoute', () => {
+ let server: ReturnType;
+ let context: ElasticAssistantRequestHandlerContextMock;
+ let mockUser: AuthenticatedUser;
+ let mockDataClient: DefendInsightsDataClient;
+ let mockCurrentInsight: any;
+
+ function getDefaultUser(): AuthenticatedUser {
+ return {
+ username: 'my_username',
+ authentication_realm: {
+ type: 'my_realm_type',
+ name: 'my_realm_name',
+ },
+ } as AuthenticatedUser;
+ }
+
+ function getDefaultDataClient(): DefendInsightsDataClient {
+ return {
+ findDefendInsightByConnectorId: jest.fn(),
+ updateDefendInsight: jest.fn(),
+ createDefendInsight: jest.fn(),
+ getDefendInsight: jest.fn(),
+ } as unknown as DefendInsightsDataClient;
+ }
+
+ beforeEach(() => {
+ const tools = requestContextMock.createTools();
+ context = tools.context;
+ server = serverMock.create();
+ tools.clients.core.elasticsearch.client = elasticsearchServiceMock.createScopedClusterClient();
+
+ mockUser = getDefaultUser();
+ mockDataClient = getDefaultDataClient();
+ mockCurrentInsight = transformESSearchToDefendInsights(getDefendInsightsSearchEsMock())[0];
+
+ context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser);
+ context.elasticAssistant.getDefendInsightsDataClient.mockResolvedValue(mockDataClient);
+ getDefendInsightRoute(server.router);
+ (updateDefendInsightLastViewedAt as jest.Mock).mockResolvedValue(mockCurrentInsight);
+ (isDefendInsightsEnabled as jest.Mock).mockReturnValue(true);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should handle successful request', async () => {
+ const response = await server.inject(
+ getDefendInsightRequest('insight-id1'),
+ requestContextMock.convertContext(context)
+ );
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({
+ data: mockCurrentInsight,
+ });
+ });
+
+ it('should 404 if feature flag disabled', async () => {
+ (isDefendInsightsEnabled as jest.Mock).mockReturnValueOnce(false);
+ const response = await server.inject(
+ getDefendInsightRequest('insight-id1'),
+ requestContextMock.convertContext(context)
+ );
+ expect(response.status).toEqual(404);
+ });
+
+ it('should handle missing authenticated user', async () => {
+ context.elasticAssistant.getCurrentUser.mockReturnValueOnce(null);
+ const response = await server.inject(
+ getDefendInsightRequest('insight-id1'),
+ requestContextMock.convertContext(context)
+ );
+
+ expect(response.status).toEqual(401);
+ expect(response.body).toEqual({
+ message: 'Authenticated user not found',
+ status_code: 401,
+ });
+ });
+
+ it('should handle missing data client', async () => {
+ context.elasticAssistant.getDefendInsightsDataClient.mockResolvedValueOnce(null);
+ const response = await server.inject(
+ getDefendInsightRequest('insight-id1'),
+ requestContextMock.convertContext(context)
+ );
+
+ expect(response.status).toEqual(500);
+ expect(response.body).toEqual({
+ message: 'Defend insights data client not initialized',
+ status_code: 500,
+ });
+ });
+
+ it('should handle updateDefendInsightLastViewedAt empty array', async () => {
+ (updateDefendInsightLastViewedAt as jest.Mock).mockResolvedValueOnce([]);
+ const response = await server.inject(
+ getDefendInsightRequest('insight-id1'),
+ requestContextMock.convertContext(context)
+ );
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({ data: [] });
+ });
+
+ it('should handle updateDefendInsightLastViewedAt error', async () => {
+ (updateDefendInsightLastViewedAt as jest.Mock).mockRejectedValueOnce(new Error('Oh no!'));
+ const response = await server.inject(
+ getDefendInsightRequest('insight-id1'),
+ requestContextMock.convertContext(context)
+ );
+ expect(response.status).toEqual(500);
+ expect(response.body).toEqual({
+ message: {
+ error: 'Oh no!',
+ success: false,
+ },
+ status_code: 500,
+ });
+ });
+});
diff --git a/x-pack/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.ts b/x-pack/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.ts
new file mode 100644
index 0000000000000..5766b3d1b014b
--- /dev/null
+++ b/x-pack/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.ts
@@ -0,0 +1,96 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { IKibanaResponse } from '@kbn/core/server';
+
+import { IRouter, Logger } from '@kbn/core/server';
+import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common';
+import {
+ DEFEND_INSIGHTS_BY_ID,
+ DefendInsightGetResponse,
+ DefendInsightGetRequestParams,
+ ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION,
+} from '@kbn/elastic-assistant-common';
+import { transformError } from '@kbn/securitysolution-es-utils';
+
+import { buildResponse } from '../../lib/build_response';
+import { ElasticAssistantRequestHandlerContext } from '../../types';
+import { isDefendInsightsEnabled, updateDefendInsightLastViewedAt } from './helpers';
+
+export const getDefendInsightRoute = (router: IRouter