diff --git a/.buildkite/scripts/common/util.sh b/.buildkite/scripts/common/util.sh index 1ce05856ec6b..748babfc0650 100755 --- a/.buildkite/scripts/common/util.sh +++ b/.buildkite/scripts/common/util.sh @@ -39,6 +39,7 @@ check_for_changed_files() { C_RESET='\033[0m' # Reset color SHOULD_AUTO_COMMIT_CHANGES="${2:-}" + CUSTOM_FIX_MESSAGE="${3:-}" GIT_CHANGES="$(git ls-files --modified -- . ':!:.bazelrc')" if [ "$GIT_CHANGES" ]; then @@ -75,7 +76,11 @@ check_for_changed_files() { else echo -e "\n${RED}ERROR: '$1' caused changes to the following files:${C_RESET}\n" echo -e "$GIT_CHANGES\n" - echo -e "\n${YELLOW}TO FIX: Run '$1' locally, commit the changes and push to your branch${C_RESET}\n" + if [ "$CUSTOM_FIX_MESSAGE" ]; then + echo "$CUSTOM_FIX_MESSAGE" + else + echo -e "\n${YELLOW}TO FIX: Run '$1' locally, commit the changes and push to your branch${C_RESET}\n" + fi exit 1 fi fi diff --git a/.buildkite/scripts/steps/checks.sh b/.buildkite/scripts/steps/checks.sh index 4af63d318c80..0e11ac04eea1 100755 --- a/.buildkite/scripts/steps/checks.sh +++ b/.buildkite/scripts/steps/checks.sh @@ -8,6 +8,7 @@ export DISABLE_BOOTSTRAP_VALIDATION=false .buildkite/scripts/steps/checks/precommit_hook.sh .buildkite/scripts/steps/checks/ftr_configs.sh .buildkite/scripts/steps/checks/bazel_packages.sh +.buildkite/scripts/steps/checks/event_log.sh .buildkite/scripts/steps/checks/telemetry.sh .buildkite/scripts/steps/checks/ts_projects.sh .buildkite/scripts/steps/checks/jest_configs.sh diff --git a/.buildkite/scripts/steps/checks/event_log.sh b/.buildkite/scripts/steps/checks/event_log.sh new file mode 100755 index 000000000000..dc9c01902c01 --- /dev/null +++ b/.buildkite/scripts/steps/checks/event_log.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +echo --- Check Event Log Schema + +# event log schema is pinned to a specific version of ECS +ECS_STABLE_VERSION=1.8 +git clone --depth 1 -b $ECS_STABLE_VERSION https://github.com/elastic/ecs.git ../ecs + +node x-pack/plugins/event_log/scripts/create_schemas.js + +check_for_changed_files 'node x-pack/plugins/event_log/scripts/create_schemas.js' false 'Follow the directions in x-pack/plugins/event_log/generated/README.md to make schema changes for the event log.' diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index 8575ee683d82..dcceca784891 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -41,6 +41,7 @@ const STORYBOOKS = [ 'presentation', 'security_solution', 'shared_ux', + 'triggers_actions_ui', 'ui_actions_enhanced', 'unified_search', ]; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5633cf2e6e1b..0a1fcc60d55b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -132,6 +132,7 @@ x-pack/examples/files_example @elastic/kibana-app-services /x-pack/test/fleet_api_integration @elastic/fleet /x-pack/test/fleet_cypress @elastic/fleet /x-pack/test/fleet_functional @elastic/fleet +/src/dev/build/tasks/bundle_fleet_packages.ts # APM /x-pack/plugins/apm/ @elastic/apm-ui @@ -742,6 +743,8 @@ packages/core/http/core-http-browser-mocks @elastic/kibana-core packages/core/http/core-http-common @elastic/kibana-core packages/core/http/core-http-context-server-internal @elastic/kibana-core packages/core/http/core-http-context-server-mocks @elastic/kibana-core +packages/core/http/core-http-request-handler-context-server @elastic/kibana-core +packages/core/http/core-http-request-handler-context-server-internal @elastic/kibana-core packages/core/http/core-http-router-server-internal @elastic/kibana-core packages/core/http/core-http-router-server-mocks @elastic/kibana-core packages/core/http/core-http-server @elastic/kibana-core @@ -912,6 +915,7 @@ packages/kbn-rule-data-utils @elastic/apm-ui packages/kbn-safer-lodash-set @elastic/kibana-security packages/kbn-securitysolution-autocomplete @elastic/security-solution-platform packages/kbn-securitysolution-es-utils @elastic/security-solution-platform +packages/kbn-securitysolution-exception-list-components @elastic/security-solution-platform packages/kbn-securitysolution-hook-utils @elastic/security-solution-platform packages/kbn-securitysolution-io-ts-alerting-types @elastic/security-solution-platform packages/kbn-securitysolution-io-ts-list-types @elastic/security-solution-platform diff --git a/.i18nrc.json b/.i18nrc.json index 20b2588ca2fa..462daff20de6 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -60,6 +60,7 @@ "kibana-react": "src/plugins/kibana_react", "kibanaOverview": "src/plugins/kibana_overview", "lists": "packages/kbn-securitysolution-list-utils/src", + "exceptionList-components": "packages/kbn-securitysolution-exception-list-components/src", "management": ["src/legacy/core_plugins/management", "src/plugins/management"], "monaco": "packages/kbn-monaco/src", "navigation": "src/plugins/navigation", diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index c298d8a395df..929e069b2bf5 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github description: API docs for the actions plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] --- import actionsObj from './actions.devdocs.json'; diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index 6da505acae8b..43856104f328 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github description: API docs for the advancedSettings plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] --- import advancedSettingsObj from './advanced_settings.devdocs.json'; diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index 4e5199b31ef9..7694fbde6190 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github description: API docs for the aiops plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] --- import aiopsObj from './aiops.devdocs.json'; diff --git a/api_docs/alerting.devdocs.json b/api_docs/alerting.devdocs.json index a19bdb320740..9971fcee0a58 100644 --- a/api_docs/alerting.devdocs.json +++ b/api_docs/alerting.devdocs.json @@ -2666,8 +2666,6 @@ "Alert", "; scheduleActions: (actionGroup: ActionGroupIds, context?: Context) => ", "Alert", - "; scheduleActionsWithSubGroup: (actionGroup: ActionGroupIds, subgroup: string, context?: Context) => ", - "Alert", "; setContext: (context: Context) => ", "Alert", "; getContext: () => Context; hasContext: () => boolean; }" @@ -2832,7 +2830,11 @@ "section": "def-common.IExecutionErrorsResult", "text": "IExecutionErrorsResult" }, - ">; bulkEdit: ; getGlobalExecutionKpiWithAuth: ({ dateStart, dateEnd, filter, }: ", + "GetGlobalExecutionKPIParams", + ") => Promise<{ success: number; unknown: number; failure: number; activeAlerts: number; newAlerts: number; recoveredAlerts: number; erroredActions: number; triggeredActions: number; }>; getRuleExecutionKPI: ({ id, dateStart, dateEnd, filter }: ", + "GetRuleExecutionKPIParams", + ") => Promise<{ success: number; unknown: number; failure: number; activeAlerts: number; newAlerts: number; recoveredAlerts: number; erroredActions: number; triggeredActions: number; }>; bulkEdit: ; }; observability: { setup: { getScopedAnnotationsClient: (requestContext: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", " & { licensing: Promise<", { "pluginId": "licensing", @@ -5696,13 +5684,7 @@ "label": "context", "description": [], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", " & { licensing: Promise<", { "pluginId": "licensing", diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index 84c92a1ce864..934ffbbd3380 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github description: API docs for the apm plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] --- import apmObj from './apm.devdocs.json'; diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index e10dd6028faf..12a25911e2da 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github description: API docs for the banners plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] --- import bannersObj from './banners.devdocs.json'; diff --git a/api_docs/bfetch.devdocs.json b/api_docs/bfetch.devdocs.json index 74f1cf1aee45..f060159bc0f3 100644 --- a/api_docs/bfetch.devdocs.json +++ b/api_docs/bfetch.devdocs.json @@ -357,13 +357,7 @@ "(path: string, params: (request: ", "KibanaRequest", ", context: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", ") => ", { "pluginId": "bfetch", @@ -375,13 +369,7 @@ ", method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | undefined, pluginRouter?: ", "IRouter", "<", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", "> | undefined) => void" ], "path": "src/plugins/bfetch/server/plugin.ts", @@ -414,13 +402,7 @@ "(request: ", "KibanaRequest", ", context: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", ") => ", { "pluginId": "bfetch", @@ -461,13 +443,7 @@ "signature": [ "IRouter", "<", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", "> | undefined" ], "path": "src/plugins/bfetch/server/plugin.ts", diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index 504dc027276f..c4055d656405 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github description: API docs for the bfetch plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] --- import bfetchObj from './bfetch.devdocs.json'; diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index 5dc73b2b9000..4ef1b1b03a0e 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github description: API docs for the canvas plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] --- import canvasObj from './canvas.devdocs.json'; diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index aaa279d69844..6df29978541a 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github description: API docs for the cases plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] --- import casesObj from './cases.devdocs.json'; diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index 5e129b2343ca..718e93231dff 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github description: API docs for the charts plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] --- import chartsObj from './charts.devdocs.json'; diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index 5c1a4fea8ca5..74692e016ccf 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github description: API docs for the cloud plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; diff --git a/api_docs/cloud_experiments.mdx b/api_docs/cloud_experiments.mdx index 6314cca4f901..9b5efb2aa51f 100644 --- a/api_docs/cloud_experiments.mdx +++ b/api_docs/cloud_experiments.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudExperiments title: "cloudExperiments" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudExperiments plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudExperiments'] --- import cloudExperimentsObj from './cloud_experiments.devdocs.json'; diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index 9c7112ee6e3a..1ccfb42f61d0 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudSecurityPosture plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] --- import cloudSecurityPostureObj from './cloud_security_posture.devdocs.json'; diff --git a/api_docs/console.mdx b/api_docs/console.mdx index a673eebc1e1c..00e0779286aa 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github description: API docs for the console plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index 849d5fc71f96..e1427ab3d19f 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github description: API docs for the controls plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; diff --git a/api_docs/core.devdocs.json b/api_docs/core.devdocs.json index 08008a2bfe5c..67fa46292601 100644 --- a/api_docs/core.devdocs.json +++ b/api_docs/core.devdocs.json @@ -584,6 +584,10 @@ "plugin": "@kbn/core-analytics-server-internal", "path": "packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts" }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.ts" + }, { "plugin": "@kbn/core-status-server-internal", "path": "packages/core/status/core-status-server-internal/src/status_service.ts" @@ -620,6 +624,30 @@ "plugin": "@kbn/core-analytics-browser-mocks", "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts" }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, { "plugin": "@kbn/core-analytics-browser-mocks", "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts" @@ -10361,7 +10389,7 @@ "\nThe outcome for a successful `resolve` call is one of the following values:\n\n * `'exactMatch'` -- One document exactly matched the given ID.\n * `'aliasMatch'` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different\n than the given ID.\n * `'conflict'` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the\n `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID." ], "signature": [ - "\"exactMatch\" | \"aliasMatch\" | \"conflict\"" + "\"conflict\" | \"exactMatch\" | \"aliasMatch\"" ], "path": "node_modules/@types/kbn__core-saved-objects-api-browser/index.d.ts", "deprecated": false, @@ -10822,6 +10850,14 @@ "plugin": "visualizations", "path": "src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx" }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object.ts" + }, { "plugin": "infra", "path": "x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx" @@ -11156,11 +11192,15 @@ }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts" + "path": "src/plugins/dashboard/server/saved_objects/migrations/migrate_extract_panel_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts" + "path": "src/plugins/dashboard/server/saved_objects/migrations/migrate_extract_panel_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts" }, { "plugin": "dashboard", @@ -11204,43 +11244,35 @@ }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" - }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts" - }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "ml", @@ -11250,6 +11282,14 @@ "plugin": "ml", "path": "x-pack/plugins/ml/common/types/modules.ts" }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/services/dashboard_saved_object/lib/save_dashboard_state_to_saved_object.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/services/dashboard_saved_object/lib/save_dashboard_state_to_saved_object.ts" + }, { "plugin": "@kbn/core-saved-objects-server-internal", "path": "packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts" @@ -15328,9 +15368,9 @@ "label": "SavedObjectsFindOptions", "description": [], "signature": [ - "{ type: string | string[]; filter?: any; search?: string | undefined; aggs?: Record | undefined; fields?: string[] | undefined; page?: number | undefined; perPage?: number | undefined; sortField?: string | undefined; searchFields?: string[] | undefined; hasReference?: ", + "> | undefined; page?: number | undefined; perPage?: number | undefined; sortField?: string | undefined; searchFields?: string[] | undefined; hasReference?: ", "SavedObjectsFindOptionsReference", " | ", "SavedObjectsFindOptionsReference", @@ -19295,6 +19335,10 @@ "plugin": "@kbn/core-analytics-server-internal", "path": "packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts" }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.ts" + }, { "plugin": "@kbn/core-status-server-internal", "path": "packages/core/status/core-status-server-internal/src/status_service.ts" @@ -19331,6 +19375,30 @@ "plugin": "@kbn/core-analytics-browser-mocks", "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts" }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, + { + "plugin": "@kbn/core-root-browser-internal", + "path": "packages/core/root/core-root-browser-internal/src/core_system.test.ts" + }, { "plugin": "@kbn/core-analytics-browser-mocks", "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts" @@ -21478,13 +21546,7 @@ "signature": [ "HttpServicePreboot", "<", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", ">" ], "path": "src/core/server/index.ts", @@ -21519,7 +21581,10 @@ "description": [ "\nThe `core` context provided to route handler.\n\nProvides the following clients and services:\n - {@link SavedObjectsClient | savedObjects.client} - Saved Objects client\n which uses the credentials of the incoming request\n - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing\n all the registered types.\n - {@link IScopedClusterClient | elasticsearch.client} - Elasticsearch\n data client which uses the credentials of the incoming request\n - {@link IUiSettingsClient | uiSettings.client} - uiSettings client\n which uses the credentials of the incoming request" ], - "path": "src/core/server/core_route_handler_context.ts", + "signature": [ + "CoreRequestHandlerContext" + ], + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -21533,7 +21598,7 @@ "signature": [ "SavedObjectsRequestHandlerContext" ], - "path": "src/core/server/core_route_handler_context.ts", + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -21547,7 +21612,7 @@ "signature": [ "ElasticsearchRequestHandlerContext" ], - "path": "src/core/server/core_route_handler_context.ts", + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -21561,7 +21626,7 @@ "signature": [ "UiSettingsRequestHandlerContext" ], - "path": "src/core/server/core_route_handler_context.ts", + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", "deprecated": false, "trackAdoption": false }, @@ -21575,7 +21640,7 @@ "signature": [ "DeprecationsRequestHandlerContext" ], - "path": "src/core/server/core_route_handler_context.ts", + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", "deprecated": false, "trackAdoption": false } @@ -21746,13 +21811,7 @@ "signature": [ "HttpServiceSetup", "<", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", "> & { resources: ", { "pluginId": "core", @@ -25894,21 +25953,9 @@ ], "signature": [ "(route: ", "RouteConfig", ", handler: ", @@ -39364,6 +39411,37 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "core", + "id": "def-server.PrebootCoreRequestHandlerContext", + "type": "Interface", + "tags": [], + "label": "PrebootCoreRequestHandlerContext", + "description": [], + "signature": [ + "PrebootCoreRequestHandlerContext" + ], + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "core", + "id": "def-server.PrebootCoreRequestHandlerContext.uiSettings", + "type": "Object", + "tags": [], + "label": "uiSettings", + "description": [], + "signature": [ + "PrebootUiSettingsRequestHandlerContext" + ], + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "core", "id": "def-server.PrebootPlugin", @@ -39467,6 +39545,41 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "core", + "id": "def-server.PrebootRequestHandlerContext", + "type": "Interface", + "tags": [], + "label": "PrebootRequestHandlerContext", + "description": [], + "signature": [ + "PrebootRequestHandlerContext", + " extends ", + "RequestHandlerContextBase" + ], + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "core", + "id": "def-server.PrebootRequestHandlerContext.core", + "type": "Object", + "tags": [], + "label": "core", + "description": [], + "signature": [ + "Promise<", + "PrebootCoreRequestHandlerContext", + ">" + ], + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "core", "id": "def-server.RegisterDeprecationsConfig", @@ -39532,17 +39645,11 @@ "\nBase context passed to a route handler, containing the `core` context part.\n" ], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", " extends ", "RequestHandlerContextBase" ], - "path": "src/core/server/index.ts", + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", "deprecated": false, "trackAdoption": false, "children": [ @@ -39555,16 +39662,10 @@ "description": [], "signature": [ "Promise<", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.CoreRequestHandlerContext", - "text": "CoreRequestHandlerContext" - }, + "CoreRequestHandlerContext", ">" ], - "path": "src/core/server/index.ts", + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", "deprecated": false, "trackAdoption": false } @@ -40431,6 +40532,14 @@ "plugin": "visualizations", "path": "src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx" }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object.ts" + }, { "plugin": "infra", "path": "x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx" @@ -40765,11 +40874,15 @@ }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts" + "path": "src/plugins/dashboard/server/saved_objects/migrations/migrate_extract_panel_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts" + "path": "src/plugins/dashboard/server/saved_objects/migrations/migrate_extract_panel_references.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts" }, { "plugin": "dashboard", @@ -40813,43 +40926,35 @@ }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" - }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/common/saved_dashboard_references.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts" - }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts" + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts" }, { "plugin": "ml", @@ -40859,6 +40964,14 @@ "plugin": "ml", "path": "x-pack/plugins/ml/common/types/modules.ts" }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/services/dashboard_saved_object/lib/save_dashboard_state_to_saved_object.ts" + }, + { + "plugin": "dashboard", + "path": "src/plugins/dashboard/public/services/dashboard_saved_object/lib/save_dashboard_state_to_saved_object.ts" + }, { "plugin": "@kbn/core-saved-objects-server-internal", "path": "packages/core/saved-objects/core-saved-objects-server-internal/src/routes/legacy_import_export/lib/collect_references_deep.test.ts" @@ -45678,11 +45791,11 @@ }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/server/saved_objects/migrations_730.ts" + "path": "src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.ts" }, { "plugin": "dashboard", - "path": "src/plugins/dashboard/server/saved_objects/migrations_730.ts" + "path": "src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.ts" }, { "plugin": "@kbn/core-saved-objects-migration-server-internal", @@ -46663,7 +46776,7 @@ "\nThe outcome for a successful `resolve` call is one of the following values:\n\n * `'exactMatch'` -- One document exactly matched the given ID.\n * `'aliasMatch'` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different\n than the given ID.\n * `'conflict'` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the\n `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID." ], "signature": [ - "\"exactMatch\" | \"aliasMatch\" | \"conflict\"" + "\"conflict\" | \"exactMatch\" | \"aliasMatch\"" ], "path": "node_modules/@types/kbn__core-saved-objects-api-server/index.d.ts", "deprecated": false, @@ -50120,16 +50233,10 @@ "\nMixin allowing plugins to define their own request handler contexts.\n" ], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", " & { [Key in keyof T]: T[Key] extends Promise ? T[Key] : Promise; }" ], - "path": "src/core/server/index.ts", + "path": "node_modules/@types/kbn__core-http-request-handler-context-server/index.d.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false @@ -53606,9 +53713,9 @@ "label": "SavedObjectsCreatePointInTimeFinderOptions", "description": [], "signature": [ - "{ type: string | string[]; filter?: any; search?: string | undefined; aggs?: Record | undefined; fields?: string[] | undefined; perPage?: number | undefined; sortField?: string | undefined; sortOrder?: ", + "> | undefined; perPage?: number | undefined; sortField?: string | undefined; sortOrder?: ", "SortOrder", " | undefined; searchFields?: string[] | undefined; rootSearchFields?: string[] | undefined; hasReference?: ", "SavedObjectsFindOptionsReference", diff --git a/api_docs/core.mdx b/api_docs/core.mdx index d63230e7ed58..798c6c53ce34 100644 --- a/api_docs/core.mdx +++ b/api_docs/core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/core title: "core" image: https://source.unsplash.com/400x175/?github description: API docs for the core plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core'] --- import coreObj from './core.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2684 | 0 | 35 | 0 | +| 2688 | 0 | 30 | 0 | ## Client diff --git a/api_docs/custom_integrations.devdocs.json b/api_docs/custom_integrations.devdocs.json index 7a339106c7a6..03090384a055 100644 --- a/api_docs/custom_integrations.devdocs.json +++ b/api_docs/custom_integrations.devdocs.json @@ -438,7 +438,7 @@ "label": "categories", "description": [], "signature": [ - "(\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\")[]" + "(\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"infrastructure\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\")[]" ], "path": "src/plugins/custom_integrations/common/index.ts", "deprecated": false, @@ -488,7 +488,7 @@ "\nA category applicable to an Integration." ], "signature": [ - "\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\"" + "\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"infrastructure\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\"" ], "path": "src/plugins/custom_integrations/common/index.ts", "deprecated": false, @@ -728,7 +728,7 @@ "label": "categories", "description": [], "signature": [ - "(\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\")[]" + "(\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"infrastructure\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\")[]" ], "path": "src/plugins/custom_integrations/common/index.ts", "deprecated": false, @@ -838,7 +838,7 @@ "label": "id", "description": [], "signature": [ - "\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\"" + "\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"infrastructure\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\"" ], "path": "src/plugins/custom_integrations/common/index.ts", "deprecated": false, @@ -860,7 +860,7 @@ "\nThe list of all available categories." ], "signature": [ - "(\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\")[]" + "(\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"infrastructure\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\")[]" ], "path": "src/plugins/custom_integrations/common/index.ts", "deprecated": false, @@ -877,7 +877,7 @@ "\nA category applicable to an Integration." ], "signature": [ - "\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\"" + "\"custom\" | \"enterprise_search\" | \"sample_data\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"infrastructure\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"microsoft_365\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\" | \"communications\" | \"file_storage\" | \"language_client\" | \"upload_file\" | \"website_search\" | \"geo\"" ], "path": "src/plugins/custom_integrations/common/index.ts", "deprecated": false, @@ -1097,6 +1097,17 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "customIntegrations", + "id": "def-common.INTEGRATION_CATEGORY_DISPLAY.infrastructure", + "type": "string", + "tags": [], + "label": "infrastructure", + "description": [], + "path": "src/plugins/custom_integrations/common/index.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "customIntegrations", "id": "def-common.INTEGRATION_CATEGORY_DISPLAY.kubernetes", diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index 7c8ab77b2d5a..77c928f34e58 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github description: API docs for the customIntegrations plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Fleet](https://github.com/orgs/elastic/teams/fleet) for questions regar | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 103 | 0 | 84 | 1 | +| 104 | 0 | 85 | 1 | ## Client diff --git a/api_docs/dashboard.devdocs.json b/api_docs/dashboard.devdocs.json index 447c0d0af07e..c185507fd910 100644 --- a/api_docs/dashboard.devdocs.json +++ b/api_docs/dashboard.devdocs.json @@ -1,33 +1,99 @@ { "id": "dashboard", "client": { - "classes": [ + "classes": [], + "functions": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer", - "type": "Class", + "id": "def-public.cleanEmptyKeys", + "type": "Function", "tags": [], - "label": "DashboardContainer", + "label": "cleanEmptyKeys", "description": [], "signature": [ + "(stateObj: Record) => Record" + ], + "path": "src/plugins/dashboard/public/locator.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardContainer", - "text": "DashboardContainer" - }, - " extends ", + "parentPluginId": "dashboard", + "id": "def-public.cleanEmptyKeys.$1", + "type": "Object", + "tags": [], + "label": "stateObj", + "description": [], + "signature": [ + "Record" + ], + "path": "src/plugins/dashboard/public/locator.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-public.createDashboardEditUrl", + "type": "Function", + "tags": [], + "label": "createDashboardEditUrl", + "description": [], + "signature": [ + "(id: string | undefined, editMode: boolean | undefined) => string" + ], + "path": "src/plugins/dashboard/public/dashboard_constants.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.Container", - "text": "Container" + "parentPluginId": "dashboard", + "id": "def-public.createDashboardEditUrl.$1", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/dashboard/public/dashboard_constants.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false }, - "<", - "InheritedChildInput", - ", ", + { + "parentPluginId": "dashboard", + "id": "def-public.createDashboardEditUrl.$2", + "type": "CompoundType", + "tags": [], + "label": "editMode", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/dashboard/public/dashboard_constants.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "dashboard", + "id": "def-public.DashboardContainerInput", + "type": "Interface", + "tags": [], + "label": "DashboardContainerInput", + "description": [], + "signature": [ { "pluginId": "dashboard", "scope": "public", @@ -35,1674 +101,1479 @@ "section": "def-public.DashboardContainerInput", "text": "DashboardContainerInput" }, - ", ", + " extends ", { "pluginId": "embeddable", "scope": "public", "docId": "kibEmbeddablePluginApi", - "section": "def-public.ContainerOutput", - "text": "ContainerOutput" + "section": "def-public.ContainerInput", + "text": "ContainerInput" }, - ">" + "<{}>" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.type", - "type": "string", - "tags": [], - "label": "type", - "description": [], - "signature": [ - "\"dashboard\"" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.controlGroup", + "id": "def-public.DashboardContainerInput.controlGroupInput", "type": "Object", "tags": [], - "label": "controlGroup", + "label": "controlGroupInput", "description": [], "signature": [ { "pluginId": "controls", - "scope": "public", + "scope": "common", "docId": "kibControlsPluginApi", - "section": "def-public.ControlGroupContainer", - "text": "ControlGroupContainer" + "section": "def-common.PersistableControlGroupInput", + "text": "PersistableControlGroupInput" }, " | undefined" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.getAllDataViews", - "type": "Function", + "id": "def-public.DashboardContainerInput.refreshConfig", + "type": "Object", "tags": [], - "label": "getAllDataViews", - "description": [ - "\nGets all the dataviews that are actively being used in the dashboard" - ], + "label": "refreshConfig", + "description": [], "signature": [ - "() => ", { - "pluginId": "dataViews", + "pluginId": "data", "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" + "docId": "kibDataQueryPluginApi", + "section": "def-common.RefreshInterval", + "text": "RefreshInterval" }, - "[]" + " | undefined" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [ - "An array of dataviews" - ] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.setAllDataViews", - "type": "Function", + "id": "def-public.DashboardContainerInput.isEmbeddedExternally", + "type": "CompoundType", "tags": [], - "label": "setAllDataViews", - "description": [ - "\nUse this to set the dataviews that are used in the dashboard when they change/update" - ], + "label": "isEmbeddedExternally", + "description": [], "signature": [ - "(newDataViews: ", - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" - }, - "[]) => void" + "boolean | undefined" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.setAllDataViews.$1", - "type": "Array", - "tags": [], - "label": "newDataViews", - "description": [ - "The new array of dataviews that will overwrite the old dataviews array" - ], - "signature": [ - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" - }, - "[]" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.getPanelCount", - "type": "Function", + "id": "def-public.DashboardContainerInput.isFullScreenMode", + "type": "boolean", "tags": [], - "label": "getPanelCount", + "label": "isFullScreenMode", "description": [], - "signature": [ - "() => number" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.getPanelTitles", - "type": "Function", + "id": "def-public.DashboardContainerInput.expandedPanelId", + "type": "string", "tags": [], - "label": "getPanelTitles", + "label": "expandedPanelId", "description": [], "signature": [ - "() => Promise" + "string | undefined" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.Unnamed", - "type": "Function", + "id": "def-public.DashboardContainerInput.timeRange", + "type": "Object", "tags": [], - "label": "Constructor", + "label": "timeRange", "description": [], "signature": [ - "any" + "{ from: string; to: string; mode?: \"absolute\" | \"relative\" | undefined; }" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.Unnamed.$1", - "type": "Object", - "tags": [], - "label": "initialInput", - "description": [], - "signature": [ - { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardContainerInput", - "text": "DashboardContainerInput" - } - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.Unnamed.$2", - "type": "Object", - "tags": [], - "label": "parent", - "description": [], - "signature": [ - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.Container", - "text": "Container" - }, - "<{}, ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.ContainerInput", - "text": "ContainerInput" - }, - "<{}>, ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.ContainerOutput", - "text": "ContainerOutput" - }, - "> | undefined" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.Unnamed.$3", - "type": "CompoundType", - "tags": [], - "label": "controlGroup", - "description": [], - "signature": [ - { - "pluginId": "controls", - "scope": "public", - "docId": "kibControlsPluginApi", - "section": "def-public.ControlGroupContainer", - "text": "ControlGroupContainer" - }, - " | ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.ErrorEmbeddable", - "text": "ErrorEmbeddable" - }, - " | undefined" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } - ], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.createNewPanelState", - "type": "Function", + "id": "def-public.DashboardContainerInput.timeslice", + "type": "Object", "tags": [], - "label": "createNewPanelState", + "label": "timeslice", "description": [], "signature": [ - ">(factory: ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.EmbeddableFactory", - "text": "EmbeddableFactory" - }, - ", partial?: Partial) => ", - "DashboardPanelState", - "" + "[number, number] | undefined" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.createNewPanelState.$1", - "type": "Object", - "tags": [], - "label": "factory", - "description": [], - "signature": [ - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.EmbeddableFactory", - "text": "EmbeddableFactory" - }, - "" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.createNewPanelState.$2", - "type": "Object", - "tags": [], - "label": "partial", - "description": [], - "signature": [ - "Partial" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.showPlaceholderUntil", - "type": "Function", + "id": "def-public.DashboardContainerInput.timeRestore", + "type": "boolean", "tags": [], - "label": "showPlaceholderUntil", + "label": "timeRestore", "description": [], - "signature": [ - "(newStateComplete: Promise>>, placementMethod?: ", - "PanelPlacementMethod", - " | undefined, placementArgs?: TPlacementMethodArgs | undefined) => void" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.showPlaceholderUntil.$1", - "type": "Object", - "tags": [], - "label": "newStateComplete", - "description": [], - "signature": [ - "Promise>>" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.showPlaceholderUntil.$2", - "type": "Function", - "tags": [], - "label": "placementMethod", - "description": [], - "signature": [ - "PanelPlacementMethod", - " | undefined" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.showPlaceholderUntil.$3", - "type": "Uncategorized", - "tags": [], - "label": "placementArgs", - "description": [], - "signature": [ - "TPlacementMethodArgs | undefined" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } - ], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.replacePanel", - "type": "Function", + "id": "def-public.DashboardContainerInput.description", + "type": "string", "tags": [], - "label": "replacePanel", + "label": "description", "description": [], "signature": [ - "(previousPanelState: ", - "DashboardPanelState", - "<", - { - "pluginId": "embeddable", - "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.EmbeddableInput", - "text": "EmbeddableInput" - }, - ">, newPanelState: Partial<", - { - "pluginId": "embeddable", - "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.PanelState", - "text": "PanelState" - }, - "<{ id: string; }>>, generateNewId?: boolean | undefined) => void" + "string | undefined" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.replacePanel.$1", - "type": "Object", - "tags": [], - "label": "previousPanelState", - "description": [], - "signature": [ - "DashboardPanelState", - "<", - { - "pluginId": "embeddable", - "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.EmbeddableInput", - "text": "EmbeddableInput" - }, - ">" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.replacePanel.$2", - "type": "Object", - "tags": [], - "label": "newPanelState", - "description": [], - "signature": [ - "Partial<", - { - "pluginId": "embeddable", - "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.PanelState", - "text": "PanelState" - }, - "<{ id: string; }>>" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.replacePanel.$3", - "type": "CompoundType", - "tags": [], - "label": "generateNewId", - "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } - ], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.addOrUpdateEmbeddable", - "type": "Function", + "id": "def-public.DashboardContainerInput.useMargins", + "type": "boolean", "tags": [], - "label": "addOrUpdateEmbeddable", + "label": "useMargins", "description": [], - "signature": [ - " = ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.IEmbeddable", - "text": "IEmbeddable" - }, - ">(type: string, explicitInput: Partial, embeddableId?: string | undefined) => Promise" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.addOrUpdateEmbeddable.$1", - "type": "string", - "tags": [], - "label": "type", - "description": [], - "signature": [ - "string" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.addOrUpdateEmbeddable.$2", - "type": "Object", - "tags": [], - "label": "explicitInput", - "description": [], - "signature": [ - "Partial" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.addOrUpdateEmbeddable.$3", - "type": "string", - "tags": [], - "label": "embeddableId", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } - ], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.render", - "type": "Function", + "id": "def-public.DashboardContainerInput.syncColors", + "type": "CompoundType", "tags": [], - "label": "render", + "label": "syncColors", "description": [], "signature": [ - "(dom: HTMLElement) => void" + "boolean | undefined" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.render.$1", - "type": "Object", - "tags": [], - "label": "dom", - "description": [], - "signature": [ - "HTMLElement" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.destroy", - "type": "Function", + "id": "def-public.DashboardContainerInput.syncTooltips", + "type": "CompoundType", "tags": [], - "label": "destroy", + "label": "syncTooltips", "description": [], "signature": [ - "() => void" + "boolean | undefined" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.getInheritedInput", - "type": "Function", + "id": "def-public.DashboardContainerInput.viewMode", + "type": "Enum", "tags": [], - "label": "getInheritedInput", + "label": "viewMode", "description": [], "signature": [ - "(id: string) => ", - "InheritedChildInput" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainer.getInheritedInput.$1", - "type": "string", - "tags": [], - "label": "id", - "description": [], - "signature": [ - "string" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.ViewMode", + "text": "ViewMode" } ], - "returnComment": [] - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition", - "type": "Class", - "tags": [], - "label": "DashboardContainerFactoryDefinition", - "description": [], - "signature": [ - { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardContainerFactoryDefinition", - "text": "DashboardContainerFactoryDefinition" - }, - " implements ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.EmbeddableFactoryDefinition", - "text": "EmbeddableFactoryDefinition" - }, - "<", - { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardContainerInput", - "text": "DashboardContainerInput" - }, - ", ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.ContainerOutput", - "text": "ContainerOutput" - }, - ", ", - { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardContainer", - "text": "DashboardContainer" + "path": "src/plugins/dashboard/public/types.ts", + "deprecated": false, + "trackAdoption": false }, - ", unknown>" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.isContainerType", - "type": "boolean", + "id": "def-public.DashboardContainerInput.filters", + "type": "Array", "tags": [], - "label": "isContainerType", + "label": "filters", "description": [], "signature": [ - "true" + "Filter", + "[]" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.type", + "id": "def-public.DashboardContainerInput.title", "type": "string", "tags": [], - "label": "type", + "label": "title", "description": [], - "signature": [ - "\"dashboard\"" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.inject", - "type": "Function", + "id": "def-public.DashboardContainerInput.query", + "type": "Object", "tags": [], - "label": "inject", + "label": "query", "description": [], "signature": [ - "(state: ", - { - "pluginId": "embeddable", - "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.EmbeddableStateWithType", - "text": "EmbeddableStateWithType" - }, - ", references: ", - "SavedObjectReference", - "[]) => ", - { - "pluginId": "embeddable", - "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.EmbeddableStateWithType", - "text": "EmbeddableStateWithType" - } + "{ query: string | { [key: string]: any; }; language: string; }" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.inject.$1", - "type": "Uncategorized", - "tags": [], - "label": "state", - "description": [], - "signature": [ - "P" - ], - "path": "src/plugins/kibana_utils/common/persistable_state/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.inject.$2", - "type": "Array", - "tags": [], - "label": "references", - "description": [], - "signature": [ - "SavedObjectReference", - "[]" - ], - "path": "src/plugins/kibana_utils/common/persistable_state/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.extract", - "type": "Function", + "id": "def-public.DashboardContainerInput.panels", + "type": "Object", "tags": [], - "label": "extract", + "label": "panels", "description": [], "signature": [ - "(state: ", + "{ [panelId: string]: ", { - "pluginId": "embeddable", + "pluginId": "dashboard", "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.EmbeddableStateWithType", - "text": "EmbeddableStateWithType" + "docId": "kibDashboardPluginApi", + "section": "def-common.DashboardPanelState", + "text": "DashboardPanelState" }, - ") => { state: ", + "<", { "pluginId": "embeddable", "scope": "common", "docId": "kibEmbeddablePluginApi", - "section": "def-common.EmbeddableStateWithType", - "text": "EmbeddableStateWithType" + "section": "def-common.EmbeddableInput", + "text": "EmbeddableInput" }, - "; references: ", - "SavedObjectReference", - "[]; }" + " & { [k: string]: unknown; }>; }" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.extract.$1", - "type": "Uncategorized", - "tags": [], - "label": "state", - "description": [], - "signature": [ - "P" - ], - "path": "src/plugins/kibana_utils/common/persistable_state/types.ts", - "deprecated": false, - "trackAdoption": false - } - ] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.Unnamed", - "type": "Function", + "id": "def-public.DashboardContainerInput.executionContext", + "type": "Object", "tags": [], - "label": "Constructor", + "label": "executionContext", "description": [], "signature": [ - "any" + "KibanaExecutionContext", + " | undefined" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", + "path": "src/plugins/dashboard/public/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.Unnamed.$1", - "type": "Object", - "tags": [], - "label": "persistableStateService", - "description": [], - "signature": [ - { - "pluginId": "embeddable", - "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.EmbeddablePersistableStateService", - "text": "EmbeddablePersistableStateService" - } - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-public.DashboardFeatureFlagConfig", + "type": "Interface", + "tags": [], + "label": "DashboardFeatureFlagConfig", + "description": [], + "path": "src/plugins/dashboard/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dashboard", + "id": "def-public.DashboardFeatureFlagConfig.allowByValueEmbeddables", + "type": "boolean", + "tags": [], + "label": "allowByValueEmbeddables", + "description": [], + "path": "src/plugins/dashboard/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-public.SavedDashboardPanel", + "type": "Interface", + "tags": [], + "label": "SavedDashboardPanel", + "description": [ + "\nA saved dashboard panel parsed directly from the Dashboard Attributes panels JSON" + ], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dashboard", + "id": "def-public.SavedDashboardPanel.embeddableConfig", + "type": "Object", + "tags": [], + "label": "embeddableConfig", + "description": [], + "signature": [ + "{ [key: string]: ", + "Serializable", + "; }" ], - "returnComment": [] + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.isEditable", - "type": "Function", + "id": "def-public.SavedDashboardPanel.id", + "type": "string", "tags": [], - "label": "isEditable", + "label": "id", "description": [], "signature": [ - "() => Promise" + "string | undefined" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] + "trackAdoption": false + }, + { + "parentPluginId": "dashboard", + "id": "def-public.SavedDashboardPanel.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.getDisplayName", - "type": "Function", + "id": "def-public.SavedDashboardPanel.panelRefName", + "type": "string", "tags": [], - "label": "getDisplayName", + "label": "panelRefName", "description": [], "signature": [ - "() => string" + "string | undefined" ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.getDefaultInput", - "type": "Function", + "id": "def-public.SavedDashboardPanel.gridData", + "type": "Object", "tags": [], - "label": "getDefaultInput", + "label": "gridData", "description": [], "signature": [ - "() => Partial<", { "pluginId": "dashboard", - "scope": "public", + "scope": "common", "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardContainerInput", - "text": "DashboardContainerInput" - }, - ">" + "section": "def-common.GridData", + "text": "GridData" + } ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.create", - "type": "Function", + "id": "def-public.SavedDashboardPanel.panelIndex", + "type": "string", "tags": [], - "label": "create", + "label": "panelIndex", "description": [], - "signature": [ - "(initialInput: ", - { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardContainerInput", - "text": "DashboardContainerInput" - }, - ", parent?: ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.Container", - "text": "Container" - }, - "<{}, ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.ContainerInput", - "text": "ContainerInput" - }, - "<{}>, ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.ContainerOutput", - "text": "ContainerOutput" - }, - "> | undefined) => Promise<", - { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardContainer", - "text": "DashboardContainer" - }, - " | ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.ErrorEmbeddable", - "text": "ErrorEmbeddable" - }, - ">" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.create.$1", - "type": "Object", - "tags": [], - "label": "initialInput", - "description": [], - "signature": [ - { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardContainerInput", - "text": "DashboardContainerInput" - } - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerFactoryDefinition.create.$2", - "type": "Object", - "tags": [], - "label": "parent", - "description": [], - "signature": [ - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.Container", - "text": "Container" - }, - "<{}, ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.ContainerInput", - "text": "ContainerInput" - }, - "<{}>, ", - { - "pluginId": "embeddable", - "scope": "public", - "docId": "kibEmbeddablePluginApi", - "section": "def-public.ContainerOutput", - "text": "ContainerOutput" - }, - "> | undefined" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } + "trackAdoption": false + }, + { + "parentPluginId": "dashboard", + "id": "def-public.SavedDashboardPanel.version", + "type": "string", + "tags": [], + "label": "version", + "description": [], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "dashboard", + "id": "def-public.SavedDashboardPanel.title", + "type": "string", + "tags": [], + "label": "title", + "description": [], + "signature": [ + "string | undefined" ], - "returnComment": [] + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false } ], - "functions": [ + "enums": [], + "misc": [ { "parentPluginId": "dashboard", - "id": "def-public.cleanEmptyKeys", - "type": "Function", + "id": "def-public.DASHBOARD_CONTAINER_TYPE", + "type": "string", "tags": [], - "label": "cleanEmptyKeys", + "label": "DASHBOARD_CONTAINER_TYPE", "description": [], "signature": [ - "(stateObj: Record) => Record" + "\"dashboard\"" ], - "path": "src/plugins/dashboard/public/locator.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.cleanEmptyKeys.$1", - "type": "Object", - "tags": [], - "label": "stateObj", - "description": [], - "signature": [ - "Record" - ], - "path": "src/plugins/dashboard/public/locator.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "dashboard", - "id": "def-public.createDashboardEditUrl", - "type": "Function", + "id": "def-public.DashboardAppLocator", + "type": "Type", "tags": [], - "label": "createDashboardEditUrl", + "label": "DashboardAppLocator", "description": [], "signature": [ - "(id: string | undefined, editMode: boolean | undefined) => string" - ], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ { - "parentPluginId": "dashboard", - "id": "def-public.createDashboardEditUrl.$1", - "type": "string", - "tags": [], - "label": "id", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": false + "pluginId": "share", + "scope": "common", + "docId": "kibSharePluginApi", + "section": "def-common.LocatorPublic", + "text": "LocatorPublic" }, + "<", { - "parentPluginId": "dashboard", - "id": "def-public.createDashboardEditUrl.$2", - "type": "CompoundType", - "tags": [], - "label": "editMode", - "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } + "pluginId": "dashboard", + "scope": "public", + "docId": "kibDashboardPluginApi", + "section": "def-public.DashboardAppLocatorParams", + "text": "DashboardAppLocatorParams" + }, + ">" ], - "returnComment": [], + "path": "src/plugins/dashboard/public/locator.ts", + "deprecated": false, + "trackAdoption": false, "initialIsOpen": false - } - ], - "interfaces": [ + }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput", - "type": "Interface", + "id": "def-public.DashboardAppLocatorParams", + "type": "Type", "tags": [], - "label": "DashboardContainerInput", - "description": [], + "label": "DashboardAppLocatorParams", + "description": [ + "\nWe use `type` instead of `interface` to avoid having to extend this type with\n`SerializableRecord`. See https://github.com/microsoft/TypeScript/issues/15300." + ], "signature": [ + "{ dashboardId?: string | undefined; timeRange?: ", + "TimeRange", + " | undefined; refreshInterval?: ", { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardContainerInput", - "text": "DashboardContainerInput" + "pluginId": "data", + "scope": "common", + "docId": "kibDataQueryPluginApi", + "section": "def-common.RefreshInterval", + "text": "RefreshInterval" }, - " extends ", + " | undefined; filters?: ", + "Filter", + "[] | undefined; query?: ", + "Query", + " | undefined; useHash?: boolean | undefined; preserveSavedFilters?: boolean | undefined; viewMode?: ", { "pluginId": "embeddable", - "scope": "public", + "scope": "common", "docId": "kibEmbeddablePluginApi", - "section": "def-public.ContainerInput", - "text": "ContainerInput" + "section": "def-common.ViewMode", + "text": "ViewMode" }, - "<{}>" + " | undefined; searchSessionId?: string | undefined; panels?: (", + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.SavedDashboardPanel", + "text": "SavedDashboardPanel" + }, + " & ", + "SerializableRecord", + ")[] | undefined; savedQuery?: string | undefined; tags?: string[] | undefined; options?: ", + "DashboardOptions", + " | undefined; controlGroupInput?: ", + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.SerializableControlGroupInput", + "text": "SerializableControlGroupInput" + }, + " | undefined; }" ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/locator.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [ + { + "parentPluginId": "dashboard", + "id": "def-public.DashboardConstants", + "type": "Object", + "tags": [], + "label": "DashboardConstants", + "description": [], + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.controlGroupInput", - "type": "Object", + "id": "def-public.DashboardConstants.LANDING_PAGE_PATH", + "type": "string", "tags": [], - "label": "controlGroupInput", + "label": "LANDING_PAGE_PATH", "description": [], - "signature": [ - { - "pluginId": "controls", - "scope": "common", - "docId": "kibControlsPluginApi", - "section": "def-common.PersistableControlGroupInput", - "text": "PersistableControlGroupInput" - }, - " | undefined" - ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.refreshConfig", - "type": "Object", + "id": "def-public.DashboardConstants.CREATE_NEW_DASHBOARD_URL", + "type": "string", "tags": [], - "label": "refreshConfig", + "label": "CREATE_NEW_DASHBOARD_URL", "description": [], - "signature": [ - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataQueryPluginApi", - "section": "def-common.RefreshInterval", - "text": "RefreshInterval" - }, - " | undefined" - ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.isEmbeddedExternally", - "type": "CompoundType", + "id": "def-public.DashboardConstants.VIEW_DASHBOARD_URL", + "type": "string", "tags": [], - "label": "isEmbeddedExternally", + "label": "VIEW_DASHBOARD_URL", "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.isFullScreenMode", - "type": "boolean", + "id": "def-public.DashboardConstants.PRINT_DASHBOARD_URL", + "type": "string", "tags": [], - "label": "isFullScreenMode", + "label": "PRINT_DASHBOARD_URL", "description": [], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.expandedPanelId", + "id": "def-public.DashboardConstants.ADD_EMBEDDABLE_ID", "type": "string", "tags": [], - "label": "expandedPanelId", + "label": "ADD_EMBEDDABLE_ID", "description": [], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.timeRange", - "type": "Object", + "id": "def-public.DashboardConstants.ADD_EMBEDDABLE_TYPE", + "type": "string", "tags": [], - "label": "timeRange", + "label": "ADD_EMBEDDABLE_TYPE", "description": [], - "signature": [ - "{ from: string; to: string; mode?: \"absolute\" | \"relative\" | undefined; }" - ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.timeslice", - "type": "Object", + "id": "def-public.DashboardConstants.DASHBOARDS_ID", + "type": "string", "tags": [], - "label": "timeslice", + "label": "DASHBOARDS_ID", "description": [], - "signature": [ - "[number, number] | undefined" - ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.timeRestore", - "type": "boolean", + "id": "def-public.DashboardConstants.DASHBOARD_ID", + "type": "string", "tags": [], - "label": "timeRestore", + "label": "DASHBOARD_ID", "description": [], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.description", + "id": "def-public.DashboardConstants.DASHBOARD_SAVED_OBJECT_TYPE", "type": "string", "tags": [], - "label": "description", + "label": "DASHBOARD_SAVED_OBJECT_TYPE", "description": [], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.useMargins", - "type": "boolean", + "id": "def-public.DashboardConstants.SEARCH_SESSION_ID", + "type": "string", "tags": [], - "label": "useMargins", + "label": "SEARCH_SESSION_ID", "description": [], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.syncColors", - "type": "CompoundType", + "id": "def-public.DashboardConstants.CHANGE_CHECK_DEBOUNCE", + "type": "number", "tags": [], - "label": "syncColors", + "label": "CHANGE_CHECK_DEBOUNCE", "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.syncTooltips", - "type": "CompoundType", + "id": "def-public.DashboardConstants.CHANGE_APPLY_DEBOUNCE", + "type": "number", "tags": [], - "label": "syncTooltips", + "label": "CHANGE_APPLY_DEBOUNCE", "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/public/dashboard_constants.ts", "deprecated": false, "trackAdoption": false - }, + } + ], + "initialIsOpen": false + } + ], + "setup": { + "parentPluginId": "dashboard", + "id": "def-public.DashboardSetup", + "type": "Interface", + "tags": [], + "label": "DashboardSetup", + "description": [], + "path": "src/plugins/dashboard/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dashboard", + "id": "def-public.DashboardSetup.locator", + "type": "Object", + "tags": [], + "label": "locator", + "description": [], + "signature": [ + { + "pluginId": "dashboard", + "scope": "public", + "docId": "kibDashboardPluginApi", + "section": "def-public.DashboardAppLocator", + "text": "DashboardAppLocator" + }, + " | undefined" + ], + "path": "src/plugins/dashboard/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "dashboard", + "id": "def-public.DashboardStart", + "type": "Interface", + "tags": [], + "label": "DashboardStart", + "description": [], + "path": "src/plugins/dashboard/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dashboard", + "id": "def-public.DashboardStart.getDashboardContainerByValueRenderer", + "type": "Function", + "tags": [], + "label": "getDashboardContainerByValueRenderer", + "description": [], + "signature": [ + "() => React.FC" + ], + "path": "src/plugins/dashboard/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "dashboard", + "id": "def-public.DashboardStart.locator", + "type": "Object", + "tags": [], + "label": "locator", + "description": [], + "signature": [ + { + "pluginId": "dashboard", + "scope": "public", + "docId": "kibDashboardPluginApi", + "section": "def-public.DashboardAppLocator", + "text": "DashboardAppLocator" + }, + " | undefined" + ], + "path": "src/plugins/dashboard/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "dashboard", + "id": "def-public.DashboardStart.dashboardFeatureFlagConfig", + "type": "Object", + "tags": [], + "label": "dashboardFeatureFlagConfig", + "description": [], + "signature": [ + { + "pluginId": "dashboard", + "scope": "public", + "docId": "kibDashboardPluginApi", + "section": "def-public.DashboardFeatureFlagConfig", + "text": "DashboardFeatureFlagConfig" + } + ], + "path": "src/plugins/dashboard/public/plugin.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "server": { + "classes": [], + "functions": [ + { + "parentPluginId": "dashboard", + "id": "def-server.findByValueEmbeddables", + "type": "Function", + "tags": [], + "label": "findByValueEmbeddables", + "description": [], + "signature": [ + "(savedObjectClient: Pick<", + "ISavedObjectsRepository", + ", \"find\">, embeddableType: string) => Promise<{ [key: string]: ", + "Serializable", + "; }[]>" + ], + "path": "src/plugins/dashboard/server/usage/find_by_value_embeddables.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.viewMode", - "type": "Enum", + "id": "def-server.findByValueEmbeddables.$1", + "type": "Object", "tags": [], - "label": "viewMode", + "label": "savedObjectClient", "description": [], "signature": [ - { - "pluginId": "embeddable", - "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.ViewMode", - "text": "ViewMode" - } + "Pick<", + "ISavedObjectsRepository", + ", \"find\">" ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/server/usage/find_by_value_embeddables.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": true }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.filters", - "type": "Array", + "id": "def-server.findByValueEmbeddables.$2", + "type": "string", "tags": [], - "label": "filters", + "label": "embeddableType", "description": [], "signature": [ - "Filter", - "[]" + "string" ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/server/usage/find_by_value_embeddables.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [], + "setup": { + "parentPluginId": "dashboard", + "id": "def-server.DashboardPluginSetup", + "type": "Interface", + "tags": [], + "label": "DashboardPluginSetup", + "description": [], + "path": "src/plugins/dashboard/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "dashboard", + "id": "def-server.DashboardPluginStart", + "type": "Interface", + "tags": [], + "label": "DashboardPluginStart", + "description": [], + "path": "src/plugins/dashboard/server/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "dashboard", + "id": "def-common.convertPanelMapToSavedPanels", + "type": "Function", + "tags": [], + "label": "convertPanelMapToSavedPanels", + "description": [], + "signature": [ + "(panels: ", + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.DashboardPanelMap", + "text": "DashboardPanelMap" + }, + ", version: string) => ", + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.SavedDashboardPanel", + "text": "SavedDashboardPanel" }, + "[]" + ], + "path": "src/plugins/dashboard/common/lib/dashboard_panel_converters.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.title", - "type": "string", + "id": "def-common.convertPanelMapToSavedPanels.$1", + "type": "Object", "tags": [], - "label": "title", + "label": "panels", "description": [], - "path": "src/plugins/dashboard/public/types.ts", + "signature": [ + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.DashboardPanelMap", + "text": "DashboardPanelMap" + } + ], + "path": "src/plugins/dashboard/common/lib/dashboard_panel_converters.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": true }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.query", - "type": "Object", + "id": "def-common.convertPanelMapToSavedPanels.$2", + "type": "string", "tags": [], - "label": "query", + "label": "version", "description": [], "signature": [ - "{ query: string | { [key: string]: any; }; language: string; }" + "string" ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/common/lib/dashboard_panel_converters.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.convertPanelStateToSavedDashboardPanel", + "type": "Function", + "tags": [], + "label": "convertPanelStateToSavedDashboardPanel", + "description": [], + "signature": [ + "(panelState: ", + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.DashboardPanelState", + "text": "DashboardPanelState" + }, + "<", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.SavedObjectEmbeddableInput", + "text": "SavedObjectEmbeddableInput" }, + ">, version: string) => ", + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.SavedDashboardPanel", + "text": "SavedDashboardPanel" + } + ], + "path": "src/plugins/dashboard/common/lib/dashboard_panel_converters.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.panels", + "id": "def-common.convertPanelStateToSavedDashboardPanel.$1", "type": "Object", "tags": [], - "label": "panels", + "label": "panelState", "description": [], "signature": [ - "{ [panelId: string]: ", - "DashboardPanelState", + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.DashboardPanelState", + "text": "DashboardPanelState" + }, "<", { "pluginId": "embeddable", "scope": "common", "docId": "kibEmbeddablePluginApi", - "section": "def-common.EmbeddableInput", - "text": "EmbeddableInput" + "section": "def-common.SavedObjectEmbeddableInput", + "text": "SavedObjectEmbeddableInput" }, - " & { [k: string]: unknown; }>; }" + ">" ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/common/lib/dashboard_panel_converters.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": true }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardContainerInput.executionContext", - "type": "Object", + "id": "def-common.convertPanelStateToSavedDashboardPanel.$2", + "type": "string", "tags": [], - "label": "executionContext", + "label": "version", "description": [], "signature": [ - "KibanaExecutionContext", - " | undefined" + "string" ], - "path": "src/plugins/dashboard/public/types.ts", + "path": "src/plugins/dashboard/common/lib/dashboard_panel_converters.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": true } ], + "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardFeatureFlagConfig", - "type": "Interface", + "id": "def-common.convertSavedDashboardPanelToPanelState", + "type": "Function", "tags": [], - "label": "DashboardFeatureFlagConfig", + "label": "convertSavedDashboardPanelToPanelState", "description": [], - "path": "src/plugins/dashboard/public/plugin.tsx", + "signature": [ + "(savedDashboardPanel: ", + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.SavedDashboardPanel", + "text": "SavedDashboardPanel" + }, + ") => ", + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.DashboardPanelState", + "text": "DashboardPanelState" + }, + "" + ], + "path": "src/plugins/dashboard/common/lib/dashboard_panel_converters.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardFeatureFlagConfig.allowByValueEmbeddables", - "type": "boolean", + "id": "def-common.convertSavedDashboardPanelToPanelState.$1", + "type": "Object", "tags": [], - "label": "allowByValueEmbeddables", + "label": "savedDashboardPanel", "description": [], - "path": "src/plugins/dashboard/public/plugin.tsx", + "signature": [ + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.SavedDashboardPanel", + "text": "SavedDashboardPanel" + } + ], + "path": "src/plugins/dashboard/common/lib/dashboard_panel_converters.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": true } ], + "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject", - "type": "Interface", + "id": "def-common.convertSavedPanelsToPanelMap", + "type": "Function", "tags": [], - "label": "DashboardSavedObject", + "label": "convertSavedPanelsToPanelMap", "description": [], "signature": [ + "(panels?: ", { "pluginId": "dashboard", - "scope": "public", + "scope": "common", "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardSavedObject", - "text": "DashboardSavedObject" + "section": "def-common.SavedDashboardPanel", + "text": "SavedDashboardPanel" }, - " extends ", + "[] | undefined) => ", { - "pluginId": "savedObjects", - "scope": "public", - "docId": "kibSavedObjectsPluginApi", - "section": "def-public.SavedObject", - "text": "SavedObject" - }, - "<", - "SavedObjectAttributes", - ">" + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.DashboardPanelMap", + "text": "DashboardPanelMap" + } ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/lib/dashboard_panel_converters.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.id", - "type": "string", + "id": "def-common.convertSavedPanelsToPanelMap.$1", + "type": "Array", "tags": [], - "label": "id", + "label": "panels", "description": [], "signature": [ - "string | undefined" + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.SavedDashboardPanel", + "text": "SavedDashboardPanel" + }, + "[] | undefined" ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/lib/dashboard_panel_converters.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.createExtract", + "type": "Function", + "tags": [], + "label": "createExtract", + "description": [], + "signature": [ + "(persistableStateService: ", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddablePersistableStateService", + "text": "EmbeddablePersistableStateService" }, + ") => (state: ", { - "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.timeRestore", - "type": "boolean", - "tags": [], - "label": "timeRestore", - "description": [], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", - "deprecated": false, - "trackAdoption": false + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddableStateWithType", + "text": "EmbeddableStateWithType" + }, + ") => { state: ", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddableStateWithType", + "text": "EmbeddableStateWithType" }, + "; references: ", + "SavedObjectReference", + "[]; }" + ], + "path": "src/plugins/dashboard/common/persistable_state/dashboard_container_references.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.timeTo", - "type": "string", + "id": "def-common.createExtract.$1", + "type": "Object", "tags": [], - "label": "timeTo", + "label": "persistableStateService", "description": [], "signature": [ - "string | undefined" - ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", - "deprecated": false, - "trackAdoption": false - }, + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddablePersistableStateService", + "text": "EmbeddablePersistableStateService" + } + ], + "path": "src/plugins/dashboard/common/persistable_state/dashboard_container_references.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.createInject", + "type": "Function", + "tags": [], + "label": "createInject", + "description": [], + "signature": [ + "(persistableStateService: ", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddablePersistableStateService", + "text": "EmbeddablePersistableStateService" + }, + ") => (state: ", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddableStateWithType", + "text": "EmbeddableStateWithType" + }, + ", references: ", + "SavedObjectReference", + "[]) => ", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddableStateWithType", + "text": "EmbeddableStateWithType" + } + ], + "path": "src/plugins/dashboard/common/persistable_state/dashboard_container_references.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.timeFrom", - "type": "string", + "id": "def-common.createInject.$1", + "type": "Object", "tags": [], - "label": "timeFrom", + "label": "persistableStateService", "description": [], "signature": [ - "string | undefined" + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddablePersistableStateService", + "text": "EmbeddablePersistableStateService" + } + ], + "path": "src/plugins/dashboard/common/persistable_state/dashboard_container_references.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.extractReferences", + "type": "Function", + "tags": [], + "label": "extractReferences", + "description": [], + "signature": [ + "({ attributes, references = [] }: SavedObjectAttributesAndReferences, deps: ", + "ExtractDeps", + ") => SavedObjectAttributesAndReferences" + ], + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dashboard", + "id": "def-common.extractReferences.$1", + "type": "Object", + "tags": [], + "label": "{ attributes, references = [] }", + "description": [], + "signature": [ + "SavedObjectAttributesAndReferences" + ], + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "dashboard", + "id": "def-common.extractReferences.$2", + "type": "Object", + "tags": [], + "label": "deps", + "description": [], + "signature": [ + "ExtractDeps" + ], + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.injectReferences", + "type": "Function", + "tags": [], + "label": "injectReferences", + "description": [], + "signature": [ + "({ attributes, references = [] }: SavedObjectAttributesAndReferences, deps: ", + "InjectDeps", + ") => ", + "SavedObjectAttributes" + ], + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dashboard", + "id": "def-common.injectReferences.$1", + "type": "Object", + "tags": [], + "label": "{ attributes, references = [] }", + "description": [], + "signature": [ + "SavedObjectAttributesAndReferences" + ], + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "dashboard", + "id": "def-common.injectReferences.$2", + "type": "Object", + "tags": [], + "label": "deps", + "description": [], + "signature": [ + "InjectDeps" + ], + "path": "src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "dashboard", + "id": "def-common.DashboardAttributes", + "type": "Interface", + "tags": [], + "label": "DashboardAttributes", + "description": [ + "\nThe attributes of the dashboard saved object. This interface should be the\nsource of truth for the latest dashboard attributes shape after all migrations." + ], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dashboard", + "id": "def-common.DashboardAttributes.controlGroupInput", + "type": "CompoundType", + "tags": [], + "label": "controlGroupInput", + "description": [], + "signature": [ + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.RawControlGroupAttributes", + "text": "RawControlGroupAttributes" + }, + " | undefined" ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.description", - "type": "string", + "id": "def-common.DashboardAttributes.refreshInterval", + "type": "Object", "tags": [], - "label": "description", + "label": "refreshInterval", "description": [], "signature": [ - "string | undefined" + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataQueryPluginApi", + "section": "def-common.RefreshInterval", + "text": "RefreshInterval" + }, + " | undefined" ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.panelsJSON", - "type": "string", + "id": "def-common.DashboardAttributes.timeRestore", + "type": "boolean", "tags": [], - "label": "panelsJSON", + "label": "timeRestore", "description": [], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.optionsJSON", + "id": "def-common.DashboardAttributes.optionsJSON", "type": "string", "tags": [], "label": "optionsJSON", @@ -1710,687 +1581,501 @@ "signature": [ "string | undefined" ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.DashboardAttributes.useMargins", + "type": "CompoundType", + "tags": [], + "label": "useMargins", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.DashboardAttributes.description", + "type": "string", + "tags": [], + "label": "description", + "description": [], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.DashboardAttributes.panelsJSON", + "type": "string", + "tags": [], + "label": "panelsJSON", + "description": [], + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.uiStateJSON", + "id": "def-common.DashboardAttributes.timeFrom", "type": "string", "tags": [], - "label": "uiStateJSON", + "label": "timeFrom", "description": [], "signature": [ "string | undefined" ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.lastSavedTitle", - "type": "string", + "id": "def-common.DashboardAttributes.version", + "type": "number", "tags": [], - "label": "lastSavedTitle", + "label": "version", "description": [], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.refreshInterval", - "type": "Object", + "id": "def-common.DashboardAttributes.timeTo", + "type": "string", "tags": [], - "label": "refreshInterval", + "label": "timeTo", "description": [], "signature": [ - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataQueryPluginApi", - "section": "def-common.RefreshInterval", - "text": "RefreshInterval" - }, - " | undefined" + "string | undefined" ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.DashboardAttributes.title", + "type": "string", + "tags": [], + "label": "title", + "description": [], + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.searchSource", + "id": "def-common.DashboardAttributes.kibanaSavedObjectMeta", "type": "Object", "tags": [], - "label": "searchSource", + "label": "kibanaSavedObjectMeta", "description": [], "signature": [ - "{ create: () => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSource", - "text": "SearchSource" - }, - "; history: ", - "SearchRequest", - "[]; setOverwriteDataViewType: (overwriteType: string | false | undefined) => void; setField: (field: K, value: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSourceFields", - "text": "SearchSourceFields" - }, - "[K]) => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSource", - "text": "SearchSource" - }, - "; removeField: (field: K) => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSource", - "text": "SearchSource" - }, - "; setFields: (newFields: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSourceFields", - "text": "SearchSourceFields" - }, - ") => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSource", - "text": "SearchSource" - }, - "; getId: () => string; getFields: () => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSourceFields", - "text": "SearchSourceFields" - }, - "; getField: (field: K, recurse?: boolean) => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSourceFields", - "text": "SearchSourceFields" - }, - "[K]; getActiveIndexFilter: () => string[]; getOwnField: (field: K) => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSourceFields", - "text": "SearchSourceFields" - }, - "[K]; createCopy: () => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSource", - "text": "SearchSource" - }, - "; createChild: (options?: {}) => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSource", - "text": "SearchSource" - }, - "; setParent: (parent?: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.ISearchSource", - "text": "ISearchSource" - }, - " | undefined, options?: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSourceOptions", - "text": "SearchSourceOptions" - }, - ") => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSource", - "text": "SearchSource" - }, - "; getParent: () => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSource", - "text": "SearchSource" - }, - " | undefined; fetch$: (options?: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSourceSearchOptions", - "text": "SearchSourceSearchOptions" - }, - ") => ", - "Observable", - "<", + "{ searchSourceJSON: string; }" + ], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.DashboardContainerStateWithType", + "type": "Interface", + "tags": [], + "label": "DashboardContainerStateWithType", + "description": [ + "--------------------------------------------------------------------\nDashboard container types\n -----------------------------------------------------------------------\n\nTypes below this line are copied here because so many important types are tied up in public. These types should be\nmoved from public into common." + ], + "signature": [ + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.DashboardContainerStateWithType", + "text": "DashboardContainerStateWithType" + }, + " extends ", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddableStateWithType", + "text": "EmbeddableStateWithType" + } + ], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "dashboard", + "id": "def-common.DashboardContainerStateWithType.panels", + "type": "Object", + "tags": [], + "label": "panels", + "description": [], + "signature": [ + "{ [panelId: string]: ", { - "pluginId": "data", + "pluginId": "dashboard", "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.IKibanaSearchResponse", - "text": "IKibanaSearchResponse" + "docId": "kibDashboardPluginApi", + "section": "def-common.DashboardPanelState", + "text": "DashboardPanelState" }, "<", - "SearchResponse", - ">>>; fetch: (options?: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSourceSearchOptions", - "text": "SearchSourceSearchOptions" - }, - ") => Promise<", - "SearchResponse", - ">>; onRequestStart: (handler: (searchSource: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSource", - "text": "SearchSource" - }, - ", options?: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SearchSourceSearchOptions", - "text": "SearchSourceSearchOptions" - }, - " | undefined) => Promise) => void; getSearchRequestBody: () => any; destroy: () => void; getSerializedFields: (recurse?: boolean) => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataSearchPluginApi", - "section": "def-common.SerializedSearchSourceFields", - "text": "SerializedSearchSourceFields" - }, - "; serialize: () => { searchSourceJSON: string; references: ", - "SavedObjectReference", - "[]; }; toExpressionAst: ({ asDatatable }?: ExpressionAstOptions) => ", { - "pluginId": "expressions", + "pluginId": "embeddable", "scope": "common", - "docId": "kibExpressionsPluginApi", - "section": "def-common.ExpressionAstExpression", - "text": "ExpressionAstExpression" + "docId": "kibEmbeddablePluginApi", + "section": "def-common.EmbeddableInput", + "text": "EmbeddableInput" }, - "; parseActiveIndexPatternFromQueryString: (queryString: string) => string[]; }" + " & { [k: string]: unknown; }>; }" ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.getQuery", - "type": "Function", + "id": "def-common.DashboardContainerStateWithType.controlGroupInput", + "type": "Object", "tags": [], - "label": "getQuery", + "label": "controlGroupInput", "description": [], "signature": [ - "() => ", - "Query" + { + "pluginId": "controls", + "scope": "common", + "docId": "kibControlsPluginApi", + "section": "def-common.PersistableControlGroupInput", + "text": "PersistableControlGroupInput" + }, + " | undefined" ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.DashboardPanelMap", + "type": "Interface", + "tags": [], + "label": "DashboardPanelMap", + "description": [], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.getFilters", - "type": "Function", + "id": "def-common.DashboardPanelMap.Unnamed", + "type": "IndexSignature", "tags": [], - "label": "getFilters", + "label": "[key: string]: DashboardPanelState", "description": [], "signature": [ - "() => ", - "Filter", - "[]" + "[key: string]: ", + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.DashboardPanelState", + "text": "DashboardPanelState" + }, + "<", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.SavedObjectEmbeddableInput", + "text": "SavedObjectEmbeddableInput" + }, + ">" ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.DashboardPanelState", + "type": "Interface", + "tags": [], + "label": "DashboardPanelState", + "description": [ + "--------------------------------------------------------------------\nDashboard panel types\n -----------------------------------------------------------------------\n\nThe dashboard panel format expected by the embeddable container." + ], + "signature": [ + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.DashboardPanelState", + "text": "DashboardPanelState" }, + " extends ", + { + "pluginId": "embeddable", + "scope": "common", + "docId": "kibEmbeddablePluginApi", + "section": "def-common.PanelState", + "text": "PanelState" + }, + "" + ], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.getFullEditPath", - "type": "Function", + "id": "def-common.DashboardPanelState.gridData", + "type": "Object", "tags": [], - "label": "getFullEditPath", + "label": "gridData", "description": [], "signature": [ - "(editMode?: boolean | undefined) => string" - ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ { - "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.getFullEditPath.$1", - "type": "CompoundType", - "tags": [], - "label": "editMode", - "description": [], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": false + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.GridData", + "text": "GridData" } ], - "returnComment": [] + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.outcome", - "type": "CompoundType", + "id": "def-common.DashboardPanelState.panelRefName", + "type": "string", "tags": [], - "label": "outcome", + "label": "panelRefName", "description": [], "signature": [ - "\"exactMatch\" | \"aliasMatch\" | \"conflict\" | undefined" + "string | undefined" ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false - }, + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dashboard", + "id": "def-common.GridData", + "type": "Interface", + "tags": [], + "label": "GridData", + "description": [ + "\nGrid type for React Grid Layout" + ], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.aliasId", - "type": "string", + "id": "def-common.GridData.w", + "type": "number", "tags": [], - "label": "aliasId", + "label": "w", "description": [], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.aliasPurpose", - "type": "CompoundType", + "id": "def-common.GridData.h", + "type": "number", "tags": [], - "label": "aliasPurpose", + "label": "h", "description": [], - "signature": [ - "\"savedObjectConversion\" | \"savedObjectImport\" | undefined" - ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardSavedObject.controlGroupInput", - "type": "Object", + "id": "def-common.GridData.x", + "type": "number", "tags": [], - "label": "controlGroupInput", + "label": "x", "description": [], - "signature": [ - "Omit<", - { - "pluginId": "controls", - "scope": "common", - "docId": "kibControlsPluginApi", - "section": "def-common.RawControlGroupAttributes", - "text": "RawControlGroupAttributes" - }, - ", \"id\"> | undefined" - ], - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false - } - ], - "initialIsOpen": false - } - ], - "enums": [], - "misc": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DASHBOARD_CONTAINER_TYPE", - "type": "string", - "tags": [], - "label": "DASHBOARD_CONTAINER_TYPE", - "description": [], - "signature": [ - "\"dashboard\"" - ], - "path": "src/plugins/dashboard/public/application/embeddable/dashboard_constants.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardAppLocator", - "type": "Type", - "tags": [], - "label": "DashboardAppLocator", - "description": [], - "signature": [ - { - "pluginId": "share", - "scope": "common", - "docId": "kibSharePluginApi", - "section": "def-common.LocatorPublic", - "text": "LocatorPublic" - }, - "<", - { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardAppLocatorParams", - "text": "DashboardAppLocatorParams" - }, - ">" - ], - "path": "src/plugins/dashboard/public/locator.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardAppLocatorParams", - "type": "Type", - "tags": [], - "label": "DashboardAppLocatorParams", - "description": [ - "\nWe use `type` instead of `interface` to avoid having to extend this type with\n`SerializableRecord`. See https://github.com/microsoft/TypeScript/issues/15300." - ], - "signature": [ - "{ dashboardId?: string | undefined; timeRange?: ", - "TimeRange", - " | undefined; refreshInterval?: ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataQueryPluginApi", - "section": "def-common.RefreshInterval", - "text": "RefreshInterval" - }, - " | undefined; filters?: ", - "Filter", - "[] | undefined; query?: ", - "Query", - " | undefined; useHash?: boolean | undefined; preserveSavedFilters?: boolean | undefined; viewMode?: ", - { - "pluginId": "embeddable", - "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.ViewMode", - "text": "ViewMode" }, - " | undefined; searchSessionId?: string | undefined; panels?: ", { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.SavedDashboardPanel730ToLatest", - "text": "SavedDashboardPanel730ToLatest" + "parentPluginId": "dashboard", + "id": "def-common.GridData.y", + "type": "number", + "tags": [], + "label": "y", + "description": [], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false }, - "[] | undefined; savedQuery?: string | undefined; tags?: string[] | undefined; options?: ", - "DashboardOptions", - " | undefined; controlGroupInput?: ", { - "pluginId": "controls", - "scope": "common", - "docId": "kibControlsPluginApi", - "section": "def-common.SerializableControlGroupInput", - "text": "SerializableControlGroupInput" - }, - " | undefined; }" + "parentPluginId": "dashboard", + "id": "def-common.GridData.i", + "type": "string", + "tags": [], + "label": "i", + "description": [], + "path": "src/plugins/dashboard/common/types.ts", + "deprecated": false, + "trackAdoption": false + } ], - "path": "src/plugins/dashboard/public/locator.ts", - "deprecated": false, - "trackAdoption": false, "initialIsOpen": false }, { "parentPluginId": "dashboard", - "id": "def-public.SavedDashboardPanel", - "type": "Type", + "id": "def-common.SavedDashboardPanel", + "type": "Interface", "tags": [], "label": "SavedDashboardPanel", "description": [ - "\nThis should always represent the latest dashboard panel shape, after all possible migrations." - ], - "signature": [ - "Pick<", - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.RawSavedDashboardPanel730ToLatest", - "text": "RawSavedDashboardPanel730ToLatest" - }, - ", \"type\" | \"title\" | \"panelIndex\" | \"gridData\" | \"version\" | \"embeddableConfig\" | \"panelRefName\"> & { readonly id?: string | undefined; readonly type: string; }" + "\nA saved dashboard panel parsed directly from the Dashboard Attributes panels JSON" ], "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false, - "initialIsOpen": false - } - ], - "objects": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants", - "type": "Object", - "tags": [], - "label": "DashboardConstants", - "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", - "deprecated": false, - "trackAdoption": false, "children": [ { "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants.LANDING_PAGE_PATH", - "type": "string", - "tags": [], - "label": "LANDING_PAGE_PATH", - "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants.CREATE_NEW_DASHBOARD_URL", - "type": "string", + "id": "def-common.SavedDashboardPanel.embeddableConfig", + "type": "Object", "tags": [], - "label": "CREATE_NEW_DASHBOARD_URL", + "label": "embeddableConfig", "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", + "signature": [ + "{ [key: string]: ", + "Serializable", + "; }" + ], + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants.VIEW_DASHBOARD_URL", + "id": "def-common.SavedDashboardPanel.id", "type": "string", "tags": [], - "label": "VIEW_DASHBOARD_URL", + "label": "id", "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", + "signature": [ + "string | undefined" + ], + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants.PRINT_DASHBOARD_URL", + "id": "def-common.SavedDashboardPanel.type", "type": "string", "tags": [], - "label": "PRINT_DASHBOARD_URL", + "label": "type", "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants.ADD_EMBEDDABLE_ID", + "id": "def-common.SavedDashboardPanel.panelRefName", "type": "string", "tags": [], - "label": "ADD_EMBEDDABLE_ID", + "label": "panelRefName", "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", + "signature": [ + "string | undefined" + ], + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants.ADD_EMBEDDABLE_TYPE", - "type": "string", + "id": "def-common.SavedDashboardPanel.gridData", + "type": "Object", "tags": [], - "label": "ADD_EMBEDDABLE_TYPE", + "label": "gridData", "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", + "signature": [ + { + "pluginId": "dashboard", + "scope": "common", + "docId": "kibDashboardPluginApi", + "section": "def-common.GridData", + "text": "GridData" + } + ], + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants.DASHBOARDS_ID", + "id": "def-common.SavedDashboardPanel.panelIndex", "type": "string", "tags": [], - "label": "DASHBOARDS_ID", + "label": "panelIndex", "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants.DASHBOARD_ID", + "id": "def-common.SavedDashboardPanel.version", "type": "string", "tags": [], - "label": "DASHBOARD_ID", + "label": "version", "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants.SEARCH_SESSION_ID", + "id": "def-common.SavedDashboardPanel.title", "type": "string", "tags": [], - "label": "SEARCH_SESSION_ID", - "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants.CHANGE_CHECK_DEBOUNCE", - "type": "number", - "tags": [], - "label": "CHANGE_CHECK_DEBOUNCE", - "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardConstants.CHANGE_APPLY_DEBOUNCE", - "type": "number", - "tags": [], - "label": "CHANGE_APPLY_DEBOUNCE", + "label": "title", "description": [], - "path": "src/plugins/dashboard/public/dashboard_constants.ts", + "signature": [ + "string | undefined" + ], + "path": "src/plugins/dashboard/common/types.ts", "deprecated": false, "trackAdoption": false } @@ -2398,674 +2083,8 @@ "initialIsOpen": false } ], - "setup": { - "parentPluginId": "dashboard", - "id": "def-public.DashboardSetup", - "type": "Interface", - "tags": [], - "label": "DashboardSetup", - "description": [], - "path": "src/plugins/dashboard/public/plugin.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardSetup.locator", - "type": "Object", - "tags": [], - "label": "locator", - "description": [], - "signature": [ - { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardAppLocator", - "text": "DashboardAppLocator" - }, - " | undefined" - ], - "path": "src/plugins/dashboard/public/plugin.tsx", - "deprecated": false, - "trackAdoption": false - } - ], - "lifecycle": "setup", - "initialIsOpen": true - }, - "start": { - "parentPluginId": "dashboard", - "id": "def-public.DashboardStart", - "type": "Interface", - "tags": [], - "label": "DashboardStart", - "description": [], - "path": "src/plugins/dashboard/public/plugin.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardStart.getSavedDashboardLoader", - "type": "Function", - "tags": [], - "label": "getSavedDashboardLoader", - "description": [], - "signature": [ - "() => ", - "SavedObjectLoader" - ], - "path": "src/plugins/dashboard/public/plugin.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardStart.getDashboardContainerByValueRenderer", - "type": "Function", - "tags": [], - "label": "getDashboardContainerByValueRenderer", - "description": [], - "signature": [ - "() => React.FC" - ], - "path": "src/plugins/dashboard/public/plugin.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardStart.locator", - "type": "Object", - "tags": [], - "label": "locator", - "description": [], - "signature": [ - { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardAppLocator", - "text": "DashboardAppLocator" - }, - " | undefined" - ], - "path": "src/plugins/dashboard/public/plugin.tsx", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "dashboard", - "id": "def-public.DashboardStart.dashboardFeatureFlagConfig", - "type": "Object", - "tags": [], - "label": "dashboardFeatureFlagConfig", - "description": [], - "signature": [ - { - "pluginId": "dashboard", - "scope": "public", - "docId": "kibDashboardPluginApi", - "section": "def-public.DashboardFeatureFlagConfig", - "text": "DashboardFeatureFlagConfig" - } - ], - "path": "src/plugins/dashboard/public/plugin.tsx", - "deprecated": false, - "trackAdoption": false - } - ], - "lifecycle": "start", - "initialIsOpen": true - } - }, - "server": { - "classes": [], - "functions": [ - { - "parentPluginId": "dashboard", - "id": "def-server.findByValueEmbeddables", - "type": "Function", - "tags": [], - "label": "findByValueEmbeddables", - "description": [], - "signature": [ - "(savedObjectClient: Pick<", - "ISavedObjectsRepository", - ", \"find\">, embeddableType: string) => Promise<{ [key: string]: ", - "Serializable", - "; }[]>" - ], - "path": "src/plugins/dashboard/server/usage/find_by_value_embeddables.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-server.findByValueEmbeddables.$1", - "type": "Object", - "tags": [], - "label": "savedObjectClient", - "description": [], - "signature": [ - "Pick<", - "ISavedObjectsRepository", - ", \"find\">" - ], - "path": "src/plugins/dashboard/server/usage/find_by_value_embeddables.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-server.findByValueEmbeddables.$2", - "type": "string", - "tags": [], - "label": "embeddableType", - "description": [], - "signature": [ - "string" - ], - "path": "src/plugins/dashboard/server/usage/find_by_value_embeddables.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - } - ], - "interfaces": [], "enums": [], "misc": [], - "objects": [], - "setup": { - "parentPluginId": "dashboard", - "id": "def-server.DashboardPluginSetup", - "type": "Interface", - "tags": [], - "label": "DashboardPluginSetup", - "description": [], - "path": "src/plugins/dashboard/server/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "lifecycle": "setup", - "initialIsOpen": true - }, - "start": { - "parentPluginId": "dashboard", - "id": "def-server.DashboardPluginStart", - "type": "Interface", - "tags": [], - "label": "DashboardPluginStart", - "description": [], - "path": "src/plugins/dashboard/server/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [], - "lifecycle": "start", - "initialIsOpen": true - } - }, - "common": { - "classes": [], - "functions": [ - { - "parentPluginId": "dashboard", - "id": "def-common.migratePanelsTo730", - "type": "Function", - "tags": [], - "label": "migratePanelsTo730", - "description": [], - "signature": [ - "(panels: (", - "RawSavedDashboardPanelTo60", - " | ", - "RawSavedDashboardPanel610", - " | ", - "RawSavedDashboardPanel620", - " | ", - "RawSavedDashboardPanel640To720", - " | ", - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.SavedDashboardPanelTo60", - "text": "SavedDashboardPanelTo60" - }, - " | ", - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.SavedDashboardPanel610", - "text": "SavedDashboardPanel610" - }, - " | ", - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.SavedDashboardPanel620", - "text": "SavedDashboardPanel620" - }, - " | ", - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.SavedDashboardPanel630", - "text": "SavedDashboardPanel630" - }, - ")[], version: string, useMargins: boolean, uiState: { [key: string]: ", - "SerializableRecord", - "; } | undefined) => ", - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.RawSavedDashboardPanel730ToLatest", - "text": "RawSavedDashboardPanel730ToLatest" - }, - "[]" - ], - "path": "src/plugins/dashboard/common/migrate_to_730_panels.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-common.migratePanelsTo730.$1", - "type": "Array", - "tags": [], - "label": "panels", - "description": [], - "signature": [ - "(", - "RawSavedDashboardPanelTo60", - " | ", - "RawSavedDashboardPanel610", - " | ", - "RawSavedDashboardPanel620", - " | ", - "RawSavedDashboardPanel640To720", - " | ", - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.SavedDashboardPanelTo60", - "text": "SavedDashboardPanelTo60" - }, - " | ", - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.SavedDashboardPanel610", - "text": "SavedDashboardPanel610" - }, - " | ", - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.SavedDashboardPanel620", - "text": "SavedDashboardPanel620" - }, - " | ", - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.SavedDashboardPanel630", - "text": "SavedDashboardPanel630" - }, - ")[]" - ], - "path": "src/plugins/dashboard/common/migrate_to_730_panels.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-common.migratePanelsTo730.$2", - "type": "string", - "tags": [], - "label": "version", - "description": [], - "signature": [ - "string" - ], - "path": "src/plugins/dashboard/common/migrate_to_730_panels.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-common.migratePanelsTo730.$3", - "type": "boolean", - "tags": [], - "label": "useMargins", - "description": [], - "signature": [ - "boolean" - ], - "path": "src/plugins/dashboard/common/migrate_to_730_panels.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - }, - { - "parentPluginId": "dashboard", - "id": "def-common.migratePanelsTo730.$4", - "type": "Object", - "tags": [], - "label": "uiState", - "description": [], - "path": "src/plugins/dashboard/common/migrate_to_730_panels.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-common.migratePanelsTo730.$4.Unnamed", - "type": "IndexSignature", - "tags": [], - "label": "[key: string]: SerializableRecord", - "description": [], - "signature": [ - "[key: string]: ", - "SerializableRecord" - ], - "path": "src/plugins/dashboard/common/migrate_to_730_panels.ts", - "deprecated": false, - "trackAdoption": false - } - ] - } - ], - "returnComment": [], - "initialIsOpen": false - } - ], - "interfaces": [ - { - "parentPluginId": "dashboard", - "id": "def-common.DashboardContainerStateWithType", - "type": "Interface", - "tags": [], - "label": "DashboardContainerStateWithType", - "description": [], - "signature": [ - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.DashboardContainerStateWithType", - "text": "DashboardContainerStateWithType" - }, - " extends ", - { - "pluginId": "embeddable", - "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.EmbeddableStateWithType", - "text": "EmbeddableStateWithType" - } - ], - "path": "src/plugins/dashboard/common/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "dashboard", - "id": "def-common.DashboardContainerStateWithType.panels", - "type": "Object", - "tags": [], - "label": "panels", - "description": [], - "signature": [ - "{ [panelId: string]: ", - "DashboardPanelState", - "<", - { - "pluginId": "embeddable", - "scope": "common", - "docId": "kibEmbeddablePluginApi", - "section": "def-common.EmbeddableInput", - "text": "EmbeddableInput" - }, - " & { [k: string]: unknown; }>; }" - ], - "path": "src/plugins/dashboard/common/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "dashboard", - "id": "def-common.DashboardContainerStateWithType.controlGroupInput", - "type": "Object", - "tags": [], - "label": "controlGroupInput", - "description": [], - "signature": [ - { - "pluginId": "controls", - "scope": "common", - "docId": "kibControlsPluginApi", - "section": "def-common.PersistableControlGroupInput", - "text": "PersistableControlGroupInput" - }, - " | undefined" - ], - "path": "src/plugins/dashboard/common/types.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - } - ], - "enums": [], - "misc": [ - { - "parentPluginId": "dashboard", - "id": "def-common.DashboardDoc700To720", - "type": "Type", - "tags": [], - "label": "DashboardDoc700To720", - "description": [], - "signature": [ - "Doc" - ], - "path": "src/plugins/dashboard/common/bwc/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-common.DashboardDoc730ToLatest", - "type": "Type", - "tags": [], - "label": "DashboardDoc730ToLatest", - "description": [], - "signature": [ - "Doc" - ], - "path": "src/plugins/dashboard/common/bwc/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-common.DashboardDocPre700", - "type": "Type", - "tags": [], - "label": "DashboardDocPre700", - "description": [], - "signature": [ - "DocPre700" - ], - "path": "src/plugins/dashboard/common/bwc/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-common.GridData", - "type": "Type", - "tags": [], - "label": "GridData", - "description": [], - "signature": [ - "{ w: number; h: number; x: number; y: number; i: string; }" - ], - "path": "src/plugins/dashboard/common/embeddable/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-common.RawSavedDashboardPanel730ToLatest", - "type": "Type", - "tags": [], - "label": "RawSavedDashboardPanel730ToLatest", - "description": [], - "signature": [ - "Pick<", - "RawSavedDashboardPanel640To720", - ", \"title\" | \"panelIndex\" | \"gridData\" | \"version\" | \"embeddableConfig\"> & { readonly type?: string | undefined; readonly name?: string | undefined; panelIndex: string; panelRefName?: string | undefined; }" - ], - "path": "src/plugins/dashboard/common/bwc/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-common.SavedDashboardPanel610", - "type": "Type", - "tags": [], - "label": "SavedDashboardPanel610", - "description": [], - "signature": [ - "Pick<", - "RawSavedDashboardPanel610", - ", \"columns\" | \"title\" | \"sort\" | \"panelIndex\" | \"gridData\" | \"version\"> & { readonly id: string; readonly type: string; }" - ], - "path": "src/plugins/dashboard/common/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-common.SavedDashboardPanel620", - "type": "Type", - "tags": [], - "label": "SavedDashboardPanel620", - "description": [], - "signature": [ - "Pick<", - "RawSavedDashboardPanel620", - ", \"columns\" | \"title\" | \"sort\" | \"panelIndex\" | \"gridData\" | \"version\" | \"embeddableConfig\"> & { readonly id: string; readonly type: string; }" - ], - "path": "src/plugins/dashboard/common/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-common.SavedDashboardPanel630", - "type": "Type", - "tags": [], - "label": "SavedDashboardPanel630", - "description": [], - "signature": [ - "Pick<", - "RawSavedDashboardPanel620", - ", \"columns\" | \"title\" | \"sort\" | \"panelIndex\" | \"gridData\" | \"version\" | \"embeddableConfig\"> & { readonly id: string; readonly type: string; }" - ], - "path": "src/plugins/dashboard/common/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-common.SavedDashboardPanel640To720", - "type": "Type", - "tags": [], - "label": "SavedDashboardPanel640To720", - "description": [], - "signature": [ - "Pick<", - "RawSavedDashboardPanel640To720", - ", \"title\" | \"panelIndex\" | \"gridData\" | \"version\" | \"embeddableConfig\"> & { readonly id: string; readonly type: string; }" - ], - "path": "src/plugins/dashboard/common/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-common.SavedDashboardPanel730ToLatest", - "type": "Type", - "tags": [], - "label": "SavedDashboardPanel730ToLatest", - "description": [], - "signature": [ - "Pick<", - { - "pluginId": "dashboard", - "scope": "common", - "docId": "kibDashboardPluginApi", - "section": "def-common.RawSavedDashboardPanel730ToLatest", - "text": "RawSavedDashboardPanel730ToLatest" - }, - ", \"type\" | \"title\" | \"panelIndex\" | \"gridData\" | \"version\" | \"embeddableConfig\" | \"panelRefName\"> & { readonly id?: string | undefined; readonly type: string; }" - ], - "path": "src/plugins/dashboard/common/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "dashboard", - "id": "def-common.SavedDashboardPanelTo60", - "type": "Type", - "tags": [], - "label": "SavedDashboardPanelTo60", - "description": [], - "signature": [ - "Pick<", - "RawSavedDashboardPanelTo60", - ", \"columns\" | \"title\" | \"sort\" | \"size_x\" | \"size_y\" | \"row\" | \"col\" | \"panelIndex\"> & { readonly id: string; readonly type: string; }" - ], - "path": "src/plugins/dashboard/common/types.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - } - ], "objects": [ { "parentPluginId": "dashboard", diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index 507845391c77..7a36e763bfea 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboard plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] --- import dashboardObj from './dashboard.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-prese | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 144 | 0 | 139 | 10 | +| 120 | 0 | 113 | 3 | ## Client @@ -37,9 +37,6 @@ Contact [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-prese ### Functions -### Classes - - ### Interfaces @@ -68,6 +65,3 @@ Contact [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-prese ### Interfaces -### Consts, variables and types - - diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index ef9680b1325d..06821933db67 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboardEnhanced plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.mdx b/api_docs/data.mdx index f849f08374de..e5c75be5fa09 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github description: API docs for the data plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] --- import dataObj from './data.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3211 | 33 | 2508 | 23 | +| 3213 | 33 | 2509 | 23 | ## Client diff --git a/api_docs/data_query.devdocs.json b/api_docs/data_query.devdocs.json index 2507050e905f..051270f93955 100644 --- a/api_docs/data_query.devdocs.json +++ b/api_docs/data_query.devdocs.json @@ -1855,14 +1855,6 @@ "plugin": "discover", "path": "src/plugins/discover/public/application/main/services/discover_state.ts" }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts" - }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts" - }, { "plugin": "maps", "path": "x-pack/plugins/maps/public/routes/map_page/url_state/global_sync.ts" @@ -3785,10 +3777,10 @@ }, { "parentPluginId": "data", - "id": "def-common.queryStateToExpressionAst", + "id": "def-common.textBasedQueryStateToAstWithValidation", "type": "Function", "tags": [], - "label": "queryStateToExpressionAst", + "label": "textBasedQueryStateToAstWithValidation", "description": [ "\nConverts QueryState to expression AST" ], @@ -3801,15 +3793,15 @@ "section": "def-common.ExpressionAstExpression", "text": "ExpressionAstExpression" }, - ">" + " | undefined>" ], - "path": "src/plugins/data/common/query/to_expression_ast.ts", + "path": "src/plugins/data/common/query/text_based_query_state_to_ast_with_validation.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "data", - "id": "def-common.queryStateToExpressionAst.$1", + "id": "def-common.textBasedQueryStateToAstWithValidation.$1", "type": "Object", "tags": [], "label": "{\n filters,\n query,\n inputQuery,\n time,\n dataViewsService,\n}", @@ -3817,7 +3809,49 @@ "signature": [ "Args" ], - "path": "src/plugins/data/common/query/to_expression_ast.ts", + "path": "src/plugins/data/common/query/text_based_query_state_to_ast_with_validation.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "data", + "id": "def-common.textBasedQueryStateToExpressionAst", + "type": "Function", + "tags": [], + "label": "textBasedQueryStateToExpressionAst", + "description": [ + "\nConverts QueryState to expression AST" + ], + "signature": [ + "({\n filters,\n query,\n inputQuery,\n time,\n timeFieldName,\n}: Args) => ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionAstExpression", + "text": "ExpressionAstExpression" + } + ], + "path": "src/plugins/data/common/query/text_based_query_state_to_ast.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "data", + "id": "def-common.textBasedQueryStateToExpressionAst.$1", + "type": "Object", + "tags": [], + "label": "{\n filters,\n query,\n inputQuery,\n time,\n timeFieldName,\n}", + "description": [], + "signature": [ + "Args" + ], + "path": "src/plugins/data/common/query/text_based_query_state_to_ast.ts", "deprecated": false, "trackAdoption": false, "isRequired": true diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index bb88b05d16a9..fdd7cb4298da 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github description: API docs for the data.query plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3211 | 33 | 2508 | 23 | +| 3213 | 33 | 2509 | 23 | ## Client diff --git a/api_docs/data_search.devdocs.json b/api_docs/data_search.devdocs.json index 415500b49904..3be213361786 100644 --- a/api_docs/data_search.devdocs.json +++ b/api_docs/data_search.devdocs.json @@ -2794,9 +2794,9 @@ "label": "options", "description": [], "signature": [ - "{ filter?: any; search?: string | undefined; aggs?: Record | undefined; fields?: string[] | undefined; searchAfter?: string[] | undefined; page?: number | undefined; perPage?: number | undefined; sortField?: string | undefined; sortOrder?: ", + "> | undefined; searchAfter?: string[] | undefined; page?: number | undefined; perPage?: number | undefined; sortField?: string | undefined; sortOrder?: ", "SortOrder", " | undefined; searchFields?: string[] | undefined; rootSearchFields?: string[] | undefined; hasReference?: ", "SavedObjectsFindOptionsReference", @@ -3475,13 +3475,7 @@ "label": "DataRequestHandlerContext", "description": [], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", " & { search: Promise<", { "pluginId": "data", diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 3cf065a95772..619cec4220ea 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github description: API docs for the data.search plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] --- import dataSearchObj from './data_search.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3211 | 33 | 2508 | 23 | +| 3213 | 33 | 2509 | 23 | ## Client diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index 6fca87db82f9..f0d1bf1f9d45 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewEditor plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] --- import dataViewEditorObj from './data_view_editor.devdocs.json'; diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index 9e4d3deac15c..9800b25ff8ba 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewFieldEditor plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] --- import dataViewFieldEditorObj from './data_view_field_editor.devdocs.json'; diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index cf43b6fa9dd9..d2f23607c286 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewManagement plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index a5ea60ca6db8..3c9d9dc38bae 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViews plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] --- import dataViewsObj from './data_views.devdocs.json'; diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index 797cd5596c31..86bbfd4e83cf 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github description: API docs for the dataVisualizer plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] --- import dataVisualizerObj from './data_visualizer.devdocs.json'; diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index ec45435a4e87..547c12c8570c 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -24,9 +24,9 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | alerting, discover, securitySolution | - | | | stackAlerts, alerting, securitySolution, inputControlVis | - | | | actions, alerting | - | -| | savedObjects, embeddable, fleet, visualizations, infra, canvas, graph, securitySolution, actions, alerting, enterpriseSearch, taskManager, dashboard, savedSearch, ml, @kbn/core-saved-objects-server-internal | - | -| | savedObjects, embeddable, fleet, visualizations, infra, canvas, graph, securitySolution, actions, alerting, enterpriseSearch, taskManager, dashboard, savedSearch, ml, @kbn/core-saved-objects-server-internal | - | -| | discover, dashboard, maps, monitoring | - | +| | savedObjects, embeddable, fleet, visualizations, dashboard, infra, canvas, graph, securitySolution, actions, alerting, enterpriseSearch, taskManager, savedSearch, ml, @kbn/core-saved-objects-server-internal | - | +| | savedObjects, embeddable, fleet, visualizations, dashboard, infra, canvas, graph, securitySolution, actions, alerting, enterpriseSearch, taskManager, savedSearch, ml, @kbn/core-saved-objects-server-internal | - | +| | discover, maps, monitoring | - | | | unifiedSearch, discover, maps, infra, graph, securitySolution, stackAlerts, inputControlVis, savedObjects | - | | | data, discover, embeddable | - | | | advancedSettings, discover | - | @@ -34,7 +34,6 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | securitySolution | - | | | encryptedSavedObjects, actions, data, cloud, ml, logstash, securitySolution | - | | | dashboard, dataVisualizer, stackAlerts, expressionPartitionVis | - | -| | dashboard | - | | | dataViews, maps | - | | | dataViews, maps | - | | | maps | - | @@ -43,6 +42,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | visTypeTimeseries, graph, dataViewManagement, dataViews | - | | | visTypeTimeseries, graph, dataViewManagement, dataViews | - | | | visTypeTimeseries, graph, dataViewManagement | - | +| | dashboard | - | | | observability, dataVisualizer, fleet, cloudSecurityPosture, discoverEnhanced, osquery, synthetics | - | | | dataViewManagement, dataViews | - | | | dataViews, dataViewManagement | - | @@ -75,10 +75,9 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | apm, security, securitySolution | 8.8.0 | | | visualizations, dashboard, lens, maps, ml, securitySolution, security, @kbn/core-application-browser-internal, @kbn/core-application-browser-mocks | 8.8.0 | | | securitySolution, @kbn/core-application-browser-internal | 8.8.0 | -| | savedObjectsTaggingOss, dashboard | 8.8.0 | -| | dashboard | 8.8.0 | | | maps, dashboard, @kbn/core-saved-objects-migration-server-internal | 8.8.0 | | | monitoring, kibanaUsageCollection, @kbn/core-apps-browser-internal, @kbn/core-metrics-server-internal, @kbn/core-status-server-internal, @kbn/core-usage-data-server-internal | 8.8.0 | +| | savedObjectsTaggingOss, dashboard | 8.8.0 | | | security, fleet | 8.8.0 | | | security, fleet | 8.8.0 | | | security, fleet | 8.8.0 | @@ -131,6 +130,7 @@ Safe to remove. | | expressions | | | expressions | | | kibanaReact | +| | savedObjects | | | savedObjects | | | licensing | | | licensing | diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 430ba5481352..3943c847158f 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -211,16 +211,14 @@ so TS and code-reference navigation might not highlight them. | | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [sync_dashboard_filter_state.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts#:~:text=syncQueryStateWithUrl), [sync_dashboard_filter_state.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts#:~:text=syncQueryStateWithUrl) | - | | | [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/data/types.ts#:~:text=fieldFormats), [data_service.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/data/data_service.ts#:~:text=fieldFormats) | - | | | [dashboard_viewport.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx#:~:text=ExitFullScreenButton), [dashboard_viewport.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx#:~:text=ExitFullScreenButton), [dashboard_viewport.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx#:~:text=ExitFullScreenButton) | - | | | [save_modal.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/top_nav/save_modal.tsx#:~:text=SavedObjectSaveModal), [save_modal.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/top_nav/save_modal.tsx#:~:text=SavedObjectSaveModal) | 8.8.0 | -| | [saved_object_loader.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/saved_object_loader.ts#:~:text=SavedObject), [saved_object_loader.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/saved_object_loader.ts#:~:text=SavedObject), [saved_object_loader.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/saved_object_loader.ts#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObject), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObject), [dashboard_tagging.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/lib/dashboard_tagging.ts#:~:text=SavedObject), [dashboard_tagging.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/lib/dashboard_tagging.ts#:~:text=SavedObject) | 8.8.0 | -| | [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObjectClass) | 8.8.0 | +| | [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject) | 8.8.0 | | | [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/types.ts#:~:text=onAppLeave), [plugin.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/plugin.tsx#:~:text=onAppLeave) | 8.8.0 | -| | [dashboard_migrations.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts#:~:text=SavedObjectAttributes), [dashboard_migrations.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry_collection_task.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry_collection_task.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry.ts#:~:text=SavedObjectAttributes), [find_by_value_embeddables.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts#:~:text=SavedObjectAttributes), [find_by_value_embeddables.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes)+ 8 more | - | -| | [dashboard_migrations.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts#:~:text=SavedObjectAttributes), [dashboard_migrations.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry_collection_task.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry_collection_task.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry.ts#:~:text=SavedObjectAttributes), [find_by_value_embeddables.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts#:~:text=SavedObjectAttributes), [find_by_value_embeddables.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes), [saved_dashboard_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/common/saved_dashboard_references.ts#:~:text=SavedObjectAttributes)+ 8 more | - | -| | [migrations_730.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations_730.ts#:~:text=warning), [migrations_730.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations_730.ts#:~:text=warning) | 8.8.0 | +| | [load_dashboard_state_from_saved_object.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object.ts#:~:text=SavedObjectAttributes), [load_dashboard_state_from_saved_object.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object.ts#:~:text=SavedObjectAttributes), [migrate_extract_panel_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations/migrate_extract_panel_references.ts#:~:text=SavedObjectAttributes), [migrate_extract_panel_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations/migrate_extract_panel_references.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry_collection_task.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry_collection_task.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry_collection_task.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry.ts#:~:text=SavedObjectAttributes), [find_by_value_embeddables.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts#:~:text=SavedObjectAttributes)+ 11 more | - | +| | [load_dashboard_state_from_saved_object.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object.ts#:~:text=SavedObjectAttributes), [load_dashboard_state_from_saved_object.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object.ts#:~:text=SavedObjectAttributes), [migrate_extract_panel_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations/migrate_extract_panel_references.ts#:~:text=SavedObjectAttributes), [migrate_extract_panel_references.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations/migrate_extract_panel_references.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry_collection_task.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry_collection_task.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry_collection_task.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry.ts#:~:text=SavedObjectAttributes), [dashboard_telemetry.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/dashboard_telemetry.ts#:~:text=SavedObjectAttributes), [find_by_value_embeddables.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts#:~:text=SavedObjectAttributes)+ 11 more | - | +| | [migrations_730.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.ts#:~:text=warning), [migrations_730.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.ts#:~:text=warning) | 8.8.0 | @@ -525,7 +523,7 @@ so TS and code-reference navigation might not highlight them. | | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [pack_queries_status_table.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx#:~:text=indexPatternId), [pack_queries_status_table.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/osquery/public/live_queries/form/pack_queries_status_table.tsx#:~:text=indexPatternId), [use_discover_link.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/osquery/public/common/hooks/use_discover_link.tsx#:~:text=indexPatternId) | - | +| | [pack_queries_status_table.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx#:~:text=indexPatternId), [view_results_in_discover.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/osquery/public/discover/view_results_in_discover.tsx#:~:text=indexPatternId), [use_discover_link.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/osquery/public/common/hooks/use_discover_link.tsx#:~:text=indexPatternId) | - | | | [empty_state.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/osquery/public/components/empty_state.tsx#:~:text=KibanaPageTemplate), [empty_state.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/osquery/public/components/empty_state.tsx#:~:text=KibanaPageTemplate) | - | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index 30143657a7b1..da0e278bffa6 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team description: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -89,10 +89,9 @@ so TS and code-reference navigation might not highlight them. | | Plugin | Deprecated API | Reference location(s) | Remove By | | --------|-------|-----------|-----------| | dashboard | | [save_modal.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/top_nav/save_modal.tsx#:~:text=SavedObjectSaveModal), [save_modal.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/top_nav/save_modal.tsx#:~:text=SavedObjectSaveModal), [saved_object_save_modal_dashboard.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx#:~:text=SavedObjectSaveModal), [saved_object_save_modal_dashboard.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx#:~:text=SavedObjectSaveModal) | 8.8.0 | -| dashboard | | [saved_object_loader.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/saved_object_loader.ts#:~:text=SavedObject), [saved_object_loader.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/saved_object_loader.ts#:~:text=SavedObject), [saved_object_loader.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/services/saved_object_loader.ts#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObject), [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObject), [dashboard_tagging.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/lib/dashboard_tagging.ts#:~:text=SavedObject), [dashboard_tagging.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/lib/dashboard_tagging.ts#:~:text=SavedObject) | 8.8.0 | -| dashboard | | [saved_dashboard.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts#:~:text=SavedObjectClass) | 8.8.0 | +| dashboard | | [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject), [clone_panel_action.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#:~:text=SavedObject) | 8.8.0 | | dashboard | | [types.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/types.ts#:~:text=onAppLeave), [plugin.tsx](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/public/plugin.tsx#:~:text=onAppLeave) | 8.8.0 | -| dashboard | | [migrations_730.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations_730.ts#:~:text=warning), [migrations_730.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations_730.ts#:~:text=warning) | 8.8.0 | +| dashboard | | [migrations_730.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.ts#:~:text=warning), [migrations_730.ts](https://github.com/elastic/kibana/tree/main/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.ts#:~:text=warning) | 8.8.0 | diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 95bc923cc9da..b724ab15e695 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github description: API docs for the devTools plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] --- import devToolsObj from './dev_tools.devdocs.json'; diff --git a/api_docs/discover.devdocs.json b/api_docs/discover.devdocs.json index 695319775e98..8c4a6308dd17 100644 --- a/api_docs/discover.devdocs.json +++ b/api_docs/discover.devdocs.json @@ -346,7 +346,7 @@ }, { "plugin": "osquery", - "path": "x-pack/plugins/osquery/public/live_queries/form/pack_queries_status_table.tsx" + "path": "x-pack/plugins/osquery/public/discover/view_results_in_discover.tsx" }, { "plugin": "osquery", @@ -1101,7 +1101,7 @@ "label": "sharingSavedObjectProps", "description": [], "signature": [ - "{ outcome?: \"exactMatch\" | \"aliasMatch\" | \"conflict\" | undefined; aliasTargetId?: string | undefined; aliasPurpose?: \"savedObjectConversion\" | \"savedObjectImport\" | undefined; errorJSON?: string | undefined; } | undefined" + "{ outcome?: \"conflict\" | \"exactMatch\" | \"aliasMatch\" | undefined; aliasTargetId?: string | undefined; aliasPurpose?: \"savedObjectConversion\" | \"savedObjectImport\" | undefined; errorJSON?: string | undefined; } | undefined" ], "path": "src/plugins/saved_search/public/services/saved_searches/types.ts", "deprecated": false, diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index 8f51330d8bc6..39b2814d4136 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github description: API docs for the discover plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] --- import discoverObj from './discover.devdocs.json'; diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index 7263ed936315..e9974fbb2bf4 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the discoverEnhanced plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index f5d8586e5194..8bbe0a063796 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddable plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] --- import embeddableObj from './embeddable.devdocs.json'; diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index 326d612509b6..c53b6a4aba7e 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddableEnhanced plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] --- import embeddableEnhancedObj from './embeddable_enhanced.devdocs.json'; diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index e5a8bb47a052..6cc990a42eba 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the encryptedSavedObjects plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] --- import encryptedSavedObjectsObj from './encrypted_saved_objects.devdocs.json'; diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index 23ae523bade6..8f80e15d3886 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the enterpriseSearch plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] --- import enterpriseSearchObj from './enterprise_search.devdocs.json'; diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index d5982b5a2a86..6a4ca6fcb773 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github description: API docs for the esUiShared plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] --- import esUiSharedObj from './es_ui_shared.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-ma | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 114 | 3 | 110 | 3 | +| 114 | 3 | 110 | 5 | ## Client diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index 705fa73b04c1..159a2f7167b0 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotation plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] --- import eventAnnotationObj from './event_annotation.devdocs.json'; diff --git a/api_docs/event_log.devdocs.json b/api_docs/event_log.devdocs.json index f821ced3a76e..8361059c9107 100644 --- a/api_docs/event_log.devdocs.json +++ b/api_docs/event_log.devdocs.json @@ -1312,7 +1312,7 @@ "label": "data", "description": [], "signature": [ - "(Readonly<{ error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; outcome?: string | undefined; created?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; code?: string | undefined; url?: string | undefined; action?: string | undefined; kind?: string | undefined; hash?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}> | undefined)[]" + "(Readonly<{ error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; created?: string | undefined; outcome?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; code?: string | undefined; url?: string | undefined; action?: string | undefined; kind?: string | undefined; hash?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}> | undefined)[]" ], "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", "deprecated": false, @@ -1332,7 +1332,7 @@ "label": "IEvent", "description": [], "signature": [ - "DeepPartial | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; outcome?: string | undefined; created?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; code?: string | undefined; url?: string | undefined; action?: string | undefined; kind?: string | undefined; hash?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}>>> | undefined" + "DeepPartial | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; created?: string | undefined; outcome?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; code?: string | undefined; url?: string | undefined; action?: string | undefined; kind?: string | undefined; hash?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}>>> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, @@ -1347,7 +1347,7 @@ "label": "IValidatedEvent", "description": [], "signature": [ - "Readonly<{ error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; outcome?: string | undefined; created?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; code?: string | undefined; url?: string | undefined; action?: string | undefined; kind?: string | undefined; hash?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}> | undefined" + "Readonly<{ error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; tags?: string[] | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; message?: string | undefined; kibana?: Readonly<{ alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; } & {}> | undefined; version?: string | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; rule?: Readonly<{ name?: string | undefined; description?: string | undefined; category?: string | undefined; id?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; event?: Readonly<{ start?: string | undefined; category?: string[] | undefined; type?: string[] | undefined; id?: string | undefined; created?: string | undefined; outcome?: string | undefined; end?: string | undefined; original?: string | undefined; duration?: string | number | undefined; code?: string | undefined; url?: string | undefined; action?: string | undefined; kind?: string | undefined; hash?: string | undefined; severity?: string | number | undefined; dataset?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; } & {}> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index 53b40226f0d7..b9be569d3a3e 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github description: API docs for the eventLog plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index 51a1fd22bc03..6d9a8d244589 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionError plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] --- import expressionErrorObj from './expression_error.devdocs.json'; diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index c23d6a727bb3..b38b786ca55b 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionGauge plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] --- import expressionGaugeObj from './expression_gauge.devdocs.json'; diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index 1196f0794502..9d856b882a31 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionHeatmap plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] --- import expressionHeatmapObj from './expression_heatmap.devdocs.json'; diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 046b3c31f107..84225e80ad00 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionImage plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] --- import expressionImageObj from './expression_image.devdocs.json'; diff --git a/api_docs/expression_legacy_metric_vis.mdx b/api_docs/expression_legacy_metric_vis.mdx index 9a3e6bd0dc82..e6902356f091 100644 --- a/api_docs/expression_legacy_metric_vis.mdx +++ b/api_docs/expression_legacy_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionLegacyMetricVis title: "expressionLegacyMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionLegacyMetricVis plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionLegacyMetricVis'] --- import expressionLegacyMetricVisObj from './expression_legacy_metric_vis.devdocs.json'; diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index 76fd8a4068f1..60a4d843aad3 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetric plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] --- import expressionMetricObj from './expression_metric.devdocs.json'; diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index 337717de0a65..85c6421d292d 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetricVis plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] --- import expressionMetricVisObj from './expression_metric_vis.devdocs.json'; diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index 32c3971bd090..4d4a23fe3a6f 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionPartitionVis plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] --- import expressionPartitionVisObj from './expression_partition_vis.devdocs.json'; diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index ca6a5be21ec3..ff4af965c963 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRepeatImage plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] --- import expressionRepeatImageObj from './expression_repeat_image.devdocs.json'; diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index 90854571dc71..2d3ede34cf14 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRevealImage plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] --- import expressionRevealImageObj from './expression_reveal_image.devdocs.json'; diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index 898224f9aba1..114bb0846272 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionShape plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] --- import expressionShapeObj from './expression_shape.devdocs.json'; diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index f6686201cb13..6a87768ff01e 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionTagcloud plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] --- import expressionTagcloudObj from './expression_tagcloud.devdocs.json'; diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index c090283f6974..bf0c8f9c49ae 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionXY plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; diff --git a/api_docs/expressions.devdocs.json b/api_docs/expressions.devdocs.json index cf0d00e1dd47..bae5e63702f4 100644 --- a/api_docs/expressions.devdocs.json +++ b/api_docs/expressions.devdocs.json @@ -11507,7 +11507,7 @@ "\nThis type represents the `type` of any `DatatableColumn` in a `Datatable`.\nits duplicated from KBN_FIELD_TYPES" ], "signature": [ - "\"string\" | \"number\" | \"boolean\" | \"object\" | \"date\" | \"null\" | \"ip\" | \"nested\" | \"conflict\" | \"_source\" | \"attachment\" | \"geo_point\" | \"geo_shape\" | \"murmur3\" | \"unknown\" | \"histogram\"" + "\"string\" | \"number\" | \"boolean\" | \"object\" | \"date\" | \"null\" | \"ip\" | \"nested\" | \"_source\" | \"attachment\" | \"geo_point\" | \"geo_shape\" | \"murmur3\" | \"unknown\" | \"conflict\" | \"histogram\"" ], "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", "deprecated": false, @@ -21037,7 +21037,7 @@ "\nThis type represents the `type` of any `DatatableColumn` in a `Datatable`.\nits duplicated from KBN_FIELD_TYPES" ], "signature": [ - "\"string\" | \"number\" | \"boolean\" | \"object\" | \"date\" | \"null\" | \"ip\" | \"nested\" | \"conflict\" | \"_source\" | \"attachment\" | \"geo_point\" | \"geo_shape\" | \"murmur3\" | \"unknown\" | \"histogram\"" + "\"string\" | \"number\" | \"boolean\" | \"object\" | \"date\" | \"null\" | \"ip\" | \"nested\" | \"_source\" | \"attachment\" | \"geo_point\" | \"geo_shape\" | \"murmur3\" | \"unknown\" | \"conflict\" | \"histogram\"" ], "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", "deprecated": false, @@ -29210,7 +29210,7 @@ "label": "type", "description": [], "signature": [ - "\"string\" | \"number\" | \"boolean\" | \"object\" | \"date\" | \"null\" | \"ip\" | \"nested\" | \"conflict\" | \"_source\" | \"attachment\" | \"geo_point\" | \"geo_shape\" | \"murmur3\" | \"unknown\" | \"histogram\"" + "\"string\" | \"number\" | \"boolean\" | \"object\" | \"date\" | \"null\" | \"ip\" | \"nested\" | \"_source\" | \"attachment\" | \"geo_point\" | \"geo_shape\" | \"murmur3\" | \"unknown\" | \"conflict\" | \"histogram\"" ], "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", "deprecated": false, @@ -35226,7 +35226,7 @@ "\nThis type represents the `type` of any `DatatableColumn` in a `Datatable`.\nits duplicated from KBN_FIELD_TYPES" ], "signature": [ - "\"string\" | \"number\" | \"boolean\" | \"object\" | \"date\" | \"null\" | \"ip\" | \"nested\" | \"conflict\" | \"_source\" | \"attachment\" | \"geo_point\" | \"geo_shape\" | \"murmur3\" | \"unknown\" | \"histogram\"" + "\"string\" | \"number\" | \"boolean\" | \"object\" | \"date\" | \"null\" | \"ip\" | \"nested\" | \"_source\" | \"attachment\" | \"geo_point\" | \"geo_shape\" | \"murmur3\" | \"unknown\" | \"conflict\" | \"histogram\"" ], "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", "deprecated": false, diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index cc0dfccc5915..cb5617b237f7 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github description: API docs for the expressions plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] --- import expressionsObj from './expressions.devdocs.json'; diff --git a/api_docs/features.mdx b/api_docs/features.mdx index 116d58a6218b..1c1d57aa91eb 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github description: API docs for the features plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] --- import featuresObj from './features.devdocs.json'; diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index dbb19f977cc7..dc89d286ddfe 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github description: API docs for the fieldFormats plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] --- import fieldFormatsObj from './field_formats.devdocs.json'; diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index 31c1b78e9438..f29f32174d12 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github description: API docs for the fileUpload plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] --- import fileUploadObj from './file_upload.devdocs.json'; diff --git a/api_docs/files.devdocs.json b/api_docs/files.devdocs.json index 358859310c79..7c94fcdac902 100644 --- a/api_docs/files.devdocs.json +++ b/api_docs/files.devdocs.json @@ -438,10 +438,10 @@ "tags": [], "label": "getDownloadHref", "description": [ - "\nGet a string for downloading a file that can be passed to a button element's\nhref for download." + "\nGet a string for downloading a file that can be passed to a button element's\nhref for download.\n" ], "signature": [ - "(file: ", + "(args: Pick<", { "pluginId": "files", "scope": "common", @@ -449,7 +449,7 @@ "section": "def-common.FileJSON", "text": "FileJSON" }, - ") => string" + ", \"id\" | \"fileKind\">) => string" ], "path": "x-pack/plugins/files/public/types.ts", "deprecated": false, @@ -460,9 +460,12 @@ "id": "def-public.FilesClient.getDownloadHref.$1", "type": "Object", "tags": [], - "label": "file", - "description": [], + "label": "args", + "description": [ + "- get download URL args" + ], "signature": [ + "Pick<", { "pluginId": "files", "scope": "common", @@ -470,7 +473,7 @@ "section": "def-common.FileJSON", "text": "FileJSON" }, - "" + ", \"id\" | \"fileKind\">" ], "path": "x-pack/plugins/files/public/types.ts", "deprecated": false, @@ -1129,7 +1132,7 @@ "section": "def-common.FileJSON", "text": "FileJSON" }, - "; }>; upload: (arg: Omit & Readonly<{ selfDestructOnAbort?: boolean | undefined; } & {}> & { body: unknown; kind: string; abortSignal?: AbortSignal | undefined; contentType?: string | undefined; }, \"kind\">) => Promise<{ ok: true; size: number; }>; download: (arg: Omit & { kind: string; }, \"kind\">) => Promise; getDownloadHref: (arg: Omit<", + "; }>; upload: (arg: Omit & Readonly<{ selfDestructOnAbort?: boolean | undefined; } & {}> & { body: unknown; kind: string; abortSignal?: AbortSignal | undefined; contentType?: string | undefined; }, \"kind\">) => Promise<{ ok: true; size: number; }>; download: (arg: Omit & { kind: string; }, \"kind\">) => Promise; getDownloadHref: (arg: Omit, \"kind\">) => string; share: (arg: Omit & Readonly<{} & { fileId: string; }> & { kind: string; }, \"kind\">) => Promise<", + ", \"id\" | \"fileKind\">, \"kind\">) => string; share: (arg: Omit & Readonly<{} & { fileId: string; }> & { kind: string; }, \"kind\">) => Promise<", { "pluginId": "files", "scope": "common", diff --git a/api_docs/files.mdx b/api_docs/files.mdx index 149bec1a8acb..3cc10c4a1612 100644 --- a/api_docs/files.mdx +++ b/api_docs/files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/files title: "files" image: https://source.unsplash.com/400x175/?github description: API docs for the files plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-app-services](https://github.com/orgs/elastic/teams/tea | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 263 | 0 | 15 | 2 | +| 263 | 0 | 14 | 2 | ## Client diff --git a/api_docs/fleet.devdocs.json b/api_docs/fleet.devdocs.json index a35508ec3930..3059a8f9c9f8 100644 --- a/api_docs/fleet.devdocs.json +++ b/api_docs/fleet.devdocs.json @@ -1225,6 +1225,146 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "fleet", + "id": "def-public.PackagePolicyCreateMultiStepExtension", + "type": "Interface", + "tags": [], + "label": "PackagePolicyCreateMultiStepExtension", + "description": [ + "Extension point registration contract for Integration Policy Create views in multi-step onboarding" + ], + "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "fleet", + "id": "def-public.PackagePolicyCreateMultiStepExtension.package", + "type": "string", + "tags": [], + "label": "package", + "description": [], + "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "fleet", + "id": "def-public.PackagePolicyCreateMultiStepExtension.view", + "type": "string", + "tags": [], + "label": "view", + "description": [], + "signature": [ + "\"package-policy-create-multi-step\"" + ], + "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "fleet", + "id": "def-public.PackagePolicyCreateMultiStepExtension.Component", + "type": "Function", + "tags": [], + "label": "Component", + "description": [], + "signature": [ + "React.ExoticComponent<(", + { + "pluginId": "fleet", + "scope": "public", + "docId": "kibFleetPluginApi", + "section": "def-public.PackagePolicyCreateMultiStepExtensionComponentProps", + "text": "PackagePolicyCreateMultiStepExtensionComponentProps" + }, + " & React.RefAttributes>) | (", + { + "pluginId": "fleet", + "scope": "public", + "docId": "kibFleetPluginApi", + "section": "def-public.PackagePolicyCreateMultiStepExtensionComponentProps", + "text": "PackagePolicyCreateMultiStepExtensionComponentProps" + }, + " & { children?: React.ReactNode; })> & { readonly _result: ", + { + "pluginId": "fleet", + "scope": "public", + "docId": "kibFleetPluginApi", + "section": "def-public.PackagePolicyCreateMultiStepExtensionComponent", + "text": "PackagePolicyCreateMultiStepExtensionComponent" + }, + "; }" + ], + "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "fleet", + "id": "def-public.PackagePolicyCreateMultiStepExtension.Component.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "fleet", + "id": "def-public.PackagePolicyCreateMultiStepExtensionComponentProps", + "type": "Interface", + "tags": [], + "label": "PackagePolicyCreateMultiStepExtensionComponentProps", + "description": [], + "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "fleet", + "id": "def-public.PackagePolicyCreateMultiStepExtensionComponentProps.newPolicy", + "type": "Object", + "tags": [], + "label": "newPolicy", + "description": [ + "The integration policy being created" + ], + "signature": [ + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.NewPackagePolicy", + "text": "NewPackagePolicy" + } + ], + "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "fleet", "id": "def-public.PackagePolicyEditExtension", @@ -1925,10 +2065,10 @@ "id": "def-public.UIExtensionsStorage.Unnamed", "type": "IndexSignature", "tags": [], - "label": "[key: string]: Partial>", + "label": "[key: string]: Partial>", "description": [], "signature": [ - "[key: string]: Partial | React.FunctionComponent<", + { + "pluginId": "fleet", + "scope": "public", + "docId": "kibFleetPluginApi", + "section": "def-public.PackagePolicyCreateMultiStepExtensionComponentProps", + "text": "PackagePolicyCreateMultiStepExtensionComponentProps" + }, + ">" + ], + "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "fleet", "id": "def-public.PackagePolicyEditExtensionComponent", @@ -2269,6 +2442,14 @@ "docId": "kibFleetPluginApi", "section": "def-public.AgentEnrollmentFlyoutFinalStepExtension", "text": "AgentEnrollmentFlyoutFinalStepExtension" + }, + " | ", + { + "pluginId": "fleet", + "scope": "public", + "docId": "kibFleetPluginApi", + "section": "def-public.PackagePolicyCreateMultiStepExtension", + "text": "PackagePolicyCreateMultiStepExtension" } ], "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", @@ -2365,6 +2546,14 @@ "docId": "kibFleetPluginApi", "section": "def-public.AgentEnrollmentFlyoutFinalStepExtension", "text": "AgentEnrollmentFlyoutFinalStepExtension" + }, + " | ", + { + "pluginId": "fleet", + "scope": "public", + "docId": "kibFleetPluginApi", + "section": "def-public.PackagePolicyCreateMultiStepExtension", + "text": "PackagePolicyCreateMultiStepExtension" } ], "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", @@ -3725,6 +3914,14 @@ "docId": "kibFleetPluginApi", "section": "def-public.AgentEnrollmentFlyoutFinalStepExtension", "text": "AgentEnrollmentFlyoutFinalStepExtension" + }, + " | ", + { + "pluginId": "fleet", + "scope": "public", + "docId": "kibFleetPluginApi", + "section": "def-public.PackagePolicyCreateMultiStepExtension", + "text": "PackagePolicyCreateMultiStepExtension" } ], "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", @@ -6227,7 +6424,7 @@ "section": "def-common.AuthenticatedUser", "text": "AuthenticatedUser" }, - " | undefined; force?: boolean | undefined; } | undefined, currentVersion?: string | undefined) => Promise<", + " | undefined; force?: boolean | undefined; skipUniqueNameVerification?: boolean | undefined; } | undefined, currentVersion?: string | undefined) => Promise<", { "pluginId": "fleet", "scope": "common", @@ -6352,6 +6549,20 @@ "path": "x-pack/plugins/fleet/server/services/package_policy_service.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "fleet", + "id": "def-server.PackagePolicyClient.update.$5.skipUniqueNameVerification", + "type": "CompoundType", + "tags": [], + "label": "skipUniqueNameVerification", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "x-pack/plugins/fleet/server/services/package_policy_service.ts", + "deprecated": false, + "trackAdoption": false } ] }, @@ -6959,13 +7170,7 @@ "text": "NewPackagePolicy" }, ", context: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", ", request: ", "KibanaRequest", ") => Promise) => Promise<", @@ -7400,13 +7593,7 @@ "label": "context", "description": [], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - } + "RequestHandlerContext" ], "path": "x-pack/plugins/fleet/server/types/extensions.ts", "deprecated": false, @@ -7498,13 +7685,7 @@ "text": "PackagePolicy" }, ", context: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", ", request: ", "KibanaRequest", ") => Promise<", @@ -7550,13 +7731,7 @@ "label": "context", "description": [], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - } + "RequestHandlerContext" ], "path": "x-pack/plugins/fleet/server/types/extensions.ts", "deprecated": false, @@ -7597,13 +7772,7 @@ "text": "UpdatePackagePolicy" }, ", context: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", ", request: ", "KibanaRequest", ") => Promise<", @@ -7649,13 +7818,7 @@ "label": "context", "description": [], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - } + "RequestHandlerContext" ], "path": "x-pack/plugins/fleet/server/types/extensions.ts", "deprecated": false, @@ -9276,14 +9439,14 @@ { "parentPluginId": "fleet", "id": "def-common.FleetServerAgent.upgraded_at", - "type": "string", + "type": "CompoundType", "tags": [], "label": "upgraded_at", "description": [ "\nDate/time the Elastic Agent was last upgraded" ], "signature": [ - "string | undefined" + "string | null | undefined" ], "path": "x-pack/plugins/fleet/common/types/models/agent.ts", "deprecated": false, @@ -9305,22 +9468,6 @@ "deprecated": false, "trackAdoption": false }, - { - "parentPluginId": "fleet", - "id": "def-common.FleetServerAgent.upgrade_status", - "type": "CompoundType", - "tags": [], - "label": "upgrade_status", - "description": [ - "\nUpgrade status" - ], - "signature": [ - "\"completed\" | \"started\" | undefined" - ], - "path": "x-pack/plugins/fleet/common/types/models/agent.ts", - "deprecated": false, - "trackAdoption": false - }, { "parentPluginId": "fleet", "id": "def-common.FleetServerAgent.access_api_key_id", @@ -14202,21 +14349,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "fleet", - "id": "def-common.ENDPOINT_PRIVILEGES", - "type": "Array", - "tags": [], - "label": "ENDPOINT_PRIVILEGES", - "description": [], - "signature": [ - "string[]" - ], - "path": "x-pack/plugins/fleet/common/constants/authz.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "fleet", "id": "def-common.EsAssetReference", @@ -15112,7 +15244,7 @@ "label": "PackageSpecCategory", "description": [], "signature": [ - "\"custom\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\"" + "\"custom\" | \"aws\" | \"azure\" | \"cloud\" | \"config_management\" | \"containers\" | \"crm\" | \"datastore\" | \"elastic_stack\" | \"google_cloud\" | \"infrastructure\" | \"kubernetes\" | \"languages\" | \"message_queue\" | \"monitoring\" | \"network\" | \"notification\" | \"os_system\" | \"productivity\" | \"security\" | \"support\" | \"threat_intel\" | \"ticketing\" | \"version_control\" | \"web\"" ], "path": "x-pack/plugins/fleet/common/types/models/package_spec.ts", "deprecated": false, @@ -16546,6 +16678,21 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "fleet", + "id": "def-common.ENDPOINT_PRIVILEGES", + "type": "Object", + "tags": [], + "label": "ENDPOINT_PRIVILEGES", + "description": [], + "signature": [ + "readonly [\"writeEndpointList\", \"readEndpointList\", \"writeTrustedApplications\", \"readTrustedApplications\", \"writeHostIsolationExceptions\", \"readHostIsolationExceptions\", \"writeBlocklist\", \"readBlocklist\", \"writeEventFilters\", \"readEventFilters\", \"writePolicyManagement\", \"readPolicyManagement\", \"writeActionsLogManagement\", \"readActionsLogManagement\", \"writeHostIsolation\", \"writeProcessOperations\", \"writeFileOperations\"]" + ], + "path": "x-pack/plugins/fleet/common/constants/authz.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "fleet", "id": "def-common.EPM_API_ROUTES", diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index 173f9f76092f..7c5beab0d5c9 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github description: API docs for the fleet plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Fleet](https://github.com/orgs/elastic/teams/fleet) for questions regar | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 988 | 3 | 888 | 17 | +| 996 | 3 | 893 | 17 | ## Client diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index c689891e7444..6803fa9d095e 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the globalSearch plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] --- import globalSearchObj from './global_search.devdocs.json'; diff --git a/api_docs/guided_onboarding.mdx b/api_docs/guided_onboarding.mdx index 5dfb91864c06..062ffb1effb3 100644 --- a/api_docs/guided_onboarding.mdx +++ b/api_docs/guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/guidedOnboarding title: "guidedOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the guidedOnboarding plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'guidedOnboarding'] --- import guidedOnboardingObj from './guided_onboarding.devdocs.json'; diff --git a/api_docs/home.mdx b/api_docs/home.mdx index 62db97259ee5..8f772016026c 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github description: API docs for the home plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index 87bd3e83a63b..29de31f6cf68 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexLifecycleManagement plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] --- import indexLifecycleManagementObj from './index_lifecycle_management.devdocs.json'; diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index eee8cda64e24..3dac89fa09d7 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexManagement plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] --- import indexManagementObj from './index_management.devdocs.json'; diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index fd32cbab9919..315978011a0b 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github description: API docs for the infra plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] --- import infraObj from './infra.devdocs.json'; diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index 00175e67b30b..1eec7a67db10 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github description: API docs for the inspector plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] --- import inspectorObj from './inspector.devdocs.json'; diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index 33ee5123bb5c..ba3797cab67d 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github description: API docs for the interactiveSetup plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] --- import interactiveSetupObj from './interactive_setup.devdocs.json'; diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx index bf08287ddc42..c6dcd6bde783 100644 --- a/api_docs/kbn_ace.mdx +++ b/api_docs/kbn_ace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ace title: "@kbn/ace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ace plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] --- import kbnAceObj from './kbn_ace.devdocs.json'; diff --git a/api_docs/kbn_aiops_components.mdx b/api_docs/kbn_aiops_components.mdx index d4cb51aa5139..78c88adbe04a 100644 --- a/api_docs/kbn_aiops_components.mdx +++ b/api_docs/kbn_aiops_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-components title: "@kbn/aiops-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-components plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-components'] --- import kbnAiopsComponentsObj from './kbn_aiops_components.devdocs.json'; diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index 389117150bd5..8e0003a6394a 100644 --- a/api_docs/kbn_aiops_utils.mdx +++ b/api_docs/kbn_aiops_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-utils title: "@kbn/aiops-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] --- import kbnAiopsUtilsObj from './kbn_aiops_utils.devdocs.json'; diff --git a/api_docs/kbn_alerts.mdx b/api_docs/kbn_alerts.mdx index 749a781cf0e4..b936fa218da1 100644 --- a/api_docs/kbn_alerts.mdx +++ b/api_docs/kbn_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts title: "@kbn/alerts" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts'] --- import kbnAlertsObj from './kbn_alerts.devdocs.json'; diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index c0f7031e47a7..480d1389d1d5 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] --- import kbnAnalyticsObj from './kbn_analytics.devdocs.json'; diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index 1595bab913e8..67d5f65e938a 100644 --- a/api_docs/kbn_analytics_client.mdx +++ b/api_docs/kbn_analytics_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-client title: "@kbn/analytics-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-client plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] --- import kbnAnalyticsClientObj from './kbn_analytics_client.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx index ba7823ee64ac..2c75bd5664b6 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-browser title: "@kbn/analytics-shippers-elastic-v3-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-browser'] --- import kbnAnalyticsShippersElasticV3BrowserObj from './kbn_analytics_shippers_elastic_v3_browser.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx index 105ff1b565b7..af39931531e6 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-common title: "@kbn/analytics-shippers-elastic-v3-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-common plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-common'] --- import kbnAnalyticsShippersElasticV3CommonObj from './kbn_analytics_shippers_elastic_v3_common.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx index ef9a534fa347..5515fe767880 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-server title: "@kbn/analytics-shippers-elastic-v3-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-server'] --- import kbnAnalyticsShippersElasticV3ServerObj from './kbn_analytics_shippers_elastic_v3_server.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_fullstory.mdx b/api_docs/kbn_analytics_shippers_fullstory.mdx index 6a263b0defbb..d85797746210 100644 --- a/api_docs/kbn_analytics_shippers_fullstory.mdx +++ b/api_docs/kbn_analytics_shippers_fullstory.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-fullstory title: "@kbn/analytics-shippers-fullstory" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-fullstory plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index defb49cbe69e..ec16bbb3798a 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-config-loader plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] --- import kbnApmConfigLoaderObj from './kbn_apm_config_loader.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace.mdx b/api_docs/kbn_apm_synthtrace.mdx index 0a80a8f9d7c6..cd86d11a33dc 100644 --- a/api_docs/kbn_apm_synthtrace.mdx +++ b/api_docs/kbn_apm_synthtrace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace title: "@kbn/apm-synthtrace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index f87f80a173be..24e76f781315 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] --- import kbnApmUtilsObj from './kbn_apm_utils.devdocs.json'; diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index 465473d3ef7c..a38a76d26da3 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/axe-config plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index 8bc8181b1c66..96f35a5ca85a 100644 --- a/api_docs/kbn_chart_icons.mdx +++ b/api_docs/kbn_chart_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-icons title: "@kbn/chart-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-icons plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-icons'] --- import kbnChartIconsObj from './kbn_chart_icons.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index 26ac1ad8da54..efc5f97cca9f 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-core plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] --- import kbnCiStatsCoreObj from './kbn_ci_stats_core.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_performance_metrics.mdx b/api_docs/kbn_ci_stats_performance_metrics.mdx index 79c65aec69db..7030b63abaa1 100644 --- a/api_docs/kbn_ci_stats_performance_metrics.mdx +++ b/api_docs/kbn_ci_stats_performance_metrics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-performance-metrics title: "@kbn/ci-stats-performance-metrics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-performance-metrics plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-performance-metrics'] --- import kbnCiStatsPerformanceMetricsObj from './kbn_ci_stats_performance_metrics.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_reporter.devdocs.json b/api_docs/kbn_ci_stats_reporter.devdocs.json index f831ac502a93..18389c93d07c 100644 --- a/api_docs/kbn_ci_stats_reporter.devdocs.json +++ b/api_docs/kbn_ci_stats_reporter.devdocs.json @@ -654,7 +654,7 @@ "\nOverall result of this test group" ], "signature": [ - "\"fail\" | \"pass\" | \"skip\"" + "\"skip\" | \"fail\" | \"pass\"" ], "path": "packages/kbn-ci-stats-reporter/src/ci_stats_test_group_types.ts", "deprecated": false, @@ -755,7 +755,7 @@ "\n\"fail\", \"pass\" or \"skip\", the result of the tests" ], "signature": [ - "\"fail\" | \"pass\" | \"skip\"" + "\"skip\" | \"fail\" | \"pass\"" ], "path": "packages/kbn-ci-stats-reporter/src/ci_stats_test_group_types.ts", "deprecated": false, @@ -1041,7 +1041,7 @@ "label": "CiStatsTestResult", "description": [], "signature": [ - "\"fail\" | \"pass\" | \"skip\"" + "\"skip\" | \"fail\" | \"pass\"" ], "path": "packages/kbn-ci-stats-reporter/src/ci_stats_test_group_types.ts", "deprecated": false, diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index 4b5645cc4b13..464ef1f62934 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-reporter plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] --- import kbnCiStatsReporterObj from './kbn_ci_stats_reporter.devdocs.json'; diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index fb74c109385e..7aac22dfeead 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cli-dev-mode plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index 6dfdbdc6a7e1..edd9cb125e75 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/coloring plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] --- import kbnColoringObj from './kbn_coloring.devdocs.json'; diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index e5787da85b66..b82dc689dba2 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] --- import kbnConfigObj from './kbn_config.devdocs.json'; diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index 9f30996b585d..cebca9163e79 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] --- import kbnConfigMocksObj from './kbn_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 97256b751610..bd788d6e1868 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-schema plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list.mdx b/api_docs/kbn_content_management_table_list.mdx index 12779749648d..ac56135aa292 100644 --- a/api_docs/kbn_content_management_table_list.mdx +++ b/api_docs/kbn_content_management_table_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list title: "@kbn/content-management-table-list" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list'] --- import kbnContentManagementTableListObj from './kbn_content_management_table_list.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index 89fc744b1c70..eefeafb88d5d 100644 --- a/api_docs/kbn_core_analytics_browser.mdx +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser title: "@kbn/core-analytics-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] --- import kbnCoreAnalyticsBrowserObj from './kbn_core_analytics_browser.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx index bad36ac54b21..36b99e421559 100644 --- a/api_docs/kbn_core_analytics_browser_internal.mdx +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal title: "@kbn/core-analytics-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] --- import kbnCoreAnalyticsBrowserInternalObj from './kbn_core_analytics_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx index 5db786b7754f..abf361ac547b 100644 --- a/api_docs/kbn_core_analytics_browser_mocks.mdx +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks title: "@kbn/core-analytics-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] --- import kbnCoreAnalyticsBrowserMocksObj from './kbn_core_analytics_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server.mdx b/api_docs/kbn_core_analytics_server.mdx index 0141718a2ccd..ec45043e4726 100644 --- a/api_docs/kbn_core_analytics_server.mdx +++ b/api_docs/kbn_core_analytics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server title: "@kbn/core-analytics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server'] --- import kbnCoreAnalyticsServerObj from './kbn_core_analytics_server.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_internal.mdx b/api_docs/kbn_core_analytics_server_internal.mdx index 2d21be701e13..04e49781988c 100644 --- a/api_docs/kbn_core_analytics_server_internal.mdx +++ b/api_docs/kbn_core_analytics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-internal title: "@kbn/core-analytics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-internal'] --- import kbnCoreAnalyticsServerInternalObj from './kbn_core_analytics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_mocks.mdx b/api_docs/kbn_core_analytics_server_mocks.mdx index 1dc30aff04e5..126ab218962c 100644 --- a/api_docs/kbn_core_analytics_server_mocks.mdx +++ b/api_docs/kbn_core_analytics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-mocks title: "@kbn/core-analytics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-mocks'] --- import kbnCoreAnalyticsServerMocksObj from './kbn_core_analytics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser.mdx b/api_docs/kbn_core_application_browser.mdx index 7e999f21f3e1..71e7fae504f5 100644 --- a/api_docs/kbn_core_application_browser.mdx +++ b/api_docs/kbn_core_application_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser title: "@kbn/core-application-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser'] --- import kbnCoreApplicationBrowserObj from './kbn_core_application_browser.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_internal.mdx b/api_docs/kbn_core_application_browser_internal.mdx index c957561aa4ec..55beb31993bb 100644 --- a/api_docs/kbn_core_application_browser_internal.mdx +++ b/api_docs/kbn_core_application_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-internal title: "@kbn/core-application-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-internal'] --- import kbnCoreApplicationBrowserInternalObj from './kbn_core_application_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_mocks.mdx b/api_docs/kbn_core_application_browser_mocks.mdx index 6ba6188770d4..c777505da45d 100644 --- a/api_docs/kbn_core_application_browser_mocks.mdx +++ b/api_docs/kbn_core_application_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-mocks title: "@kbn/core-application-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-mocks'] --- import kbnCoreApplicationBrowserMocksObj from './kbn_core_application_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_common.mdx b/api_docs/kbn_core_application_common.mdx index 09bb2339c978..10828fe0de71 100644 --- a/api_docs/kbn_core_application_common.mdx +++ b/api_docs/kbn_core_application_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-common title: "@kbn/core-application-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-common plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-common'] --- import kbnCoreApplicationCommonObj from './kbn_core_application_common.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_internal.mdx b/api_docs/kbn_core_apps_browser_internal.mdx index a036a5bb51c0..978ac4faed76 100644 --- a/api_docs/kbn_core_apps_browser_internal.mdx +++ b/api_docs/kbn_core_apps_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-internal title: "@kbn/core-apps-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-internal'] --- import kbnCoreAppsBrowserInternalObj from './kbn_core_apps_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_mocks.mdx b/api_docs/kbn_core_apps_browser_mocks.mdx index 42db1a258137..16fd9445d124 100644 --- a/api_docs/kbn_core_apps_browser_mocks.mdx +++ b/api_docs/kbn_core_apps_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-mocks title: "@kbn/core-apps-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-mocks'] --- import kbnCoreAppsBrowserMocksObj from './kbn_core_apps_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index cd0875458844..cdaf427d6802 100644 --- a/api_docs/kbn_core_base_browser_mocks.mdx +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks title: "@kbn/core-base-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] --- import kbnCoreBaseBrowserMocksObj from './kbn_core_base_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index 468ebd13bf1b..5bd2a717b5b8 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-common plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] --- import kbnCoreBaseCommonObj from './kbn_core_base_common.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx index c986f9acf0ef..76f931b4d756 100644 --- a/api_docs/kbn_core_base_server_internal.mdx +++ b/api_docs/kbn_core_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-internal title: "@kbn/core-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-internal'] --- import kbnCoreBaseServerInternalObj from './kbn_core_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index d6cd91c67140..77aa19a6800c 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] --- import kbnCoreBaseServerMocksObj from './kbn_core_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_browser_mocks.mdx b/api_docs/kbn_core_capabilities_browser_mocks.mdx index 7a6ba37310aa..e6bfe1ecd7db 100644 --- a/api_docs/kbn_core_capabilities_browser_mocks.mdx +++ b/api_docs/kbn_core_capabilities_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-browser-mocks title: "@kbn/core-capabilities-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-browser-mocks'] --- import kbnCoreCapabilitiesBrowserMocksObj from './kbn_core_capabilities_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_common.mdx b/api_docs/kbn_core_capabilities_common.mdx index 24b234f7f811..e9475861b697 100644 --- a/api_docs/kbn_core_capabilities_common.mdx +++ b/api_docs/kbn_core_capabilities_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-common title: "@kbn/core-capabilities-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-common plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-common'] --- import kbnCoreCapabilitiesCommonObj from './kbn_core_capabilities_common.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server.mdx b/api_docs/kbn_core_capabilities_server.mdx index c5359e8345a3..d6dc431b07fd 100644 --- a/api_docs/kbn_core_capabilities_server.mdx +++ b/api_docs/kbn_core_capabilities_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server title: "@kbn/core-capabilities-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server'] --- import kbnCoreCapabilitiesServerObj from './kbn_core_capabilities_server.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server_mocks.mdx b/api_docs/kbn_core_capabilities_server_mocks.mdx index b847a51cd7a0..fdf7cc31d41f 100644 --- a/api_docs/kbn_core_capabilities_server_mocks.mdx +++ b/api_docs/kbn_core_capabilities_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server-mocks title: "@kbn/core-capabilities-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server-mocks'] --- import kbnCoreCapabilitiesServerMocksObj from './kbn_core_capabilities_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser.mdx b/api_docs/kbn_core_chrome_browser.mdx index cc02f6073ea9..73d538523658 100644 --- a/api_docs/kbn_core_chrome_browser.mdx +++ b/api_docs/kbn_core_chrome_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser title: "@kbn/core-chrome-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser'] --- import kbnCoreChromeBrowserObj from './kbn_core_chrome_browser.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser_mocks.mdx b/api_docs/kbn_core_chrome_browser_mocks.mdx index b1d46f4e5fe8..a1f512749d43 100644 --- a/api_docs/kbn_core_chrome_browser_mocks.mdx +++ b/api_docs/kbn_core_chrome_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser-mocks title: "@kbn/core-chrome-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser-mocks'] --- import kbnCoreChromeBrowserMocksObj from './kbn_core_chrome_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_config_server_internal.mdx b/api_docs/kbn_core_config_server_internal.mdx index ee601e6352e3..42574e3029ea 100644 --- a/api_docs/kbn_core_config_server_internal.mdx +++ b/api_docs/kbn_core_config_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-config-server-internal title: "@kbn/core-config-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-config-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-config-server-internal'] --- import kbnCoreConfigServerInternalObj from './kbn_core_config_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index 6eec129fe261..6bb0bb64c5ba 100644 --- a/api_docs/kbn_core_deprecations_browser.mdx +++ b/api_docs/kbn_core_deprecations_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser title: "@kbn/core-deprecations-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser'] --- import kbnCoreDeprecationsBrowserObj from './kbn_core_deprecations_browser.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_internal.mdx b/api_docs/kbn_core_deprecations_browser_internal.mdx index 983047e82691..938d8d6b38af 100644 --- a/api_docs/kbn_core_deprecations_browser_internal.mdx +++ b/api_docs/kbn_core_deprecations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-internal title: "@kbn/core-deprecations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-internal'] --- import kbnCoreDeprecationsBrowserInternalObj from './kbn_core_deprecations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_mocks.mdx b/api_docs/kbn_core_deprecations_browser_mocks.mdx index 0190b56cf973..9d33c64bd7f4 100644 --- a/api_docs/kbn_core_deprecations_browser_mocks.mdx +++ b/api_docs/kbn_core_deprecations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-mocks title: "@kbn/core-deprecations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-mocks'] --- import kbnCoreDeprecationsBrowserMocksObj from './kbn_core_deprecations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_common.mdx b/api_docs/kbn_core_deprecations_common.mdx index 60d444ced08d..7f254dda6eee 100644 --- a/api_docs/kbn_core_deprecations_common.mdx +++ b/api_docs/kbn_core_deprecations_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-common title: "@kbn/core-deprecations-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-common plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-common'] --- import kbnCoreDeprecationsCommonObj from './kbn_core_deprecations_common.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server.mdx b/api_docs/kbn_core_deprecations_server.mdx index e9f8e03e21a2..527a5f2be91e 100644 --- a/api_docs/kbn_core_deprecations_server.mdx +++ b/api_docs/kbn_core_deprecations_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server title: "@kbn/core-deprecations-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server'] --- import kbnCoreDeprecationsServerObj from './kbn_core_deprecations_server.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_internal.mdx b/api_docs/kbn_core_deprecations_server_internal.mdx index cd76ecc1e5de..7b470aeff355 100644 --- a/api_docs/kbn_core_deprecations_server_internal.mdx +++ b/api_docs/kbn_core_deprecations_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-internal title: "@kbn/core-deprecations-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-internal'] --- import kbnCoreDeprecationsServerInternalObj from './kbn_core_deprecations_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_mocks.mdx b/api_docs/kbn_core_deprecations_server_mocks.mdx index adcf635bd089..34e0e5fb4d1f 100644 --- a/api_docs/kbn_core_deprecations_server_mocks.mdx +++ b/api_docs/kbn_core_deprecations_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-mocks title: "@kbn/core-deprecations-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-mocks'] --- import kbnCoreDeprecationsServerMocksObj from './kbn_core_deprecations_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index d8f11b0a7890..97c4d2e6d4b4 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] --- import kbnCoreDocLinksBrowserObj from './kbn_core_doc_links_browser.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index 04891d7eee77..0347b3f309a8 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] --- import kbnCoreDocLinksBrowserMocksObj from './kbn_core_doc_links_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index a211477391f8..5cccdf33b503 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] --- import kbnCoreDocLinksServerObj from './kbn_core_doc_links_server.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index cabbd8bcaf86..e2c4d921a70b 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] --- import kbnCoreDocLinksServerMocksObj from './kbn_core_doc_links_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx index c6a7f734219d..1d786ddcd349 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-internal title: "@kbn/core-elasticsearch-client-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-internal'] --- import kbnCoreElasticsearchClientServerInternalObj from './kbn_core_elasticsearch_client_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx index 7af48bcbf5f1..25079df745e3 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-mocks title: "@kbn/core-elasticsearch-client-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-mocks'] --- import kbnCoreElasticsearchClientServerMocksObj from './kbn_core_elasticsearch_client_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server.mdx b/api_docs/kbn_core_elasticsearch_server.mdx index ca2a47bc9933..a5712d10acd3 100644 --- a/api_docs/kbn_core_elasticsearch_server.mdx +++ b/api_docs/kbn_core_elasticsearch_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server title: "@kbn/core-elasticsearch-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server'] --- import kbnCoreElasticsearchServerObj from './kbn_core_elasticsearch_server.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_internal.mdx b/api_docs/kbn_core_elasticsearch_server_internal.mdx index 5c6a4b84856c..2b2107d52082 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-internal title: "@kbn/core-elasticsearch-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-internal'] --- import kbnCoreElasticsearchServerInternalObj from './kbn_core_elasticsearch_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_server_mocks.mdx index dd0e97e30fc7..c9aa728e95fb 100644 --- a/api_docs/kbn_core_elasticsearch_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-mocks title: "@kbn/core-elasticsearch-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-mocks'] --- import kbnCoreElasticsearchServerMocksObj from './kbn_core_elasticsearch_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_internal.mdx b/api_docs/kbn_core_environment_server_internal.mdx index 2d4f36ed093a..630ee26b5024 100644 --- a/api_docs/kbn_core_environment_server_internal.mdx +++ b/api_docs/kbn_core_environment_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-internal title: "@kbn/core-environment-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-internal'] --- import kbnCoreEnvironmentServerInternalObj from './kbn_core_environment_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_mocks.mdx b/api_docs/kbn_core_environment_server_mocks.mdx index 587243bb6406..62ed3d4b8afa 100644 --- a/api_docs/kbn_core_environment_server_mocks.mdx +++ b/api_docs/kbn_core_environment_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-mocks title: "@kbn/core-environment-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-mocks'] --- import kbnCoreEnvironmentServerMocksObj from './kbn_core_environment_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser.mdx b/api_docs/kbn_core_execution_context_browser.mdx index 96b79793ed09..1c33ab738b0e 100644 --- a/api_docs/kbn_core_execution_context_browser.mdx +++ b/api_docs/kbn_core_execution_context_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser title: "@kbn/core-execution-context-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser'] --- import kbnCoreExecutionContextBrowserObj from './kbn_core_execution_context_browser.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_internal.mdx b/api_docs/kbn_core_execution_context_browser_internal.mdx index d8cfe4d9f349..8d4402a7a2cd 100644 --- a/api_docs/kbn_core_execution_context_browser_internal.mdx +++ b/api_docs/kbn_core_execution_context_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-internal title: "@kbn/core-execution-context-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-internal'] --- import kbnCoreExecutionContextBrowserInternalObj from './kbn_core_execution_context_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_mocks.mdx b/api_docs/kbn_core_execution_context_browser_mocks.mdx index fe90efd44105..5e923792bc93 100644 --- a/api_docs/kbn_core_execution_context_browser_mocks.mdx +++ b/api_docs/kbn_core_execution_context_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-mocks title: "@kbn/core-execution-context-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-mocks'] --- import kbnCoreExecutionContextBrowserMocksObj from './kbn_core_execution_context_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_common.mdx b/api_docs/kbn_core_execution_context_common.mdx index e7b47436fa76..f2336d9fdfb0 100644 --- a/api_docs/kbn_core_execution_context_common.mdx +++ b/api_docs/kbn_core_execution_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-common title: "@kbn/core-execution-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-common plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-common'] --- import kbnCoreExecutionContextCommonObj from './kbn_core_execution_context_common.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server.mdx b/api_docs/kbn_core_execution_context_server.mdx index 8fa86908f719..61e72d43c392 100644 --- a/api_docs/kbn_core_execution_context_server.mdx +++ b/api_docs/kbn_core_execution_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server title: "@kbn/core-execution-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server'] --- import kbnCoreExecutionContextServerObj from './kbn_core_execution_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_internal.mdx b/api_docs/kbn_core_execution_context_server_internal.mdx index 76ca7ae6c111..1c31d922fc68 100644 --- a/api_docs/kbn_core_execution_context_server_internal.mdx +++ b/api_docs/kbn_core_execution_context_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-internal title: "@kbn/core-execution-context-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-internal'] --- import kbnCoreExecutionContextServerInternalObj from './kbn_core_execution_context_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_mocks.mdx b/api_docs/kbn_core_execution_context_server_mocks.mdx index 3e17e9ed9463..00772d47658c 100644 --- a/api_docs/kbn_core_execution_context_server_mocks.mdx +++ b/api_docs/kbn_core_execution_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-mocks title: "@kbn/core-execution-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-mocks'] --- import kbnCoreExecutionContextServerMocksObj from './kbn_core_execution_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser.mdx b/api_docs/kbn_core_fatal_errors_browser.mdx index 01d683396e2b..7a8edbe3e3b3 100644 --- a/api_docs/kbn_core_fatal_errors_browser.mdx +++ b/api_docs/kbn_core_fatal_errors_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser title: "@kbn/core-fatal-errors-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser'] --- import kbnCoreFatalErrorsBrowserObj from './kbn_core_fatal_errors_browser.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx index 891c8d876754..6dd6b8aebce6 100644 --- a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx +++ b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser-mocks title: "@kbn/core-fatal-errors-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser-mocks'] --- import kbnCoreFatalErrorsBrowserMocksObj from './kbn_core_fatal_errors_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser.mdx b/api_docs/kbn_core_http_browser.mdx index a935d019adeb..8014dbb2ceb6 100644 --- a/api_docs/kbn_core_http_browser.mdx +++ b/api_docs/kbn_core_http_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser title: "@kbn/core-http-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser'] --- import kbnCoreHttpBrowserObj from './kbn_core_http_browser.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_internal.mdx b/api_docs/kbn_core_http_browser_internal.mdx index 04f71c530c9a..ed565e7b8f3b 100644 --- a/api_docs/kbn_core_http_browser_internal.mdx +++ b/api_docs/kbn_core_http_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-internal title: "@kbn/core-http-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-internal'] --- import kbnCoreHttpBrowserInternalObj from './kbn_core_http_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_mocks.mdx b/api_docs/kbn_core_http_browser_mocks.mdx index 53f1d9fe670a..632f5546a721 100644 --- a/api_docs/kbn_core_http_browser_mocks.mdx +++ b/api_docs/kbn_core_http_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-mocks title: "@kbn/core-http-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-mocks'] --- import kbnCoreHttpBrowserMocksObj from './kbn_core_http_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_common.mdx b/api_docs/kbn_core_http_common.mdx index f8ff0087dbbf..92184cbab278 100644 --- a/api_docs/kbn_core_http_common.mdx +++ b/api_docs/kbn_core_http_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-common title: "@kbn/core-http-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-common plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-common'] --- import kbnCoreHttpCommonObj from './kbn_core_http_common.devdocs.json'; diff --git a/api_docs/kbn_core_http_context_server_mocks.mdx b/api_docs/kbn_core_http_context_server_mocks.mdx index c93bb2546524..52f62aad5c93 100644 --- a/api_docs/kbn_core_http_context_server_mocks.mdx +++ b/api_docs/kbn_core_http_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-context-server-mocks title: "@kbn/core-http-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-context-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-context-server-mocks'] --- import kbnCoreHttpContextServerMocksObj from './kbn_core_http_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_request_handler_context_server.devdocs.json b/api_docs/kbn_core_http_request_handler_context_server.devdocs.json new file mode 100644 index 000000000000..ebf6567c0a8b --- /dev/null +++ b/api_docs/kbn_core_http_request_handler_context_server.devdocs.json @@ -0,0 +1,283 @@ +{ + "id": "@kbn/core-http-request-handler-context-server", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [ + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.CoreRequestHandlerContext", + "type": "Interface", + "tags": [], + "label": "CoreRequestHandlerContext", + "description": [ + "\nThe `core` context provided to route handler.\n\nProvides the following clients and services:\n - {@link SavedObjectsClient | savedObjects.client} - Saved Objects client\n which uses the credentials of the incoming request\n - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing\n all the registered types.\n - {@link IScopedClusterClient | elasticsearch.client} - Elasticsearch\n data client which uses the credentials of the incoming request\n - {@link IUiSettingsClient | uiSettings.client} - uiSettings client\n which uses the credentials of the incoming request" + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.CoreRequestHandlerContext.savedObjects", + "type": "Object", + "tags": [], + "label": "savedObjects", + "description": [], + "signature": [ + "SavedObjectsRequestHandlerContext" + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.CoreRequestHandlerContext.elasticsearch", + "type": "Object", + "tags": [], + "label": "elasticsearch", + "description": [], + "signature": [ + "ElasticsearchRequestHandlerContext" + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.CoreRequestHandlerContext.uiSettings", + "type": "Object", + "tags": [], + "label": "uiSettings", + "description": [], + "signature": [ + "UiSettingsRequestHandlerContext" + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.CoreRequestHandlerContext.deprecations", + "type": "Object", + "tags": [], + "label": "deprecations", + "description": [], + "signature": [ + "DeprecationsRequestHandlerContext" + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.PrebootCoreRequestHandlerContext", + "type": "Interface", + "tags": [], + "label": "PrebootCoreRequestHandlerContext", + "description": [], + "path": "packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.PrebootCoreRequestHandlerContext.uiSettings", + "type": "Object", + "tags": [], + "label": "uiSettings", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-http-request-handler-context-server", + "scope": "server", + "docId": "kibKbnCoreHttpRequestHandlerContextServerPluginApi", + "section": "def-server.PrebootUiSettingsRequestHandlerContext", + "text": "PrebootUiSettingsRequestHandlerContext" + } + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.PrebootRequestHandlerContext", + "type": "Interface", + "tags": [], + "label": "PrebootRequestHandlerContext", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-http-request-handler-context-server", + "scope": "server", + "docId": "kibKbnCoreHttpRequestHandlerContextServerPluginApi", + "section": "def-server.PrebootRequestHandlerContext", + "text": "PrebootRequestHandlerContext" + }, + " extends ", + "RequestHandlerContextBase" + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.PrebootRequestHandlerContext.core", + "type": "Object", + "tags": [], + "label": "core", + "description": [], + "signature": [ + "Promise<", + { + "pluginId": "@kbn/core-http-request-handler-context-server", + "scope": "server", + "docId": "kibKbnCoreHttpRequestHandlerContextServerPluginApi", + "section": "def-server.PrebootCoreRequestHandlerContext", + "text": "PrebootCoreRequestHandlerContext" + }, + ">" + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.PrebootUiSettingsRequestHandlerContext", + "type": "Interface", + "tags": [], + "label": "PrebootUiSettingsRequestHandlerContext", + "description": [], + "path": "packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.PrebootUiSettingsRequestHandlerContext.client", + "type": "Object", + "tags": [], + "label": "client", + "description": [], + "signature": [ + "IUiSettingsClient" + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.RequestHandlerContext", + "type": "Interface", + "tags": [], + "label": "RequestHandlerContext", + "description": [ + "\nBase context passed to a route handler, containing the `core` context part.\n" + ], + "signature": [ + { + "pluginId": "@kbn/core-http-request-handler-context-server", + "scope": "server", + "docId": "kibKbnCoreHttpRequestHandlerContextServerPluginApi", + "section": "def-server.RequestHandlerContext", + "text": "RequestHandlerContext" + }, + " extends ", + "RequestHandlerContextBase" + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.RequestHandlerContext.core", + "type": "Object", + "tags": [], + "label": "core", + "description": [], + "signature": [ + "Promise<", + { + "pluginId": "@kbn/core-http-request-handler-context-server", + "scope": "server", + "docId": "kibKbnCoreHttpRequestHandlerContextServerPluginApi", + "section": "def-server.CoreRequestHandlerContext", + "text": "CoreRequestHandlerContext" + }, + ">" + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/core-http-request-handler-context-server", + "id": "def-server.CustomRequestHandlerContext", + "type": "Type", + "tags": [], + "label": "CustomRequestHandlerContext", + "description": [ + "\nMixin allowing plugins to define their own request handler contexts.\n" + ], + "signature": [ + { + "pluginId": "@kbn/core-http-request-handler-context-server", + "scope": "server", + "docId": "kibKbnCoreHttpRequestHandlerContextServerPluginApi", + "section": "def-server.RequestHandlerContext", + "text": "RequestHandlerContext" + }, + " & { [Key in keyof T]: T[Key] extends Promise ? T[Key] : Promise; }" + ], + "path": "packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_http_request_handler_context_server.mdx b/api_docs/kbn_core_http_request_handler_context_server.mdx new file mode 100644 index 000000000000..0e41263673dd --- /dev/null +++ b/api_docs/kbn_core_http_request_handler_context_server.mdx @@ -0,0 +1,33 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnCoreHttpRequestHandlerContextServerPluginApi +slug: /kibana-dev-docs/api/kbn-core-http-request-handler-context-server +title: "@kbn/core-http-request-handler-context-server" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/core-http-request-handler-context-server plugin +date: 2022-09-29 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-request-handler-context-server'] +--- +import kbnCoreHttpRequestHandlerContextServerObj from './kbn_core_http_request_handler_context_server.devdocs.json'; + + + +Contact Kibana Core for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 14 | 0 | 11 | 0 | + +## Server + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/kbn_core_http_router_server_internal.mdx b/api_docs/kbn_core_http_router_server_internal.mdx index 6121b09f8c1b..0fdb1bfe4318 100644 --- a/api_docs/kbn_core_http_router_server_internal.mdx +++ b/api_docs/kbn_core_http_router_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-internal title: "@kbn/core-http-router-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-internal'] --- import kbnCoreHttpRouterServerInternalObj from './kbn_core_http_router_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_mocks.mdx b/api_docs/kbn_core_http_router_server_mocks.mdx index 49dffda39dcc..b758dea2dd26 100644 --- a/api_docs/kbn_core_http_router_server_mocks.mdx +++ b/api_docs/kbn_core_http_router_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-mocks title: "@kbn/core-http-router-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-mocks'] --- import kbnCoreHttpRouterServerMocksObj from './kbn_core_http_router_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_server.mdx b/api_docs/kbn_core_http_server.mdx index 74c3216b75eb..bb3c7c1bd350 100644 --- a/api_docs/kbn_core_http_server.mdx +++ b/api_docs/kbn_core_http_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server title: "@kbn/core-http-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server'] --- import kbnCoreHttpServerObj from './kbn_core_http_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_internal.mdx b/api_docs/kbn_core_http_server_internal.mdx index 50ebcbe4058b..220d5282e1fc 100644 --- a/api_docs/kbn_core_http_server_internal.mdx +++ b/api_docs/kbn_core_http_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-internal title: "@kbn/core-http-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-internal'] --- import kbnCoreHttpServerInternalObj from './kbn_core_http_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_mocks.mdx b/api_docs/kbn_core_http_server_mocks.mdx index c7649dcf597c..ed120c45d977 100644 --- a/api_docs/kbn_core_http_server_mocks.mdx +++ b/api_docs/kbn_core_http_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-mocks title: "@kbn/core-http-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-mocks'] --- import kbnCoreHttpServerMocksObj from './kbn_core_http_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser.mdx b/api_docs/kbn_core_i18n_browser.mdx index 7e8106d7e3c6..9cb11bc93a1f 100644 --- a/api_docs/kbn_core_i18n_browser.mdx +++ b/api_docs/kbn_core_i18n_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser title: "@kbn/core-i18n-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser'] --- import kbnCoreI18nBrowserObj from './kbn_core_i18n_browser.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser_mocks.mdx b/api_docs/kbn_core_i18n_browser_mocks.mdx index 32e6b9184dbe..1bb71320adbc 100644 --- a/api_docs/kbn_core_i18n_browser_mocks.mdx +++ b/api_docs/kbn_core_i18n_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser-mocks title: "@kbn/core-i18n-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser-mocks'] --- import kbnCoreI18nBrowserMocksObj from './kbn_core_i18n_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server.mdx b/api_docs/kbn_core_i18n_server.mdx index 4e5cbf65974a..edbbec7b5aac 100644 --- a/api_docs/kbn_core_i18n_server.mdx +++ b/api_docs/kbn_core_i18n_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server title: "@kbn/core-i18n-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server'] --- import kbnCoreI18nServerObj from './kbn_core_i18n_server.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_internal.mdx b/api_docs/kbn_core_i18n_server_internal.mdx index 93c620cca65a..408a8656056a 100644 --- a/api_docs/kbn_core_i18n_server_internal.mdx +++ b/api_docs/kbn_core_i18n_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-internal title: "@kbn/core-i18n-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-internal'] --- import kbnCoreI18nServerInternalObj from './kbn_core_i18n_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_mocks.mdx b/api_docs/kbn_core_i18n_server_mocks.mdx index 13734215059b..549c463617a5 100644 --- a/api_docs/kbn_core_i18n_server_mocks.mdx +++ b/api_docs/kbn_core_i18n_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-mocks title: "@kbn/core-i18n-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-mocks'] --- import kbnCoreI18nServerMocksObj from './kbn_core_i18n_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser.mdx b/api_docs/kbn_core_injected_metadata_browser.mdx index 08dbe296b432..11a4ba2c541e 100644 --- a/api_docs/kbn_core_injected_metadata_browser.mdx +++ b/api_docs/kbn_core_injected_metadata_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser title: "@kbn/core-injected-metadata-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser'] --- import kbnCoreInjectedMetadataBrowserObj from './kbn_core_injected_metadata_browser.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index b88675d50c20..579e9087903c 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] --- import kbnCoreInjectedMetadataBrowserMocksObj from './kbn_core_injected_metadata_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_internal.mdx b/api_docs/kbn_core_integrations_browser_internal.mdx index fc53dc77eda1..c88718d0e930 100644 --- a/api_docs/kbn_core_integrations_browser_internal.mdx +++ b/api_docs/kbn_core_integrations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-internal title: "@kbn/core-integrations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-internal'] --- import kbnCoreIntegrationsBrowserInternalObj from './kbn_core_integrations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_mocks.mdx b/api_docs/kbn_core_integrations_browser_mocks.mdx index 2526e6dc2b6b..6be7ee221009 100644 --- a/api_docs/kbn_core_integrations_browser_mocks.mdx +++ b/api_docs/kbn_core_integrations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-mocks title: "@kbn/core-integrations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-mocks'] --- import kbnCoreIntegrationsBrowserMocksObj from './kbn_core_integrations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser.mdx b/api_docs/kbn_core_lifecycle_browser.mdx index 8b557e2e502b..863f69b31518 100644 --- a/api_docs/kbn_core_lifecycle_browser.mdx +++ b/api_docs/kbn_core_lifecycle_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser title: "@kbn/core-lifecycle-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser'] --- import kbnCoreLifecycleBrowserObj from './kbn_core_lifecycle_browser.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser_mocks.mdx b/api_docs/kbn_core_lifecycle_browser_mocks.mdx index 74231286ffe0..add898c16abe 100644 --- a/api_docs/kbn_core_lifecycle_browser_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser-mocks title: "@kbn/core-lifecycle-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser-mocks'] --- import kbnCoreLifecycleBrowserMocksObj from './kbn_core_lifecycle_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index a4e8cb4241be..a488e40d01dd 100644 --- a/api_docs/kbn_core_logging_server.mdx +++ b/api_docs/kbn_core_logging_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server title: "@kbn/core-logging-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server'] --- import kbnCoreLoggingServerObj from './kbn_core_logging_server.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_internal.mdx b/api_docs/kbn_core_logging_server_internal.mdx index f56eecbd6955..87082d1e3319 100644 --- a/api_docs/kbn_core_logging_server_internal.mdx +++ b/api_docs/kbn_core_logging_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-internal title: "@kbn/core-logging-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-internal'] --- import kbnCoreLoggingServerInternalObj from './kbn_core_logging_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_mocks.mdx b/api_docs/kbn_core_logging_server_mocks.mdx index 2067fdbc72fd..7a746c627805 100644 --- a/api_docs/kbn_core_logging_server_mocks.mdx +++ b/api_docs/kbn_core_logging_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-mocks title: "@kbn/core-logging-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-mocks'] --- import kbnCoreLoggingServerMocksObj from './kbn_core_logging_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_internal.mdx b/api_docs/kbn_core_metrics_collectors_server_internal.mdx index a7e7a11ae05e..a413d283cb4c 100644 --- a/api_docs/kbn_core_metrics_collectors_server_internal.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-internal title: "@kbn/core-metrics-collectors-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-internal'] --- import kbnCoreMetricsCollectorsServerInternalObj from './kbn_core_metrics_collectors_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx index febc5d303893..3ad9c040ab8f 100644 --- a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-mocks title: "@kbn/core-metrics-collectors-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-mocks'] --- import kbnCoreMetricsCollectorsServerMocksObj from './kbn_core_metrics_collectors_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server.mdx b/api_docs/kbn_core_metrics_server.mdx index 47cc583e2df0..4e9e0445e3f7 100644 --- a/api_docs/kbn_core_metrics_server.mdx +++ b/api_docs/kbn_core_metrics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server title: "@kbn/core-metrics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server'] --- import kbnCoreMetricsServerObj from './kbn_core_metrics_server.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_internal.mdx b/api_docs/kbn_core_metrics_server_internal.mdx index 9e16d8304dec..0cfd64309de9 100644 --- a/api_docs/kbn_core_metrics_server_internal.mdx +++ b/api_docs/kbn_core_metrics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-internal title: "@kbn/core-metrics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-internal'] --- import kbnCoreMetricsServerInternalObj from './kbn_core_metrics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_mocks.mdx b/api_docs/kbn_core_metrics_server_mocks.mdx index 5db83c7383b6..40f97497e1cf 100644 --- a/api_docs/kbn_core_metrics_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-mocks title: "@kbn/core-metrics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-mocks'] --- import kbnCoreMetricsServerMocksObj from './kbn_core_metrics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_mount_utils_browser.mdx b/api_docs/kbn_core_mount_utils_browser.mdx index 8fafac583163..3e4d25261b36 100644 --- a/api_docs/kbn_core_mount_utils_browser.mdx +++ b/api_docs/kbn_core_mount_utils_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-mount-utils-browser title: "@kbn/core-mount-utils-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-mount-utils-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-mount-utils-browser'] --- import kbnCoreMountUtilsBrowserObj from './kbn_core_mount_utils_browser.devdocs.json'; diff --git a/api_docs/kbn_core_node_server.mdx b/api_docs/kbn_core_node_server.mdx index 9fbffa9c5df9..9ec6da0be76f 100644 --- a/api_docs/kbn_core_node_server.mdx +++ b/api_docs/kbn_core_node_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server title: "@kbn/core-node-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server'] --- import kbnCoreNodeServerObj from './kbn_core_node_server.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_internal.mdx b/api_docs/kbn_core_node_server_internal.mdx index d253278b97f1..e1be058f6694 100644 --- a/api_docs/kbn_core_node_server_internal.mdx +++ b/api_docs/kbn_core_node_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-internal title: "@kbn/core-node-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-internal'] --- import kbnCoreNodeServerInternalObj from './kbn_core_node_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_mocks.mdx b/api_docs/kbn_core_node_server_mocks.mdx index 5dd3d790c3ea..2f8dd3d2d3cf 100644 --- a/api_docs/kbn_core_node_server_mocks.mdx +++ b/api_docs/kbn_core_node_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-mocks title: "@kbn/core-node-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-mocks'] --- import kbnCoreNodeServerMocksObj from './kbn_core_node_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser.mdx b/api_docs/kbn_core_notifications_browser.mdx index ed0c7c4e240c..99c39b3ae3bf 100644 --- a/api_docs/kbn_core_notifications_browser.mdx +++ b/api_docs/kbn_core_notifications_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser title: "@kbn/core-notifications-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser'] --- import kbnCoreNotificationsBrowserObj from './kbn_core_notifications_browser.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_internal.mdx b/api_docs/kbn_core_notifications_browser_internal.mdx index 5c44022f6082..b1abfcae9d1d 100644 --- a/api_docs/kbn_core_notifications_browser_internal.mdx +++ b/api_docs/kbn_core_notifications_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-internal title: "@kbn/core-notifications-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-internal'] --- import kbnCoreNotificationsBrowserInternalObj from './kbn_core_notifications_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_mocks.mdx b/api_docs/kbn_core_notifications_browser_mocks.mdx index ef4b9a440b59..6af7ea7a401a 100644 --- a/api_docs/kbn_core_notifications_browser_mocks.mdx +++ b/api_docs/kbn_core_notifications_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-mocks title: "@kbn/core-notifications-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-mocks'] --- import kbnCoreNotificationsBrowserMocksObj from './kbn_core_notifications_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser.mdx b/api_docs/kbn_core_overlays_browser.mdx index 559df72a076e..4e53620f210c 100644 --- a/api_docs/kbn_core_overlays_browser.mdx +++ b/api_docs/kbn_core_overlays_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser title: "@kbn/core-overlays-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser'] --- import kbnCoreOverlaysBrowserObj from './kbn_core_overlays_browser.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_internal.mdx b/api_docs/kbn_core_overlays_browser_internal.mdx index 6160c98032fe..3cdf63fa196b 100644 --- a/api_docs/kbn_core_overlays_browser_internal.mdx +++ b/api_docs/kbn_core_overlays_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-internal title: "@kbn/core-overlays-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-internal'] --- import kbnCoreOverlaysBrowserInternalObj from './kbn_core_overlays_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_mocks.mdx b/api_docs/kbn_core_overlays_browser_mocks.mdx index 5765d85478d6..ed9822ce6524 100644 --- a/api_docs/kbn_core_overlays_browser_mocks.mdx +++ b/api_docs/kbn_core_overlays_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-mocks title: "@kbn/core-overlays-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-mocks'] --- import kbnCoreOverlaysBrowserMocksObj from './kbn_core_overlays_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser.mdx b/api_docs/kbn_core_plugins_browser.mdx index 09d09fe70857..0e35863807e9 100644 --- a/api_docs/kbn_core_plugins_browser.mdx +++ b/api_docs/kbn_core_plugins_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser title: "@kbn/core-plugins-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser'] --- import kbnCorePluginsBrowserObj from './kbn_core_plugins_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser_mocks.mdx b/api_docs/kbn_core_plugins_browser_mocks.mdx index 5416044cac95..e8513df53c90 100644 --- a/api_docs/kbn_core_plugins_browser_mocks.mdx +++ b/api_docs/kbn_core_plugins_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser-mocks title: "@kbn/core-plugins-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser-mocks'] --- import kbnCorePluginsBrowserMocksObj from './kbn_core_plugins_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index 962d718e0042..a33965c6f024 100644 --- a/api_docs/kbn_core_preboot_server.mdx +++ b/api_docs/kbn_core_preboot_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server title: "@kbn/core-preboot-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server'] --- import kbnCorePrebootServerObj from './kbn_core_preboot_server.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server_mocks.mdx b/api_docs/kbn_core_preboot_server_mocks.mdx index 07d55b8bdc9a..499f42fd91b2 100644 --- a/api_docs/kbn_core_preboot_server_mocks.mdx +++ b/api_docs/kbn_core_preboot_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server-mocks title: "@kbn/core-preboot-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server-mocks'] --- import kbnCorePrebootServerMocksObj from './kbn_core_preboot_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_browser_mocks.mdx b/api_docs/kbn_core_rendering_browser_mocks.mdx index f31451fde9e7..cedfc5029275 100644 --- a/api_docs/kbn_core_rendering_browser_mocks.mdx +++ b/api_docs/kbn_core_rendering_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-browser-mocks title: "@kbn/core-rendering-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-browser-mocks'] --- import kbnCoreRenderingBrowserMocksObj from './kbn_core_rendering_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_browser.devdocs.json b/api_docs/kbn_core_saved_objects_api_browser.devdocs.json index b12a698b2160..bc6024dea788 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.devdocs.json +++ b/api_docs/kbn_core_saved_objects_api_browser.devdocs.json @@ -76,7 +76,7 @@ "\nThe outcome for a successful `resolve` call is one of the following values:\n\n * `'exactMatch'` -- One document exactly matched the given ID.\n * `'aliasMatch'` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different\n than the given ID.\n * `'conflict'` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the\n `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID." ], "signature": [ - "\"exactMatch\" | \"aliasMatch\" | \"conflict\"" + "\"conflict\" | \"exactMatch\" | \"aliasMatch\"" ], "path": "packages/core/saved-objects/core-saved-objects-api-browser/src/apis/resolve.ts", "deprecated": false, @@ -1974,9 +1974,9 @@ "label": "SavedObjectsFindOptions", "description": [], "signature": [ - "{ type: string | string[]; filter?: any; search?: string | undefined; aggs?: Record | undefined; fields?: string[] | undefined; page?: number | undefined; perPage?: number | undefined; sortField?: string | undefined; searchFields?: string[] | undefined; hasReference?: ", + "> | undefined; page?: number | undefined; perPage?: number | undefined; sortField?: string | undefined; searchFields?: string[] | undefined; hasReference?: ", "SavedObjectsFindOptionsReference", " | ", "SavedObjectsFindOptionsReference", diff --git a/api_docs/kbn_core_saved_objects_api_browser.mdx b/api_docs/kbn_core_saved_objects_api_browser.mdx index 97d1f20eba7a..de5b9aa205ca 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.mdx +++ b/api_docs/kbn_core_saved_objects_api_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-browser title: "@kbn/core-saved-objects-api-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-browser'] --- import kbnCoreSavedObjectsApiBrowserObj from './kbn_core_saved_objects_api_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server.devdocs.json b/api_docs/kbn_core_saved_objects_api_server.devdocs.json index 0e040b72d2d3..1bff25681bdb 100644 --- a/api_docs/kbn_core_saved_objects_api_server.devdocs.json +++ b/api_docs/kbn_core_saved_objects_api_server.devdocs.json @@ -5949,7 +5949,7 @@ "\nThe outcome for a successful `resolve` call is one of the following values:\n\n * `'exactMatch'` -- One document exactly matched the given ID.\n * `'aliasMatch'` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different\n than the given ID.\n * `'conflict'` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the\n `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID." ], "signature": [ - "\"exactMatch\" | \"aliasMatch\" | \"conflict\"" + "\"conflict\" | \"exactMatch\" | \"aliasMatch\"" ], "path": "packages/core/saved-objects/core-saved-objects-api-server/src/apis/resolve.ts", "deprecated": false, @@ -6427,9 +6427,9 @@ "label": "SavedObjectsCreatePointInTimeFinderOptions", "description": [], "signature": [ - "{ type: string | string[]; filter?: any; search?: string | undefined; aggs?: Record | undefined; fields?: string[] | undefined; perPage?: number | undefined; sortField?: string | undefined; sortOrder?: ", + "> | undefined; perPage?: number | undefined; sortField?: string | undefined; sortOrder?: ", "SortOrder", " | undefined; searchFields?: string[] | undefined; rootSearchFields?: string[] | undefined; hasReference?: ", { diff --git a/api_docs/kbn_core_saved_objects_api_server.mdx b/api_docs/kbn_core_saved_objects_api_server.mdx index 1c1ec6407d24..7628f073dc94 100644 --- a/api_docs/kbn_core_saved_objects_api_server.mdx +++ b/api_docs/kbn_core_saved_objects_api_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server title: "@kbn/core-saved-objects-api-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server'] --- import kbnCoreSavedObjectsApiServerObj from './kbn_core_saved_objects_api_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_internal.mdx b/api_docs/kbn_core_saved_objects_api_server_internal.mdx index 678cf37d24e8..a91351cf9f73 100644 --- a/api_docs/kbn_core_saved_objects_api_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-internal title: "@kbn/core-saved-objects-api-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-internal'] --- import kbnCoreSavedObjectsApiServerInternalObj from './kbn_core_saved_objects_api_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx index da93d1e0b855..fa562b3b2acf 100644 --- a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-mocks title: "@kbn/core-saved-objects-api-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-mocks'] --- import kbnCoreSavedObjectsApiServerMocksObj from './kbn_core_saved_objects_api_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_internal.mdx b/api_docs/kbn_core_saved_objects_base_server_internal.mdx index 73d6c70c1a9a..acc5f724b1bc 100644 --- a/api_docs/kbn_core_saved_objects_base_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-internal title: "@kbn/core-saved-objects-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-internal'] --- import kbnCoreSavedObjectsBaseServerInternalObj from './kbn_core_saved_objects_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx index 2b499a47657b..114ad87c44b3 100644 --- a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-mocks title: "@kbn/core-saved-objects-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-mocks'] --- import kbnCoreSavedObjectsBaseServerMocksObj from './kbn_core_saved_objects_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser.mdx b/api_docs/kbn_core_saved_objects_browser.mdx index fcec7c16e5f8..6731e2111977 100644 --- a/api_docs/kbn_core_saved_objects_browser.mdx +++ b/api_docs/kbn_core_saved_objects_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser title: "@kbn/core-saved-objects-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser'] --- import kbnCoreSavedObjectsBrowserObj from './kbn_core_saved_objects_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_internal.mdx b/api_docs/kbn_core_saved_objects_browser_internal.mdx index 197152a6dae0..276569792284 100644 --- a/api_docs/kbn_core_saved_objects_browser_internal.mdx +++ b/api_docs/kbn_core_saved_objects_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-internal title: "@kbn/core-saved-objects-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-internal'] --- import kbnCoreSavedObjectsBrowserInternalObj from './kbn_core_saved_objects_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_mocks.mdx b/api_docs/kbn_core_saved_objects_browser_mocks.mdx index 80320e731248..9f0c9ea82ba6 100644 --- a/api_docs/kbn_core_saved_objects_browser_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-mocks title: "@kbn/core-saved-objects-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-mocks'] --- import kbnCoreSavedObjectsBrowserMocksObj from './kbn_core_saved_objects_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_common.mdx b/api_docs/kbn_core_saved_objects_common.mdx index 63fa1e47ac1b..bc3ea822a942 100644 --- a/api_docs/kbn_core_saved_objects_common.mdx +++ b/api_docs/kbn_core_saved_objects_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-common title: "@kbn/core-saved-objects-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-common plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-common'] --- import kbnCoreSavedObjectsCommonObj from './kbn_core_saved_objects_common.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx index 127679f75147..b5f3e61df18b 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-internal title: "@kbn/core-saved-objects-import-export-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-internal'] --- import kbnCoreSavedObjectsImportExportServerInternalObj from './kbn_core_saved_objects_import_export_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx index f633f52c6174..99e95bfdab28 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-mocks title: "@kbn/core-saved-objects-import-export-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-mocks'] --- import kbnCoreSavedObjectsImportExportServerMocksObj from './kbn_core_saved_objects_import_export_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx index bc446d004b24..5edf8cbfddef 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-internal title: "@kbn/core-saved-objects-migration-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-internal'] --- import kbnCoreSavedObjectsMigrationServerInternalObj from './kbn_core_saved_objects_migration_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx index ecbb1447d1e5..5ed2a4976d1c 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-mocks title: "@kbn/core-saved-objects-migration-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-mocks'] --- import kbnCoreSavedObjectsMigrationServerMocksObj from './kbn_core_saved_objects_migration_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server.mdx b/api_docs/kbn_core_saved_objects_server.mdx index c694b3c9f658..8f12d48e0f7a 100644 --- a/api_docs/kbn_core_saved_objects_server.mdx +++ b/api_docs/kbn_core_saved_objects_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server title: "@kbn/core-saved-objects-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server'] --- import kbnCoreSavedObjectsServerObj from './kbn_core_saved_objects_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_internal.mdx b/api_docs/kbn_core_saved_objects_server_internal.mdx index 39099151043a..6de1b336be40 100644 --- a/api_docs/kbn_core_saved_objects_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-internal title: "@kbn/core-saved-objects-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-internal'] --- import kbnCoreSavedObjectsServerInternalObj from './kbn_core_saved_objects_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_mocks.mdx b/api_docs/kbn_core_saved_objects_server_mocks.mdx index 73e0b0550438..4fb52fecf871 100644 --- a/api_docs/kbn_core_saved_objects_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-mocks title: "@kbn/core-saved-objects-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-mocks'] --- import kbnCoreSavedObjectsServerMocksObj from './kbn_core_saved_objects_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_utils_server.mdx b/api_docs/kbn_core_saved_objects_utils_server.mdx index 0012db4e553f..71fdb228446e 100644 --- a/api_docs/kbn_core_saved_objects_utils_server.mdx +++ b/api_docs/kbn_core_saved_objects_utils_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-utils-server title: "@kbn/core-saved-objects-utils-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-utils-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-utils-server'] --- import kbnCoreSavedObjectsUtilsServerObj from './kbn_core_saved_objects_utils_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_common.mdx b/api_docs/kbn_core_status_common.mdx index 1ef8c6abfc97..49314722d46a 100644 --- a/api_docs/kbn_core_status_common.mdx +++ b/api_docs/kbn_core_status_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common title: "@kbn/core-status-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common'] --- import kbnCoreStatusCommonObj from './kbn_core_status_common.devdocs.json'; diff --git a/api_docs/kbn_core_status_common_internal.mdx b/api_docs/kbn_core_status_common_internal.mdx index 7c9440c92271..266265f2ee05 100644 --- a/api_docs/kbn_core_status_common_internal.mdx +++ b/api_docs/kbn_core_status_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common-internal title: "@kbn/core-status-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common-internal'] --- import kbnCoreStatusCommonInternalObj from './kbn_core_status_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx index bd5556b6ce87..8d222376f5bf 100644 --- a/api_docs/kbn_core_status_server.mdx +++ b/api_docs/kbn_core_status_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server title: "@kbn/core-status-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server'] --- import kbnCoreStatusServerObj from './kbn_core_status_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_internal.mdx b/api_docs/kbn_core_status_server_internal.mdx index 364b20ab80d6..f8cae5c4a550 100644 --- a/api_docs/kbn_core_status_server_internal.mdx +++ b/api_docs/kbn_core_status_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-internal title: "@kbn/core-status-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-internal'] --- import kbnCoreStatusServerInternalObj from './kbn_core_status_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_mocks.mdx b/api_docs/kbn_core_status_server_mocks.mdx index aa7990a556b5..0dacdfd5af97 100644 --- a/api_docs/kbn_core_status_server_mocks.mdx +++ b/api_docs/kbn_core_status_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-mocks title: "@kbn/core-status-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-mocks'] --- import kbnCoreStatusServerMocksObj from './kbn_core_status_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx index d4a0a789572f..99b4036abb6b 100644 --- a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx +++ b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-deprecations-getters title: "@kbn/core-test-helpers-deprecations-getters" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-deprecations-getters plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-deprecations-getters'] --- import kbnCoreTestHelpersDeprecationsGettersObj from './kbn_core_test_helpers_deprecations_getters.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx index 826ee5d1b345..549cdfe3a58e 100644 --- a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx +++ b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-http-setup-browser title: "@kbn/core-test-helpers-http-setup-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-http-setup-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-http-setup-browser'] --- import kbnCoreTestHelpersHttpSetupBrowserObj from './kbn_core_test_helpers_http_setup_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index e68a43d0ce9f..c429de87e13d 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] --- import kbnCoreThemeBrowserObj from './kbn_core_theme_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_internal.mdx b/api_docs/kbn_core_theme_browser_internal.mdx index 8c938db4082e..0888bc8875a2 100644 --- a/api_docs/kbn_core_theme_browser_internal.mdx +++ b/api_docs/kbn_core_theme_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-internal title: "@kbn/core-theme-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-internal'] --- import kbnCoreThemeBrowserInternalObj from './kbn_core_theme_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index 0cbb8955ac42..254373b7a14d 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] --- import kbnCoreThemeBrowserMocksObj from './kbn_core_theme_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser.mdx b/api_docs/kbn_core_ui_settings_browser.mdx index 67a42c6099d9..61ceae147d70 100644 --- a/api_docs/kbn_core_ui_settings_browser.mdx +++ b/api_docs/kbn_core_ui_settings_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser title: "@kbn/core-ui-settings-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser'] --- import kbnCoreUiSettingsBrowserObj from './kbn_core_ui_settings_browser.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_internal.mdx b/api_docs/kbn_core_ui_settings_browser_internal.mdx index 791006e8cea6..9b9365bc3081 100644 --- a/api_docs/kbn_core_ui_settings_browser_internal.mdx +++ b/api_docs/kbn_core_ui_settings_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-internal title: "@kbn/core-ui-settings-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-internal'] --- import kbnCoreUiSettingsBrowserInternalObj from './kbn_core_ui_settings_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_mocks.mdx b/api_docs/kbn_core_ui_settings_browser_mocks.mdx index b1ae46812b1d..baf48b87a0eb 100644 --- a/api_docs/kbn_core_ui_settings_browser_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-mocks title: "@kbn/core-ui-settings-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-mocks'] --- import kbnCoreUiSettingsBrowserMocksObj from './kbn_core_ui_settings_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_common.mdx b/api_docs/kbn_core_ui_settings_common.mdx index ef778256b799..1aba4f31960a 100644 --- a/api_docs/kbn_core_ui_settings_common.mdx +++ b/api_docs/kbn_core_ui_settings_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-common title: "@kbn/core-ui-settings-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-common plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-common'] --- import kbnCoreUiSettingsCommonObj from './kbn_core_ui_settings_common.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server.mdx b/api_docs/kbn_core_ui_settings_server.mdx index 545b597ef2b8..b2fe92199314 100644 --- a/api_docs/kbn_core_ui_settings_server.mdx +++ b/api_docs/kbn_core_ui_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server title: "@kbn/core-ui-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server'] --- import kbnCoreUiSettingsServerObj from './kbn_core_ui_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_internal.mdx b/api_docs/kbn_core_ui_settings_server_internal.mdx index 99402f8c657c..91c104b8b9de 100644 --- a/api_docs/kbn_core_ui_settings_server_internal.mdx +++ b/api_docs/kbn_core_ui_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-internal title: "@kbn/core-ui-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-internal'] --- import kbnCoreUiSettingsServerInternalObj from './kbn_core_ui_settings_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_mocks.mdx b/api_docs/kbn_core_ui_settings_server_mocks.mdx index a5081a8a512e..31ba42e2554f 100644 --- a/api_docs/kbn_core_ui_settings_server_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-mocks title: "@kbn/core-ui-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-mocks'] --- import kbnCoreUiSettingsServerMocksObj from './kbn_core_ui_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server.mdx b/api_docs/kbn_core_usage_data_server.mdx index b490e18b9988..a25b0cfc501c 100644 --- a/api_docs/kbn_core_usage_data_server.mdx +++ b/api_docs/kbn_core_usage_data_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server title: "@kbn/core-usage-data-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server'] --- import kbnCoreUsageDataServerObj from './kbn_core_usage_data_server.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_internal.mdx b/api_docs/kbn_core_usage_data_server_internal.mdx index 01b13b900344..101431a9c73d 100644 --- a/api_docs/kbn_core_usage_data_server_internal.mdx +++ b/api_docs/kbn_core_usage_data_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-internal title: "@kbn/core-usage-data-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-internal plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-internal'] --- import kbnCoreUsageDataServerInternalObj from './kbn_core_usage_data_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_mocks.mdx b/api_docs/kbn_core_usage_data_server_mocks.mdx index 90ea1b666c8a..710a3c62d544 100644 --- a/api_docs/kbn_core_usage_data_server_mocks.mdx +++ b/api_docs/kbn_core_usage_data_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-mocks title: "@kbn/core-usage-data-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-mocks'] --- import kbnCoreUsageDataServerMocksObj from './kbn_core_usage_data_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index a9fb17b3cd06..a8c5ebefe66c 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] --- import kbnCryptoObj from './kbn_crypto.devdocs.json'; diff --git a/api_docs/kbn_crypto_browser.mdx b/api_docs/kbn_crypto_browser.mdx index 0afac1bca8d3..b7a8b4187db8 100644 --- a/api_docs/kbn_crypto_browser.mdx +++ b/api_docs/kbn_crypto_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto-browser title: "@kbn/crypto-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto-browser plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index d2e455811513..77c10c205e81 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/datemath plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] --- import kbnDatemathObj from './kbn_datemath.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index 42e02f978350..f79bd78824a0 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-errors plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] --- import kbnDevCliErrorsObj from './kbn_dev_cli_errors.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index 5169411f49a2..156f2ad1e34d 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-runner plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] --- import kbnDevCliRunnerObj from './kbn_dev_cli_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index 5872d911b8f4..c40a114fb200 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-proc-runner plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] --- import kbnDevProcRunnerObj from './kbn_dev_proc_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index 8f8299fb3972..6c817ebf71e7 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index 93faea017a0f..055b95affc24 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/doc-links plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] --- import kbnDocLinksObj from './kbn_doc_links.devdocs.json'; diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index f29fe40eb21d..f6a0a8c29e49 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/docs-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] --- import kbnDocsUtilsObj from './kbn_docs_utils.devdocs.json'; diff --git a/api_docs/kbn_ebt_tools.mdx b/api_docs/kbn_ebt_tools.mdx index 2d7b26292e0e..fa63acb9d852 100644 --- a/api_docs/kbn_ebt_tools.mdx +++ b/api_docs/kbn_ebt_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ebt-tools title: "@kbn/ebt-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ebt-tools plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index da7db13bb38b..9f608a801f2c 100644 --- a/api_docs/kbn_es_archiver.mdx +++ b/api_docs/kbn_es_archiver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-archiver title: "@kbn/es-archiver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-archiver plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-archiver'] --- import kbnEsArchiverObj from './kbn_es_archiver.devdocs.json'; diff --git a/api_docs/kbn_es_errors.mdx b/api_docs/kbn_es_errors.mdx index 3d7f8993dd9f..1498ddb5d263 100644 --- a/api_docs/kbn_es_errors.mdx +++ b/api_docs/kbn_es_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-errors title: "@kbn/es-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-errors plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-errors'] --- import kbnEsErrorsObj from './kbn_es_errors.devdocs.json'; diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index db9354454f22..537db266a40f 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-query plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] --- import kbnEsQueryObj from './kbn_es_query.devdocs.json'; diff --git a/api_docs/kbn_es_types.mdx b/api_docs/kbn_es_types.mdx index 5bee2fdbf5f6..923c644404d7 100644 --- a/api_docs/kbn_es_types.mdx +++ b/api_docs/kbn_es_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-types title: "@kbn/es-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-types plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-types'] --- import kbnEsTypesObj from './kbn_es_types.devdocs.json'; diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index f6c2a308c267..23893848967a 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/eslint-plugin-imports plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index 52dcd1a4217e..a29e11c7f3f5 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-types plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] --- import kbnFieldTypesObj from './kbn_field_types.devdocs.json'; diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index e8d91e141f19..c3673495d2d9 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/find-used-node-modules plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] --- import kbnFindUsedNodeModulesObj from './kbn_find_used_node_modules.devdocs.json'; diff --git a/api_docs/kbn_ftr_common_functional_services.mdx b/api_docs/kbn_ftr_common_functional_services.mdx index 93882b49ca42..29968a934531 100644 --- a/api_docs/kbn_ftr_common_functional_services.mdx +++ b/api_docs/kbn_ftr_common_functional_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-services title: "@kbn/ftr-common-functional-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-services plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-services'] --- import kbnFtrCommonFunctionalServicesObj from './kbn_ftr_common_functional_services.devdocs.json'; diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index 5955c7f01158..535192fd2288 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] --- import kbnGenerateObj from './kbn_generate.devdocs.json'; diff --git a/api_docs/kbn_get_repo_files.mdx b/api_docs/kbn_get_repo_files.mdx index e50741bdb3a6..270341543e97 100644 --- a/api_docs/kbn_get_repo_files.mdx +++ b/api_docs/kbn_get_repo_files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-get-repo-files title: "@kbn/get-repo-files" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/get-repo-files plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/get-repo-files'] --- import kbnGetRepoFilesObj from './kbn_get_repo_files.devdocs.json'; diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index 24eedfb0387a..75d249e64bf7 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/handlebars plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] --- import kbnHandlebarsObj from './kbn_handlebars.devdocs.json'; diff --git a/api_docs/kbn_hapi_mocks.mdx b/api_docs/kbn_hapi_mocks.mdx index 1d4fddcc0225..d7346ce1b677 100644 --- a/api_docs/kbn_hapi_mocks.mdx +++ b/api_docs/kbn_hapi_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-hapi-mocks title: "@kbn/hapi-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/hapi-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index 59d37e8a5d09..b1ce2043e27f 100644 --- a/api_docs/kbn_home_sample_data_card.mdx +++ b/api_docs/kbn_home_sample_data_card.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-card title: "@kbn/home-sample-data-card" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-card plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-card'] --- import kbnHomeSampleDataCardObj from './kbn_home_sample_data_card.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_tab.mdx b/api_docs/kbn_home_sample_data_tab.mdx index 5e218789e57f..63510d74b009 100644 --- a/api_docs/kbn_home_sample_data_tab.mdx +++ b/api_docs/kbn_home_sample_data_tab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-tab title: "@kbn/home-sample-data-tab" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-tab plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-tab'] --- import kbnHomeSampleDataTabObj from './kbn_home_sample_data_tab.devdocs.json'; diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index 15647319a90f..ce7de735a110 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index 56039851af76..a96adba4a09b 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/import-resolver plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] --- import kbnImportResolverObj from './kbn_import_resolver.devdocs.json'; diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index 807a82e9cbff..d35da6c7ae88 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/interpreter plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] --- import kbnInterpreterObj from './kbn_interpreter.devdocs.json'; diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index dfb4aea12e2d..623552a19ab9 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/io-ts-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] --- import kbnIoTsUtilsObj from './kbn_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index 28487fab7140..46b40beb8bf3 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/jest-serializers plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] --- import kbnJestSerializersObj from './kbn_jest_serializers.devdocs.json'; diff --git a/api_docs/kbn_journeys.mdx b/api_docs/kbn_journeys.mdx index 01244be181bf..e9b4b290f7d1 100644 --- a/api_docs/kbn_journeys.mdx +++ b/api_docs/kbn_journeys.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-journeys title: "@kbn/journeys" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/journeys plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/journeys'] --- import kbnJourneysObj from './kbn_journeys.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index 7d70916d448e..3f19d6bc0c35 100644 --- a/api_docs/kbn_kibana_manifest_schema.mdx +++ b/api_docs/kbn_kibana_manifest_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-manifest-schema title: "@kbn/kibana-manifest-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/kibana-manifest-schema plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 63d9dc96362a..da43bfec51c4 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] --- import kbnLoggingObj from './kbn_logging.devdocs.json'; diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index 6b1049cee9be..2770d7ccb6ac 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] --- import kbnLoggingMocksObj from './kbn_logging_mocks.devdocs.json'; diff --git a/api_docs/kbn_managed_vscode_config.mdx b/api_docs/kbn_managed_vscode_config.mdx index a175904d0257..83b9321c6a3f 100644 --- a/api_docs/kbn_managed_vscode_config.mdx +++ b/api_docs/kbn_managed_vscode_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-vscode-config title: "@kbn/managed-vscode-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-vscode-config plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-vscode-config'] --- import kbnManagedVscodeConfigObj from './kbn_managed_vscode_config.devdocs.json'; diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index ca5e2566d884..ae4854e96ac2 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mapbox-gl plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] --- import kbnMapboxGlObj from './kbn_mapbox_gl.devdocs.json'; diff --git a/api_docs/kbn_ml_agg_utils.mdx b/api_docs/kbn_ml_agg_utils.mdx index 78c0d9fbb836..dd5f5c5ace18 100644 --- a/api_docs/kbn_ml_agg_utils.mdx +++ b/api_docs/kbn_ml_agg_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-agg-utils title: "@kbn/ml-agg-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-agg-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-agg-utils'] --- import kbnMlAggUtilsObj from './kbn_ml_agg_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index ffdc48a0fb0d..4a750b0b7dfa 100644 --- a/api_docs/kbn_ml_is_populated_object.mdx +++ b/api_docs/kbn_ml_is_populated_object.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-populated-object title: "@kbn/ml-is-populated-object" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-populated-object plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-populated-object'] --- import kbnMlIsPopulatedObjectObj from './kbn_ml_is_populated_object.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index a1f168d3fc91..170269826262 100644 --- a/api_docs/kbn_ml_string_hash.mdx +++ b/api_docs/kbn_ml_string_hash.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-string-hash title: "@kbn/ml-string-hash" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-string-hash plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index 7da86649ddf3..987ec45f387a 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/monaco plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] --- import kbnMonacoObj from './kbn_monaco.devdocs.json'; diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index 29b3596a8bc2..ed7566b575b5 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] --- import kbnOptimizerObj from './kbn_optimizer.devdocs.json'; diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index fb729fc0747e..7c45eeef92b5 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] --- import kbnOptimizerWebpackHelpersObj from './kbn_optimizer_webpack_helpers.devdocs.json'; diff --git a/api_docs/kbn_osquery_io_ts_types.mdx b/api_docs/kbn_osquery_io_ts_types.mdx index 54d57200bd5a..859b6a067644 100644 --- a/api_docs/kbn_osquery_io_ts_types.mdx +++ b/api_docs/kbn_osquery_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-osquery-io-ts-types title: "@kbn/osquery-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/osquery-io-ts-types plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/osquery-io-ts-types'] --- import kbnOsqueryIoTsTypesObj from './kbn_osquery_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index ba91201a52c6..aedd0101a45a 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] --- import kbnPerformanceTestingDatasetExtractorObj from './kbn_performance_testing_dataset_extractor.devdocs.json'; diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index 493ca235ab7a..bfd4b4e6b5af 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-generator plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] --- import kbnPluginGeneratorObj from './kbn_plugin_generator.devdocs.json'; diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index b16b234ee0f5..3ca9f9e155f1 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-helpers plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] --- import kbnPluginHelpersObj from './kbn_plugin_helpers.devdocs.json'; diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index f7d3bada225b..293fa5190545 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-field plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index 65a1a0d424f1..aaf5d45ea057 100644 --- a/api_docs/kbn_repo_source_classifier.mdx +++ b/api_docs/kbn_repo_source_classifier.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-source-classifier title: "@kbn/repo-source-classifier" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-source-classifier plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; diff --git a/api_docs/kbn_rule_data_utils.devdocs.json b/api_docs/kbn_rule_data_utils.devdocs.json index c30477647a1c..2d6403bd41df 100644 --- a/api_docs/kbn_rule_data_utils.devdocs.json +++ b/api_docs/kbn_rule_data_utils.devdocs.json @@ -912,7 +912,7 @@ "label": "AlertConsumers", "description": [], "signature": [ - "\"observability\" | \"logs\" | \"apm\" | \"uptime\" | \"siem\" | \"infrastructure\"" + "\"infrastructure\" | \"observability\" | \"logs\" | \"apm\" | \"uptime\" | \"siem\"" ], "path": "packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts", "deprecated": false, @@ -1077,7 +1077,7 @@ "label": "TechnicalRuleDataFieldName", "description": [], "signature": [ - "\"tags\" | \"kibana\" | \"@timestamp\" | \"event.action\" | \"kibana.alert.rule.parameters\" | \"kibana.alert.rule.rule_type_id\" | \"kibana.alert.rule.consumer\" | \"kibana.alert.rule.producer\" | \"kibana.space_ids\" | \"kibana.alert.uuid\" | \"kibana.alert.instance.id\" | \"kibana.alert.start\" | \"kibana.alert.end\" | \"kibana.alert.duration.us\" | \"kibana.alert.severity\" | \"kibana.alert.status\" | \"kibana.version\" | \"ecs.version\" | \"kibana.alert.risk_score\" | \"kibana.alert.workflow_status\" | \"kibana.alert.workflow_user\" | \"kibana.alert.workflow_reason\" | \"kibana.alert.system_status\" | \"kibana.alert.action_group\" | \"kibana.alert.reason\" | \"kibana.alert.rule.author\" | \"kibana.alert.rule.category\" | \"kibana.alert.rule.uuid\" | \"kibana.alert.rule.created_at\" | \"kibana.alert.rule.created_by\" | \"kibana.alert.rule.description\" | \"kibana.alert.rule.enabled\" | \"kibana.alert.rule.execution.uuid\" | \"kibana.alert.rule.from\" | \"kibana.alert.rule.interval\" | \"kibana.alert.rule.license\" | \"kibana.alert.rule.name\" | \"kibana.alert.rule.note\" | \"kibana.alert.rule.references\" | \"kibana.alert.rule.rule_id\" | \"kibana.alert.rule.rule_name_override\" | \"kibana.alert.rule.tags\" | \"kibana.alert.rule.to\" | \"kibana.alert.rule.type\" | \"kibana.alert.rule.updated_at\" | \"kibana.alert.rule.updated_by\" | \"kibana.alert.rule.version\" | \"event.kind\" | \"event.module\" | \"kibana.alert.evaluation.threshold\" | \"kibana.alert.evaluation.value\" | \"kibana.alert.building_block_type\" | \"kibana.alert.rule.exceptions_list\" | \"kibana.alert.rule.namespace\" | \"kibana.alert\" | \"kibana.alert.rule\"" + "\"tags\" | \"kibana\" | \"@timestamp\" | \"kibana.alert.rule.rule_type_id\" | \"kibana.alert.rule.consumer\" | \"event.action\" | \"kibana.alert.rule.execution.uuid\" | \"kibana.alert.rule.parameters\" | \"kibana.alert.rule.producer\" | \"kibana.space_ids\" | \"kibana.alert.uuid\" | \"kibana.alert.instance.id\" | \"kibana.alert.start\" | \"kibana.alert.end\" | \"kibana.alert.duration.us\" | \"kibana.alert.severity\" | \"kibana.alert.status\" | \"kibana.version\" | \"ecs.version\" | \"kibana.alert.risk_score\" | \"kibana.alert.workflow_status\" | \"kibana.alert.workflow_user\" | \"kibana.alert.workflow_reason\" | \"kibana.alert.system_status\" | \"kibana.alert.action_group\" | \"kibana.alert.reason\" | \"kibana.alert.rule.author\" | \"kibana.alert.rule.category\" | \"kibana.alert.rule.uuid\" | \"kibana.alert.rule.created_at\" | \"kibana.alert.rule.created_by\" | \"kibana.alert.rule.description\" | \"kibana.alert.rule.enabled\" | \"kibana.alert.rule.from\" | \"kibana.alert.rule.interval\" | \"kibana.alert.rule.license\" | \"kibana.alert.rule.name\" | \"kibana.alert.rule.note\" | \"kibana.alert.rule.references\" | \"kibana.alert.rule.rule_id\" | \"kibana.alert.rule.rule_name_override\" | \"kibana.alert.rule.tags\" | \"kibana.alert.rule.to\" | \"kibana.alert.rule.type\" | \"kibana.alert.rule.updated_at\" | \"kibana.alert.rule.updated_by\" | \"kibana.alert.rule.version\" | \"event.kind\" | \"event.module\" | \"kibana.alert.evaluation.threshold\" | \"kibana.alert.evaluation.value\" | \"kibana.alert.building_block_type\" | \"kibana.alert.rule.exceptions_list\" | \"kibana.alert.rule.namespace\" | \"kibana.alert\" | \"kibana.alert.rule\"" ], "path": "packages/kbn-rule-data-utils/src/technical_field_names.ts", "deprecated": false, @@ -1107,7 +1107,7 @@ "label": "ValidFeatureId", "description": [], "signature": [ - "\"observability\" | \"logs\" | \"apm\" | \"uptime\" | \"siem\" | \"infrastructure\"" + "\"infrastructure\" | \"observability\" | \"logs\" | \"apm\" | \"uptime\" | \"siem\"" ], "path": "packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts", "deprecated": false, diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index ef453ad073e4..4f65db7503fc 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rule-data-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index f441cc10aa50..5a9078575773 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index c81350b38632..dc5fcdbd1d0f 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-es-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] --- import kbnSecuritysolutionEsUtilsObj from './kbn_securitysolution_es_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_exception_list_components.devdocs.json b/api_docs/kbn_securitysolution_exception_list_components.devdocs.json new file mode 100644 index 000000000000..97b711804af1 --- /dev/null +++ b/api_docs/kbn_securitysolution_exception_list_components.devdocs.json @@ -0,0 +1,1209 @@ +{ + "id": "@kbn/securitysolution-exception-list-components", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.EmptyViewerState", + "type": "Function", + "tags": [], + "label": "EmptyViewerState", + "description": [], + "signature": [ + "React.NamedExoticComponent" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/empty_viewer_state/empty_viewer_state.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.EmptyViewerState.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCard", + "type": "Function", + "tags": [], + "label": "ExceptionItemCard", + "description": [], + "signature": [ + "React.NamedExoticComponent<", + { + "pluginId": "@kbn/securitysolution-exception-list-components", + "scope": "common", + "docId": "kibKbnSecuritysolutionExceptionListComponentsPluginApi", + "section": "def-common.ExceptionItemProps", + "text": "ExceptionItemProps" + }, + ">" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCard.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardComments", + "type": "Function", + "tags": [], + "label": "ExceptionItemCardComments", + "description": [], + "signature": [ + "React.NamedExoticComponent<", + { + "pluginId": "@kbn/securitysolution-exception-list-components", + "scope": "common", + "docId": "kibKbnSecuritysolutionExceptionListComponentsPluginApi", + "section": "def-common.ExceptionItemCardCommentsProps", + "text": "ExceptionItemCardCommentsProps" + }, + ">" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/comments/comments.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardComments.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardConditions", + "type": "Function", + "tags": [], + "label": "ExceptionItemCardConditions", + "description": [], + "signature": [ + "React.NamedExoticComponent<", + "CriteriaConditionsProps", + ">" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardConditions.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardHeader", + "type": "Function", + "tags": [], + "label": "ExceptionItemCardHeader", + "description": [], + "signature": [ + "React.NamedExoticComponent<", + { + "pluginId": "@kbn/securitysolution-exception-list-components", + "scope": "common", + "docId": "kibKbnSecuritysolutionExceptionListComponentsPluginApi", + "section": "def-common.ExceptionItemCardHeaderProps", + "text": "ExceptionItemCardHeaderProps" + }, + ">" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardHeader.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardMetaInfo", + "type": "Function", + "tags": [], + "label": "ExceptionItemCardMetaInfo", + "description": [], + "signature": [ + "React.NamedExoticComponent<", + { + "pluginId": "@kbn/securitysolution-exception-list-components", + "scope": "common", + "docId": "kibKbnSecuritysolutionExceptionListComponentsPluginApi", + "section": "def-common.ExceptionItemCardMetaInfoProps", + "text": "ExceptionItemCardMetaInfoProps" + }, + ">" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardMetaInfo.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItems", + "type": "Function", + "tags": [], + "label": "ExceptionItems", + "description": [], + "signature": [ + "React.NamedExoticComponent" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_items/exception_items.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItems.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.Pagination", + "type": "Function", + "tags": [], + "label": "Pagination", + "description": [], + "signature": [ + "React.NamedExoticComponent<", + { + "pluginId": "@kbn/securitysolution-exception-list-components", + "scope": "common", + "docId": "kibKbnSecuritysolutionExceptionListComponentsPluginApi", + "section": "def-common.PaginationProps", + "text": "PaginationProps" + }, + ">" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/pagination/pagination.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.Pagination.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.SearchBar", + "type": "Function", + "tags": [], + "label": "SearchBar", + "description": [], + "signature": [ + "React.NamedExoticComponent" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/search_bar/search_bar.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.SearchBar.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ValueWithSpaceWarning", + "type": "Function", + "tags": [], + "label": "ValueWithSpaceWarning", + "description": [], + "signature": [ + "({ value, tooltipIconType, tooltipIconText, }: React.PropsWithChildren) => JSX.Element | null" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/value_with_space_warning.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ValueWithSpaceWarning.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n value,\n tooltipIconType = 'iInCircle',\n tooltipIconText,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/value_with_space_warning.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardCommentsProps", + "type": "Interface", + "tags": [], + "label": "ExceptionItemCardCommentsProps", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/comments/comments.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardCommentsProps.comments", + "type": "Array", + "tags": [], + "label": "comments", + "description": [], + "signature": [ + "EuiCommentProps", + "[]" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/comments/comments.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardHeaderProps", + "type": "Interface", + "tags": [], + "label": "ExceptionItemCardHeaderProps", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardHeaderProps.item", + "type": "Object", + "tags": [], + "label": "item", + "description": [], + "signature": [ + "{ _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"text\" | \"geo_point\" | \"geo_shape\" | \"date_nanos\" | \"long\" | \"double\" | \"date_range\" | \"ip_range\" | \"shape\" | \"short\" | \"binary\" | \"float\" | \"half_float\" | \"integer\" | \"byte\" | \"long_range\" | \"integer_range\" | \"float_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; }" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardHeaderProps.actions", + "type": "Array", + "tags": [], + "label": "actions", + "description": [], + "signature": [ + "{ key: string; icon: string; label: string | boolean; onClick: () => void; }[]" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardHeaderProps.disableActions", + "type": "CompoundType", + "tags": [], + "label": "disableActions", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardHeaderProps.dataTestSubj", + "type": "string", + "tags": [], + "label": "dataTestSubj", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardMetaInfoProps", + "type": "Interface", + "tags": [], + "label": "ExceptionItemCardMetaInfoProps", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardMetaInfoProps.item", + "type": "Object", + "tags": [], + "label": "item", + "description": [], + "signature": [ + "{ _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"text\" | \"geo_point\" | \"geo_shape\" | \"date_nanos\" | \"long\" | \"double\" | \"date_range\" | \"ip_range\" | \"shape\" | \"short\" | \"binary\" | \"float\" | \"half_float\" | \"integer\" | \"byte\" | \"long_range\" | \"integer_range\" | \"float_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; }" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardMetaInfoProps.references", + "type": "Array", + "tags": [], + "label": "references", + "description": [], + "signature": [ + { + "pluginId": "@kbn/securitysolution-exception-list-components", + "scope": "common", + "docId": "kibKbnSecuritysolutionExceptionListComponentsPluginApi", + "section": "def-common.RuleReference", + "text": "RuleReference" + }, + "[]" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardMetaInfoProps.dataTestSubj", + "type": "string", + "tags": [], + "label": "dataTestSubj", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardMetaInfoProps.formattedDateComponent", + "type": "CompoundType", + "tags": [], + "label": "formattedDateComponent", + "description": [], + "signature": [ + "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"source\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"output\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"article\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"br\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemCardMetaInfoProps.securityLinkAnchorComponent", + "type": "CompoundType", + "tags": [], + "label": "securityLinkAnchorComponent", + "description": [], + "signature": [ + "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"source\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"output\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"article\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"br\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps", + "type": "Interface", + "tags": [], + "label": "ExceptionItemProps", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.dataTestSubj", + "type": "string", + "tags": [], + "label": "dataTestSubj", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.disableActions", + "type": "CompoundType", + "tags": [], + "label": "disableActions", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.exceptionItem", + "type": "Object", + "tags": [], + "label": "exceptionItem", + "description": [], + "signature": [ + "{ _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"text\" | \"geo_point\" | \"geo_shape\" | \"date_nanos\" | \"long\" | \"double\" | \"date_range\" | \"ip_range\" | \"shape\" | \"short\" | \"binary\" | \"float\" | \"half_float\" | \"integer\" | \"byte\" | \"long_range\" | \"integer_range\" | \"float_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; }" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.listType", + "type": "Enum", + "tags": [], + "label": "listType", + "description": [], + "signature": [ + "ExceptionListTypeEnum" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.ruleReferences", + "type": "Array", + "tags": [], + "label": "ruleReferences", + "description": [], + "signature": [ + "any[]" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.editActionLabel", + "type": "string", + "tags": [], + "label": "editActionLabel", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.deleteActionLabel", + "type": "string", + "tags": [], + "label": "deleteActionLabel", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.securityLinkAnchorComponent", + "type": "CompoundType", + "tags": [], + "label": "securityLinkAnchorComponent", + "description": [], + "signature": [ + "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"source\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"output\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"article\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"br\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.formattedDateComponent", + "type": "CompoundType", + "tags": [], + "label": "formattedDateComponent", + "description": [], + "signature": [ + "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"source\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"output\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"article\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"br\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.getFormattedComments", + "type": "Function", + "tags": [], + "label": "getFormattedComments", + "description": [], + "signature": [ + "(comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]) => ", + "EuiCommentProps", + "[]" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.getFormattedComments.$1", + "type": "Array", + "tags": [], + "label": "comments", + "description": [], + "signature": [ + "({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.onDeleteException", + "type": "Function", + "tags": [], + "label": "onDeleteException", + "description": [], + "signature": [ + "(arg: ", + { + "pluginId": "@kbn/securitysolution-exception-list-components", + "scope": "common", + "docId": "kibKbnSecuritysolutionExceptionListComponentsPluginApi", + "section": "def-common.ExceptionListItemIdentifiers", + "text": "ExceptionListItemIdentifiers" + }, + ") => void" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.onDeleteException.$1", + "type": "Object", + "tags": [], + "label": "arg", + "description": [], + "signature": [ + { + "pluginId": "@kbn/securitysolution-exception-list-components", + "scope": "common", + "docId": "kibKbnSecuritysolutionExceptionListComponentsPluginApi", + "section": "def-common.ExceptionListItemIdentifiers", + "text": "ExceptionListItemIdentifiers" + } + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.onEditException", + "type": "Function", + "tags": [], + "label": "onEditException", + "description": [], + "signature": [ + "(item: { _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"text\" | \"geo_point\" | \"geo_shape\" | \"date_nanos\" | \"long\" | \"double\" | \"date_range\" | \"ip_range\" | \"shape\" | \"short\" | \"binary\" | \"float\" | \"half_float\" | \"integer\" | \"byte\" | \"long_range\" | \"integer_range\" | \"float_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; }) => void" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionItemProps.onEditException.$1", + "type": "Object", + "tags": [], + "label": "item", + "description": [], + "signature": [ + "{ _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"keyword\" | \"ip\" | \"text\" | \"geo_point\" | \"geo_shape\" | \"date_nanos\" | \"long\" | \"double\" | \"date_range\" | \"ip_range\" | \"shape\" | \"short\" | \"binary\" | \"float\" | \"half_float\" | \"integer\" | \"byte\" | \"long_range\" | \"integer_range\" | \"float_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; }" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionListItemIdentifiers", + "type": "Interface", + "tags": [], + "label": "ExceptionListItemIdentifiers", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionListItemIdentifiers.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionListItemIdentifiers.name", + "type": "string", + "tags": [], + "label": "name", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionListItemIdentifiers.namespaceType", + "type": "CompoundType", + "tags": [], + "label": "namespaceType", + "description": [], + "signature": [ + "\"single\" | \"agnostic\"" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionListSummaryProps", + "type": "Interface", + "tags": [], + "label": "ExceptionListSummaryProps", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionListSummaryProps.pagination", + "type": "Object", + "tags": [], + "label": "pagination", + "description": [], + "signature": [ + "Pagination" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ExceptionListSummaryProps.lastUpdated", + "type": "CompoundType", + "tags": [], + "label": "lastUpdated", + "description": [], + "signature": [ + "string | number | null" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.GetExceptionItemProps", + "type": "Interface", + "tags": [], + "label": "GetExceptionItemProps", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.GetExceptionItemProps.pagination", + "type": "Object", + "tags": [], + "label": "pagination", + "description": [], + "signature": [ + "Pagination", + " | undefined" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.GetExceptionItemProps.search", + "type": "string", + "tags": [], + "label": "search", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.GetExceptionItemProps.filters", + "type": "string", + "tags": [], + "label": "filters", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.PaginationProps", + "type": "Interface", + "tags": [], + "label": "PaginationProps", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.PaginationProps.dataTestSubj", + "type": "string", + "tags": [], + "label": "dataTestSubj", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.PaginationProps.ariaLabel", + "type": "string", + "tags": [], + "label": "ariaLabel", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.PaginationProps.pagination", + "type": "Object", + "tags": [], + "label": "pagination", + "description": [], + "signature": [ + "Pagination" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.PaginationProps.onPaginationChange", + "type": "Function", + "tags": [], + "label": "onPaginationChange", + "description": [], + "signature": [ + "(arg: ", + { + "pluginId": "@kbn/securitysolution-exception-list-components", + "scope": "common", + "docId": "kibKbnSecuritysolutionExceptionListComponentsPluginApi", + "section": "def-common.GetExceptionItemProps", + "text": "GetExceptionItemProps" + }, + ") => void" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.PaginationProps.onPaginationChange.$1", + "type": "Object", + "tags": [], + "label": "arg", + "description": [], + "signature": [ + { + "pluginId": "@kbn/securitysolution-exception-list-components", + "scope": "common", + "docId": "kibKbnSecuritysolutionExceptionListComponentsPluginApi", + "section": "def-common.GetExceptionItemProps", + "text": "GetExceptionItemProps" + } + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.RuleReference", + "type": "Interface", + "tags": [], + "label": "RuleReference", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.RuleReference.name", + "type": "string", + "tags": [], + "label": "name", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.RuleReference.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.RuleReference.ruleId", + "type": "string", + "tags": [], + "label": "ruleId", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.RuleReference.exceptionLists", + "type": "Array", + "tags": [], + "label": "exceptionLists", + "description": [], + "signature": [ + "{ _version: string | undefined; created_at: string; created_by: string; description: string; id: string; immutable: boolean; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"endpoint\" | \"detection\" | \"rule_default\" | \"endpoint_trusted_apps\" | \"endpoint_events\" | \"endpoint_host_isolation_exceptions\" | \"endpoint_blocklists\"; updated_at: string; updated_by: string; version: number; }[]" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.RuleReferences", + "type": "Interface", + "tags": [], + "label": "RuleReferences", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.RuleReferences.Unnamed", + "type": "IndexSignature", + "tags": [], + "label": "[key: string]: any[]", + "description": [], + "signature": [ + "[key: string]: any[]" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ListTypeText", + "type": "Enum", + "tags": [], + "label": "ListTypeText", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ViewerStatus", + "type": "Enum", + "tags": [], + "label": "ViewerStatus", + "description": [], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "misc": [ + { + "parentPluginId": "@kbn/securitysolution-exception-list-components", + "id": "def-common.ViewerFlyoutName", + "type": "Type", + "tags": [], + "label": "ViewerFlyoutName", + "description": [], + "signature": [ + "\"addException\" | \"editException\" | null" + ], + "path": "packages/kbn-securitysolution-exception-list-components/src/types/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_securitysolution_exception_list_components.mdx b/api_docs/kbn_securitysolution_exception_list_components.mdx new file mode 100644 index 000000000000..1bcc5017dfc5 --- /dev/null +++ b/api_docs/kbn_securitysolution_exception_list_components.mdx @@ -0,0 +1,39 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnSecuritysolutionExceptionListComponentsPluginApi +slug: /kibana-dev-docs/api/kbn-securitysolution-exception-list-components +title: "@kbn/securitysolution-exception-list-components" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/securitysolution-exception-list-components plugin +date: 2022-09-29 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-exception-list-components'] +--- +import kbnSecuritysolutionExceptionListComponentsObj from './kbn_securitysolution_exception_list_components.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 76 | 0 | 67 | 1 | + +## Common + +### Functions + + +### Interfaces + + +### Enums + + +### Consts, variables and types + + diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index 8e6bdc15f980..a22707f2957b 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] --- import kbnSecuritysolutionHookUtilsObj from './kbn_securitysolution_hook_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.devdocs.json b/api_docs/kbn_securitysolution_io_ts_alerting_types.devdocs.json index b42cebbc4fb8..f9869d88c91c 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.devdocs.json +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.devdocs.json @@ -877,21 +877,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", - "id": "def-common.ThrottleOrUndefinedOrNull", - "type": "Type", - "tags": [], - "label": "ThrottleOrUndefinedOrNull", - "description": [], - "signature": [ - "string | null | undefined" - ], - "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.Type", @@ -1383,24 +1368,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", - "id": "def-common.DefaultThrottleNull", - "type": "Object", - "tags": [], - "label": "DefaultThrottleNull", - "description": [ - "\nTypes the DefaultThrottleNull as:\n - If null or undefined, then a null will be set" - ], - "signature": [ - "Type", - "" - ], - "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.DefaultToString", @@ -2921,7 +2888,14 @@ "label": "throttle", "description": [], "signature": [ - "StringC" + "UnionC", + "<[", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts", "deprecated": false, @@ -2938,31 +2912,15 @@ "signature": [ "UnionC", "<[", - "StringC", - ", ", - "NullC", - "]>" - ], - "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, - { - "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", - "id": "def-common.throttleOrNullOrUndefined", - "type": "Object", - "tags": [], - "label": "throttleOrNullOrUndefined", - "description": [], - "signature": [ "UnionC", "<[", - "StringC", - ", ", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>, ", "NullC", - ", ", - "UndefinedC", "]>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts", diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index 4066f50c3352..cdc1b3ea54ed 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] --- import kbnSecuritysolutionIoTsAlertingTypesObj from './kbn_securitysolution_io_ts_alerting_types.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 148 | 0 | 129 | 0 | +| 145 | 0 | 127 | 0 | ## Common diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index 720ede9fa9e8..185f8a6c2a54 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] --- import kbnSecuritysolutionIoTsListTypesObj from './kbn_securitysolution_io_ts_list_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_types.devdocs.json b/api_docs/kbn_securitysolution_io_ts_types.devdocs.json index 2ec94e9e8c1c..ef934492d28e 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.devdocs.json +++ b/api_docs/kbn_securitysolution_io_ts_types.devdocs.json @@ -405,6 +405,41 @@ ], "returnComment": [], "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-types", + "id": "def-common.TimeDuration", + "type": "Function", + "tags": [], + "label": "TimeDuration", + "description": [], + "signature": [ + "({ allowedUnits, allowedDurations }: TimeDurationType) => ", + "Type", + "" + ], + "path": "packages/kbn-securitysolution-io-ts-types/src/time_duration/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-io-ts-types", + "id": "def-common.TimeDuration.$1", + "type": "CompoundType", + "tags": [], + "label": "{ allowedUnits, allowedDurations }", + "description": [], + "signature": [ + "TimeDurationType" + ], + "path": "packages/kbn-securitysolution-io-ts-types/src/time_duration/index.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false } ], "interfaces": [], @@ -644,12 +679,30 @@ "label": "TimeDurationC", "description": [], "signature": [ + "({ allowedUnits, allowedDurations }: TimeDurationType) => ", "Type", "" ], "path": "packages/kbn-securitysolution-io-ts-types/src/time_duration/index.ts", "deprecated": false, "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/securitysolution-io-ts-types", + "id": "def-common.TimeDurationC.$1", + "type": "CompoundType", + "tags": [], + "label": "__0", + "description": [], + "signature": [ + "TimeDurationWithAllowedDurations | TimeDurationWithAllowedUnits" + ], + "path": "packages/kbn-securitysolution-io-ts-types/src/time_duration/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], "initialIsOpen": false }, { @@ -1042,24 +1095,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "@kbn/securitysolution-io-ts-types", - "id": "def-common.TimeDuration", - "type": "Object", - "tags": [], - "label": "TimeDuration", - "description": [ - "\nTypes the TimeDuration as:\n - A string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time\n - in the format {safe_integer}{timeUnit}, e.g. \"30s\", \"1m\", \"2h\"" - ], - "signature": [ - "Type", - "" - ], - "path": "packages/kbn-securitysolution-io-ts-types/src/time_duration/index.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "@kbn/securitysolution-io-ts-types", "id": "def-common.UUID", diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index f2220252df9f..ee826f79d080 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] --- import kbnSecuritysolutionIoTsTypesObj from './kbn_securitysolution_io_ts_types.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 63 | 0 | 33 | 0 | +| 65 | 0 | 36 | 0 | ## Common diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index 4ddd805becee..79363d1648a7 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] --- import kbnSecuritysolutionIoTsUtilsObj from './kbn_securitysolution_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index 837b102f35b7..bad03cd4ead9 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-api plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] --- import kbnSecuritysolutionListApiObj from './kbn_securitysolution_list_api.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index 5e117e7fdd5f..d2e797f19a3e 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-constants plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] --- import kbnSecuritysolutionListConstantsObj from './kbn_securitysolution_list_constants.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index a0d87f4a94bb..aadd62030a1b 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] --- import kbnSecuritysolutionListHooksObj from './kbn_securitysolution_list_hooks.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index a42cbdfe8731..37f5836577b4 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] --- import kbnSecuritysolutionListUtilsObj from './kbn_securitysolution_list_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index c978507107c7..7aa2954ee0c0 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-rules plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] --- import kbnSecuritysolutionRulesObj from './kbn_securitysolution_rules.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index d7cd694497c7..99fe2b585b0b 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-t-grid plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] --- import kbnSecuritysolutionTGridObj from './kbn_securitysolution_t_grid.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index c6c4f053e952..e132797fea85 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-utils plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] --- import kbnSecuritysolutionUtilsObj from './kbn_securitysolution_utils.devdocs.json'; diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index fc18c7757d1d..48d0d4756ef6 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-http-tools plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] --- import kbnServerHttpToolsObj from './kbn_server_http_tools.devdocs.json'; diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index 2c9d51b46886..5c184c9a583c 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] --- import kbnServerRouteRepositoryObj from './kbn_server_route_repository.devdocs.json'; diff --git a/api_docs/kbn_shared_svg.mdx b/api_docs/kbn_shared_svg.mdx index 1a40ade046a8..249f6a00cf7e 100644 --- a/api_docs/kbn_shared_svg.mdx +++ b/api_docs/kbn_shared_svg.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-svg title: "@kbn/shared-svg" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-svg plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-svg'] --- import kbnSharedSvgObj from './kbn_shared_svg.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx index 6d9064ab9d9a..e5e0f236974f 100644 --- a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx +++ b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-user-profile-components title: "@kbn/shared-ux-avatar-user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-user-profile-components plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-user-profile-components'] --- import kbnSharedUxAvatarUserProfileComponentsObj from './kbn_shared_ux_avatar_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx index e28b92febd73..b78abda31ac5 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen-mocks title: "@kbn/shared-ux-button-exit-full-screen-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen-mocks'] --- import kbnSharedUxButtonExitFullScreenMocksObj from './kbn_shared_ux_button_exit_full_screen_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index 3515b5ee35bf..dda07aa08205 100644 --- a/api_docs/kbn_shared_ux_button_toolbar.mdx +++ b/api_docs/kbn_shared_ux_button_toolbar.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-toolbar title: "@kbn/shared-ux-button-toolbar" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-toolbar plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-toolbar'] --- import kbnSharedUxButtonToolbarObj from './kbn_shared_ux_button_toolbar.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data.mdx b/api_docs/kbn_shared_ux_card_no_data.mdx index ba2f64392e72..92fa45970a95 100644 --- a/api_docs/kbn_shared_ux_card_no_data.mdx +++ b/api_docs/kbn_shared_ux_card_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data title: "@kbn/shared-ux-card-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data'] --- import kbnSharedUxCardNoDataObj from './kbn_shared_ux_card_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx index 6942dcbd38fc..67fd1537b953 100644 --- a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data-mocks title: "@kbn/shared-ux-card-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data-mocks'] --- import kbnSharedUxCardNoDataMocksObj from './kbn_shared_ux_card_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx index 69a6794fbccd..dba90c297e1b 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app-mocks title: "@kbn/shared-ux-link-redirect-app-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app-mocks'] --- import kbnSharedUxLinkRedirectAppMocksObj from './kbn_shared_ux_link_redirect_app_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index a8465df7126e..eb63fe71719d 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] --- import kbnSharedUxPageAnalyticsNoDataObj from './kbn_shared_ux_page_analytics_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx index 9e930a85deb8..312cc6dc442d 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data-mocks title: "@kbn/shared-ux-page-analytics-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data-mocks'] --- import kbnSharedUxPageAnalyticsNoDataMocksObj from './kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index d96c9079cf5f..fc5ddb138a55 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] --- import kbnSharedUxPageKibanaNoDataObj from './kbn_shared_ux_page_kibana_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx index 86595fb7cfb1..00f4e0b05251 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data-mocks title: "@kbn/shared-ux-page-kibana-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data-mocks'] --- import kbnSharedUxPageKibanaNoDataMocksObj from './kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template.mdx b/api_docs/kbn_shared_ux_page_kibana_template.mdx index 913279665176..22e5de7cb344 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template title: "@kbn/shared-ux-page-kibana-template" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template'] --- import kbnSharedUxPageKibanaTemplateObj from './kbn_shared_ux_page_kibana_template.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx index e5b6a7845bbe..de0d8b5ff101 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template-mocks title: "@kbn/shared-ux-page-kibana-template-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template-mocks'] --- import kbnSharedUxPageKibanaTemplateMocksObj from './kbn_shared_ux_page_kibana_template_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data.mdx b/api_docs/kbn_shared_ux_page_no_data.mdx index 09f6ed5cf1fa..b1597719e26a 100644 --- a/api_docs/kbn_shared_ux_page_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data title: "@kbn/shared-ux-page-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data'] --- import kbnSharedUxPageNoDataObj from './kbn_shared_ux_page_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config.mdx b/api_docs/kbn_shared_ux_page_no_data_config.mdx index ef0e7d2b60bf..254ce88f6ed0 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config title: "@kbn/shared-ux-page-no-data-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config'] --- import kbnSharedUxPageNoDataConfigObj from './kbn_shared_ux_page_no_data_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx index 43ef0de76bb2..4b7ce948af06 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config-mocks title: "@kbn/shared-ux-page-no-data-config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config-mocks'] --- import kbnSharedUxPageNoDataConfigMocksObj from './kbn_shared_ux_page_no_data_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx index eecbe82f148c..4bebd9fa27f9 100644 --- a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-mocks title: "@kbn/shared-ux-page-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-mocks'] --- import kbnSharedUxPageNoDataMocksObj from './kbn_shared_ux_page_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_solution_nav.mdx b/api_docs/kbn_shared_ux_page_solution_nav.mdx index 001790b9086e..a3b2ddd97a17 100644 --- a/api_docs/kbn_shared_ux_page_solution_nav.mdx +++ b/api_docs/kbn_shared_ux_page_solution_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-solution-nav title: "@kbn/shared-ux-page-solution-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-solution-nav plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-solution-nav'] --- import kbnSharedUxPageSolutionNavObj from './kbn_shared_ux_page_solution_nav.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index 42efbc3a489c..0a1d772cc031 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] --- import kbnSharedUxPromptNoDataViewsObj from './kbn_shared_ux_prompt_no_data_views.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx index 71404d9e00a6..70d3cf84c5ef 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views-mocks title: "@kbn/shared-ux-prompt-no-data-views-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views-mocks'] --- import kbnSharedUxPromptNoDataViewsMocksObj from './kbn_shared_ux_prompt_no_data_views_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx index a24b3d7d985a..61fe760de503 100644 --- a/api_docs/kbn_shared_ux_router.mdx +++ b/api_docs/kbn_shared_ux_router.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router title: "@kbn/shared-ux-router" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router'] --- import kbnSharedUxRouterObj from './kbn_shared_ux_router.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router_mocks.mdx b/api_docs/kbn_shared_ux_router_mocks.mdx index b833764dfacb..02064c4eacc6 100644 --- a/api_docs/kbn_shared_ux_router_mocks.mdx +++ b/api_docs/kbn_shared_ux_router_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router-mocks title: "@kbn/shared-ux-router-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router-mocks plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router-mocks'] --- import kbnSharedUxRouterMocksObj from './kbn_shared_ux_router_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_config.mdx b/api_docs/kbn_shared_ux_storybook_config.mdx index bf5525e9d8ea..86606778f8df 100644 --- a/api_docs/kbn_shared_ux_storybook_config.mdx +++ b/api_docs/kbn_shared_ux_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-config title: "@kbn/shared-ux-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-config plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-config'] --- import kbnSharedUxStorybookConfigObj from './kbn_shared_ux_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_mock.mdx b/api_docs/kbn_shared_ux_storybook_mock.mdx index 16c867044187..180ae33f7885 100644 --- a/api_docs/kbn_shared_ux_storybook_mock.mdx +++ b/api_docs/kbn_shared_ux_storybook_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-mock title: "@kbn/shared-ux-storybook-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-mock plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-mock'] --- import kbnSharedUxStorybookMockObj from './kbn_shared_ux_storybook_mock.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index ef2f54962c58..7c2f7cc09c77 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index 8fa73d4c68da..ef8d669ed27a 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: 2022-09-27 +date: 2022-09-29 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_package_json.mdx b/api_docs/kbn_sort_package_json.mdx index 8a049d4872b7..0e8f9505b35f 100644 --- a/api_docs/kbn_sort_package_json.mdx +++ b/api_docs/kbn_sort_package_json.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sort-package-json title: "@kbn/sort-package-json" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/sort-package-json plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sort-package-json'] --- import kbnSortPackageJsonObj from './kbn_sort_package_json.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index f1ba970a4804..c2a9a2019695 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: 2022-09-27 +date: 2022-09-29 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 2ff62c2d04f3..9662e0b7958f 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: 2022-09-27 +date: 2022-09-29 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 b77c7bfc44e9..f279425a1128 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] --- import kbnStorybookObj from './kbn_storybook.devdocs.json'; diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index f81111a50279..70dd586f3b9f 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: 2022-09-27 +date: 2022-09-29 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 eaf2d94c9a05..2d4b1f7a3e30 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index 96f5d4d6ab2a..f0c472ec736d 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: 2022-09-27 +date: 2022-09-29 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 db3273ea8883..421399623af6 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-subj-selector'] --- import kbnTestSubjSelectorObj from './kbn_test_subj_selector.devdocs.json'; diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index fe4e9cade439..8c06707332c2 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_type_summarizer.mdx b/api_docs/kbn_type_summarizer.mdx index 9744d7e40941..a03912522bea 100644 --- a/api_docs/kbn_type_summarizer.mdx +++ b/api_docs/kbn_type_summarizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer title: "@kbn/type-summarizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/type-summarizer plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer'] --- import kbnTypeSummarizerObj from './kbn_type_summarizer.devdocs.json'; diff --git a/api_docs/kbn_type_summarizer_core.mdx b/api_docs/kbn_type_summarizer_core.mdx index 5bf8d7c64175..7d6e18575e25 100644 --- a/api_docs/kbn_type_summarizer_core.mdx +++ b/api_docs/kbn_type_summarizer_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer-core title: "@kbn/type-summarizer-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/type-summarizer-core plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer-core'] --- import kbnTypeSummarizerCoreObj from './kbn_type_summarizer_core.devdocs.json'; diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index 24144f46636b..3d387dbca9d5 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: 2022-09-27 +date: 2022-09-29 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_theme.mdx b/api_docs/kbn_ui_theme.mdx index fe7f2f02b819..80555e578ee9 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index 7855bb68d62d..0396c24bc40e 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: 2022-09-27 +date: 2022-09-29 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 5adb3b961a12..de93a8fe050a 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: 2022-09-27 +date: 2022-09-29 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 879d594892e8..fe8d933d19a3 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: 2022-09-27 +date: 2022-09-29 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 13a63d5a655f..e9e1e8d327df 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] --- import kbnUtilsObj from './kbn_utils.devdocs.json'; diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx index 17c5bc67927e..96ce8b5f78b1 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator'] --- import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json'; diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index 661dda80a022..2a28c3743374 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] --- import kibanaOverviewObj from './kibana_overview.devdocs.json'; diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index 3a54ee39d711..7ab80f87e401 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: 2022-09-27 +date: 2022-09-29 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 62af983d7933..21d349683d8e 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: 2022-09-27 +date: 2022-09-29 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 c101218ee3f4..b7d40f28432a 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.devdocs.json b/api_docs/lens.devdocs.json index 4fc56c34842e..4d4561547ed2 100644 --- a/api_docs/lens.devdocs.json +++ b/api_docs/lens.devdocs.json @@ -1527,6 +1527,24 @@ "children": [], "returnComment": [] }, + { + "parentPluginId": "lens", + "id": "def-public.DatasourcePublicAPI.isTextBasedLanguage", + "type": "Function", + "tags": [], + "label": "isTextBasedLanguage", + "description": [ + "\nReturns true if this is a text based language datasource" + ], + "signature": [ + "() => boolean" + ], + "path": "x-pack/plugins/lens/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "lens", "id": "def-public.DatasourcePublicAPI.getFilters", @@ -11347,6 +11365,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "lens", + "id": "def-common.ENABLE_SQL", + "type": "string", + "tags": [], + "label": "ENABLE_SQL", + "description": [], + "signature": [ + "\"discover:enableSql\"" + ], + "path": "x-pack/plugins/lens/common/constants.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "lens", "id": "def-common.FormatFactory", @@ -11495,6 +11528,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "lens", + "id": "def-common.OriginalColumn", + "type": "Type", + "tags": [], + "label": "OriginalColumn", + "description": [], + "signature": [ + "{ id: string; label: string; } & ({ operationType: \"date_histogram\"; sourceField: string; } | { operationType: string; sourceField: never; })" + ], + "path": "x-pack/plugins/lens/common/expressions/map_to_columns/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "lens", "id": "def-common.PieChartType", diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index e6eb90ca75ba..0cb5e40b3b4c 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 646 | 0 | 558 | 42 | +| 649 | 0 | 560 | 42 | ## Client diff --git a/api_docs/license_api_guard.devdocs.json b/api_docs/license_api_guard.devdocs.json index a554c2c4b789..03acec45780d 100644 --- a/api_docs/license_api_guard.devdocs.json +++ b/api_docs/license_api_guard.devdocs.json @@ -94,13 +94,7 @@ "description": [], "signature": [ "(handler: ", { "pluginId": "core", diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index 745b2e56c1c9..63d15a7c4065 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: 2022-09-27 +date: 2022-09-29 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 3aa129254bb5..88e17bcee6fe 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: 2022-09-27 +date: 2022-09-29 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 f4406dfe719d..c7000d51761c 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 6aa4f4f22519..9deb279fb73b 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] --- import listsObj from './lists.devdocs.json'; diff --git a/api_docs/management.mdx b/api_docs/management.mdx index 92b5f7e80678..2cec82774807 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: 2022-09-27 +date: 2022-09-29 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 2e5da06f70dd..2667e8f19bae 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: 2022-09-27 +date: 2022-09-29 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 a2dd183ea48b..558352f89505 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index 55b3f0eff7da..b1abe52cefe1 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index dbf1d15ca06d..b9332eb4163e 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: 2022-09-27 +date: 2022-09-29 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 16eb11d15bbf..23c398899ccb 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: 2022-09-27 +date: 2022-09-29 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 e9f3d869a207..bfa907ab0926 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: 2022-09-27 +date: 2022-09-29 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 95e0ce94137e..4336085d004c 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/observability.devdocs.json b/api_docs/observability.devdocs.json index 7d399eaa890b..fd03a2f285ab 100644 --- a/api_docs/observability.devdocs.json +++ b/api_docs/observability.devdocs.json @@ -3704,11 +3704,29 @@ "description": [], "signature": [ "{ get: (id: string) => ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, "; list: () => ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, "[]; register: (objectType: ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, ") => void; has: (id: string) => boolean; }" ], "path": "x-pack/plugins/observability/public/plugin.ts", @@ -7595,13 +7613,7 @@ "label": "context", "description": [], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", " & { licensing: Promise<", { "pluginId": "licensing", @@ -7619,13 +7631,7 @@ "text": "AlertingApiRequestHandlerContext" }, ">; core: Promise<", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.CoreRequestHandlerContext", - "text": "CoreRequestHandlerContext" - }, + "CoreRequestHandlerContext", ">; }" ], "path": "x-pack/plugins/observability/server/routes/types.ts", @@ -7926,7 +7932,7 @@ "label": "ObservabilityConfig", "description": [], "signature": [ - "{ readonly unsafe: Readonly<{} & { slo: Readonly<{} & { enabled: boolean; }>; alertDetails: Readonly<{} & { enabled: boolean; }>; }>; readonly annotations: Readonly<{} & { index: string; enabled: boolean; }>; }" + "{ readonly unsafe: Readonly<{} & { slo: Readonly<{} & { enabled: boolean; }>; alertDetails: Readonly<{} & { enabled: boolean; }>; }>; readonly annotations: Readonly<{} & { enabled: boolean; index: string; }>; }" ], "path": "x-pack/plugins/observability/server/index.ts", "deprecated": false, @@ -9780,13 +9786,7 @@ "description": [], "signature": [ "{ getScopedAnnotationsClient: (requestContext: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", " & { licensing: Promise<", { "pluginId": "licensing", diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index afdb6cd4fbb3..db3f0906e614 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index a634c2e2206c..c569e301708d 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index 00a92aa5b813..568f38705df0 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: 2022-09-27 +date: 2022-09-29 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 | |--------------|----------|------------------------| -| 473 | 395 | 38 | +| 477 | 397 | 38 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 31772 | 179 | 21354 | 1002 | +| 31953 | 179 | 21502 | 1003 | ## Plugin Directory @@ -30,7 +30,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 214 | 0 | 209 | 19 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 36 | 1 | 32 | 2 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 9 | 0 | 0 | 2 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 370 | 0 | 361 | 22 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 379 | 0 | 370 | 24 | | | [APM UI](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 38 | 0 | 38 | 52 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 9 | 0 | 9 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Considering using bfetch capabilities when fetching large amounts of data. This services supports batching HTTP requests and streaming responses back. | 80 | 1 | 71 | 2 | @@ -42,12 +42,12 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Cloud Security Posture](https://github.com/orgs/elastic/teams/cloud-posture-security) | The cloud security posture plugin | 18 | 0 | 2 | 3 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 13 | 0 | 13 | 1 | | | [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 | 212 | 0 | 204 | 7 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2684 | 0 | 35 | 0 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2688 | 0 | 30 | 0 | | crossClusterReplication | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | -| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | Add custom data integrations so they can be displayed in the Fleet integrations app | 103 | 0 | 84 | 1 | -| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds the Dashboard app to Kibana | 144 | 0 | 139 | 10 | +| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | Add custom data integrations so they can be displayed in the Fleet integrations app | 104 | 0 | 85 | 1 | +| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds the Dashboard app to Kibana | 120 | 0 | 113 | 3 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 52 | 0 | 51 | 0 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3211 | 33 | 2508 | 23 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3213 | 33 | 2509 | 23 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | This plugin provides the ability to create data views via a modal flyout inside Kibana apps | 15 | 0 | 7 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Reusable data view field editor across Kibana | 60 | 0 | 30 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data view management app | 2 | 0 | 2 | 0 | @@ -60,7 +60,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends embeddable plugin with more functionality | 14 | 0 | 14 | 0 | | | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides encryption and decryption utilities for saved objects containing sensitive information. | 51 | 0 | 42 | 0 | | | [Enterprise Search](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | Adds dashboards for discovering and managing Enterprise Search products. | 9 | 0 | 9 | 0 | -| | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 114 | 3 | 110 | 3 | +| | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 114 | 3 | 110 | 5 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | The Event Annotation service contains expressions for event annotations | 174 | 0 | 174 | 3 | | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 106 | 0 | 106 | 10 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'error' renderer to expressions | 17 | 0 | 15 | 2 | @@ -80,8 +80,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 222 | 0 | 95 | 2 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Index pattern fields and ambiguous values formatters | 288 | 5 | 249 | 3 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | 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. | 62 | 0 | 62 | 2 | -| | [@elastic/kibana-app-services](https://github.com/orgs/elastic/teams/team:AppServicesUx) | File upload, download, sharing, and serving over HTTP implementation in Kibana. | 263 | 0 | 15 | 2 | -| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 988 | 3 | 888 | 17 | +| | [@elastic/kibana-app-services](https://github.com/orgs/elastic/teams/team:AppServicesUx) | File upload, download, sharing, and serving over HTTP implementation in Kibana. | 263 | 0 | 14 | 2 | +| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 996 | 3 | 893 | 17 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 68 | 0 | 14 | 5 | | globalSearchBar | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | | globalSearchProviders | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | @@ -101,7 +101,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | kibanaUsageCollection | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 0 | 0 | 0 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 615 | 3 | 418 | 9 | | | [Security Team](https://github.com/orgs/elastic/teams/security-team) | - | 3 | 0 | 3 | 1 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 646 | 0 | 558 | 42 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 649 | 0 | 560 | 42 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 8 | 0 | 8 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 3 | 0 | 3 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 117 | 0 | 42 | 10 | @@ -152,7 +152,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 452 | 1 | 346 | 33 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 4 | 0 | 4 | 1 | | translations | [Kibana Localization](https://github.com/orgs/elastic/teams/kibana-localization) | - | 0 | 0 | 0 | 0 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 437 | 1 | 416 | 45 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 512 | 1 | 485 | 48 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds UI Actions service to Kibana | 132 | 0 | 91 | 11 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends UI Actions plugin with more functionality | 206 | 0 | 142 | 9 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list which can be integrated into apps | 61 | 0 | 59 | 2 | @@ -175,7 +175,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Registers the vega visualization. Is the elastic version of vega and vega-lite libraries. | 2 | 0 | 2 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and heatmap charts. We want to replace them with elastic-charts. | 26 | 0 | 25 | 1 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the new xy-axis chart using the elastic-charts library, which will eventually replace the vislib xy-axis charts including bar, area, and line. | 53 | 0 | 50 | 5 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the shared architecture among all the legacy visualizations, e.g. the visualization type registry or the visualization embeddable. | 679 | 12 | 649 | 18 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the shared architecture among all the legacy visualizations, e.g. the visualization type registry or the visualization embeddable. | 693 | 12 | 663 | 18 | | watcher | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | ## Package Directory @@ -261,6 +261,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Kibana Core | - | 16 | 0 | 16 | 0 | | | Kibana Core | - | 4 | 0 | 0 | 0 | | | Kibana Core | - | 10 | 1 | 10 | 0 | +| | Kibana Core | - | 14 | 0 | 11 | 0 | | | Kibana Core | - | 25 | 5 | 25 | 1 | | | Kibana Core | - | 7 | 0 | 7 | 1 | | | Kibana Core | - | 392 | 1 | 154 | 0 | @@ -388,10 +389,11 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 74 | 0 | 71 | 0 | | | [Owner missing] | Security Solution auto complete | 56 | 1 | 41 | 1 | | | [Owner missing] | security solution elastic search utilities to use across plugins such lists, security_solution, cases, etc... | 67 | 0 | 61 | 1 | +| | [Owner missing] | - | 76 | 0 | 67 | 1 | | | [Owner missing] | Security Solution utilities for React hooks | 15 | 0 | 7 | 0 | -| | [Owner missing] | io ts utilities and types to be shared with plugins from the security solution project | 148 | 0 | 129 | 0 | +| | [Owner missing] | io ts utilities and types to be shared with plugins from the security solution project | 145 | 0 | 127 | 0 | | | [Owner missing] | io ts utilities and types to be shared with plugins from the security solution project | 505 | 1 | 492 | 0 | -| | [Owner missing] | io ts utilities and types to be shared with plugins from the security solution project | 63 | 0 | 33 | 0 | +| | [Owner missing] | io ts utilities and types to be shared with plugins from the security solution project | 65 | 0 | 36 | 0 | | | [Owner missing] | io ts utilities and types to be shared with plugins from the security solution project | 28 | 0 | 21 | 0 | | | [Owner missing] | security solution list REST API | 67 | 0 | 64 | 0 | | | [Owner missing] | security solution list constants to use across plugins such lists, security_solution, cases, etc... | 33 | 0 | 17 | 0 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index 971ed39aee0e..099351a2d0f2 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: 2022-09-27 +date: 2022-09-29 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 8d2be8c0f9dd..2ef17ce2192f 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling'] --- import profilingObj from './profiling.devdocs.json'; diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index 916ffb9e1e4e..8a248f78f1fa 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: 2022-09-27 +date: 2022-09-29 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 aa8b69de4519..9589edc812df 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: 2022-09-27 +date: 2022-09-29 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 7ecab5c862bf..1964dc994cd2 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.devdocs.json b/api_docs/rule_registry.devdocs.json index 424057fd0db7..089f973bc0bf 100644 --- a/api_docs/rule_registry.devdocs.json +++ b/api_docs/rule_registry.devdocs.json @@ -1917,7 +1917,7 @@ "\nID of the Kibana feature associated with the index.\nUsed by alerts-as-data RBAC.\n\nNote from @dhurley14\nThe purpose of the `feature` param is to force the user to update\nthe data structure which contains the mapping of consumers to alerts\nas data indices. The idea is it is typed such that it forces the\nuser to go to the code and modify it. At least until a better system\nis put in place or we move the alerts as data client out of rule registry.\n" ], "signature": [ - "\"observability\" | \"logs\" | \"apm\" | \"uptime\" | \"siem\" | \"infrastructure\"" + "\"infrastructure\" | \"observability\" | \"logs\" | \"apm\" | \"uptime\" | \"siem\"" ], "path": "x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_options.ts", "deprecated": false, @@ -4270,7 +4270,7 @@ "label": "ParsedTechnicalFields", "description": [], "signature": [ - "{ readonly '@timestamp': string; readonly \"kibana.alert.rule.rule_type_id\": string; readonly \"kibana.alert.rule.consumer\": string; readonly \"kibana.alert.rule.producer\": string; readonly \"kibana.space_ids\": string[]; readonly \"kibana.alert.uuid\": string; readonly \"kibana.alert.instance.id\": string; readonly \"kibana.alert.status\": string; readonly \"kibana.alert.rule.category\": string; readonly \"kibana.alert.rule.uuid\": string; readonly \"kibana.alert.rule.name\": string; readonly tags?: string[] | undefined; readonly 'event.action'?: string | undefined; readonly \"kibana.alert.rule.parameters\"?: { [key: string]: unknown; } | undefined; readonly \"kibana.alert.start\"?: string | undefined; readonly \"kibana.alert.end\"?: string | undefined; readonly \"kibana.alert.duration.us\"?: number | undefined; readonly \"kibana.alert.severity\"?: string | undefined; readonly \"kibana.version\"?: string | undefined; readonly \"ecs.version\"?: string | undefined; readonly \"kibana.alert.risk_score\"?: number | undefined; readonly \"kibana.alert.workflow_status\"?: string | undefined; readonly \"kibana.alert.workflow_user\"?: string | undefined; readonly \"kibana.alert.workflow_reason\"?: string | undefined; readonly \"kibana.alert.system_status\"?: string | undefined; readonly \"kibana.alert.action_group\"?: string | undefined; readonly \"kibana.alert.reason\"?: string | undefined; readonly \"kibana.alert.rule.author\"?: string | undefined; readonly \"kibana.alert.rule.created_at\"?: string | undefined; readonly \"kibana.alert.rule.created_by\"?: string | undefined; readonly \"kibana.alert.rule.description\"?: string | undefined; readonly \"kibana.alert.rule.enabled\"?: string | undefined; readonly \"kibana.alert.rule.execution.uuid\"?: string | undefined; readonly \"kibana.alert.rule.from\"?: string | undefined; readonly \"kibana.alert.rule.interval\"?: string | undefined; readonly \"kibana.alert.rule.license\"?: string | undefined; readonly \"kibana.alert.rule.note\"?: string | undefined; readonly \"kibana.alert.rule.references\"?: string[] | undefined; readonly \"kibana.alert.rule.rule_id\"?: string | undefined; readonly \"kibana.alert.rule.rule_name_override\"?: string | undefined; readonly \"kibana.alert.rule.tags\"?: string[] | undefined; readonly \"kibana.alert.rule.to\"?: string | undefined; readonly \"kibana.alert.rule.type\"?: string | undefined; readonly \"kibana.alert.rule.updated_at\"?: string | undefined; readonly \"kibana.alert.rule.updated_by\"?: string | undefined; readonly \"kibana.alert.rule.version\"?: string | undefined; readonly 'event.kind'?: string | undefined; }" + "{ readonly '@timestamp': string; readonly \"kibana.alert.rule.rule_type_id\": string; readonly \"kibana.alert.rule.consumer\": string; readonly \"kibana.alert.rule.producer\": string; readonly \"kibana.space_ids\": string[]; readonly \"kibana.alert.uuid\": string; readonly \"kibana.alert.instance.id\": string; readonly \"kibana.alert.status\": string; readonly \"kibana.alert.rule.category\": string; readonly \"kibana.alert.rule.uuid\": string; readonly \"kibana.alert.rule.name\": string; readonly tags?: string[] | undefined; readonly 'event.action'?: string | undefined; readonly \"kibana.alert.rule.execution.uuid\"?: string | undefined; readonly \"kibana.alert.rule.parameters\"?: { [key: string]: unknown; } | undefined; readonly \"kibana.alert.start\"?: string | undefined; readonly \"kibana.alert.end\"?: string | undefined; readonly \"kibana.alert.duration.us\"?: number | undefined; readonly \"kibana.alert.severity\"?: string | undefined; readonly \"kibana.version\"?: string | undefined; readonly \"ecs.version\"?: string | undefined; readonly \"kibana.alert.risk_score\"?: number | undefined; readonly \"kibana.alert.workflow_status\"?: string | undefined; readonly \"kibana.alert.workflow_user\"?: string | undefined; readonly \"kibana.alert.workflow_reason\"?: string | undefined; readonly \"kibana.alert.system_status\"?: string | undefined; readonly \"kibana.alert.action_group\"?: string | undefined; readonly \"kibana.alert.reason\"?: string | undefined; readonly \"kibana.alert.rule.author\"?: string | undefined; readonly \"kibana.alert.rule.created_at\"?: string | undefined; readonly \"kibana.alert.rule.created_by\"?: string | undefined; readonly \"kibana.alert.rule.description\"?: string | undefined; readonly \"kibana.alert.rule.enabled\"?: string | undefined; readonly \"kibana.alert.rule.from\"?: string | undefined; readonly \"kibana.alert.rule.interval\"?: string | undefined; readonly \"kibana.alert.rule.license\"?: string | undefined; readonly \"kibana.alert.rule.note\"?: string | undefined; readonly \"kibana.alert.rule.references\"?: string[] | undefined; readonly \"kibana.alert.rule.rule_id\"?: string | undefined; readonly \"kibana.alert.rule.rule_name_override\"?: string | undefined; readonly \"kibana.alert.rule.tags\"?: string[] | undefined; readonly \"kibana.alert.rule.to\"?: string | undefined; readonly \"kibana.alert.rule.type\"?: string | undefined; readonly \"kibana.alert.rule.updated_at\"?: string | undefined; readonly \"kibana.alert.rule.updated_by\"?: string | undefined; readonly \"kibana.alert.rule.version\"?: string | undefined; readonly 'event.kind'?: string | undefined; }" ], "path": "x-pack/plugins/rule_registry/common/parse_technical_fields.ts", "deprecated": false, diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 42e1a51c2ed9..d4539ef64efa 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: 2022-09-27 +date: 2022-09-29 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 4c4a4a4be8d7..8112f8684b55 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] --- import runtimeFieldsObj from './runtime_fields.devdocs.json'; diff --git a/api_docs/saved_objects.devdocs.json b/api_docs/saved_objects.devdocs.json index 6d40e242e3cd..71d53634b754 100644 --- a/api_docs/saved_objects.devdocs.json +++ b/api_docs/saved_objects.devdocs.json @@ -1557,18 +1557,6 @@ "plugin": "savedObjectsTaggingOss", "path": "src/plugins/saved_objects_tagging_oss/public/api.ts" }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/services/saved_object_loader.ts" - }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/services/saved_object_loader.ts" - }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/services/saved_object_loader.ts" - }, { "plugin": "dashboard", "path": "src/plugins/dashboard/public/application/actions/clone_panel_action.tsx" @@ -1580,22 +1568,6 @@ { "plugin": "dashboard", "path": "src/plugins/dashboard/public/application/actions/clone_panel_action.tsx" - }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts" - }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts" - }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/application/lib/dashboard_tagging.ts" - }, - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/application/lib/dashboard_tagging.ts" } ], "children": [ @@ -3121,12 +3093,7 @@ "deprecated": true, "removeBy": "8.8.0", "trackAdoption": false, - "references": [ - { - "plugin": "dashboard", - "path": "src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts" - } - ] + "references": [] }, { "parentPluginId": "savedObjects", diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index d180b9df678f..613f942ef906 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] --- import savedObjectsObj from './saved_objects.devdocs.json'; diff --git a/api_docs/saved_objects_finder.mdx b/api_docs/saved_objects_finder.mdx index d8ee1bd800f9..23cf7d31ca48 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index a57ba63fec00..c9cc4aa61ea7 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: 2022-09-27 +date: 2022-09-29 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 785d7f8ea6a3..1450399aefd8 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: 2022-09-27 +date: 2022-09-29 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 bf65a9aa5c64..1ed5aa9d511e 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] --- import savedObjectsTaggingOssObj from './saved_objects_tagging_oss.devdocs.json'; diff --git a/api_docs/saved_search.devdocs.json b/api_docs/saved_search.devdocs.json index df52fe959fea..8e6b2e5503d3 100644 --- a/api_docs/saved_search.devdocs.json +++ b/api_docs/saved_search.devdocs.json @@ -661,7 +661,7 @@ "label": "sharingSavedObjectProps", "description": [], "signature": [ - "{ outcome?: \"exactMatch\" | \"aliasMatch\" | \"conflict\" | undefined; aliasTargetId?: string | undefined; aliasPurpose?: \"savedObjectConversion\" | \"savedObjectImport\" | undefined; errorJSON?: string | undefined; } | undefined" + "{ outcome?: \"conflict\" | \"exactMatch\" | \"aliasMatch\" | undefined; aliasTargetId?: string | undefined; aliasPurpose?: \"savedObjectConversion\" | \"savedObjectImport\" | undefined; errorJSON?: string | undefined; } | undefined" ], "path": "src/plugins/saved_search/public/services/saved_searches/types.ts", "deprecated": false, diff --git a/api_docs/saved_search.mdx b/api_docs/saved_search.mdx index 5e294d8d6ea5..6c1900781f66 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedSearch'] --- import savedSearchObj from './saved_search.devdocs.json'; diff --git a/api_docs/screenshot_mode.devdocs.json b/api_docs/screenshot_mode.devdocs.json index c6be83e92847..ab834dfc733b 100644 --- a/api_docs/screenshot_mode.devdocs.json +++ b/api_docs/screenshot_mode.devdocs.json @@ -139,13 +139,7 @@ "label": "ScreenshotModeRequestHandlerContext", "description": [], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.RequestHandlerContext", - "text": "RequestHandlerContext" - }, + "RequestHandlerContext", " & { screenshotMode: Promise<{ isScreenshot: boolean; }>; }" ], "path": "src/plugins/screenshot_mode/server/types.ts", diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index 4c436c7b23e6..883f8e26e2cc 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: 2022-09-27 +date: 2022-09-29 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 5bc1fcd05800..df0feab2c445 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] --- import screenshottingObj from './screenshotting.devdocs.json'; diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 952c20eac620..814d0173f69c 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: 2022-09-27 +date: 2022-09-29 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 8ad8f2f5fe21..c3ab461b2eb2 100644 --- a/api_docs/security_solution.devdocs.json +++ b/api_docs/security_solution.devdocs.json @@ -768,13 +768,7 @@ "label": "core", "description": [], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.CoreRequestHandlerContext", - "text": "CoreRequestHandlerContext" - } + "CoreRequestHandlerContext" ], "path": "x-pack/plugins/security_solution/server/types.ts", "deprecated": false, diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index 9cf5459a07a3..a86c48a48dfc 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] --- import securitySolutionObj from './security_solution.devdocs.json'; diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index 23f8e18b5e27..66978ad664e0 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: 2022-09-27 +date: 2022-09-29 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 7ba452349a66..0086ad02ab5a 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] --- import shareObj from './share.devdocs.json'; diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index 194b818f081b..7bce31d6e183 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: 2022-09-27 +date: 2022-09-29 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 95118ea8b805..decf22f6da09 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: 2022-09-27 +date: 2022-09-29 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 b3467e6a698d..e68ecfcdfb99 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: 2022-09-27 +date: 2022-09-29 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 aabbd1805b61..5363b913fbe0 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackConnectors'] --- import stackConnectorsObj from './stack_connectors.devdocs.json'; diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index d35966ac57c9..7377ef34d529 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: 2022-09-27 +date: 2022-09-29 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 1dbef1ee43f3..1a95f8384f24 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: 2022-09-27 +date: 2022-09-29 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 438e8d307b1d..355a38255c9c 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] --- import telemetryCollectionManagerObj from './telemetry_collection_manager.devdocs.json'; diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index f032f6eee5e0..e607fc3598bd 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionXpack title: "telemetryCollectionXpack" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionXpack plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionXpack'] --- import telemetryCollectionXpackObj from './telemetry_collection_xpack.devdocs.json'; diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index 0b1523b1a10d..f538309dd462 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: 2022-09-27 +date: 2022-09-29 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 b584556618af..bcebf8f8b934 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; diff --git a/api_docs/timelines.devdocs.json b/api_docs/timelines.devdocs.json index 5875aa47c130..4b5ec5ce2897 100644 --- a/api_docs/timelines.devdocs.json +++ b/api_docs/timelines.devdocs.json @@ -3192,7 +3192,7 @@ "IFieldSubType", " | undefined; type?: string | undefined; })[]; id: string; title: string; filters?: ", "Filter", - "[] | undefined; dataViewId: string | null; sort: ", + "[] | undefined; dataViewId: string | null; savedObjectId: string | null; sort: ", "SortColumnTimeline", "[]; version: string | null; filterManager?: ", { @@ -3218,7 +3218,7 @@ }, "; description?: string | null | undefined; esTypes?: string[] | undefined; example?: string | number | null | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", "IFieldSubType", - " | undefined; type?: string | undefined; })[]; savedObjectId: string | null; isLoading: boolean; dataProviders: ", + " | undefined; type?: string | undefined; })[]; isLoading: boolean; dataProviders: ", { "pluginId": "timelines", "scope": "common", diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index 14ef93c594cc..7a96d016a99c 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: 2022-09-27 +date: 2022-09-29 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 b66dc03b52ac..7d3c611c084b 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.devdocs.json b/api_docs/triggers_actions_ui.devdocs.json index 59be983bb00f..d6351e414ae4 100644 --- a/api_docs/triggers_actions_ui.devdocs.json +++ b/api_docs/triggers_actions_ui.devdocs.json @@ -303,6 +303,38 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ButtonGroupField", + "type": "Function", + "tags": [], + "label": "ButtonGroupField", + "description": [], + "signature": [ + "React.NamedExoticComponent" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/button_group_field.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ButtonGroupField.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.CreateConnectorFlyout", @@ -832,6 +864,145 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.hasMustacheTokens", + "type": "Function", + "tags": [], + "label": "hasMustacheTokens", + "description": [], + "signature": [ + "(str: string) => boolean" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.hasMustacheTokens.$1", + "type": "string", + "tags": [], + "label": "str", + "description": [], + "signature": [ + "string" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.HiddenField", + "type": "Function", + "tags": [], + "label": "HiddenField", + "description": [], + "signature": [ + "React.NamedExoticComponent<", + "Props", + "> & { readonly type: (props: ", + "Props", + ") => JSX.Element; }" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/hidden_field.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.HiddenField.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.JsonEditorWithMessageVariables", + "type": "Function", + "tags": [], + "label": "JsonEditorWithMessageVariables", + "description": [], + "signature": [ + "({ buttonTitle, messageVariables, paramsProperty, inputTargetValue, label, errors, areaLabel, onDocumentsChange, helpText, onBlur, showButtonTitle, euiCodeEditorProps, }: React.PropsWithChildren) => JSX.Element" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.JsonEditorWithMessageVariables.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n buttonTitle,\n messageVariables,\n paramsProperty,\n inputTargetValue,\n label,\n errors,\n areaLabel,\n onDocumentsChange,\n helpText,\n onBlur,\n showButtonTitle,\n euiCodeEditorProps = {},\n}", + "description": [], + "signature": [ + "React.PropsWithChildren" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.JsonFieldWrapper", + "type": "Function", + "tags": [], + "label": "JsonFieldWrapper", + "description": [], + "signature": [ + "({ field, ...rest }: Props) => JSX.Element" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.JsonFieldWrapper.$1", + "type": "Object", + "tags": [], + "label": "{ field, ...rest }", + "description": [], + "signature": [ + "Props" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.loadActionErrorLog", @@ -1398,6 +1569,39 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.MustacheTextFieldWrapper", + "type": "Function", + "tags": [], + "label": "MustacheTextFieldWrapper", + "description": [], + "signature": [ + "({ field, euiFieldProps, idAria, ...rest }: Props) => JSX.Element" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.MustacheTextFieldWrapper.$1", + "type": "Object", + "tags": [], + "label": "{ field, euiFieldProps, idAria, ...rest }", + "description": [], + "signature": [ + "Props" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.muteRule", @@ -1490,6 +1694,70 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.PasswordField", + "type": "Function", + "tags": [], + "label": "PasswordField", + "description": [], + "signature": [ + "React.NamedExoticComponent" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/password_field.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.PasswordField.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.SimpleConnectorForm", + "type": "Function", + "tags": [], + "label": "SimpleConnectorForm", + "description": [], + "signature": [ + "React.NamedExoticComponent" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.tsx", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.SimpleConnectorForm.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.snoozeRule", @@ -1614,72 +1882,185 @@ }, { "parentPluginId": "triggersActionsUi", - "id": "def-public.ThresholdExpression", + "id": "def-public.templateActionVariable", "type": "Function", "tags": [], - "label": "ThresholdExpression", + "label": "templateActionVariable", "description": [], "signature": [ - "(props: ", - "ThresholdExpressionProps", - ") => JSX.Element" + "(variable: ", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.ActionVariable", + "text": "ActionVariable" + }, + ") => string" ], - "path": "x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts", + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/template_action_variable.ts", "deprecated": false, "trackAdoption": false, - "returnComment": [], "children": [ { "parentPluginId": "triggersActionsUi", - "id": "def-public.ThresholdExpression.$1", - "type": "Uncategorized", + "id": "def-public.templateActionVariable.$1", + "type": "Object", "tags": [], - "label": "props", + "label": "variable", "description": [], "signature": [ - "T" + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.ActionVariable", + "text": "ActionVariable" + } ], - "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/suspended_component_with_props.tsx", + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/template_action_variable.ts", "deprecated": false, - "trackAdoption": false + "trackAdoption": false, + "isRequired": true } ], + "returnComment": [], "initialIsOpen": false }, { "parentPluginId": "triggersActionsUi", - "id": "def-public.unmuteRule", + "id": "def-public.TextAreaWithMessageVariables", "type": "Function", "tags": [], - "label": "unmuteRule", + "label": "TextAreaWithMessageVariables", "description": [], "signature": [ - "({ id, http }: { id: string; http: ", - "HttpSetup", - "; }) => Promise" + "({ messageVariables, paramsProperty, index, inputTargetValue, isDisabled, editAction, label, errors, }: React.PropsWithChildren) => JSX.Element" ], - "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute.ts", + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "triggersActionsUi", - "id": "def-public.unmuteRule.$1", - "type": "Object", + "id": "def-public.TextAreaWithMessageVariables.$1", + "type": "CompoundType", "tags": [], - "label": "{ id, http }", + "label": "{\n messageVariables,\n paramsProperty,\n index,\n inputTargetValue,\n isDisabled = false,\n editAction,\n label,\n errors,\n}", "description": [], - "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute.ts", + "signature": [ + "React.PropsWithChildren" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx", "deprecated": false, "trackAdoption": false, - "children": [ - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.unmuteRule.$1.id", - "type": "string", - "tags": [], - "label": "id", - "description": [], + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.TextFieldWithMessageVariables", + "type": "Function", + "tags": [], + "label": "TextFieldWithMessageVariables", + "description": [], + "signature": [ + "({ buttonTitle, messageVariables, paramsProperty, index, inputTargetValue, editAction, errors, formRowProps, defaultValue, wrapField, showButtonTitle, }: React.PropsWithChildren) => JSX.Element" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.TextFieldWithMessageVariables.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n buttonTitle,\n messageVariables,\n paramsProperty,\n index,\n inputTargetValue,\n editAction,\n errors,\n formRowProps,\n defaultValue,\n wrapField = false,\n showButtonTitle,\n}", + "description": [], + "signature": [ + "React.PropsWithChildren" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ThresholdExpression", + "type": "Function", + "tags": [], + "label": "ThresholdExpression", + "description": [], + "signature": [ + "(props: ", + "ThresholdExpressionProps", + ") => JSX.Element" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ThresholdExpression.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "T" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/suspended_component_with_props.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.unmuteRule", + "type": "Function", + "tags": [], + "label": "unmuteRule", + "description": [], + "signature": [ + "({ id, http }: { id: string; http: ", + "HttpSetup", + "; }) => Promise" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.unmuteRule.$1", + "type": "Object", + "tags": [], + "label": "{ id, http }", + "description": [], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.unmuteRule.$1.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unmute.ts", "deprecated": false, "trackAdoption": false @@ -1776,6 +2157,106 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.updateActionConnector", + "type": "Function", + "tags": [], + "label": "updateActionConnector", + "description": [], + "signature": [ + "({\n http,\n connector,\n id,\n}: { http: ", + "HttpSetup", + "; connector: Pick<", + "ActionConnectorWithoutId", + ", Record>, \"name\" | \"config\" | \"secrets\">; id: string; }) => Promise<", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionConnector", + "text": "ActionConnector" + }, + ", Record>>" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.updateActionConnector.$1", + "type": "Object", + "tags": [], + "label": "{\n http,\n connector,\n id,\n}", + "description": [], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.updateActionConnector.$1.http", + "type": "Object", + "tags": [], + "label": "http", + "description": [], + "signature": [ + "HttpSetup" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.updateActionConnector.$1.connector", + "type": "Object", + "tags": [], + "label": "connector", + "description": [], + "signature": [ + "{ name: string; config: Record; secrets: Record; }" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.updateActionConnector.$1.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.useConnectorContext", + "type": "Function", + "tags": [], + "label": "useConnectorContext", + "description": [], + "signature": [ + "() => ", + "ConnectorContextValue" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/context/use_connector_context.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.useLoadRuleTypes", @@ -1819,6 +2300,41 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.useTypedKibana", + "type": "Function", + "tags": [], + "label": "useTypedKibana", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "kibanaReact", + "scope": "public", + "docId": "kibKibanaReactPluginApi", + "section": "def-public.KibanaReactContextValue", + "text": "KibanaReactContextValue" + }, + " & ", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.TriggersAndActionsUiServices", + "text": "TriggersAndActionsUiServices" + }, + ">" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.ValueExpression", @@ -1891,95 +2407,651 @@ "interfaces": [ { "parentPluginId": "triggersActionsUi", - "id": "def-public.ActionType", + "id": "def-public.ActionConnectorFieldsProps", "type": "Interface", "tags": [], - "label": "ActionType", + "label": "ActionConnectorFieldsProps", "description": [], - "path": "x-pack/plugins/actions/common/types.ts", + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, "trackAdoption": false, "children": [ { "parentPluginId": "triggersActionsUi", - "id": "def-public.ActionType.id", - "type": "string", - "tags": [], - "label": "id", - "description": [], - "path": "x-pack/plugins/actions/common/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.ActionType.name", - "type": "string", - "tags": [], - "label": "name", - "description": [], - "path": "x-pack/plugins/actions/common/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.ActionType.enabled", + "id": "def-public.ActionConnectorFieldsProps.readOnly", "type": "boolean", "tags": [], - "label": "enabled", + "label": "readOnly", "description": [], - "path": "x-pack/plugins/actions/common/types.ts", + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "triggersActionsUi", - "id": "def-public.ActionType.enabledInConfig", + "id": "def-public.ActionConnectorFieldsProps.isEdit", "type": "boolean", "tags": [], - "label": "enabledInConfig", + "label": "isEdit", "description": [], - "path": "x-pack/plugins/actions/common/types.ts", + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "triggersActionsUi", - "id": "def-public.ActionType.enabledInLicense", - "type": "boolean", + "id": "def-public.ActionConnectorFieldsProps.registerPreSubmitValidator", + "type": "Function", "tags": [], - "label": "enabledInLicense", + "label": "registerPreSubmitValidator", "description": [], - "path": "x-pack/plugins/actions/common/types.ts", + "signature": [ + "(validator: ", + "ConnectorValidationFunc", + ") => void" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionConnectorFieldsProps.registerPreSubmitValidator.$1", + "type": "Function", + "tags": [], + "label": "validator", + "description": [], + "signature": [ + "ConnectorValidationFunc" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps", + "type": "Interface", + "tags": [], + "label": "ActionParamsProps", + "description": [], + "signature": [ + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionParamsProps", + "text": "ActionParamsProps" + }, + "" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.actionParams", + "type": "Object", + "tags": [], + "label": "actionParams", + "description": [], + "signature": [ + "{ [P in keyof TParams]?: TParams[P] | undefined; }" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.index", + "type": "number", + "tags": [], + "label": "index", + "description": [], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.editAction", + "type": "Function", + "tags": [], + "label": "editAction", + "description": [], + "signature": [ + "(key: string, value: ", + "SavedObjectAttribute", + ", index: number) => void" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.editAction.$1", + "type": "string", + "tags": [], + "label": "key", + "description": [], + "signature": [ + "string" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.editAction.$2", + "type": "CompoundType", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "SavedObjectAttribute" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.editAction.$3", + "type": "number", + "tags": [], + "label": "index", + "description": [], + "signature": [ + "number" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.errors", + "type": "Object", + "tags": [], + "label": "errors", + "description": [], + "signature": [ + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.IErrorObject", + "text": "IErrorObject" + } + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.messageVariables", + "type": "Array", + "tags": [], + "label": "messageVariables", + "description": [], + "signature": [ + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.ActionVariable", + "text": "ActionVariable" + }, + "[] | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.defaultMessage", + "type": "string", + "tags": [], + "label": "defaultMessage", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.actionConnector", + "type": "CompoundType", + "tags": [], + "label": "actionConnector", + "description": [], + "signature": [ + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionConnector", + "text": "ActionConnector" + }, + ", Record> | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.isLoading", + "type": "CompoundType", + "tags": [], + "label": "isLoading", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.isDisabled", + "type": "CompoundType", + "tags": [], + "label": "isDisabled", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionParamsProps.showEmailSubjectAndMessage", + "type": "CompoundType", + "tags": [], + "label": "showEmailSubjectAndMessage", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionType", + "type": "Interface", + "tags": [], + "label": "ActionType", + "description": [], + "path": "x-pack/plugins/actions/common/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionType.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "x-pack/plugins/actions/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionType.name", + "type": "string", + "tags": [], + "label": "name", + "description": [], + "path": "x-pack/plugins/actions/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionType.enabled", + "type": "boolean", + "tags": [], + "label": "enabled", + "description": [], + "path": "x-pack/plugins/actions/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionType.enabledInConfig", + "type": "boolean", + "tags": [], + "label": "enabledInConfig", + "description": [], + "path": "x-pack/plugins/actions/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionType.enabledInLicense", + "type": "boolean", + "tags": [], + "label": "enabledInLicense", + "description": [], + "path": "x-pack/plugins/actions/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionType.minimumLicenseRequired", + "type": "CompoundType", + "tags": [], + "label": "minimumLicenseRequired", + "description": [], + "signature": [ + "\"basic\" | \"standard\" | \"gold\" | \"platinum\" | \"enterprise\" | \"trial\"" + ], + "path": "x-pack/plugins/actions/common/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionType.supportedFeatureIds", + "type": "Array", + "tags": [], + "label": "supportedFeatureIds", + "description": [], + "signature": [ + "string[]" + ], + "path": "x-pack/plugins/actions/common/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel", + "type": "Interface", + "tags": [], + "label": "ActionTypeModel", + "description": [], + "signature": [ + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, + "" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel.iconClass", + "type": "CompoundType", + "tags": [], + "label": "iconClass", + "description": [], + "signature": [ + "string | React.ComponentType<{}>" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel.selectMessage", + "type": "string", + "tags": [], + "label": "selectMessage", + "description": [], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel.actionTypeTitle", + "type": "string", + "tags": [], + "label": "actionTypeTitle", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel.validateParams", + "type": "Function", + "tags": [], + "label": "validateParams", + "description": [], + "signature": [ + "(actionParams: ActionParams) => Promise<", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.GenericValidationResult", + "text": "GenericValidationResult" + }, + ">" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel.validateParams.$1", + "type": "Uncategorized", + "tags": [], + "label": "actionParams", + "description": [], + "signature": [ + "ActionParams" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel.actionConnectorFields", + "type": "CompoundType", + "tags": [], + "label": "actionConnectorFields", + "description": [], + "signature": [ + "React.LazyExoticComponent> | null" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel.actionParamsFields", + "type": "Function", + "tags": [], + "label": "actionParamsFields", + "description": [], + "signature": [ + "React.ExoticComponent<(", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionParamsProps", + "text": "ActionParamsProps" + }, + " & React.RefAttributes, any, any>>) | (", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionParamsProps", + "text": "ActionParamsProps" + }, + " & { children?: React.ReactNode; })> & { readonly _result: React.ComponentType<", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionParamsProps", + "text": "ActionParamsProps" + }, + ">; }" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel.actionParamsFields.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ] + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel.defaultActionParams", + "type": "Object", + "tags": [], + "label": "defaultActionParams", + "description": [], + "signature": [ + "Partial | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ActionTypeModel.defaultRecoveredActionParams", + "type": "Object", + "tags": [], + "label": "defaultRecoveredActionParams", + "description": [], + "signature": [ + "Partial | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "triggersActionsUi", - "id": "def-public.ActionType.minimumLicenseRequired", - "type": "CompoundType", + "id": "def-public.ActionTypeModel.customConnectorSelectItem", + "type": "Object", "tags": [], - "label": "minimumLicenseRequired", + "label": "customConnectorSelectItem", "description": [], "signature": [ - "\"basic\" | \"standard\" | \"gold\" | \"platinum\" | \"enterprise\" | \"trial\"" + "CustomConnectorSelectionItem | undefined" ], - "path": "x-pack/plugins/actions/common/types.ts", + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, "trackAdoption": false }, { "parentPluginId": "triggersActionsUi", - "id": "def-public.ActionType.supportedFeatureIds", - "type": "Array", + "id": "def-public.ActionTypeModel.isExperimental", + "type": "CompoundType", "tags": [], - "label": "supportedFeatureIds", + "label": "isExperimental", "description": [], "signature": [ - "string[]" + "boolean | undefined" ], - "path": "x-pack/plugins/actions/common/types.ts", + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, "trackAdoption": false } @@ -2067,7 +3139,7 @@ "description": [], "signature": [ "BasicFields", - " & { tags?: string[] | undefined; kibana?: string[] | undefined; \"@timestamp\"?: string[] | undefined; \"event.action\"?: string[] | undefined; \"kibana.alert.rule.parameters\"?: string[] | undefined; \"kibana.alert.rule.rule_type_id\"?: string[] | undefined; \"kibana.alert.rule.consumer\"?: string[] | undefined; \"kibana.alert.rule.producer\"?: string[] | undefined; \"kibana.space_ids\"?: string[] | undefined; \"kibana.alert.uuid\"?: string[] | undefined; \"kibana.alert.instance.id\"?: string[] | undefined; \"kibana.alert.start\"?: string[] | undefined; \"kibana.alert.end\"?: string[] | undefined; \"kibana.alert.duration.us\"?: string[] | undefined; \"kibana.alert.severity\"?: string[] | undefined; \"kibana.alert.status\"?: string[] | undefined; \"kibana.version\"?: string[] | undefined; \"ecs.version\"?: string[] | undefined; \"kibana.alert.risk_score\"?: string[] | undefined; \"kibana.alert.workflow_status\"?: string[] | undefined; \"kibana.alert.workflow_user\"?: string[] | undefined; \"kibana.alert.workflow_reason\"?: string[] | undefined; \"kibana.alert.system_status\"?: string[] | undefined; \"kibana.alert.action_group\"?: string[] | undefined; \"kibana.alert.reason\"?: string[] | undefined; \"kibana.alert.rule.author\"?: string[] | undefined; \"kibana.alert.rule.category\"?: string[] | undefined; \"kibana.alert.rule.uuid\"?: string[] | undefined; \"kibana.alert.rule.created_at\"?: string[] | undefined; \"kibana.alert.rule.created_by\"?: string[] | undefined; \"kibana.alert.rule.description\"?: string[] | undefined; \"kibana.alert.rule.enabled\"?: string[] | undefined; \"kibana.alert.rule.execution.uuid\"?: string[] | undefined; \"kibana.alert.rule.from\"?: string[] | undefined; \"kibana.alert.rule.interval\"?: string[] | undefined; \"kibana.alert.rule.license\"?: string[] | undefined; \"kibana.alert.rule.name\"?: string[] | undefined; \"kibana.alert.rule.note\"?: string[] | undefined; \"kibana.alert.rule.references\"?: string[] | undefined; \"kibana.alert.rule.rule_id\"?: string[] | undefined; \"kibana.alert.rule.rule_name_override\"?: string[] | undefined; \"kibana.alert.rule.tags\"?: string[] | undefined; \"kibana.alert.rule.to\"?: string[] | undefined; \"kibana.alert.rule.type\"?: string[] | undefined; \"kibana.alert.rule.updated_at\"?: string[] | undefined; \"kibana.alert.rule.updated_by\"?: string[] | undefined; \"kibana.alert.rule.version\"?: string[] | undefined; \"event.kind\"?: string[] | undefined; \"event.module\"?: string[] | undefined; \"kibana.alert.evaluation.threshold\"?: string[] | undefined; \"kibana.alert.evaluation.value\"?: string[] | undefined; \"kibana.alert.building_block_type\"?: string[] | undefined; \"kibana.alert.rule.exceptions_list\"?: string[] | undefined; \"kibana.alert.rule.namespace\"?: string[] | undefined; \"kibana.alert\"?: string[] | undefined; \"kibana.alert.rule\"?: string[] | undefined; } & { [x: string]: unknown[]; }" + " & { tags?: string[] | undefined; kibana?: string[] | undefined; \"@timestamp\"?: string[] | undefined; \"kibana.alert.rule.rule_type_id\"?: string[] | undefined; \"kibana.alert.rule.consumer\"?: string[] | undefined; \"event.action\"?: string[] | undefined; \"kibana.alert.rule.execution.uuid\"?: string[] | undefined; \"kibana.alert.rule.parameters\"?: string[] | undefined; \"kibana.alert.rule.producer\"?: string[] | undefined; \"kibana.space_ids\"?: string[] | undefined; \"kibana.alert.uuid\"?: string[] | undefined; \"kibana.alert.instance.id\"?: string[] | undefined; \"kibana.alert.start\"?: string[] | undefined; \"kibana.alert.end\"?: string[] | undefined; \"kibana.alert.duration.us\"?: string[] | undefined; \"kibana.alert.severity\"?: string[] | undefined; \"kibana.alert.status\"?: string[] | undefined; \"kibana.version\"?: string[] | undefined; \"ecs.version\"?: string[] | undefined; \"kibana.alert.risk_score\"?: string[] | undefined; \"kibana.alert.workflow_status\"?: string[] | undefined; \"kibana.alert.workflow_user\"?: string[] | undefined; \"kibana.alert.workflow_reason\"?: string[] | undefined; \"kibana.alert.system_status\"?: string[] | undefined; \"kibana.alert.action_group\"?: string[] | undefined; \"kibana.alert.reason\"?: string[] | undefined; \"kibana.alert.rule.author\"?: string[] | undefined; \"kibana.alert.rule.category\"?: string[] | undefined; \"kibana.alert.rule.uuid\"?: string[] | undefined; \"kibana.alert.rule.created_at\"?: string[] | undefined; \"kibana.alert.rule.created_by\"?: string[] | undefined; \"kibana.alert.rule.description\"?: string[] | undefined; \"kibana.alert.rule.enabled\"?: string[] | undefined; \"kibana.alert.rule.from\"?: string[] | undefined; \"kibana.alert.rule.interval\"?: string[] | undefined; \"kibana.alert.rule.license\"?: string[] | undefined; \"kibana.alert.rule.name\"?: string[] | undefined; \"kibana.alert.rule.note\"?: string[] | undefined; \"kibana.alert.rule.references\"?: string[] | undefined; \"kibana.alert.rule.rule_id\"?: string[] | undefined; \"kibana.alert.rule.rule_name_override\"?: string[] | undefined; \"kibana.alert.rule.tags\"?: string[] | undefined; \"kibana.alert.rule.to\"?: string[] | undefined; \"kibana.alert.rule.type\"?: string[] | undefined; \"kibana.alert.rule.updated_at\"?: string[] | undefined; \"kibana.alert.rule.updated_by\"?: string[] | undefined; \"kibana.alert.rule.version\"?: string[] | undefined; \"event.kind\"?: string[] | undefined; \"event.module\"?: string[] | undefined; \"kibana.alert.evaluation.threshold\"?: string[] | undefined; \"kibana.alert.evaluation.value\"?: string[] | undefined; \"kibana.alert.building_block_type\"?: string[] | undefined; \"kibana.alert.rule.exceptions_list\"?: string[] | undefined; \"kibana.alert.rule.namespace\"?: string[] | undefined; \"kibana.alert\"?: string[] | undefined; \"kibana.alert.rule\"?: string[] | undefined; } & { [x: string]: unknown[]; }" ], "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", "deprecated": false, @@ -2513,20 +3585,6 @@ "deprecated": false, "trackAdoption": false }, - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.AlertStatus.actionSubgroup", - "type": "string", - "tags": [], - "label": "actionSubgroup", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "x-pack/plugins/alerting/common/alert_summary.ts", - "deprecated": false, - "trackAdoption": false - }, { "parentPluginId": "triggersActionsUi", "id": "def-public.AlertStatus.activeStartDate", @@ -2805,6 +3863,45 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ConfigFieldSchema", + "type": "Interface", + "tags": [], + "label": "ConfigFieldSchema", + "description": [], + "signature": [ + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ConfigFieldSchema", + "text": "ConfigFieldSchema" + }, + " extends ", + "CommonFieldSchema" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ConfigFieldSchema.isUrlField", + "type": "CompoundType", + "tags": [], + "label": "isUrlField", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.FieldBrowserOptions", @@ -3010,6 +4107,44 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.GenericValidationResult", + "type": "Interface", + "tags": [], + "label": "GenericValidationResult", + "description": [], + "signature": [ + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.GenericValidationResult", + "text": "GenericValidationResult" + }, + "" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.GenericValidationResult.errors", + "type": "Object", + "tags": [], + "label": "errors", + "description": [], + "signature": [ + "{ [P in Extract]: unknown; }" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.IErrorObject", @@ -3218,11 +4353,29 @@ "description": [], "signature": [ "{ get: (id: string) => ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, "; list: () => ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, "[]; register: (objectType: ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, ") => void; has: (id: string) => boolean; }" ], "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", @@ -4147,6 +5300,45 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.SecretsFieldSchema", + "type": "Interface", + "tags": [], + "label": "SecretsFieldSchema", + "description": [], + "signature": [ + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.SecretsFieldSchema", + "text": "SecretsFieldSchema" + }, + " extends ", + "CommonFieldSchema" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.SecretsFieldSchema.isPasswordField", + "type": "CompoundType", + "tags": [], + "label": "isPasswordField", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.TriggersAndActionsUiServices", @@ -4390,11 +5582,29 @@ "description": [], "signature": [ "{ get: (id: string) => ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, "; list: () => ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, "[]; register: (objectType: ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, ") => void; has: (id: string) => boolean; }" ], "path": "x-pack/plugins/triggers_actions_ui/public/application/app.tsx", @@ -4603,6 +5813,18 @@ } ], "enums": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.AlertProvidedActionVariables", + "type": "Enum", + "tags": [], + "label": "AlertProvidedActionVariables", + "description": [], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.COMPARATORS", @@ -4690,11 +5912,29 @@ "description": [], "signature": [ "{ get: (id: string) => ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, "; list: () => ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, "[]; register: (objectType: ", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, ") => void; has: (id: string) => boolean; }" ], "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", @@ -4717,6 +5957,63 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ALERT_HISTORY_PREFIX", + "type": "string", + "tags": [], + "label": "ALERT_HISTORY_PREFIX", + "description": [], + "signature": [ + "\"kibana-alert-history-\"" + ], + "path": "x-pack/plugins/actions/common/alert_history_schema.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.AlertHistoryDefaultIndexName", + "type": "string", + "tags": [], + "label": "AlertHistoryDefaultIndexName", + "description": [], + "path": "x-pack/plugins/actions/common/alert_history_schema.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.AlertHistoryDocumentTemplate", + "type": "CompoundType", + "tags": [], + "label": "AlertHistoryDocumentTemplate", + "description": [], + "signature": [ + "Readonly<{ event: { kind: string; }; kibana?: { alert: { actionGroupName?: string | undefined; actionGroup?: string | undefined; context?: { [x: string]: Record; } | undefined; id?: string | undefined; }; } | undefined; rule?: { type?: string | undefined; space?: string | undefined; params?: { [x: string]: Record; } | undefined; name?: string | undefined; id?: string | undefined; } | undefined; message?: unknown; tags?: string[] | undefined; '@timestamp': string; }> | null" + ], + "path": "x-pack/plugins/actions/common/alert_history_schema.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.AlertHistoryEsIndexConnectorId", + "type": "string", + "tags": [], + "label": "AlertHistoryEsIndexConnectorId", + "description": [], + "signature": [ + "\"preconfigured-alert-history-es-index\"" + ], + "path": "x-pack/plugins/actions/common/alert_history_schema.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.AlertsTableConfigurationRegistryContract", @@ -4784,6 +6081,51 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.connectorDeprecatedMessage", + "type": "string", + "tags": [], + "label": "connectorDeprecatedMessage", + "description": [], + "path": "x-pack/plugins/triggers_actions_ui/public/common/connectors_selection.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.ConnectorFormSchema", + "type": "Type", + "tags": [], + "label": "ConnectorFormSchema", + "description": [ + "\nThe following type is equivalent to:\n\ninterface ConnectorFormSchema {\n id?: string,\n name?: string,\n actionTypeId: string,\n isDeprecated: boolean,\n config: Config,\n secrets: Secrets,\n}" + ], + "signature": [ + "Pick<", + "UserConfiguredActionConnector", + ", \"config\" | \"secrets\" | \"actionTypeId\" | \"isDeprecated\"> & Partial, \"name\" | \"id\">>" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.deprecatedMessage", + "type": "string", + "tags": [], + "label": "deprecatedMessage", + "description": [], + "path": "x-pack/plugins/triggers_actions_ui/public/common/connectors_selection.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.GetRenderCellValue", @@ -5801,7 +7143,13 @@ "signature": [ "TypeRegistry", "<", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, ">" ], "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", @@ -5873,7 +7221,13 @@ "signature": [ "TypeRegistry", "<", - "ActionTypeModel", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.ActionTypeModel", + "text": "ActionTypeModel" + }, ">" ], "path": "x-pack/plugins/triggers_actions_ui/public/plugin.ts", diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 0a3cbb630285..e40b4b0c4d44 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Response Ops](https://github.com/orgs/elastic/teams/response-ops) for q | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 437 | 1 | 416 | 45 | +| 512 | 1 | 485 | 48 | ## Client diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index 2a01df283087..9a4994646988 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: 2022-09-27 +date: 2022-09-29 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 f46cdf9d62ee..e1fd951d5e2a 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] --- import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json'; diff --git a/api_docs/unified_field_list.mdx b/api_docs/unified_field_list.mdx index f70e303ebe4d..254b1228a31f 100644 --- a/api_docs/unified_field_list.mdx +++ b/api_docs/unified_field_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedFieldList title: "unifiedFieldList" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedFieldList plugin -date: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedFieldList'] --- import unifiedFieldListObj from './unified_field_list.devdocs.json'; diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index 2e10e0a54e2c..5015d2debdc4 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: 2022-09-27 +date: 2022-09-29 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 18b782fa7081..5a40018e6081 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] --- import unifiedSearchAutocompleteObj from './unified_search_autocomplete.devdocs.json'; diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index 7a9ded502796..f57d70892e70 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: 2022-09-27 +date: 2022-09-29 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 34bbb721a372..f40f15636c94 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: 2022-09-27 +date: 2022-09-29 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 c3afd8d86827..d462e4d19616 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: 2022-09-27 +date: 2022-09-29 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 271d79629a0b..0b2993331260 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: 2022-09-27 +date: 2022-09-29 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 f28ab39c716f..cc20cda9db10 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: 2022-09-27 +date: 2022-09-29 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 32a96558c72f..609c06b1a92c 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: 2022-09-27 +date: 2022-09-29 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 827eb1f6f096..3f8c15b314a0 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: 2022-09-27 +date: 2022-09-29 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 d9fc5b663679..5aa2bd8b01f5 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: 2022-09-27 +date: 2022-09-29 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 e8dfbf76d6ff..2219fc96f2e3 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: 2022-09-27 +date: 2022-09-29 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 c962200fe1fd..2608ea12c24f 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: 2022-09-27 +date: 2022-09-29 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 abe648760bf1..e535d8ca970e 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: 2022-09-27 +date: 2022-09-29 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 4260c1aaeb25..c6bd5fba0b06 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: 2022-09-27 +date: 2022-09-29 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 f03c9b37afbb..8caa2a67dab8 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] --- import visTypeXyObj from './vis_type_xy.devdocs.json'; diff --git a/api_docs/visualizations.devdocs.json b/api_docs/visualizations.devdocs.json index adc8f3022cb1..95b3390b94ce 100644 --- a/api_docs/visualizations.devdocs.json +++ b/api_docs/visualizations.devdocs.json @@ -3042,7 +3042,7 @@ "label": "sharingSavedObjectProps", "description": [], "signature": [ - "{ outcome?: \"exactMatch\" | \"aliasMatch\" | \"conflict\" | undefined; aliasTargetId?: string | undefined; aliasPurpose?: \"savedObjectConversion\" | \"savedObjectImport\" | undefined; errorJSON?: string | undefined; } | undefined" + "{ outcome?: \"conflict\" | \"exactMatch\" | \"aliasMatch\" | undefined; aliasTargetId?: string | undefined; aliasPurpose?: \"savedObjectConversion\" | \"savedObjectImport\" | undefined; errorJSON?: string | undefined; } | undefined" ], "path": "src/plugins/visualizations/public/types.ts", "deprecated": false, @@ -9284,6 +9284,203 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration", + "type": "Interface", + "tags": [], + "label": "MetricVisConfiguration", + "description": [], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.layerId", + "type": "string", + "tags": [], + "label": "layerId", + "description": [], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.layerType", + "type": "string", + "tags": [], + "label": "layerType", + "description": [], + "signature": [ + "\"data\"" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.metricAccessor", + "type": "string", + "tags": [], + "label": "metricAccessor", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.secondaryMetricAccessor", + "type": "string", + "tags": [], + "label": "secondaryMetricAccessor", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.maxAccessor", + "type": "string", + "tags": [], + "label": "maxAccessor", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.breakdownByAccessor", + "type": "string", + "tags": [], + "label": "breakdownByAccessor", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.collapseFn", + "type": "string", + "tags": [], + "label": "collapseFn", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.subtitle", + "type": "string", + "tags": [], + "label": "subtitle", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.secondaryPrefix", + "type": "string", + "tags": [], + "label": "secondaryPrefix", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.progressDirection", + "type": "CompoundType", + "tags": [], + "label": "progressDirection", + "description": [], + "signature": [ + "LayoutDirection", + " | undefined" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.color", + "type": "string", + "tags": [], + "label": "color", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.palette", + "type": "Object", + "tags": [], + "label": "palette", + "description": [], + "signature": [ + "PaletteOutput", + "<", + "CustomPaletteParams", + "> | undefined" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "visualizations", + "id": "def-common.MetricVisConfiguration.maxCols", + "type": "number", + "tags": [], + "label": "maxCols", + "description": [], + "signature": [ + "number | undefined" + ], + "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "visualizations", "id": "def-common.MovingAverageParams", @@ -11891,6 +12088,14 @@ "docId": "kibVisualizationsPluginApi", "section": "def-common.TableVisConfiguration", "text": "TableVisConfiguration" + }, + " | ", + { + "pluginId": "visualizations", + "scope": "common", + "docId": "kibVisualizationsPluginApi", + "section": "def-common.MetricVisConfiguration", + "text": "MetricVisConfiguration" } ], "path": "src/plugins/visualizations/common/convert_to_lens/types/configurations.ts", diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index c4ea2e6c61e7..6e29fc3d03f2 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: 2022-09-27 +date: 2022-09-29 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 679 | 12 | 649 | 18 | +| 693 | 12 | 663 | 18 | ## Client diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 819f20005d7a..36c8b46e7b80 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -98,3 +98,10 @@ user without those privileges updates the rule, the rule will no longer function. Conversely, if a user with greater or administrator privileges modifies the rule, it will begin running with increased privileges. ============================================== + +[float] +[[alerting-ccs-setup]] +=== {ccs-cap} + +If you want to use alerting rules with {ccs}, you must configure +{ref}/remote-clusters-privileges.html#clusters-privileges-ccs-kibana[privileges for {ccs-init} and {kib}]. \ No newline at end of file diff --git a/docs/user/dashboard/images/dashboard_timeSliderControl_8.5.0.gif b/docs/user/dashboard/images/dashboard_timeSliderControl_8.5.0.gif new file mode 100644 index 000000000000..89ca09dccc71 Binary files /dev/null and b/docs/user/dashboard/images/dashboard_timeSliderControl_8.5.0.gif differ diff --git a/docs/user/dashboard/images/dashboard_timeSliderControl_advanceBackward_8.5.0.png b/docs/user/dashboard/images/dashboard_timeSliderControl_advanceBackward_8.5.0.png new file mode 100644 index 000000000000..e1c2e9921f68 Binary files /dev/null and b/docs/user/dashboard/images/dashboard_timeSliderControl_advanceBackward_8.5.0.png differ diff --git a/docs/user/dashboard/images/dashboard_timeSliderControl_advanceForward_8.5.0.png b/docs/user/dashboard/images/dashboard_timeSliderControl_advanceForward_8.5.0.png new file mode 100644 index 000000000000..788621037cb0 Binary files /dev/null and b/docs/user/dashboard/images/dashboard_timeSliderControl_advanceForward_8.5.0.png differ diff --git a/docs/user/dashboard/images/dashboard_timeSliderControl_animate_8.5.0.png b/docs/user/dashboard/images/dashboard_timeSliderControl_animate_8.5.0.png new file mode 100644 index 000000000000..63a93323d6a3 Binary files /dev/null and b/docs/user/dashboard/images/dashboard_timeSliderControl_animate_8.5.0.png differ diff --git a/docs/user/dashboard/make-dashboards-interactive.asciidoc b/docs/user/dashboard/make-dashboards-interactive.asciidoc index 7c80fa858853..127c0a4a79e0 100644 --- a/docs/user/dashboard/make-dashboards-interactive.asciidoc +++ b/docs/user/dashboard/make-dashboards-interactive.asciidoc @@ -28,7 +28,7 @@ data-type="inline" *Controls* are interactive panels you add to your dashboards to filter and display only the data you want to explore. -There are two types of controls: +There are three types of controls: * *Options list* — Adds a dropdown that allows you to filter the data with one or more options that you select. + @@ -44,11 +44,17 @@ For example, if you are using the *[Logs] Web Traffic* dashboard from the sample [role="screenshot"] image::images/dashboard_controlsRangeSlider_8.3.0.png[Range slider control for the `hour_of_day` field with a range of `9` to `17` selected] +* *Time slider* — Adds a time range slider that allows you to filter the data within a specified range of time, advance the time range backward and forward, and animate your change in data over the specified time range. ++ +For example, you are using the *[Logs] Web Traffic* dashboard from the sample web logs data, and the global time filter is *Last 7 days*. When you add the time slider, you can click the previous and next buttons to advance the time range backward or forward, and click the play button to watch how the data changes over the last 7 days. +[role="screenshot"] +image::images/dashboard_timeSliderControl_8.5.0.gif[Time slider control for the the Last 7 days] + [float] -[[create-and-add-controls]] -==== Create and add controls +[[create-and-add-options-list-and-range-slider-controls]] +==== Create and add Options list and Range slider controls -To add interactive filters, create controls, then add them to your dashboard. +To add interactive Options list and Range slider controls, create the controls, then add them to your dashboard. . Open or create a new dashboard. @@ -79,8 +85,22 @@ The *Control type* is automatically applied for the field you selected. . Click *Save and close*. [float] -[[filter-the-data-with-options-lists]] -==== Filter the data with Options lists +[[add-time-slider-controls]] +==== Add time slider controls + +To add interactive time slider controls, create the control, then add it to your dashboard. + +. Open or create a new dashboard. + +. In the dashboard toolbar, click *Controls*, then select *Add time slider control*. + +. The time slider control uses the time range from the global time filter. To change the time range in the time slider control, <>. + +. Save the dashboard. + +[float] +[[filter-the-data-with-options-list-controls]] +==== Filter the data with Options list controls Filter the data with one or more options that you select. . Open the Options list dropdown. @@ -94,8 +114,8 @@ The dashboard displays only the data for the options you selected. . To display only the options you selected in the dropdown, click image:images/dashboard_showOnlySelectedOptions_8.3.0.png[The icon to display only the options you have selected in the Options list]. [float] -[[filter-the-data-with-range-sliders]] -==== Filter the data with Range sliders +[[filter-the-data-with-range-slider-controls]] +==== Filter the data with Range slider controls Filter the data within a specified range of values. . On the Range slider, click a value. @@ -106,6 +126,21 @@ The dashboard displays only the data for the range of values you specified. . To clear the specified values, click image:images/dashboard_controlsClearSelections_8.3.0.png[The icon to clear all specified values in the Range slider]. +[float] +[[filter-the-data-with-time-slider-controls]] +==== Filter the data with time slider controls +Filter the data within a specified range of time. + +. To view a different time range, click the time slider, then move the sliders to specify the time range you want to display. + +. To advance the time range forward, click image:images/dashboard_timeSliderControl_advanceForward_8.5.0.png[The icon to advance the time range forward]. + +. To advance the time range backward, click image:images/dashboard_timeSliderControl_advanceBackward_8.5.0.png[The icon to advance the time range backward]. + +. To animate the data changes over time, click image:images/dashboard_timeSliderControl_animate_8.5.0.png[The icon to clear all specified values in the Range slider]. + +. To clear the specified values, click image:images/dashboard_controlsClearSelections_8.3.0.png[The icon to clear all specified values in the Range slider]. + [float] [[configure-controls-settings]] ==== Configure the controls settings @@ -126,9 +161,9 @@ The dashboard displays only the data for the range of values you specified. [float] [[edit-controls]] -==== Edit controls +==== Edit Options list and Range slider control settings -Change the settings for a control. +Change the settings for the Options list and Range slider controls. . Hover over the control you want to edit, then click image:images/dashboard_controlsEditControl_8.3.0.png[The Edit control icon that opens the Edit control flyout]. diff --git a/docs/user/reporting/script-example.asciidoc b/docs/user/reporting/script-example.asciidoc index 1d8e824798e7..937e140bd67a 100644 --- a/docs/user/reporting/script-example.asciidoc +++ b/docs/user/reporting/script-example.asciidoc @@ -3,7 +3,7 @@ URL that you use to download the report. Use the `GET` method in the HTTP reques To queue CSV report generation using the `POST` URL with cURL: -["source","sh",subs="attributes"] +[source,curl] --------------------------------------------------------- curl \ -XPOST \ <1> @@ -11,7 +11,6 @@ curl \ -H 'kbn-xsrf: true' \ <3> 'http://0.0.0.0:5601/api/reporting/generate/csv?jobParams=...' <4> --------------------------------------------------------- -// CONSOLE <1> The required `POST` method. <2> The user credentials for a user with permission to access {kib} and {report-features}. @@ -20,7 +19,7 @@ curl \ An example response for a successfully queued report: -[source,json] +[source,js] --------------------------------------------------------- { "path": "/api/reporting/jobs/download/jxzaofkc0ykpf4062305t068", <1> @@ -35,7 +34,6 @@ An example response for a successfully queued report: } } --------------------------------------------------------- -// CONSOLE <1> The relative path on the {kib} host for downloading the report. <2> (Not included in the example) Internal representation of the reporting job, as found in the `.reporting-*` index. diff --git a/fleet_packages.json b/fleet_packages.json index c4a7f87127f1..4651e8628758 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -20,7 +20,7 @@ [ { "name": "apm", - "version": "8.4.0", + "version": "8.6.0-preview-1663775281", "forceAlignStackVersion": true }, { @@ -29,7 +29,7 @@ }, { "name": "endpoint", - "version": "8.4.1" + "version": "8.5.0" }, { "name": "fleet_server", @@ -39,4 +39,4 @@ "name": "synthetics", "version": "0.10.2" } -] +] \ No newline at end of file diff --git a/package.json b/package.json index 6c0913394e24..3bbcabba0cdd 100644 --- a/package.json +++ b/package.json @@ -213,6 +213,8 @@ "@kbn/core-http-common": "link:bazel-bin/packages/core/http/core-http-common", "@kbn/core-http-context-server-internal": "link:bazel-bin/packages/core/http/core-http-context-server-internal", "@kbn/core-http-context-server-mocks": "link:bazel-bin/packages/core/http/core-http-context-server-mocks", + "@kbn/core-http-request-handler-context-server": "link:bazel-bin/packages/core/http/core-http-request-handler-context-server", + "@kbn/core-http-request-handler-context-server-internal": "link:bazel-bin/packages/core/http/core-http-request-handler-context-server-internal", "@kbn/core-http-router-server-internal": "link:bazel-bin/packages/core/http/core-http-router-server-internal", "@kbn/core-http-router-server-mocks": "link:bazel-bin/packages/core/http/core-http-router-server-mocks", "@kbn/core-http-server": "link:bazel-bin/packages/core/http/core-http-server", @@ -333,6 +335,7 @@ "@kbn/safer-lodash-set": "link:bazel-bin/packages/kbn-safer-lodash-set", "@kbn/securitysolution-autocomplete": "link:bazel-bin/packages/kbn-securitysolution-autocomplete", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils", + "@kbn/securitysolution-exception-list-components": "link:bazel-bin/packages/kbn-securitysolution-exception-list-components", "@kbn/securitysolution-hook-utils": "link:bazel-bin/packages/kbn-securitysolution-hook-utils", "@kbn/securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types", "@kbn/securitysolution-io-ts-list-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types", @@ -639,7 +642,7 @@ "uuid": "3.3.2", "vega": "^5.22.1", "vega-interpreter": "^1.0.4", - "vega-lite": "^5.3.0", + "vega-lite": "^5.5.0", "vega-schema-url-parser": "^2.2.0", "vega-spec-injector": "^0.0.2", "vega-tooltip": "^0.28.0", @@ -934,6 +937,8 @@ "@types/kbn__core-http-common": "link:bazel-bin/packages/core/http/core-http-common/npm_module_types", "@types/kbn__core-http-context-server-internal": "link:bazel-bin/packages/core/http/core-http-context-server-internal/npm_module_types", "@types/kbn__core-http-context-server-mocks": "link:bazel-bin/packages/core/http/core-http-context-server-mocks/npm_module_types", + "@types/kbn__core-http-request-handler-context-server": "link:bazel-bin/packages/core/http/core-http-request-handler-context-server/npm_module_types", + "@types/kbn__core-http-request-handler-context-server-internal": "link:bazel-bin/packages/core/http/core-http-request-handler-context-server-internal/npm_module_types", "@types/kbn__core-http-router-server-internal": "link:bazel-bin/packages/core/http/core-http-router-server-internal/npm_module_types", "@types/kbn__core-http-router-server-mocks": "link:bazel-bin/packages/core/http/core-http-router-server-mocks/npm_module_types", "@types/kbn__core-http-server": "link:bazel-bin/packages/core/http/core-http-server/npm_module_types", @@ -1082,6 +1087,7 @@ "@types/kbn__rule-data-utils": "link:bazel-bin/packages/kbn-rule-data-utils/npm_module_types", "@types/kbn__securitysolution-autocomplete": "link:bazel-bin/packages/kbn-securitysolution-autocomplete/npm_module_types", "@types/kbn__securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module_types", + "@types/kbn__securitysolution-exception-list-components": "link:bazel-bin/packages/kbn-securitysolution-exception-list-components/npm_module_types", "@types/kbn__securitysolution-hook-utils": "link:bazel-bin/packages/kbn-securitysolution-hook-utils/npm_module_types", "@types/kbn__securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types/npm_module_types", "@types/kbn__securitysolution-io-ts-list-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 62cc4b4ce9ad..cf97e501df09 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -79,6 +79,8 @@ filegroup( "//packages/core/http/core-http-common:build", "//packages/core/http/core-http-context-server-internal:build", "//packages/core/http/core-http-context-server-mocks:build", + "//packages/core/http/core-http-request-handler-context-server:build", + "//packages/core/http/core-http-request-handler-context-server-internal:build", "//packages/core/http/core-http-router-server-internal:build", "//packages/core/http/core-http-router-server-mocks:build", "//packages/core/http/core-http-server:build", @@ -249,6 +251,7 @@ filegroup( "//packages/kbn-safer-lodash-set:build", "//packages/kbn-securitysolution-autocomplete:build", "//packages/kbn-securitysolution-es-utils:build", + "//packages/kbn-securitysolution-exception-list-components:build", "//packages/kbn-securitysolution-hook-utils:build", "//packages/kbn-securitysolution-io-ts-alerting-types:build", "//packages/kbn-securitysolution-io-ts-list-types:build", @@ -410,6 +413,8 @@ filegroup( "//packages/core/http/core-http-common:build_types", "//packages/core/http/core-http-context-server-internal:build_types", "//packages/core/http/core-http-context-server-mocks:build_types", + "//packages/core/http/core-http-request-handler-context-server:build_types", + "//packages/core/http/core-http-request-handler-context-server-internal:build_types", "//packages/core/http/core-http-router-server-internal:build_types", "//packages/core/http/core-http-router-server-mocks:build_types", "//packages/core/http/core-http-server:build_types", @@ -570,6 +575,7 @@ filegroup( "//packages/kbn-safer-lodash-set:build_types", "//packages/kbn-securitysolution-autocomplete:build_types", "//packages/kbn-securitysolution-es-utils:build_types", + "//packages/kbn-securitysolution-exception-list-components:build_types", "//packages/kbn-securitysolution-hook-utils:build_types", "//packages/kbn-securitysolution-io-ts-alerting-types:build_types", "//packages/kbn-securitysolution-io-ts-list-types:build_types", diff --git a/packages/core/http/core-http-request-handler-context-server-internal/BUILD.bazel b/packages/core/http/core-http-request-handler-context-server-internal/BUILD.bazel new file mode 100644 index 000000000000..82040b5fb1ad --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server-internal/BUILD.bazel @@ -0,0 +1,116 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-http-request-handler-context-server-internal" +PKG_REQUIRE_NAME = "@kbn/core-http-request-handler-context-server-internal" + +SOURCE_FILES = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "//packages/core/elasticsearch/core-elasticsearch-server-internal", + "//packages/core/saved-objects/core-saved-objects-server-internal", + "//packages/core/deprecations/core-deprecations-server-internal", + "//packages/core/ui-settings/core-ui-settings-server-internal", +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "//packages/core/http/core-http-server:npm_module_types", + "//packages/core/http/core-http-request-handler-context-server:npm_module_types", + "//packages/core/elasticsearch/core-elasticsearch-server-internal:npm_module_types", + "//packages/core/saved-objects/core-saved-objects-server-internal:npm_module_types", + "//packages/core/deprecations/core-deprecations-server-internal:npm_module_types", + "//packages/core/ui-settings/core-ui-settings-server:npm_module_types", + "//packages/core/ui-settings/core-ui-settings-server-internal:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/http/core-http-request-handler-context-server-internal/README.md b/packages/core/http/core-http-request-handler-context-server-internal/README.md new file mode 100644 index 000000000000..aa27c116c4f6 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server-internal/README.md @@ -0,0 +1,3 @@ +# @kbn/core-http-request-handler-context-server-internal + +This package contains the internal types and implementation of Core's `CoreRequestHandlerContext` diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_constants.ts b/packages/core/http/core-http-request-handler-context-server-internal/index.ts similarity index 58% rename from src/plugins/dashboard/public/application/embeddable/dashboard_constants.ts rename to packages/core/http/core-http-request-handler-context-server-internal/index.ts index 2f7854e81ad9..2ee514f67265 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_constants.ts +++ b/packages/core/http/core-http-request-handler-context-server-internal/index.ts @@ -6,8 +6,5 @@ * Side Public License, v 1. */ -export const DASHBOARD_GRID_COLUMN_COUNT = 48; -export const DASHBOARD_GRID_HEIGHT = 20; -export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2; -export const DEFAULT_PANEL_HEIGHT = 15; -export const DASHBOARD_CONTAINER_TYPE = 'dashboard'; +export { CoreRouteHandlerContext, PrebootCoreRouteHandlerContext } from './src'; +export type { CoreRouteHandlerContextParams, PrebootCoreRouteHandlerContextParams } from './src'; diff --git a/packages/core/http/core-http-request-handler-context-server-internal/jest.config.js b/packages/core/http/core-http-request-handler-context-server-internal/jest.config.js new file mode 100644 index 000000000000..68a5f2b3a03a --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server-internal/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../..', + roots: ['/packages/core/http/core-http-request-handler-context-server-internal'], +}; diff --git a/packages/core/http/core-http-request-handler-context-server-internal/kibana.jsonc b/packages/core/http/core-http-request-handler-context-server-internal/kibana.jsonc new file mode 100644 index 000000000000..98b452aec0d9 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server-internal/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/core-http-request-handler-context-server-internal", + "owner": "@elastic/kibana-core", + "runtimeDeps": [], + "typeDeps": [], +} diff --git a/packages/core/http/core-http-request-handler-context-server-internal/package.json b/packages/core/http/core-http-request-handler-context-server-internal/package.json new file mode 100644 index 000000000000..672bb6ce7271 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server-internal/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/core-http-request-handler-context-server-internal", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "author": "Kibana Core", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/src/core/server/core_route_handler_context.test.ts b/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.test.ts similarity index 86% rename from src/core/server/core_route_handler_context.test.ts rename to packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.test.ts index ace0144eae54..f8e906650fcf 100644 --- a/src/core/server/core_route_handler_context.test.ts +++ b/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.test.ts @@ -7,13 +7,14 @@ */ import { CoreRouteHandlerContext } from './core_route_handler_context'; -import { coreMock, httpServerMock } from './mocks'; +import { httpServerMock } from '@kbn/core-http-server-mocks'; +import { createCoreRouteHandlerContextParamsMock } from './test_helpers'; describe('#elasticsearch', () => { describe('#client', () => { test('returns the results of coreStart.elasticsearch.client.asScoped', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); const client = context.elasticsearch.client; @@ -22,7 +23,7 @@ describe('#elasticsearch', () => { test('lazily created', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); expect(coreStart.elasticsearch.client.asScoped).not.toHaveBeenCalled(); @@ -33,7 +34,7 @@ describe('#elasticsearch', () => { test('only creates one instance', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); const client1 = context.elasticsearch.client; @@ -50,7 +51,7 @@ describe('#savedObjects', () => { describe('#client', () => { test('returns the results of coreStart.savedObjects.getScopedClient', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); const client = context.savedObjects.client; @@ -59,7 +60,7 @@ describe('#savedObjects', () => { test('lazily created', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); const savedObjects = context.savedObjects; @@ -71,7 +72,7 @@ describe('#savedObjects', () => { test('only creates one instance', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); const client1 = context.savedObjects.client; @@ -86,7 +87,7 @@ describe('#savedObjects', () => { describe('#typeRegistry', () => { test('returns the results of coreStart.savedObjects.getTypeRegistry', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); const typeRegistry = context.savedObjects.typeRegistry; @@ -95,7 +96,7 @@ describe('#savedObjects', () => { test('lazily created', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); expect(coreStart.savedObjects.getTypeRegistry).not.toHaveBeenCalled(); @@ -106,7 +107,7 @@ describe('#savedObjects', () => { test('only creates one instance', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); const typeRegistry1 = context.savedObjects.typeRegistry; @@ -123,7 +124,7 @@ describe('#uiSettings', () => { describe('#client', () => { test('returns the results of coreStart.uiSettings.asScopedToClient', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); const client = context.uiSettings.client; @@ -132,7 +133,7 @@ describe('#uiSettings', () => { test('lazily created', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); expect(coreStart.uiSettings.asScopedToClient).not.toHaveBeenCalled(); @@ -143,7 +144,7 @@ describe('#uiSettings', () => { test('only creates one instance', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); const client1 = context.uiSettings.client; @@ -160,7 +161,7 @@ describe('#deprecations', () => { describe('#client', () => { test('returns the results of coreStart.deprecations.asScopedToClient', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); const client = context.deprecations.client; @@ -169,7 +170,7 @@ describe('#deprecations', () => { test('lazily created', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); expect(coreStart.deprecations.asScopedToClient).not.toHaveBeenCalled(); @@ -180,7 +181,7 @@ describe('#deprecations', () => { test('only creates one instance', () => { const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); + const coreStart = createCoreRouteHandlerContextParamsMock(); const context = new CoreRouteHandlerContext(coreStart, request); const client1 = context.deprecations.client; diff --git a/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.ts b/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.ts new file mode 100644 index 000000000000..6cc54130a575 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { KibanaRequest } from '@kbn/core-http-server'; +import type { CoreRequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; +import { + CoreElasticsearchRouteHandlerContext, + type InternalElasticsearchServiceStart, +} from '@kbn/core-elasticsearch-server-internal'; +import { + CoreSavedObjectsRouteHandlerContext, + type InternalSavedObjectsServiceStart, +} from '@kbn/core-saved-objects-server-internal'; +import { + CoreDeprecationsRouteHandlerContext, + type InternalDeprecationsServiceStart, +} from '@kbn/core-deprecations-server-internal'; +import { + CoreUiSettingsRouteHandlerContext, + type InternalUiSettingsServiceStart, +} from '@kbn/core-ui-settings-server-internal'; + +/** + * Subset of `InternalCoreStart` used by {@link CoreRouteHandlerContext} + * @internal + */ +export interface CoreRouteHandlerContextParams { + elasticsearch: InternalElasticsearchServiceStart; + savedObjects: InternalSavedObjectsServiceStart; + uiSettings: InternalUiSettingsServiceStart; + deprecations: InternalDeprecationsServiceStart; +} + +/** + * The concrete implementation for Core's route handler context. + * + * @internal + */ +export class CoreRouteHandlerContext implements CoreRequestHandlerContext { + readonly elasticsearch: CoreElasticsearchRouteHandlerContext; + readonly savedObjects: CoreSavedObjectsRouteHandlerContext; + readonly uiSettings: CoreUiSettingsRouteHandlerContext; + readonly deprecations: CoreDeprecationsRouteHandlerContext; + + constructor(coreStart: CoreRouteHandlerContextParams, request: KibanaRequest) { + this.elasticsearch = new CoreElasticsearchRouteHandlerContext(coreStart.elasticsearch, request); + this.savedObjects = new CoreSavedObjectsRouteHandlerContext(coreStart.savedObjects, request); + this.uiSettings = new CoreUiSettingsRouteHandlerContext( + coreStart.uiSettings, + this.savedObjects + ); + this.deprecations = new CoreDeprecationsRouteHandlerContext( + coreStart.deprecations, + this.elasticsearch, + this.savedObjects + ); + } +} diff --git a/packages/core/http/core-http-request-handler-context-server-internal/src/index.ts b/packages/core/http/core-http-request-handler-context-server-internal/src/index.ts new file mode 100644 index 000000000000..48d93f78b2a4 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server-internal/src/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { CoreRouteHandlerContext } from './core_route_handler_context'; +export type { CoreRouteHandlerContextParams } from './core_route_handler_context'; +export { PrebootCoreRouteHandlerContext } from './preboot_core_route_handler_context'; +export type { PrebootCoreRouteHandlerContextParams } from './preboot_core_route_handler_context'; diff --git a/src/core/server/preboot_core_route_handler_context.test.ts b/packages/core/http/core-http-request-handler-context-server-internal/src/preboot_core_route_handler_context.test.ts similarity index 85% rename from src/core/server/preboot_core_route_handler_context.test.ts rename to packages/core/http/core-http-request-handler-context-server-internal/src/preboot_core_route_handler_context.test.ts index 8d090d864463..75bfc39bfdc9 100644 --- a/src/core/server/preboot_core_route_handler_context.test.ts +++ b/packages/core/http/core-http-request-handler-context-server-internal/src/preboot_core_route_handler_context.test.ts @@ -7,12 +7,12 @@ */ import { PrebootCoreRouteHandlerContext } from './preboot_core_route_handler_context'; -import { coreMock } from './mocks'; +import { createPrebootCoreRouteHandlerContextParamsMock } from './test_helpers'; describe('#uiSettings', () => { describe('#client', () => { test('returns the results of corePreboot.uiSettings.createDefaultsClient', () => { - const corePreboot = coreMock.createInternalPreboot(); + const corePreboot = createPrebootCoreRouteHandlerContextParamsMock(); const context = new PrebootCoreRouteHandlerContext(corePreboot); const client = context.uiSettings.client; @@ -21,7 +21,7 @@ describe('#uiSettings', () => { }); test('only creates one instance', () => { - const corePreboot = coreMock.createInternalPreboot(); + const corePreboot = createPrebootCoreRouteHandlerContextParamsMock(); const context = new PrebootCoreRouteHandlerContext(corePreboot); const client1 = context.uiSettings.client; diff --git a/src/core/server/preboot_core_route_handler_context.ts b/packages/core/http/core-http-request-handler-context-server-internal/src/preboot_core_route_handler_context.ts similarity index 71% rename from src/core/server/preboot_core_route_handler_context.ts rename to packages/core/http/core-http-request-handler-context-server-internal/src/preboot_core_route_handler_context.ts index ec5ef594180d..c0bf184d8c98 100644 --- a/src/core/server/preboot_core_route_handler_context.ts +++ b/packages/core/http/core-http-request-handler-context-server-internal/src/preboot_core_route_handler_context.ts @@ -8,13 +8,17 @@ // eslint-disable-next-line max-classes-per-file import type { IUiSettingsClient } from '@kbn/core-ui-settings-server'; -import type { InternalCorePreboot } from './internal_types'; +import type { + PrebootUiSettingsRequestHandlerContext, + PrebootCoreRequestHandlerContext, +} from '@kbn/core-http-request-handler-context-server'; +import type { InternalUiSettingsServicePreboot } from '@kbn/core-ui-settings-server-internal'; /** - * @public + * @internal */ -export interface PrebootUiSettingsRequestHandlerContext { - client: IUiSettingsClient; +export interface PrebootCoreRouteHandlerContextParams { + uiSettings: InternalUiSettingsServicePreboot; } /** @@ -25,13 +29,6 @@ class PrebootCoreUiSettingsRouteHandlerContext implements PrebootUiSettingsReque constructor(public readonly client: IUiSettingsClient) {} } -/** - * @public - */ -export interface PrebootCoreRequestHandlerContext { - uiSettings: PrebootUiSettingsRequestHandlerContext; -} - /** * Implementation of {@link PrebootCoreRequestHandlerContext}. * @internal @@ -39,7 +36,7 @@ export interface PrebootCoreRequestHandlerContext { export class PrebootCoreRouteHandlerContext implements PrebootCoreRequestHandlerContext { readonly uiSettings: PrebootUiSettingsRequestHandlerContext; - constructor(private readonly corePreboot: InternalCorePreboot) { + constructor(private readonly corePreboot: PrebootCoreRouteHandlerContextParams) { this.uiSettings = new PrebootCoreUiSettingsRouteHandlerContext( this.corePreboot.uiSettings.createDefaultsClient() ); diff --git a/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/core_route_handler_context_params.mock.ts b/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/core_route_handler_context_params.mock.ts new file mode 100644 index 000000000000..eaf46eae3c8b --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/core_route_handler_context_params.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { savedObjectsServiceMock } from '@kbn/core-saved-objects-server-mocks'; +import { uiSettingsServiceMock } from '@kbn/core-ui-settings-server-mocks'; +import { deprecationsServiceMock } from '@kbn/core-deprecations-server-mocks'; + +export const createCoreRouteHandlerContextParamsMock = () => { + return { + elasticsearch: elasticsearchServiceMock.createInternalStart(), + savedObjects: savedObjectsServiceMock.createInternalStartContract(), + uiSettings: uiSettingsServiceMock.createStartContract(), + deprecations: deprecationsServiceMock.createInternalStartContract(), + }; +}; diff --git a/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/index.ts b/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/index.ts new file mode 100644 index 000000000000..5556547d4602 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { createCoreRouteHandlerContextParamsMock } from './core_route_handler_context_params.mock'; +export { createPrebootCoreRouteHandlerContextParamsMock } from './preboot_core_route_handler_context_params.mock'; diff --git a/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/preboot_core_route_handler_context_params.mock.ts b/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/preboot_core_route_handler_context_params.mock.ts new file mode 100644 index 000000000000..dc4eab6b5567 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/preboot_core_route_handler_context_params.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { uiSettingsServiceMock } from '@kbn/core-ui-settings-server-mocks'; + +export const createPrebootCoreRouteHandlerContextParamsMock = () => { + return { + uiSettings: uiSettingsServiceMock.createPrebootContract(), + }; +}; diff --git a/packages/core/http/core-http-request-handler-context-server-internal/tsconfig.json b/packages/core/http/core-http-request-handler-context-server-internal/tsconfig.json new file mode 100644 index 000000000000..71bb40fe57f3 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server-internal/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ] +} diff --git a/packages/core/http/core-http-request-handler-context-server/BUILD.bazel b/packages/core/http/core-http-request-handler-context-server/BUILD.bazel new file mode 100644 index 000000000000..45c5ebc08776 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server/BUILD.bazel @@ -0,0 +1,110 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-http-request-handler-context-server" +PKG_REQUIRE_NAME = "@kbn/core-http-request-handler-context-server" + +SOURCE_FILES = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "//packages/core/http/core-http-server:npm_module_types", + "//packages/core/elasticsearch/core-elasticsearch-server:npm_module_types", + "//packages/core/saved-objects/core-saved-objects-server:npm_module_types", + "//packages/core/deprecations/core-deprecations-server:npm_module_types", + "//packages/core/ui-settings/core-ui-settings-server:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/http/core-http-request-handler-context-server/README.md b/packages/core/http/core-http-request-handler-context-server/README.md new file mode 100644 index 000000000000..b7b191f7345f --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server/README.md @@ -0,0 +1,3 @@ +# @kbn/core-http-request-handler-context-server + +This package contains the public types for Core's server-side `RequestHandlerContext` http subdomain. diff --git a/packages/core/http/core-http-request-handler-context-server/index.ts b/packages/core/http/core-http-request-handler-context-server/index.ts new file mode 100644 index 000000000000..125c466d25d2 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { + RequestHandlerContext, + CoreRequestHandlerContext, + CustomRequestHandlerContext, + PrebootCoreRequestHandlerContext, + PrebootRequestHandlerContext, + PrebootUiSettingsRequestHandlerContext, +} from './src'; diff --git a/src/plugins/dashboard/common/embeddable/types.ts b/packages/core/http/core-http-request-handler-context-server/jest.config.js similarity index 68% rename from src/plugins/dashboard/common/embeddable/types.ts rename to packages/core/http/core-http-request-handler-context-server/jest.config.js index d786078766f7..dc60767ed088 100644 --- a/src/plugins/dashboard/common/embeddable/types.ts +++ b/packages/core/http/core-http-request-handler-context-server/jest.config.js @@ -6,11 +6,8 @@ * Side Public License, v 1. */ -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type GridData = { - w: number; - h: number; - x: number; - y: number; - i: string; +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../..', + roots: ['/packages/core/http/core-http-request-handler-context-server'], }; diff --git a/packages/core/http/core-http-request-handler-context-server/kibana.jsonc b/packages/core/http/core-http-request-handler-context-server/kibana.jsonc new file mode 100644 index 000000000000..3fba38b6444e --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/core-http-request-handler-context-server", + "owner": "@elastic/kibana-core", + "runtimeDeps": [], + "typeDeps": [], +} diff --git a/packages/core/http/core-http-request-handler-context-server/package.json b/packages/core/http/core-http-request-handler-context-server/package.json new file mode 100644 index 000000000000..da85fc826828 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/core-http-request-handler-context-server", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "author": "Kibana Core", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/core/http/core-http-request-handler-context-server/src/index.ts b/packages/core/http/core-http-request-handler-context-server/src/index.ts new file mode 100644 index 000000000000..2ab372a0ab97 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { + RequestHandlerContext, + CoreRequestHandlerContext, + CustomRequestHandlerContext, +} from './request_handler_context'; +export type { + PrebootRequestHandlerContext, + PrebootCoreRequestHandlerContext, + PrebootUiSettingsRequestHandlerContext, +} from './preboot_request_handler_context'; diff --git a/packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts b/packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts new file mode 100644 index 000000000000..62caefe5619c --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { RequestHandlerContextBase } from '@kbn/core-http-server'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-server'; + +/** + * @public + */ +export interface PrebootUiSettingsRequestHandlerContext { + client: IUiSettingsClient; +} + +/** + * @public + */ +export interface PrebootCoreRequestHandlerContext { + uiSettings: PrebootUiSettingsRequestHandlerContext; +} + +/** + * @public + */ +export interface PrebootRequestHandlerContext extends RequestHandlerContextBase { + core: Promise; +} diff --git a/src/core/server/core_route_handler_context.ts b/packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts similarity index 53% rename from src/core/server/core_route_handler_context.ts rename to packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts index 14cc99b50143..07704752bd8e 100644 --- a/src/core/server/core_route_handler_context.ts +++ b/packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts @@ -6,16 +6,11 @@ * Side Public License, v 1. */ -import type { KibanaRequest } from '@kbn/core-http-server'; +import type { RequestHandlerContextBase } from '@kbn/core-http-server'; import type { ElasticsearchRequestHandlerContext } from '@kbn/core-elasticsearch-server'; -import { CoreElasticsearchRouteHandlerContext } from '@kbn/core-elasticsearch-server-internal'; import type { SavedObjectsRequestHandlerContext } from '@kbn/core-saved-objects-server'; -import { CoreSavedObjectsRouteHandlerContext } from '@kbn/core-saved-objects-server-internal'; import type { DeprecationsRequestHandlerContext } from '@kbn/core-deprecations-server'; -import { CoreDeprecationsRouteHandlerContext } from '@kbn/core-deprecations-server-internal'; import type { UiSettingsRequestHandlerContext } from '@kbn/core-ui-settings-server'; -import { CoreUiSettingsRouteHandlerContext } from '@kbn/core-ui-settings-server-internal'; -import type { InternalCoreStart } from './internal_types'; /** * The `core` context provided to route handler. @@ -39,27 +34,19 @@ export interface CoreRequestHandlerContext { } /** - * The concrete implementation for Core's route handler context. + * Base context passed to a route handler, containing the `core` context part. * - * @internal + * @public */ -export class CoreRouteHandlerContext implements CoreRequestHandlerContext { - readonly elasticsearch: CoreElasticsearchRouteHandlerContext; - readonly savedObjects: CoreSavedObjectsRouteHandlerContext; - readonly uiSettings: CoreUiSettingsRouteHandlerContext; - readonly deprecations: CoreDeprecationsRouteHandlerContext; - - constructor(coreStart: InternalCoreStart, request: KibanaRequest) { - this.elasticsearch = new CoreElasticsearchRouteHandlerContext(coreStart.elasticsearch, request); - this.savedObjects = new CoreSavedObjectsRouteHandlerContext(coreStart.savedObjects, request); - this.uiSettings = new CoreUiSettingsRouteHandlerContext( - coreStart.uiSettings, - this.savedObjects - ); - this.deprecations = new CoreDeprecationsRouteHandlerContext( - coreStart.deprecations, - this.elasticsearch, - this.savedObjects - ); - } +export interface RequestHandlerContext extends RequestHandlerContextBase { + core: Promise; } + +/** + * Mixin allowing plugins to define their own request handler contexts. + * + * @public + */ +export type CustomRequestHandlerContext = RequestHandlerContext & { + [Key in keyof T]: T[Key] extends Promise ? T[Key] : Promise; +}; diff --git a/packages/core/http/core-http-request-handler-context-server/tsconfig.json b/packages/core/http/core-http-request-handler-context-server/tsconfig.json new file mode 100644 index 000000000000..71bb40fe57f3 --- /dev/null +++ b/packages/core/http/core-http-request-handler-context-server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ] +} diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 721fc431a3c2..3a0b89c1f0d1 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -31,6 +31,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { const ENTERPRISE_SEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/enterprise-search/${DOC_LINK_VERSION}/`; const WORKPLACE_SEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/workplace-search/${DOC_LINK_VERSION}/`; const SEARCH_UI_DOCS = `${DOCS_WEBSITE_URL}search-ui/`; + const MACHINE_LEARNING_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/`; return deepFreeze({ settings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/settings.html`, @@ -398,39 +399,39 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { savedObjectsApiList: `${KIBANA_DOCS}saved-objects-api.html#saved-objects-api`, }, ml: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/index.html`, - aggregations: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-aggregation.html`, - anomalyDetection: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-overview.html`, - anomalyDetectionJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, - anomalyDetectionConfiguringCategories: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-categories.html`, - anomalyDetectionBucketSpan: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-bucket-span`, - anomalyDetectionCardinality: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-cardinality`, - anomalyDetectionCreateJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-create-job`, - anomalyDetectionDetectors: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-detectors`, - anomalyDetectionInfluencers: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-influencers`, + guide: `${MACHINE_LEARNING_DOCS}index.html`, + aggregations: `${MACHINE_LEARNING_DOCS}ml-configuring-aggregation.html`, + anomalyDetection: `${MACHINE_LEARNING_DOCS}ml-ad-overview.html`, + anomalyDetectionJobs: `${MACHINE_LEARNING_DOCS}ml-ad-finding-anomalies.html`, + anomalyDetectionConfiguringCategories: `${MACHINE_LEARNING_DOCS}ml-configuring-categories.html`, + anomalyDetectionBucketSpan: `${MACHINE_LEARNING_DOCS}ml-ad-run-jobs.html#ml-ad-bucket-span`, + anomalyDetectionCardinality: `${MACHINE_LEARNING_DOCS}ml-ad-run-jobs.html#ml-ad-cardinality`, + anomalyDetectionCreateJobs: `${MACHINE_LEARNING_DOCS}ml-ad-run-jobs.html#ml-ad-create-job`, + anomalyDetectionDetectors: `${MACHINE_LEARNING_DOCS}ml-ad-run-jobs.html#ml-ad-detectors`, + anomalyDetectionInfluencers: `${MACHINE_LEARNING_DOCS}ml-ad-run-jobs.html#ml-ad-influencers`, anomalyDetectionJobResource: `${ELASTICSEARCH_DOCS}ml-put-job.html#ml-put-job-path-parms`, anomalyDetectionJobResourceAnalysisConfig: `${ELASTICSEARCH_DOCS}ml-put-job.html#put-analysisconfig`, - anomalyDetectionJobTips: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-job-tips`, - alertingRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-alerts.html`, - anomalyDetectionModelMemoryLimits: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-model-memory-limits`, - calendars: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-calendars`, - classificationEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-classification-evaluation`, - customRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-run-jobs.html#ml-ad-rules`, - customUrls: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-url.html`, - dataFrameAnalytics: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics.html`, - dFAPrepareData: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-overview.html#prepare-transform-data`, - featureImportance: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-feature-importance.html`, - outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-finding-outliers.html#ml-dfanalytics-roc`, - regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-regression.html#ml-dfanalytics-regression-evaluation`, - classificationAucRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-class-aucroc`, + anomalyDetectionJobTips: `${MACHINE_LEARNING_DOCS}ml-ad-run-jobs.html#ml-ad-job-tips`, + alertingRules: `${MACHINE_LEARNING_DOCS}ml-configuring-alerts.html`, + anomalyDetectionModelMemoryLimits: `${MACHINE_LEARNING_DOCS}ml-ad-run-jobs.html#ml-ad-model-memory-limits`, + calendars: `${MACHINE_LEARNING_DOCS}ml-ad-run-jobs.html#ml-ad-calendars`, + classificationEvaluation: `${MACHINE_LEARNING_DOCS}ml-dfa-classification.html#ml-dfanalytics-classification-evaluation`, + customRules: `${MACHINE_LEARNING_DOCS}ml-ad-run-jobs.html#ml-ad-rules`, + customUrls: `${MACHINE_LEARNING_DOCS}ml-configuring-url.html`, + dataFrameAnalytics: `${MACHINE_LEARNING_DOCS}ml-dfanalytics.html`, + dFAPrepareData: `${MACHINE_LEARNING_DOCS}ml-dfa-overview.html#prepare-transform-data`, + featureImportance: `${MACHINE_LEARNING_DOCS}ml-feature-importance.html`, + outlierDetectionRoc: `${MACHINE_LEARNING_DOCS}ml-dfa-finding-outliers.html#ml-dfanalytics-roc`, + regressionEvaluation: `${MACHINE_LEARNING_DOCS}ml-dfa-regression.html#ml-dfanalytics-regression-evaluation`, + classificationAucRoc: `${MACHINE_LEARNING_DOCS}ml-dfa-classification.html#ml-dfanalytics-class-aucroc`, setUpgradeMode: `${ELASTICSEARCH_DOCS}ml-set-upgrade-mode.html`, - trainedModels: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-trained-models.html`, - startTrainedModelsDeployment: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-nlp-deploy-models.html#ml-nlp-deploy-model`, + trainedModels: `${MACHINE_LEARNING_DOCS}ml-trained-models.html`, + startTrainedModelsDeployment: `${MACHINE_LEARNING_DOCS}ml-nlp-deploy-models.html#ml-nlp-deploy-model`, }, transforms: { guide: `${ELASTICSEARCH_DOCS}transforms.html`, // TODO add valid docs URL - alertingRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-alerts.html`, + alertingRules: `${MACHINE_LEARNING_DOCS}ml-configuring-alerts.html`, }, visualize: { guide: `${KIBANA_DOCS}dashboard.html`, diff --git a/packages/kbn-securitysolution-exception-list-components/BUILD.bazel b/packages/kbn-securitysolution-exception-list-components/BUILD.bazel new file mode 100644 index 000000000000..6436793fa5f3 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/BUILD.bazel @@ -0,0 +1,163 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + + +PKG_DIRNAME = "kbn-securitysolution-exception-list-components" +PKG_REQUIRE_NAME = "@kbn/securitysolution-exception-list-components" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + "**/*.svg", + "**/*.d.ts", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "jest.config.js" +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//react", + "//packages/kbn-securitysolution-io-ts-list-types", + "//packages/kbn-securitysolution-autocomplete", + "//packages/kbn-ui-theme", + "//packages/kbn-i18n-react", + "//packages/kbn-i18n", + "@npm//@elastic/eui", + "@npm//@emotion/css", + "@npm//@emotion/react", + "@npm//@testing-library/jest-dom", + "@npm//jest", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react", + "//packages/kbn-securitysolution-io-ts-list-types:npm_module_types", + "//packages/kbn-securitysolution-autocomplete:npm_module_types", + "//packages/kbn-ui-theme:npm_module_types", + "//packages/kbn-i18n-react:npm_module_types", + "@npm//@elastic/eui", + "@npm//@emotion/css", + "@npm//@emotion/react", + "@npm//jest", + +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), + additional_args = [ + "--copy-files" + ], +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, + additional_args = [ + "--copy-files" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + declaration_map = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-exception-list-components/README.md b/packages/kbn-securitysolution-exception-list-components/README.md new file mode 100644 index 000000000000..e23b85e40996 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/README.md @@ -0,0 +1,27 @@ +# @kbn/securitysolution-exception-list-components + +This is where the building UI components of the Exception-List live +Most of the components here are imported from `x-pack/plugins/security_solutions/public/detection_engine` + +# Aim + +TODO + +# Pattern used + +``` +component + index.tsx + index.styles.ts <-- to hold styles if the component has many custom styles + use_component.ts <-- for logic if the Presentational Component has logic + index.test.tsx + use_component.test.tsx +``` + +# Next + +- Now the `ExceptionItems, ExceptionItemCard +and ExceptionItemCardMetaInfo + ` receive `securityLinkAnchorComponent, exceptionsUtilityComponent +, and exceptionsUtilityComponent +` as props to avoid moving all the `common` components under the `x-pack` at once, later we should move all building blocks to this `kbn-package` diff --git a/packages/kbn-securitysolution-exception-list-components/index.ts b/packages/kbn-securitysolution-exception-list-components/index.ts new file mode 100644 index 000000000000..f5001ff35fd3 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './src/search_bar/search_bar'; +export * from './src/empty_viewer_state/empty_viewer_state'; +export * from './src/pagination/pagination'; +// export * from './src/exceptions_utility/exceptions_utility'; +export * from './src/exception_items/exception_items'; +export * from './src/exception_item_card'; +export * from './src/value_with_space_warning'; +export * from './src/types'; diff --git a/packages/kbn-securitysolution-exception-list-components/jest.config.js b/packages/kbn-securitysolution-exception-list-components/jest.config.js new file mode 100644 index 000000000000..37a11c23c75b --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/jest.config.js @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-exception-list-components'], + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/packages/kbn-securitysolution-exception-list-components/**/*.{ts,tsx}', + '!/packages/kbn-securitysolution-exception-list-components/**/*.test', + ], + setupFilesAfterEnv: [ + '/packages/kbn-securitysolution-exception-list-components/setup_test.ts', + ], +}; diff --git a/packages/kbn-securitysolution-exception-list-components/kibana.jsonc b/packages/kbn-securitysolution-exception-list-components/kibana.jsonc new file mode 100644 index 000000000000..081c50d35af0 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/securitysolution-exception-list-components", + "owner": "@elastic/security-solution-platform", + "runtimeDeps": [], + "typeDeps": [], +} diff --git a/packages/kbn-securitysolution-exception-list-components/package.json b/packages/kbn-securitysolution-exception-list-components/package.json new file mode 100644 index 000000000000..263863d725c1 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/securitysolution-exception-list-components", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/kbn-securitysolution-exception-list-components/setup_test.ts b/packages/kbn-securitysolution-exception-list-components/setup_test.ts new file mode 100644 index 000000000000..bb55d97ec930 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/setup_test.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +// eslint-disable-next-line import/no-extraneous-dependencies +import '@testing-library/jest-dom'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/assets/images/illustration_product_no_results_magnifying_glass.svg b/packages/kbn-securitysolution-exception-list-components/src/assets/images/illustration_product_no_results_magnifying_glass.svg new file mode 100644 index 000000000000..b9a0df1630b2 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/assets/images/illustration_product_no_results_magnifying_glass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/dashboard/public/services/saved_objects/types.ts b/packages/kbn-securitysolution-exception-list-components/src/custom.d.ts similarity index 70% rename from src/plugins/dashboard/public/services/saved_objects/types.ts rename to packages/kbn-securitysolution-exception-list-components/src/custom.d.ts index d7d06131f32c..9169166fe7af 100644 --- a/src/plugins/dashboard/public/services/saved_objects/types.ts +++ b/packages/kbn-securitysolution-exception-list-components/src/custom.d.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import type { CoreStart } from '@kbn/core/public'; - -export interface DashboardSavedObjectsService { - client: CoreStart['savedObjects']['client']; +declare module '*.svg' { + const content: string; + // eslint-disable-next-line import/no-default-export + export default content; } diff --git a/packages/kbn-securitysolution-exception-list-components/src/empty_viewer_state/empty_viewer_state.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/empty_viewer_state/empty_viewer_state.test.tsx new file mode 100644 index 000000000000..43943e0e8fb9 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/empty_viewer_state/empty_viewer_state.test.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { EmptyViewerState } from './empty_viewer_state'; +import { ListTypeText, ViewerStatus } from '../types'; + +describe('EmptyViewerState', () => { + it('it should render "error" with the default title and body', () => { + const wrapper = render( + + ); + + expect(wrapper.getByTestId('errorViewerState')).toBeTruthy(); + expect(wrapper.getByTestId('errorTitle')).toHaveTextContent('Unable to load exception items'); + expect(wrapper.getByTestId('errorBody')).toHaveTextContent( + 'There was an error loading the exception items. Contact your administrator for help.' + ); + }); + it('it should render "error" when sending the title and body props', () => { + const wrapper = render( + + ); + + expect(wrapper.getByTestId('errorViewerState')).toBeTruthy(); + expect(wrapper.getByTestId('errorTitle')).toHaveTextContent('Error title'); + expect(wrapper.getByTestId('errorBody')).toHaveTextContent('Error body'); + }); + it('it should render loading', () => { + const wrapper = render( + + ); + + expect(wrapper.getByTestId('loadingViewerState')).toBeTruthy(); + }); + it('it should render empty search with the default title and body', () => { + const wrapper = render( + + ); + + expect(wrapper.getByTestId('emptySearchViewerState')).toBeTruthy(); + expect(wrapper.getByTestId('emptySearchTitle')).toHaveTextContent( + 'No results match your search criteria' + ); + expect(wrapper.getByTestId('emptySearchBody')).toHaveTextContent('Try modifying your search'); + }); + it('it should render empty search when sending title and body props', () => { + const wrapper = render( + + ); + + expect(wrapper.getByTestId('emptySearchViewerState')).toBeTruthy(); + expect(wrapper.getByTestId('emptySearchTitle')).toHaveTextContent('Empty search title'); + expect(wrapper.getByTestId('emptySearchBody')).toHaveTextContent('Empty search body'); + }); + it('it should render no items screen when sending title and body props', () => { + const wrapper = render( + + ); + + const { getByTestId } = wrapper; + expect(getByTestId('emptyBody')).toHaveTextContent('There are no endpoint exceptions.'); + expect(getByTestId('emptyStateButton')).toHaveTextContent('Add endpoint exception'); + expect(getByTestId('emptyViewerState')).toBeTruthy(); + }); + it('it should render no items with default title and body props', () => { + const wrapper = render( + + ); + + const { getByTestId } = wrapper; + expect(getByTestId('emptyViewerState')).toBeTruthy(); + expect(getByTestId('emptyTitle')).toHaveTextContent('Add exceptions to this rule'); + expect(getByTestId('emptyBody')).toHaveTextContent( + 'There is no exception in your rule. Create your first rule exception.' + ); + expect(getByTestId('emptyStateButton')).toHaveTextContent('Create rule exception'); + }); + it('it should render no items screen with default title and body props and listType endPoint', () => { + const wrapper = render( + + ); + + const { getByTestId } = wrapper; + expect(getByTestId('emptyViewerState')).toBeTruthy(); + expect(getByTestId('emptyTitle')).toHaveTextContent('Add exceptions to this rule'); + expect(getByTestId('emptyBody')).toHaveTextContent( + 'There is no exception in your rule. Create your first rule exception.' + ); + expect(getByTestId('emptyStateButton')).toHaveTextContent('Create endpoint exception'); + }); +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/empty_viewer_state/empty_viewer_state.tsx b/packages/kbn-securitysolution-exception-list-components/src/empty_viewer_state/empty_viewer_state.tsx new file mode 100644 index 000000000000..060d2ecc1506 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/empty_viewer_state/empty_viewer_state.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo } from 'react'; +import type { FC } from 'react'; +import { css } from '@emotion/react'; +import { + EuiLoadingContent, + EuiImage, + EuiEmptyPrompt, + EuiButton, + useEuiTheme, + EuiPanel, +} from '@elastic/eui'; +import type { ExpressionColor } from '@elastic/eui/src/components/expression/expression'; +import type { EuiFacetGroupLayout } from '@elastic/eui/src/components/facet/facet_group'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { ListTypeText, ViewerStatus } from '../types'; +import * as i18n from '../translations'; +import illustration from '../assets/images/illustration_product_no_results_magnifying_glass.svg'; + +interface EmptyViewerStateProps { + title?: string; + body?: string; + buttonText?: string; + listType?: ListTypeText; + isReadOnly: boolean; + viewerStatus: ViewerStatus; + onCreateExceptionListItem?: () => void | null; +} + +const panelCss = css` + margin: ${euiThemeVars.euiSizeL} 0; + padding: ${euiThemeVars.euiSizeL} 0; +`; +const EmptyViewerStateComponent: FC = ({ + title, + body, + buttonText, + listType, + isReadOnly, + viewerStatus, + onCreateExceptionListItem, +}) => { + const { euiTheme } = useEuiTheme(); + + const euiEmptyPromptProps = useMemo(() => { + switch (viewerStatus) { + case ViewerStatus.ERROR: { + return { + color: 'danger' as ExpressionColor, + iconType: 'alert', + title: ( +

{title || i18n.EMPTY_VIEWER_STATE_ERROR_TITLE}

+ ), + body:

{body || i18n.EMPTY_VIEWER_STATE_ERROR_BODY}

, + 'data-test-subj': 'errorViewerState', + }; + } + case ViewerStatus.EMPTY: + return { + color: 'subdued' as ExpressionColor, + iconType: 'plusInCircle', + iconColor: euiTheme.colors.darkestShade, + title: ( +

{title || i18n.EMPTY_VIEWER_STATE_EMPTY_TITLE}

+ ), + body:

{body || i18n.EMPTY_VIEWER_STATE_EMPTY_BODY}

, + 'data-test-subj': 'emptyViewerState', + actions: [ + + {buttonText || i18n.EMPTY_VIEWER_STATE_EMPTY_VIEWER_BUTTON(listType || 'rule')} + , + ], + }; + case ViewerStatus.EMPTY_SEARCH: + return { + color: 'plain' as ExpressionColor, + layout: 'horizontal' as EuiFacetGroupLayout, + hasBorder: true, + hasShadow: false, + icon: , + title: ( +

+ {title || i18n.EMPTY_VIEWER_STATE_EMPTY_SEARCH_TITLE} +

+ ), + body: ( +

+ {body || i18n.EMPTY_VIEWER_STATE_EMPTY_SEARCH_BODY} +

+ ), + 'data-test-subj': 'emptySearchViewerState', + }; + } + }, [ + viewerStatus, + euiTheme.colors.darkestShade, + title, + body, + onCreateExceptionListItem, + isReadOnly, + buttonText, + listType, + ]); + + if (viewerStatus === ViewerStatus.LOADING || viewerStatus === ViewerStatus.SEARCHING) + return ; + + return ( + + + + ); +}; + +export const EmptyViewerState = React.memo(EmptyViewerStateComponent); + +EmptyViewerState.displayName = 'EmptyViewerState'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/comments/comments.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/comments/comments.tsx new file mode 100644 index 000000000000..ca08d10f0a04 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/comments/comments.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { memo } from 'react'; +import type { EuiCommentProps } from '@elastic/eui'; +import { EuiAccordion, EuiCommentList, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import * as i18n from '../translations'; + +const accordionCss = css` + color: ${euiThemeVars.euiColorPrimary}; +`; + +export interface ExceptionItemCardCommentsProps { + comments: EuiCommentProps[]; +} + +export const ExceptionItemCardComments = memo(({ comments }) => { + return ( + + + {i18n.exceptionItemCardCommentsAccordion(comments.length)} + + } + arrowDisplay="none" + data-test-subj="exceptionsViewerCommentAccordion" + > + + + + + + ); +}); + +ExceptionItemCardComments.displayName = 'ExceptionItemCardComments'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.config.ts b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.config.ts new file mode 100644 index 000000000000..08514a64feac --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.config.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import * as i18n from '../translations'; + +export const OS_LABELS = Object.freeze({ + linux: i18n.OS_LINUX, + mac: i18n.OS_MAC, + macos: i18n.OS_MAC, + windows: i18n.OS_WINDOWS, +}); + +export const OPERATOR_TYPE_LABELS_INCLUDED = Object.freeze({ + [ListOperatorTypeEnum.NESTED]: i18n.CONDITION_OPERATOR_TYPE_NESTED, + [ListOperatorTypeEnum.MATCH_ANY]: i18n.CONDITION_OPERATOR_TYPE_MATCH_ANY, + [ListOperatorTypeEnum.MATCH]: i18n.CONDITION_OPERATOR_TYPE_MATCH, + [ListOperatorTypeEnum.WILDCARD]: i18n.CONDITION_OPERATOR_TYPE_WILDCARD_MATCHES, + [ListOperatorTypeEnum.EXISTS]: i18n.CONDITION_OPERATOR_TYPE_EXISTS, + [ListOperatorTypeEnum.LIST]: i18n.CONDITION_OPERATOR_TYPE_LIST, +}); + +export const OPERATOR_TYPE_LABELS_EXCLUDED = Object.freeze({ + [ListOperatorTypeEnum.MATCH_ANY]: i18n.CONDITION_OPERATOR_TYPE_NOT_MATCH_ANY, + [ListOperatorTypeEnum.MATCH]: i18n.CONDITION_OPERATOR_TYPE_NOT_MATCH, + [ListOperatorTypeEnum.WILDCARD]: i18n.CONDITION_OPERATOR_TYPE_WILDCARD_DOES_NOT_MATCH, + [ListOperatorTypeEnum.EXISTS]: i18n.CONDITION_OPERATOR_TYPE_DOES_NOT_EXIST, + [ListOperatorTypeEnum.LIST]: i18n.CONDITION_OPERATOR_TYPE_NOT_IN_LIST, +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.styles.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.styles.tsx new file mode 100644 index 000000000000..3ad2d7ef21fb --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.styles.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { cx } from '@emotion/css'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; + +// TODO check font Roboto Mono +export const nestedGroupSpaceCss = css` + margin-left: ${euiThemeVars.euiSizeXL}; + margin-bottom: ${euiThemeVars.euiSizeXS}; + padding-top: ${euiThemeVars.euiSizeXS}; +`; + +export const borderCss = cx( + 'eui-xScroll', + ` + border: 1px; + border-color: #d3dae6; + border-style: solid; +` +); + +export const valueContainerCss = css` + display: flex; + align-items: center; + margin-left: ${euiThemeVars.euiSizeS}; +`; +export const expressionContainerCss = css` + display: flex; + align-items: center; +`; diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.test.tsx new file mode 100644 index 000000000000..ae4b76a4a7dc --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.test.tsx @@ -0,0 +1,350 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; + +import { ExceptionItemCardConditions } from './conditions'; + +interface TestEntry { + field: string; + operator: 'included' | 'excluded'; + type: unknown; + value?: string | string[]; + entries?: TestEntry[]; + list?: { id: string; type: string }; +} +const getEntryKey = ( + entry: TestEntry, + index: string, + list?: { id: string; type: string } | null +) => { + if (list && Object.keys(list)) { + const { field, type, list: entryList } = entry; + const { id } = entryList || {}; + return `${field}${type}${id || ''}${index}`; + } + const { field, type, value } = entry; + return `${field}${type}${value || ''}${index}`; +}; + +describe('ExceptionItemCardConditions', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + it('it includes os condition if one exists', () => { + const entries: TestEntry[] = [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'host', + }, + { + field: 'threat.indicator.port', + operator: 'included', + type: 'exists', + }, + { + entries: [ + { + field: 'valid', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + field: 'file.Ext.code_signature', + type: 'nested', + operator: 'included', + }, + ]; + const wrapper = render( + + ); + expect(wrapper.getByTestId('exceptionItemConditionsOs')).toHaveTextContent('OSIS Linux'); + + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[0], '0')}EntryContent`) + ).toHaveTextContent('host.nameIS host'); + + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[1], '1')}EntryContent`) + ).toHaveTextContent('AND threat.indicator.portexists'); + + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[2], '2')}EntryContent`) + ).toHaveTextContent('AND file.Ext.code_signature'); + + if (entries[2] && entries[2].entries) { + expect( + wrapper.getByTestId( + `exceptionItemConditions${getEntryKey(entries[2].entries[0], '0')}EntryContent` + ) + ).toHaveTextContent('validIS true'); + } + }); + + it('it renders item conditions', () => { + const entries: TestEntry[] = [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'host', + }, + { + field: 'host.name', + operator: 'excluded', + type: 'match', + value: 'host', + }, + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['foo', 'bar'], + }, + { + field: 'host.name', + operator: 'excluded', + type: 'match_any', + value: ['foo', 'bar'], + }, + { + field: 'user.name', + operator: 'included', + type: 'wildcard', + value: 'foo*', + }, + { + field: 'user.name', + operator: 'excluded', + type: 'wildcard', + value: 'foo*', + }, + { + field: 'threat.indicator.port', + operator: 'included', + type: 'exists', + }, + { + field: 'threat.indicator.port', + operator: 'excluded', + type: 'exists', + }, + { + entries: [ + { + field: 'valid', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + field: 'file.Ext.code_signature', + type: 'nested', + operator: 'included', + }, + ]; + const wrapper = render( + + ); + expect(wrapper.queryByTestId('exceptionItemConditionsOs')).not.toBeInTheDocument(); + + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[0], '0')}EntryContent`) + ).toHaveTextContent('host.nameIS host'); + // Match; + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[1], '1')}EntryContent`) + ).toHaveTextContent('AND host.nameIS NOT host'); + // MATCH_ANY; + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[2], '2')}EntryContent`) + ).toHaveTextContent('AND host.nameis one of foobar'); + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[3], '3')}EntryContent`) + ).toHaveTextContent('AND host.nameis not one of foobar'); + // WILDCARD; + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[4], '4')}EntryContent`) + ).toHaveTextContent('AND user.nameMATCHES foo*'); + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[5], '5')}EntryContent`) + ).toHaveTextContent('AND user.nameDOES NOT MATCH foo*'); + // EXISTS; + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[6], '6')}EntryContent`) + ).toHaveTextContent('AND threat.indicator.portexists'); + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[7], '7')}EntryContent`) + ).toHaveTextContent('AND threat.indicator.portdoes not exist'); + // NESTED; + expect( + wrapper.getByTestId(`exceptionItemConditions${getEntryKey(entries[8], '8')}EntryContent`) + ).toHaveTextContent('AND file.Ext.code_signature'); + if (entries[8] && entries[8].entries) { + expect( + wrapper.getByTestId( + `exceptionItemConditions${getEntryKey(entries[8].entries[0], '0')}EntryContent` + ) + ).toHaveTextContent('validIS true'); + } + }); + it('it renders list conditions', () => { + const entries: TestEntry[] = [ + { + field: 'host.name', + list: { + id: 'ips.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + { + field: 'host.name', + list: { + id: 'ips.txt', + type: 'keyword', + }, + operator: 'excluded', + type: 'list', + }, + ]; + const wrapper = render( + + ); + // /exceptionItemConditionshost.namelist0EntryContent + expect( + wrapper.getByTestId( + `exceptionItemConditions${getEntryKey(entries[0], '0', entries[0].list)}EntryContent` + ) + ).toHaveTextContent('host.nameincluded in ips.txt'); + + expect( + wrapper.getByTestId( + `exceptionItemConditions${getEntryKey(entries[1], '1', entries[1].list)}EntryContent` + ) + ).toHaveTextContent('AND host.nameis not included in ips.txt'); + }); +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.tsx new file mode 100644 index 000000000000..8b85a7343afc --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/conditions.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { memo } from 'react'; +import { EuiPanel } from '@elastic/eui'; + +import { borderCss } from './conditions.styles'; +import { EntryContent } from './entry_content/entry_content'; +import { OsCondition } from './os_conditions/os_conditions'; +import type { CriteriaConditionsProps, Entry } from './types'; + +export const ExceptionItemCardConditions = memo( + ({ os, entries, dataTestSubj }) => { + return ( + + {os?.length ? : null} + {entries.map((entry: Entry, index: number) => { + const nestedEntries = 'entries' in entry ? entry.entries : []; + return ( +
+ + {nestedEntries?.length + ? nestedEntries.map((nestedEntry: Entry, nestedIndex: number) => ( + + )) + : null} +
+ ); + })} +
+ ); + } +); +ExceptionItemCardConditions.displayName = 'ExceptionItemCardConditions'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.helper.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.helper.tsx new file mode 100644 index 000000000000..6a64bcc810c0 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.helper.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiExpression, EuiBadge } from '@elastic/eui'; +import type { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { ValueWithSpaceWarning } from '../../../..'; +import { OPERATOR_TYPE_LABELS_EXCLUDED, OPERATOR_TYPE_LABELS_INCLUDED } from '../conditions.config'; +import type { Entry } from '../types'; + +const getEntryValue = (type: string, value?: string | string[]) => { + if (type === 'match_any' && Array.isArray(value)) { + return value.map((currentValue) => {currentValue}); + } + return value ?? ''; +}; + +export const getEntryOperator = (type: ListOperatorTypeEnum, operator: string) => { + if (type === 'nested') return ''; + return operator === 'included' + ? OPERATOR_TYPE_LABELS_INCLUDED[type] ?? type + : OPERATOR_TYPE_LABELS_EXCLUDED[type] ?? type; +}; + +export const getValue = (entry: Entry) => { + if (entry.type === 'list') return entry.list.id; + + return 'value' in entry ? entry.value : ''; +}; + +export const getValueExpression = ( + type: ListOperatorTypeEnum, + operator: string, + value: string | string[] +) => ( + <> + + + +); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.tsx new file mode 100644 index 000000000000..6c321a6d0ce0 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/entry_content/entry_content.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { memo } from 'react'; +import { EuiExpression, EuiToken, EuiFlexGroup } from '@elastic/eui'; +import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { + nestedGroupSpaceCss, + valueContainerCss, + expressionContainerCss, +} from '../conditions.styles'; +import type { Entry } from '../types'; +import * as i18n from '../../translations'; +import { getValue, getValueExpression } from './entry_content.helper'; + +export const EntryContent = memo( + ({ + entry, + index, + isNestedEntry = false, + dataTestSubj, + }: { + entry: Entry; + index: number; + isNestedEntry?: boolean; + dataTestSubj?: string; + }) => { + const { field, type } = entry; + const value = getValue(entry); + const operator = 'operator' in entry ? entry.operator : ''; + + const entryKey = `${field}${type}${value}${index}`; + return ( +
+
+ {isNestedEntry ? ( + + + +
+ + {getValueExpression(type as ListOperatorTypeEnum, operator, value)} +
+
+ ) : ( + <> + + + {getValueExpression(type as ListOperatorTypeEnum, operator, value)} + + )} +
+
+ ); + } +); +EntryContent.displayName = 'EntryContent'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/os_conditions/os_conditions.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/os_conditions/os_conditions.tsx new file mode 100644 index 000000000000..701529ae6717 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/os_conditions/os_conditions.tsx @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { memo, useMemo } from 'react'; +import { EuiExpression } from '@elastic/eui'; + +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { OS_LABELS } from '../conditions.config'; +import * as i18n from '../../translations'; + +export interface OsConditionsProps { + dataTestSubj: string; + os: ExceptionListItemSchema['os_types']; +} + +export const OsCondition = memo(({ os, dataTestSubj }) => { + const osLabel = useMemo(() => { + return os.map((osValue) => OS_LABELS[osValue] ?? osValue).join(', '); + }, [os]); + return osLabel ? ( +
+ + + + +
+ ) : null; +}); +OsCondition.displayName = 'OsCondition'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/types.ts b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/types.ts new file mode 100644 index 000000000000..09067e84cafc --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/conditions/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { + EntryExists, + EntryList, + EntryMatch, + EntryMatchAny, + EntryMatchWildcard, + EntryNested, + ExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; + +export type Entry = + | EntryExists + | EntryList + | EntryMatch + | EntryMatchAny + | EntryMatchWildcard + | EntryNested; + +export type Entries = ExceptionListItemSchema['entries']; +export interface CriteriaConditionsProps { + entries: Entries; + dataTestSubj: string; + os?: ExceptionListItemSchema['os_types']; +} diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx new file mode 100644 index 000000000000..c3705750d015 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo, useCallback, FC } from 'react'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiCommentProps } from '@elastic/eui'; +import type { + CommentsArray, + ExceptionListItemSchema, + ExceptionListTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import * as i18n from './translations'; +import { + ExceptionItemCardHeader, + ExceptionItemCardConditions, + ExceptionItemCardMetaInfo, + ExceptionItemCardComments, +} from '.'; + +import type { ExceptionListItemIdentifiers } from '../types'; + +export interface ExceptionItemProps { + dataTestSubj?: string; + disableActions?: boolean; + exceptionItem: ExceptionListItemSchema; + listType: ExceptionListTypeEnum; + ruleReferences: any[]; // rulereferences + editActionLabel?: string; + deleteActionLabel?: string; + securityLinkAnchorComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common + formattedDateComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common + getFormattedComments: (comments: CommentsArray) => EuiCommentProps[]; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common + onDeleteException: (arg: ExceptionListItemIdentifiers) => void; + onEditException: (item: ExceptionListItemSchema) => void; +} + +const ExceptionItemCardComponent: FC = ({ + disableActions = false, + exceptionItem, + listType, + ruleReferences, + dataTestSubj, + editActionLabel, + deleteActionLabel, + securityLinkAnchorComponent, + formattedDateComponent, + getFormattedComments, + onDeleteException, + onEditException, +}) => { + const handleDelete = useCallback((): void => { + onDeleteException({ + id: exceptionItem.id, + name: exceptionItem.name, + namespaceType: exceptionItem.namespace_type, + }); + }, [onDeleteException, exceptionItem.id, exceptionItem.name, exceptionItem.namespace_type]); + + const handleEdit = useCallback((): void => { + onEditException(exceptionItem); + }, [onEditException, exceptionItem]); + + const formattedComments = useMemo((): EuiCommentProps[] => { + return getFormattedComments(exceptionItem.comments); + }, [exceptionItem.comments, getFormattedComments]); + + const actions: Array<{ + key: string; + icon: string; + label: string | boolean; + onClick: () => void; + }> = useMemo( + () => [ + { + key: 'edit', + icon: 'controlsHorizontal', + label: editActionLabel || i18n.exceptionItemCardEditButton(listType), + onClick: handleEdit, + }, + { + key: 'delete', + icon: 'trash', + label: deleteActionLabel || listType === i18n.exceptionItemCardDeleteButton(listType), + onClick: handleDelete, + }, + ], + [editActionLabel, listType, deleteActionLabel, handleDelete, handleEdit] + ); + return ( + + + + + + + + + + + + {formattedComments.length > 0 && ( + + )} + + + ); +}; + +ExceptionItemCardComponent.displayName = 'ExceptionItemCardComponent'; + +export const ExceptionItemCard = React.memo(ExceptionItemCardComponent); + +ExceptionItemCard.displayName = 'ExceptionItemCard'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.test.tsx new file mode 100644 index 000000000000..78feab598c14 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.test.tsx @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; + +import * as i18n from '../translations'; +import { ExceptionItemCardHeader } from './header'; +import { fireEvent, render } from '@testing-library/react'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +const handleEdit = jest.fn(); +const handleDelete = jest.fn(); +const actions = [ + { + key: 'edit', + icon: 'pencil', + label: i18n.exceptionItemCardEditButton(ExceptionListTypeEnum.DETECTION), + onClick: handleEdit, + }, + { + key: 'delete', + icon: 'trash', + label: i18n.exceptionItemCardDeleteButton(ExceptionListTypeEnum.DETECTION), + onClick: handleDelete, + }, +]; +describe('ExceptionItemCardHeader', () => { + it('it renders item name', () => { + const wrapper = render( + + ); + + expect(wrapper.getByTestId('exceptionItemHeaderTitle')).toHaveTextContent('some name'); + }); + + it('it displays actions', () => { + const wrapper = render( + + ); + + // click on popover + fireEvent.click(wrapper.getByTestId('exceptionItemHeaderActionButton')); + fireEvent.click(wrapper.getByTestId('exceptionItemHeaderActionItemedit')); + expect(handleEdit).toHaveBeenCalled(); + + fireEvent.click(wrapper.getByTestId('exceptionItemHeaderActionItemdelete')); + expect(handleDelete).toHaveBeenCalled(); + }); + + it('it disables actions if disableActions is true', () => { + const wrapper = render( + + ); + + expect(wrapper.getByTestId('exceptionItemHeaderActionButton')).toBeDisabled(); + }); +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.tsx new file mode 100644 index 000000000000..d58cb8d99b7a --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/header/header.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { memo, useMemo, useState } from 'react'; +import type { EuiContextMenuPanelProps } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiTitle, + EuiContextMenuItem, +} from '@elastic/eui'; +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +export interface ExceptionItemCardHeaderProps { + item: ExceptionListItemSchema; + actions: Array<{ key: string; icon: string; label: string | boolean; onClick: () => void }>; + disableActions?: boolean; + dataTestSubj: string; +} + +export const ExceptionItemCardHeader = memo( + ({ item, actions, disableActions = false, dataTestSubj }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onItemActionsClick = () => setIsPopoverOpen((isOpen) => !isOpen); + const onClosePopover = () => setIsPopoverOpen(false); + + const itemActions = useMemo((): EuiContextMenuPanelProps['items'] => { + return actions.map((action) => ( + { + onClosePopover(); + action.onClick(); + }} + > + {action.label} + + )); + }, [dataTestSubj, actions]); + + return ( + + + +

{item.name}

+
+
+ + + } + panelPaddingSize="none" + isOpen={isPopoverOpen} + closePopover={onClosePopover} + data-test-subj={`${dataTestSubj}Items`} + > + + + +
+ ); + } +); + +ExceptionItemCardHeader.displayName = 'ExceptionItemCardHeader'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/index.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/index.test.tsx new file mode 100644 index 000000000000..e97b03607bb6 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/index.test.tsx @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { ExceptionItemCard } from '.'; +import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; +import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +const ruleReferences: unknown[] = [ + { + exception_lists: [ + { + id: '123', + list_id: 'i_exist', + namespace_type: 'single', + type: 'detection', + }, + { + id: '456', + list_id: 'i_exist_2', + namespace_type: 'single', + type: 'detection', + }, + ], + id: '1a2b3c', + name: 'Simple Rule Query', + rule_id: 'rule-2', + }, +]; +describe('ExceptionItemCard', () => { + it('it renders header, item meta information and conditions', () => { + const exceptionItem = { ...getExceptionListItemSchemaMock(), comments: [] }; + + const wrapper = render( + null} + formattedDateComponent={() => null} + getFormattedComments={() => []} + /> + ); + + expect(wrapper.getByTestId('exceptionItemCardHeaderContainer')).toBeInTheDocument(); + // expect(wrapper.getByTestId('exceptionItemCardMetaInfo')).toBeInTheDocument(); + expect(wrapper.getByTestId('exceptionItemCardConditions')).toBeInTheDocument(); + // expect(wrapper.queryByTestId('exceptionsViewerCommentAccordion')).not.toBeInTheDocument(); + }); + + it('it renders header, item meta information, conditions, and comments if any exist', () => { + const exceptionItem = { ...getExceptionListItemSchemaMock(), comments: getCommentsArrayMock() }; + + const wrapper = render( + null} + formattedDateComponent={() => null} + getFormattedComments={() => []} + /> + ); + + expect(wrapper.getByTestId('exceptionItemCardHeaderContainer')).toBeInTheDocument(); + // expect(wrapper.getByTestId('exceptionItemCardMetaInfo')).toBeInTheDocument(); + expect(wrapper.getByTestId('exceptionItemCardConditions')).toBeInTheDocument(); + // expect(wrapper.getByTestId('exceptionsViewerCommentAccordion')).toBeInTheDocument(); + }); + + it('it does not render edit or delete action buttons when "disableActions" is "true"', () => { + const exceptionItem = getExceptionListItemSchemaMock(); + + const wrapper = render( + null} + formattedDateComponent={() => null} + getFormattedComments={() => []} + /> + ); + expect(wrapper.queryByTestId('itemActionButton')).not.toBeInTheDocument(); + }); + + it('it invokes "onEditException" when edit button clicked', () => { + const mockOnEditException = jest.fn(); + const exceptionItem = getExceptionListItemSchemaMock(); + + const wrapper = render( + null} + formattedDateComponent={() => null} + getFormattedComments={() => []} + /> + ); + + fireEvent.click(wrapper.getByTestId('exceptionItemCardHeaderActionButton')); + fireEvent.click(wrapper.getByTestId('exceptionItemCardHeaderActionItemedit')); + expect(mockOnEditException).toHaveBeenCalledWith(getExceptionListItemSchemaMock()); + }); + + it('it invokes "onDeleteException" when delete button clicked', () => { + const mockOnDeleteException = jest.fn(); + const exceptionItem = getExceptionListItemSchemaMock(); + + const wrapper = render( + null} + formattedDateComponent={() => null} + getFormattedComments={() => []} + /> + ); + fireEvent.click(wrapper.getByTestId('exceptionItemCardHeaderActionButton')); + fireEvent.click(wrapper.getByTestId('exceptionItemCardHeaderActionItemdelete')); + + expect(mockOnDeleteException).toHaveBeenCalledWith({ + id: '1', + name: 'some name', + namespaceType: 'single', + }); + }); + + // TODO Fix this Test + // it('it renders comment accordion closed to begin with', () => { + // const exceptionItem = getExceptionListItemSchemaMock(); + // exceptionItem.comments = getCommentsArrayMock(); + // const wrapper = render( + // + // ); + + // expect(wrapper.queryByTestId('accordion-comment-list')).not.toBeVisible(); + // }); +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/index.ts b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/index.ts new file mode 100644 index 000000000000..c0fd3fafc86d --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './conditions/conditions'; +export * from './header/header'; +export * from './meta/meta'; +export * from './comments/comments'; +export * from './exception_item_card'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/details_info/details_info.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/details_info/details_info.tsx new file mode 100644 index 000000000000..3d075f50096d --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/details_info/details_info.tsx @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { memo } from 'react'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import * as i18n from '../../translations'; + +interface MetaInfoDetailsProps { + fieldName: string; + label: string; + lastUpdate: JSX.Element | string; + lastUpdateValue: string; + dataTestSubj: string; +} + +const euiBadgeFontFamily = css` + font-family: ${euiThemeVars.euiFontFamily}; +`; +export const MetaInfoDetails = memo( + ({ label, lastUpdate, lastUpdateValue, dataTestSubj }) => { + return ( + + + + {label} + + + + + {lastUpdate} + + + + + {i18n.EXCEPTION_ITEM_CARD_META_BY} + + + + + + + {lastUpdateValue} + + + + + + ); + } +); + +MetaInfoDetails.displayName = 'MetaInfoDetails'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.test.tsx new file mode 100644 index 000000000000..14bdef771d6b --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.test.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; + +import { ExceptionItemCardMetaInfo } from './meta'; +import { RuleReference } from '../../types'; + +const ruleReferences = [ + { + exception_lists: [ + { + id: '123', + list_id: 'i_exist', + namespace_type: 'single', + type: 'detection', + }, + { + id: '456', + list_id: 'i_exist_2', + namespace_type: 'single', + type: 'detection', + }, + ], + id: '1a2b3c', + name: 'Simple Rule Query', + rule_id: 'rule-2', + }, +]; +describe('ExceptionItemCardMetaInfo', () => { + it('it should render creation info with sending custom formattedDateComponent', () => { + const wrapper = render( + null} + formattedDateComponent={({ fieldName, value }) => ( + <> +

{new Date(value).toDateString()}

+ + )} + /> + ); + + expect(wrapper.getByTestId('exceptionItemMetaCreatedBylastUpdate')).toHaveTextContent( + 'Mon Apr 20 2020' + ); + expect(wrapper.getByTestId('exceptionItemMetaCreatedBylastUpdateValue')).toHaveTextContent( + 'some user' + ); + }); + + it('it should render udate info with sending custom formattedDateComponent', () => { + const wrapper = render( + null} + formattedDateComponent={({ fieldName, value }) => ( + <> +

{new Date(value).toDateString()}

+ + )} + /> + ); + expect(wrapper.getByTestId('exceptionItemMetaUpdatedBylastUpdate')).toHaveTextContent( + 'Mon Apr 20 2020' + ); + expect(wrapper.getByTestId('exceptionItemMetaUpdatedBylastUpdateValue')).toHaveTextContent( + 'some user' + ); + }); + + it('it should render references info', () => { + const wrapper = render( + null} + formattedDateComponent={() => null} + /> + ); + + expect(wrapper.getByTestId('exceptionItemMetaAffectedRulesButton')).toHaveTextContent( + 'Affects 1 rule' + ); + }); + + it('it renders references info when multiple references exist', () => { + const wrapper = render( + null} + formattedDateComponent={() => null} + /> + ); + + expect(wrapper.getByTestId('exceptionItemMetaAffectedRulesButton')).toHaveTextContent( + 'Affects 2 rules' + ); + }); +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.tsx new file mode 100644 index 000000000000..91e0a9cdd19b --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/meta.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { memo, useMemo, useState } from 'react'; +import type { EuiContextMenuPanelProps } from '@elastic/eui'; +import { + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + EuiButtonEmpty, + EuiPopover, +} from '@elastic/eui'; +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +import { css } from '@emotion/react'; +import * as i18n from '../translations'; +import type { RuleReference } from '../../types'; +import { MetaInfoDetails } from './details_info/details_info'; + +const itemCss = css` + border-right: 1px solid #d3dae6; + padding: 4px 12px 4px 0; +`; + +export interface ExceptionItemCardMetaInfoProps { + item: ExceptionListItemSchema; + references: RuleReference[]; + dataTestSubj: string; + formattedDateComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common + securityLinkAnchorComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common +} + +export const ExceptionItemCardMetaInfo = memo( + ({ item, references, dataTestSubj, securityLinkAnchorComponent, formattedDateComponent }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onAffectedRulesClick = () => setIsPopoverOpen((isOpen) => !isOpen); + const onClosePopover = () => setIsPopoverOpen(false); + + const FormattedDateComponent = formattedDateComponent; + const itemActions = useMemo((): EuiContextMenuPanelProps['items'] => { + if (references == null || securityLinkAnchorComponent === null) { + return []; + } + + const SecurityLinkAnchor = securityLinkAnchorComponent; + return references.map((reference) => ( + + + + + + )); + }, [references, securityLinkAnchorComponent, dataTestSubj]); + + return ( + + {FormattedDateComponent !== null && ( + <> + + + } + lastUpdateValue={item.created_by} + dataTestSubj={`${dataTestSubj}CreatedBy`} + /> + + + + + } + lastUpdateValue={item.updated_by} + dataTestSubj={`${dataTestSubj}UpdatedBy`} + /> + + + )} + + + {i18n.AFFECTED_RULES(references.length)} + + } + panelPaddingSize="none" + isOpen={isPopoverOpen} + closePopover={onClosePopover} + data-test-subj={`${dataTestSubj}Items`} + > + + + + + ); + } +); +ExceptionItemCardMetaInfo.displayName = 'ExceptionItemCardMetaInfo'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/translations.ts b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/translations.ts new file mode 100644 index 000000000000..2fa752429102 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_item_card/translations.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const exceptionItemCardEditButton = (listType: string) => + i18n.translate('exceptionList-components.exceptions.exceptionItem.card.editItemButton', { + values: { listType }, + defaultMessage: 'Edit {listType} exception', + }); + +export const exceptionItemCardDeleteButton = (listType: string) => + i18n.translate('exceptionList-components.exceptions.exceptionItem.card.deleteItemButton', { + values: { listType }, + defaultMessage: 'Delete {listType} exception', + }); + +export const EXCEPTION_ITEM_CARD_CREATED_LABEL = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.createdLabel', + { + defaultMessage: 'Created', + } +); + +export const EXCEPTION_ITEM_CARD_UPDATED_LABEL = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.updatedLabel', + { + defaultMessage: 'Updated', + } +); + +export const EXCEPTION_ITEM_CARD_META_BY = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.metaDetailsBy', + { + defaultMessage: 'by', + } +); + +export const exceptionItemCardCommentsAccordion = (comments: number) => + i18n.translate('exceptionList-components.exceptions.exceptionItem.card.showCommentsLabel', { + values: { comments }, + defaultMessage: 'Show {comments, plural, =1 {comment} other {comments}} ({comments})', + }); + +export const CONDITION_OPERATOR_TYPE_MATCH = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.matchOperator', + { + defaultMessage: 'IS', + } +); + +export const CONDITION_OPERATOR_TYPE_NOT_MATCH = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.matchOperator.not', + { + defaultMessage: 'IS NOT', + } +); + +export const CONDITION_OPERATOR_TYPE_WILDCARD_MATCHES = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.wildcardMatchesOperator', + { + defaultMessage: 'MATCHES', + } +); + +export const CONDITION_OPERATOR_TYPE_WILDCARD_DOES_NOT_MATCH = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.wildcardDoesNotMatchOperator', + { + defaultMessage: 'DOES NOT MATCH', + } +); + +export const CONDITION_OPERATOR_TYPE_NESTED = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.nestedOperator', + { + defaultMessage: 'has', + } +); + +export const CONDITION_OPERATOR_TYPE_MATCH_ANY = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.matchAnyOperator', + { + defaultMessage: 'is one of', + } +); + +export const CONDITION_OPERATOR_TYPE_NOT_MATCH_ANY = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.matchAnyOperator.not', + { + defaultMessage: 'is not one of', + } +); + +export const CONDITION_OPERATOR_TYPE_EXISTS = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.existsOperator', + { + defaultMessage: 'exists', + } +); + +export const CONDITION_OPERATOR_TYPE_DOES_NOT_EXIST = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.existsOperator.not', + { + defaultMessage: 'does not exist', + } +); + +export const CONDITION_OPERATOR_TYPE_LIST = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.listOperator', + { + defaultMessage: 'included in', + } +); + +export const CONDITION_OPERATOR_TYPE_NOT_IN_LIST = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.listOperator.not', + { + defaultMessage: 'is not included in', + } +); + +export const CONDITION_AND = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.and', + { + defaultMessage: 'AND', + } +); + +export const CONDITION_OS = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.os', + { + defaultMessage: 'OS', + } +); + +export const OS_WINDOWS = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.windows', + { + defaultMessage: 'Windows', + } +); + +export const OS_LINUX = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.linux', + { + defaultMessage: 'Linux', + } +); + +export const OS_MAC = i18n.translate( + 'exceptionList-components.exceptions.exceptionItem.card.conditions.macos', + { + defaultMessage: 'Mac', + } +); + +export const AFFECTED_RULES = (numRules: number) => + i18n.translate('exceptionList-components.exceptions.card.exceptionItem.affectedRules', { + values: { numRules }, + defaultMessage: 'Affects {numRules} {numRules, plural, =1 {rule} other {rules}}', + }); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_items/exception_items.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_items/exception_items.test.tsx new file mode 100644 index 000000000000..3fe2d7eb6d0b --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_items/exception_items.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +import { ExceptionItems } from './exception_items'; + +import { ViewerStatus } from '../types'; +import { render } from '@testing-library/react'; + +const onCreateExceptionListItem = jest.fn(); +const onDeleteException = jest.fn(); +const onEditExceptionItem = jest.fn(); +const onPaginationChange = jest.fn(); + +const pagination = { pageIndex: 0, pageSize: 0, totalItemCount: 0 }; + +describe('ExceptionsViewerItems', () => { + describe('Viewing EmptyViewerState', () => { + it('it renders empty prompt if "viewerStatus" is "empty"', () => { + const wrapper = render( + null} + formattedDateComponent={() => null} + exceptionsUtilityComponent={() => null} + getFormattedComments={() => []} + /> + ); + // expect(wrapper).toMatchSnapshot(); + expect(wrapper.getByTestId('emptyViewerState')).toBeInTheDocument(); + expect(wrapper.queryByTestId('exceptionsContainer')).not.toBeInTheDocument(); + }); + + it('it renders no search results found prompt if "viewerStatus" is "empty_search"', () => { + const wrapper = render( + null} + formattedDateComponent={() => null} + exceptionsUtilityComponent={() => null} + getFormattedComments={() => []} + /> + ); + // expect(wrapper).toMatchSnapshot(); + expect(wrapper.getByTestId('emptySearchViewerState')).toBeInTheDocument(); + expect(wrapper.queryByTestId('exceptionsContainer')).not.toBeInTheDocument(); + }); + + it('it renders exceptions if "viewerStatus" and "null"', () => { + const wrapper = render( + null} + formattedDateComponent={() => null} + exceptionsUtilityComponent={() => null} + getFormattedComments={() => []} + /> + ); + // expect(wrapper).toMatchSnapshot(); + expect(wrapper.getByTestId('exceptionsContainer')).toBeTruthy(); + }); + }); + // TODO Add Exception Items and Pagination interactions +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/exception_items/exception_items.tsx b/packages/kbn-securitysolution-exception-list-components/src/exception_items/exception_items.tsx new file mode 100644 index 000000000000..80ab3d99f6eb --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/exception_items/exception_items.tsx @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { css } from '@emotion/react'; +import type { FC } from 'react'; +import { EuiCommentProps, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import type { Pagination as PaginationType } from '@elastic/eui'; + +import type { + CommentsArray, + ExceptionListItemSchema, + ExceptionListTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { euiThemeVars } from '@kbn/ui-theme'; +import { EmptyViewerState, ExceptionItemCard, Pagination } from '../..'; + +import type { + RuleReferences, + ExceptionListItemIdentifiers, + ViewerStatus, + GetExceptionItemProps, +} from '../types'; + +const exceptionItemCss = css` + margin: ${euiThemeVars.euiSize} 0; + &div:first-child { + margin: ${euiThemeVars.euiSizeXS} 0 ${euiThemeVars.euiSize}; + } +`; + +interface ExceptionItemsProps { + lastUpdated: string | number | null; + viewerStatus: ViewerStatus; + isReadOnly: boolean; + emptyViewerTitle?: string; + emptyViewerBody?: string; + emptyViewerButtonText?: string; + exceptions: ExceptionListItemSchema[]; + listType: ExceptionListTypeEnum; + ruleReferences: RuleReferences; + pagination: PaginationType; + securityLinkAnchorComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common + formattedDateComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common + exceptionsUtilityComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common + getFormattedComments: (comments: CommentsArray) => EuiCommentProps[]; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common + onCreateExceptionListItem?: () => void; + onDeleteException: (arg: ExceptionListItemIdentifiers) => void; + onEditExceptionItem: (item: ExceptionListItemSchema) => void; + onPaginationChange: (arg: GetExceptionItemProps) => void; +} + +const ExceptionItemsComponent: FC = ({ + lastUpdated, + viewerStatus, + isReadOnly, + exceptions, + listType, + ruleReferences, + emptyViewerTitle, + emptyViewerBody, + emptyViewerButtonText, + pagination, + securityLinkAnchorComponent, + exceptionsUtilityComponent, + formattedDateComponent, + getFormattedComments, + onPaginationChange, + onDeleteException, + onEditExceptionItem, + onCreateExceptionListItem, +}) => { + const ExceptionsUtility = exceptionsUtilityComponent; + if (!exceptions.length || viewerStatus) + return ( + + ); + return ( + <> + + + + + {exceptions.map((exception) => ( + + + + ))} + + + + + + ); +}; + +ExceptionItemsComponent.displayName = 'ExceptionItemsComponent'; + +export const ExceptionItems = React.memo(ExceptionItemsComponent); + +ExceptionItems.displayName = 'ExceptionsItems'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/pagination/pagination.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/pagination/pagination.test.tsx new file mode 100644 index 000000000000..4d97f198aa2b --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/pagination/pagination.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; + +import { Pagination } from './pagination'; + +describe('Pagination', () => { + it('it invokes "onPaginationChange" when per page item is clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = render( + + ); + + fireEvent.click(wrapper.getByTestId('tablePaginationPopoverButton')); + fireEvent.click(wrapper.getByTestId('tablePagination-50-rows')); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ + pagination: { pageIndex: 0, pageSize: 50, totalItemCount: 1 }, + }); + }); + + it('it invokes "onPaginationChange" when next clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = render( + + ); + + fireEvent.click(wrapper.getByTestId('pagination-button-next')); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ + pagination: { pageIndex: 1, pageSize: 5, totalItemCount: 160 }, + }); + }); + + it('it invokes "onPaginationChange" when page clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = render( + + ); + + fireEvent.click(wrapper.getByTestId('pagination-button-2')); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ + pagination: { pageIndex: 2, pageSize: 50, totalItemCount: 160 }, + }); + }); +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/pagination/pagination.tsx b/packages/kbn-securitysolution-exception-list-components/src/pagination/pagination.tsx new file mode 100644 index 000000000000..30b029480e17 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/pagination/pagination.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { FC } from 'react'; +import { EuiTablePagination } from '@elastic/eui'; + +import type { PaginationProps } from '../types'; +import { usePagination } from './use_pagination'; + +const PaginationComponent: FC = ({ + dataTestSubj, + ariaLabel, + pagination, + onPaginationChange, +}) => { + const { + pageIndex, + pageCount, + pageSize, + pageSizeOptions, + + handleItemsPerPageChange, + handlePageIndexChange, + } = usePagination({ pagination, onPaginationChange }); + + return ( + + ); +}; + +PaginationComponent.displayName = 'PaginationComponent'; + +export const Pagination = React.memo(PaginationComponent); + +Pagination.displayName = 'Pagination'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/pagination/use_pagination.test.ts b/packages/kbn-securitysolution-exception-list-components/src/pagination/use_pagination.test.ts new file mode 100644 index 000000000000..d190c88f1061 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/pagination/use_pagination.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { usePagination } from './use_pagination'; + +const onPaginationChange = jest.fn(); + +describe('usePagination', () => { + test('should return the correct EuiTablePagination props when all the pagination object properties are falsy', () => { + const pagination = { pageIndex: 0, pageSize: 0, totalItemCount: 0 }; + + const { result } = renderHook(() => usePagination({ pagination, onPaginationChange })); + const { pageCount, pageIndex, pageSize, pageSizeOptions } = result.current; + expect(pageCount).toEqual(0); + expect(pageIndex).toEqual(0); + expect(pageSize).toEqual(0); + expect(pageSizeOptions).toEqual(undefined); + }); + test('should return the correct pageCount when pagination properties are invalid', () => { + const pagination = { pageIndex: 0, pageSize: 10, totalItemCount: 0 }; + + const { result } = renderHook(() => usePagination({ pagination, onPaginationChange })); + const { pageCount } = result.current; + expect(pageCount).toEqual(0); + }); + test('should return the correct EuiTablePagination props when all the pagination object properties are turthy', () => { + const pagination = { pageIndex: 0, pageSize: 10, totalItemCount: 100 }; + + const { result } = renderHook(() => usePagination({ pagination, onPaginationChange })); + const { pageCount, pageIndex, pageSize } = result.current; + expect(pageCount).toEqual(10); + expect(pageIndex).toEqual(0); + expect(pageSize).toEqual(10); + }); + test('should call onPaginationChange with correct pageIndex when the Page changes', () => { + const pagination = { pageIndex: 0, pageSize: 10, totalItemCount: 100 }; + + const { result } = renderHook(() => usePagination({ pagination, onPaginationChange })); + const { handlePageIndexChange } = result.current; + + act(() => { + handlePageIndexChange(2); + }); + expect(onPaginationChange).toHaveBeenCalledWith({ + pagination: { pageIndex: 2, pageSize: 10, totalItemCount: 100 }, + }); + }); + test('should call onPaginationChange with correct pageSize when the number of items per change changes', () => { + const pagination = { pageIndex: 0, pageSize: 10, totalItemCount: 100 }; + + const { result } = renderHook(() => usePagination({ pagination, onPaginationChange })); + const { handleItemsPerPageChange } = result.current; + + act(() => { + handleItemsPerPageChange(100); + }); + expect(onPaginationChange).toHaveBeenCalledWith({ + pagination: { pageIndex: 0, pageSize: 100, totalItemCount: 100 }, + }); + }); +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/pagination/use_pagination.ts b/packages/kbn-securitysolution-exception-list-components/src/pagination/use_pagination.ts new file mode 100644 index 000000000000..d23565826368 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/pagination/use_pagination.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useCallback, useMemo } from 'react'; +import type { PaginationProps } from '../types'; + +export const usePagination = ({ pagination, onPaginationChange }: PaginationProps) => { + const { pageIndex, totalItemCount, pageSize, pageSizeOptions } = pagination; + + const pageCount = useMemo( + () => (isFinite(totalItemCount / pageSize) ? Math.ceil(totalItemCount / pageSize) : 0), + [pageSize, totalItemCount] + ); + + const handleItemsPerPageChange = useCallback( + (nextPageSize: number) => { + onPaginationChange({ + pagination: { + pageIndex, + pageSize: nextPageSize, + totalItemCount, + }, + }); + }, + [pageIndex, totalItemCount, onPaginationChange] + ); + + const handlePageIndexChange = useCallback( + (nextPageIndex: number) => { + onPaginationChange({ + pagination: { + pageIndex: nextPageIndex, + pageSize, + totalItemCount, + }, + }); + }, + [pageSize, totalItemCount, onPaginationChange] + ); + + return { + pageCount, + pageIndex, + pageSize, + pageSizeOptions, + + handleItemsPerPageChange, + handlePageIndexChange, + }; +}; diff --git a/packages/kbn-securitysolution-exception-list-components/src/search_bar/search_bar.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/search_bar/search_bar.test.tsx new file mode 100644 index 000000000000..ac82bb3b6e85 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/search_bar/search_bar.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +import { SearchBar } from './search_bar'; + +describe('SearchBar', () => { + it('it does not display add exception button if user is read only', () => { + const wrapper = render( + + ); + + expect(wrapper.queryByTestId('searchBarButton')).not.toBeInTheDocument(); + }); + + it('it invokes "onAddExceptionClick" when user selects to add an exception item', () => { + const mockOnAddExceptionClick = jest.fn(); + const wrapper = render( + + ); + + const searchBtn = wrapper.getByTestId('searchBarButton'); + + fireEvent.click(searchBtn); + expect(searchBtn).toHaveTextContent('Add rule exception'); + expect(mockOnAddExceptionClick).toHaveBeenCalledWith('detection'); + }); + + it('it invokes "onAddExceptionClick" when user selects to add an endpoint exception item', () => { + const mockOnAddExceptionClick = jest.fn(); + const wrapper = render( + + ); + + const searchBtn = wrapper.getByTestId('searchBarButton'); + + fireEvent.click(searchBtn); + expect(searchBtn).toHaveTextContent('Add endpoint exception'); + expect(mockOnAddExceptionClick).toHaveBeenCalledWith('endpoint'); + }); + it('it invokes the "handlOnSearch" when the user add search query', () => { + const mockHandleOnSearch = jest.fn(); + const wrapper = render( + + ); + + const searchInput = wrapper.getByTestId('searchBar'); + fireEvent.change(searchInput, { target: { value: 'query' } }); + expect(mockHandleOnSearch).toBeCalledWith({ search: 'query' }); + }); +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/search_bar/search_bar.tsx b/packages/kbn-securitysolution-exception-list-components/src/search_bar/search_bar.tsx new file mode 100644 index 000000000000..bb8dc6ee6255 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/search_bar/search_bar.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; +import type { FC } from 'react'; + +import type { SearchFilterConfig } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSearchBar } from '@elastic/eui'; +import type { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import type { GetExceptionItemProps } from '../types'; + +const ITEMS_SCHEMA = { + strict: true, + fields: { + created_by: { + type: 'string', + }, + description: { + type: 'string', + }, + id: { + type: 'string', + }, + item_id: { + type: 'string', + }, + list_id: { + type: 'string', + }, + name: { + type: 'string', + }, + os_types: { + type: 'string', + }, + tags: { + type: 'string', + }, + }, +}; +interface SearchBarProps { + addExceptionButtonText?: string; + placeholdertext?: string; + canAddException?: boolean; // TODO what is the default value + + // TODO: REFACTOR: not to send the listType and handle it in the Parent + // Exception list type used to determine what type of item is + // being created when "onAddExceptionClick" is invoked + listType: ExceptionListTypeEnum; + isSearching?: boolean; + dataTestSubj?: string; + filters?: SearchFilterConfig[]; // TODO about filters + onSearch: (arg: GetExceptionItemProps) => void; + onAddExceptionClick: (type: ExceptionListTypeEnum) => void; +} +const SearchBarComponent: FC = ({ + addExceptionButtonText, + placeholdertext, + canAddException, + listType, + isSearching, + dataTestSubj, + filters = [], + onSearch, + onAddExceptionClick, +}) => { + const handleOnSearch = useCallback( + ({ queryText }): void => { + onSearch({ search: queryText }); + }, + [onSearch] + ); + + const handleAddException = useCallback(() => { + // TODO: ASK YARA why we need to send the listType + onAddExceptionClick(listType); + }, [onAddExceptionClick, listType]); + + return ( + + + + + {!canAddException && ( + + + {addExceptionButtonText} + + + )} + + ); +}; + +SearchBarComponent.displayName = 'SearchBarComponent'; + +export const SearchBar = React.memo(SearchBarComponent); + +SearchBar.displayName = 'SearchBar'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/translations.ts b/packages/kbn-securitysolution-exception-list-components/src/translations.ts new file mode 100644 index 000000000000..c919ef423c54 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/translations.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const EMPTY_VIEWER_STATE_EMPTY_TITLE = i18n.translate( + 'exceptionList-components.empty.viewer.state.empty.title', + { + defaultMessage: 'Add exceptions to this rule', + } +); + +export const EMPTY_VIEWER_STATE_EMPTY_BODY = i18n.translate( + 'exceptionList-components.empty.viewer.state.empty.body', + { + defaultMessage: 'There is no exception in your rule. Create your first rule exception.', + } +); +export const EMPTY_VIEWER_STATE_EMPTY_SEARCH_TITLE = i18n.translate( + 'exceptionList-components.empty.viewer.state.empty_search.search.title', + { + defaultMessage: 'No results match your search criteria', + } +); + +export const EMPTY_VIEWER_STATE_EMPTY_SEARCH_BODY = i18n.translate( + 'exceptionList-components.empty.viewer.state.empty_search.body', + { + defaultMessage: 'Try modifying your search', + } +); + +export const EMPTY_VIEWER_STATE_EMPTY_VIEWER_BUTTON = (exceptionType: string) => + i18n.translate('exceptionList-components.empty.viewer.state.empty.viewer_button', { + values: { exceptionType }, + defaultMessage: 'Create {exceptionType} exception', + }); + +export const EMPTY_VIEWER_STATE_ERROR_TITLE = i18n.translate( + 'exceptionList-components.empty.viewer.state.error_title', + { + defaultMessage: 'Unable to load exception items', + } +); + +export const EMPTY_VIEWER_STATE_ERROR_BODY = i18n.translate( + 'exceptionList-components.empty.viewer.state.error_body', + { + defaultMessage: + 'There was an error loading the exception items. Contact your administrator for help.', + } +); diff --git a/packages/kbn-securitysolution-exception-list-components/src/types/index.ts b/packages/kbn-securitysolution-exception-list-components/src/types/index.ts new file mode 100644 index 000000000000..dbb402ca7845 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/types/index.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; + +import type { Pagination } from '@elastic/eui'; +import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; + +export interface GetExceptionItemProps { + pagination?: Pagination; + search?: string; + filters?: string; +} + +export interface PaginationProps { + dataTestSubj?: string; + ariaLabel?: string; + pagination: Pagination; + onPaginationChange: (arg: GetExceptionItemProps) => void; +} + +export enum ViewerStatus { + ERROR = 'error', + EMPTY = 'empty', + EMPTY_SEARCH = 'empty_search', + LOADING = 'loading', + SEARCHING = 'searching', + DELETING = 'deleting', +} + +export interface ExceptionListSummaryProps { + pagination: Pagination; + // Corresponds to last time exception items were fetched + lastUpdated: string | number | null; +} + +export type ViewerFlyoutName = 'addException' | 'editException' | null; + +export interface RuleReferences { + [key: string]: any[]; // TODO fix +} + +export interface ExceptionListItemIdentifiers { + id: string; + name: string; + namespaceType: NamespaceType; +} + +export enum ListTypeText { + ENDPOINT = 'endpoint', + DETECTION = 'empty', + RULE_DEFAULT = 'empty_search', +} + +export interface RuleReference { + name: string; + id: string; + ruleId: string; + exceptionLists: ExceptionListSchema[]; +} diff --git a/src/plugins/dashboard/public/saved_dashboards/index.ts b/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/index.ts similarity index 73% rename from src/plugins/dashboard/public/saved_dashboards/index.ts rename to packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/index.ts index 7a17aa6f2c0e..472a80575f27 100644 --- a/src/plugins/dashboard/public/saved_dashboards/index.ts +++ b/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/index.ts @@ -6,6 +6,4 @@ * Side Public License, v 1. */ -export * from '../../common/saved_dashboard_references'; -export * from './saved_dashboard'; -export * from './saved_dashboards'; +export { ValueWithSpaceWarning } from './value_with_space_warning'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/use_value_with_space_warning.test.ts b/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/use_value_with_space_warning.test.ts new file mode 100644 index 000000000000..8f6788d710a1 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/use_value_with_space_warning.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useValueWithSpaceWarning } from './use_value_with_space_warning'; + +describe('useValueWithSpaceWarning', () => { + it('should return true when value is string and contains space', () => { + const { result } = renderHook(() => useValueWithSpaceWarning({ value: ' space before' })); + + const { showSpaceWarningIcon, warningText } = result.current; + expect(showSpaceWarningIcon).toBeTruthy(); + expect(warningText).toBeTruthy(); + }); + it('should return true when value is string and does not contain space', () => { + const { result } = renderHook(() => useValueWithSpaceWarning({ value: 'no space' })); + + const { showSpaceWarningIcon, warningText } = result.current; + expect(showSpaceWarningIcon).toBeFalsy(); + expect(warningText).toBeTruthy(); + }); + it('should return true when value is array and one of the elements contains space', () => { + const { result } = renderHook(() => + useValueWithSpaceWarning({ value: [' space before', 'no space'] }) + ); + + const { showSpaceWarningIcon, warningText } = result.current; + expect(showSpaceWarningIcon).toBeTruthy(); + expect(warningText).toBeTruthy(); + }); + it('should return true when value is array and none contains space', () => { + const { result } = renderHook(() => + useValueWithSpaceWarning({ value: ['no space', 'no space'] }) + ); + + const { showSpaceWarningIcon, warningText } = result.current; + expect(showSpaceWarningIcon).toBeFalsy(); + expect(warningText).toBeTruthy(); + }); + it('should return the tooltipIconText', () => { + const { result } = renderHook(() => + useValueWithSpaceWarning({ value: ' space before', tooltipIconText: 'Warning Text' }) + ); + + const { showSpaceWarningIcon, warningText } = result.current; + expect(showSpaceWarningIcon).toBeTruthy(); + expect(warningText).toEqual('Warning Text'); + }); +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/use_value_with_space_warning.ts b/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/use_value_with_space_warning.ts new file mode 100644 index 000000000000..bf407d2798c7 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/use_value_with_space_warning.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { paramContainsSpace, autoCompletei18n } from '@kbn/securitysolution-autocomplete'; + +interface UseValueWithSpaceWarningResult { + showSpaceWarningIcon: boolean; + warningText: string; +} +interface UseValueWithSpaceWarningProps { + value: string | string[]; + tooltipIconText?: string; +} + +export const useValueWithSpaceWarning = ({ + value, + tooltipIconText, +}: UseValueWithSpaceWarningProps): UseValueWithSpaceWarningResult => { + const showSpaceWarningIcon = Array.isArray(value) + ? value.find(paramContainsSpace) + : paramContainsSpace(value); + + return { + showSpaceWarningIcon: !!showSpaceWarningIcon, + warningText: tooltipIconText || autoCompletei18n.FIELD_SPACE_WARNING, + }; +}; diff --git a/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/value_with_space_warning.test.tsx b/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/value_with_space_warning.test.tsx new file mode 100644 index 000000000000..e19a54be48aa --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/value_with_space_warning.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; + +import { ValueWithSpaceWarning } from '.'; + +import * as useValueWithSpaceWarningMock from './use_value_with_space_warning'; + +jest.mock('./use_value_with_space_warning'); + +describe('ValueWithSpaceWarning', () => { + beforeEach(() => { + // @ts-ignore + useValueWithSpaceWarningMock.useValueWithSpaceWarning = jest + .fn() + .mockReturnValue({ showSpaceWarningIcon: true, warningText: 'Warning Text' }); + }); + it('should not render if value is falsy', () => { + const container = render(); + expect(container.queryByTestId('valueWithSpaceWarningTooltip')).toBeFalsy(); + }); + it('should not render if showSpaceWarning is falsy', () => { + // @ts-ignore + useValueWithSpaceWarningMock.useValueWithSpaceWarning = jest + .fn() + .mockReturnValue({ showSpaceWarningIcon: false, warningText: '' }); + + const container = render(); + expect(container.queryByTestId('valueWithSpaceWarningTooltip')).toBeFalsy(); + }); + it('should render if showSpaceWarning is truthy', () => { + const container = render(); + expect(container.getByTestId('valueWithSpaceWarningTooltip')).toBeInTheDocument(); + }); + it('should show the tooltip when the icon is clicked', async () => { + const container = render(); + + fireEvent.mouseOver(container.getByTestId('valueWithSpaceWarningTooltip')); + expect(await container.findByText('Warning Text')).toBeInTheDocument(); + }); +}); diff --git a/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/value_with_space_warning.tsx b/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/value_with_space_warning.tsx new file mode 100644 index 000000000000..9cff0649efb9 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/src/value_with_space_warning/value_with_space_warning.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { FC } from 'react'; +import { css } from '@emotion/css'; +import { euiThemeVars } from '@kbn/ui-theme'; + +import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import { useValueWithSpaceWarning } from './use_value_with_space_warning'; + +interface ValueWithSpaceWarningProps { + value: string[] | string; + tooltipIconType?: string; + tooltipIconText?: string; +} +const containerCss = css` + display: inline; + margin-left: ${euiThemeVars.euiSizeXS}; +`; +export const ValueWithSpaceWarning: FC = ({ + value, + tooltipIconType = 'iInCircle', + tooltipIconText, +}) => { + const { showSpaceWarningIcon, warningText } = useValueWithSpaceWarning({ + value, + tooltipIconText, + }); + if (!showSpaceWarningIcon || !value) return null; + return ( +
+ + + +
+ ); +}; diff --git a/packages/kbn-securitysolution-exception-list-components/tsconfig.json b/packages/kbn-securitysolution-exception-list-components/tsconfig.json new file mode 100644 index 000000000000..412652e0a8f9 --- /dev/null +++ b/packages/kbn-securitysolution-exception-list-components/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "stripInternal": false, + "types": [ + "jest", + "node", + "react", + "@emotion/react/types/css-prop" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + "**/*.d.ts" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/index.ts index c2e00482cd2d..75d5352c9f63 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/index.ts @@ -18,7 +18,6 @@ export * from './src/default_per_page'; export * from './src/default_risk_score_mapping_array'; export * from './src/default_severity_mapping_array'; export * from './src/default_threat_array'; -export * from './src/default_throttle_null'; export * from './src/default_to_string'; export * from './src/default_uuid'; export * from './src/from'; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts deleted file mode 100644 index b92815d4fe82..000000000000 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { Throttle } from '../throttle'; -import { DefaultThrottleNull } from '.'; -import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; - -describe('default_throttle_null', () => { - test('it should validate a throttle string', () => { - const payload: Throttle = 'some string'; - const decoded = DefaultThrottleNull.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = 5; - const decoded = DefaultThrottleNull.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultThreatNull"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default "null" if not provided a value', () => { - const payload = undefined; - const decoded = DefaultThrottleNull.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(null); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts deleted file mode 100644 index e9b9ec27ea41..000000000000 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { throttle, ThrottleOrNull } from '../throttle'; - -/** - * Types the DefaultThrottleNull as: - * - If null or undefined, then a null will be set - */ -export const DefaultThrottleNull = new t.Type( - 'DefaultThreatNull', - throttle.is, - (input, context): Either => - input == null ? t.success(null) : throttle.validate(input, context), - t.identity -); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts index 19e75dcc3be0..d7d636ad0994 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts @@ -6,13 +6,15 @@ * Side Public License, v 1. */ +import { TimeDuration } from '@kbn/securitysolution-io-ts-types'; import * as t from 'io-ts'; -export const throttle = t.string; +export const throttle = t.union([ + t.literal('no_actions'), + t.literal('rule'), + TimeDuration({ allowedUnits: ['h', 'd'] }), +]); export type Throttle = t.TypeOf; export const throttleOrNull = t.union([throttle, t.null]); export type ThrottleOrNull = t.TypeOf; - -export const throttleOrNullOrUndefined = t.union([throttle, t.null, t.undefined]); -export type ThrottleOrUndefinedOrNull = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-types/src/time_duration/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/time_duration/index.test.ts index 872affa9989a..dc6b5e61ebbf 100644 --- a/packages/kbn-securitysolution-io-ts-types/src/time_duration/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/time_duration/index.test.ts @@ -6,122 +6,315 @@ * Side Public License, v 1. */ -import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { TimeDuration } from '.'; import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('TimeDuration', () => { - test('it should validate a correctly formed TimeDuration with time unit of seconds', () => { - const payload = '1s'; - const decoded = TimeDuration.decode(payload); - const message = pipe(decoded, foldLeftRight); + describe('with allowedDurations', () => { + test('it should validate a correctly formed TimeDuration with an allowed duration of 1s', () => { + const payload = '1s'; + const decoded = TimeDuration({ + allowedDurations: [ + [1, 's'], + [2, 'h'], + [7, 'd'], + ], + }).decode(payload); + const message = foldLeftRight(decoded); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); - test('it should validate a correctly formed TimeDuration with time unit of minutes', () => { - const payload = '100m'; - const decoded = TimeDuration.decode(payload); - const message = pipe(decoded, foldLeftRight); + test('it should validate a correctly formed TimeDuration with an allowed duration of 7d', () => { + const payload = '1s'; + const decoded = TimeDuration({ + allowedDurations: [ + [1, 's'], + [2, 'h'], + [7, 'd'], + ], + }).decode(payload); + const message = foldLeftRight(decoded); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); - test('it should validate a correctly formed TimeDuration with time unit of hours', () => { - const payload = '10000000h'; - const decoded = TimeDuration.decode(payload); - const message = pipe(decoded, foldLeftRight); + test('it should NOT validate a time duration if the allowed durations does not include it', () => { + const payload = '24h'; + const decoded = TimeDuration({ + allowedDurations: [ + [1, 's'], + [2, 'h'], + [7, 'd'], + ], + }).decode(payload); + const message = foldLeftRight(decoded); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "24h" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); - test('it should NOT validate a TimeDuration of 0 length', () => { - const payload = '0s'; - const decoded = TimeDuration.decode(payload); - const message = pipe(decoded, foldLeftRight); + test('it should NOT validate a an allowed duration with a negative number', () => { + const payload = '10s'; + const decoded = TimeDuration({ + allowedDurations: [ + [1, 's'], + [-7, 'd'], + ], + }).decode(payload); + const message = foldLeftRight(decoded); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0s" supplied to "TimeDuration"', - ]); - expect(message.schema).toEqual({}); - }); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[[1,"s"],[-7,"d"]]" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); - test('it should NOT validate a negative TimeDuration', () => { - const payload = '-10s'; - const decoded = TimeDuration.decode(payload); - const message = pipe(decoded, foldLeftRight); + test('it should NOT validate an allowed duration with a fractional number', () => { + const payload = '1.5s'; + const decoded = TimeDuration({ + allowedDurations: [ + [1, 's'], + [-7, 'd'], + ], + }).decode(payload); + const message = foldLeftRight(decoded); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-10s" supplied to "TimeDuration"', - ]); - expect(message.schema).toEqual({}); - }); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1.5s" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); - test('it should NOT validate a decimal TimeDuration', () => { - const payload = '1.5s'; - const decoded = TimeDuration.decode(payload); - const message = pipe(decoded, foldLeftRight); + test('it should NOT validate a an allowed duration with a duration of 0', () => { + const payload = '10s'; + const decoded = TimeDuration({ + allowedDurations: [ + [0, 's'], + [7, 'd'], + ], + }).decode(payload); + const message = foldLeftRight(decoded); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1.5s" supplied to "TimeDuration"', - ]); - expect(message.schema).toEqual({}); - }); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[[0,"s"],[7,"d"]]" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); - test('it should NOT validate a TimeDuration with some other time unit', () => { - const payload = '10000000w'; - const decoded = TimeDuration.decode(payload); - const message = pipe(decoded, foldLeftRight); + test('it should NOT validate a TimeDuration with an invalid time unit', () => { + const payload = '10000000days'; + const decoded = TimeDuration({ + allowedDurations: [ + [1, 'h'], + [1, 'd'], + [7, 'd'], + ], + }).decode(payload); + const message = foldLeftRight(decoded); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "10000000w" supplied to "TimeDuration"', - ]); - expect(message.schema).toEqual({}); - }); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "10000000days" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); - test('it should NOT validate a TimeDuration with a time interval with incorrect format', () => { - const payload = '100ff0000w'; - const decoded = TimeDuration.decode(payload); - const message = pipe(decoded, foldLeftRight); + test('it should NOT validate a TimeDuration with a time interval with incorrect format', () => { + const payload = '100ff0000w'; + const decoded = TimeDuration({ + allowedDurations: [ + [1, 'h'], + [1, 'd'], + [7, 'd'], + ], + }).decode(payload); + const message = foldLeftRight(decoded); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "100ff0000w" supplied to "TimeDuration"', - ]); - expect(message.schema).toEqual({}); - }); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "100ff0000w" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); - test('it should NOT validate an empty string', () => { - const payload = ''; - const decoded = TimeDuration.decode(payload); - const message = pipe(decoded, foldLeftRight); + test('it should NOT validate an empty string', () => { + const payload = ''; + const decoded = TimeDuration({ + allowedDurations: [ + [1, 'h'], + [1, 'd'], + [7, 'd'], + ], + }).decode(payload); + const message = foldLeftRight(decoded); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "TimeDuration"']); - expect(message.schema).toEqual({}); - }); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an number', () => { + const payload = 100; + const decoded = TimeDuration({ + allowedDurations: [ + [1, 'h'], + [1, 'd'], + [7, 'd'], + ], + }).decode(payload); + const message = foldLeftRight(decoded); - test('it should NOT validate an number', () => { - const payload = 100; - const decoded = TimeDuration.decode(payload); - const message = pipe(decoded, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "100" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "100" supplied to "TimeDuration"', - ]); - expect(message.schema).toEqual({}); + test('it should NOT validate an TimeDuration with a valid time unit but unsafe integer', () => { + const payload = `${Math.pow(2, 53)}h`; + const decoded = TimeDuration({ + allowedDurations: [ + [1, 'h'], + [1, 'd'], + [7, 'd'], + ], + }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([ + `Invalid value "${Math.pow(2, 53)}h" supplied to "TimeDuration"`, + ]); + expect(message.schema).toEqual({}); + }); }); + describe('with allowedUnits', () => { + test('it should validate a correctly formed TimeDuration with time unit of seconds', () => { + const payload = '1s'; + const decoded = TimeDuration({ allowedUnits: ['s'] }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a correctly formed TimeDuration with time unit of minutes', () => { + const payload = '100m'; + const decoded = TimeDuration({ allowedUnits: ['s', 'm'] }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a correctly formed TimeDuration with time unit of hours', () => { + const payload = '10000000h'; + const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a correctly formed TimeDuration with time unit of days', () => { + const payload = '7d'; + const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a correctly formed TimeDuration with time unit of seconds if it is not an allowed unit', () => { + const payload = '30s'; + const decoded = TimeDuration({ allowedUnits: ['m', 'h', 'd'] }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "30s" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a negative TimeDuration', () => { + const payload = '-10s'; + const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-10s" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a fractional number', () => { + const payload = '1.5s'; + const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1.5s" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a TimeDuration with an invalid time unit', () => { + const payload = '10000000days'; + const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "10000000days" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a TimeDuration with a time interval with incorrect format', () => { + const payload = '100ff0000w'; + const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "100ff0000w" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty string', () => { + const payload = ''; + const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an number', () => { + const payload = 100; + const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).decode(payload); + const message = foldLeftRight(decoded); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "100" supplied to "TimeDuration"', + ]); + expect(message.schema).toEqual({}); + }); - test('it should NOT validate an TimeDuration with a valid time unit but unsafe integer', () => { - const payload = `${Math.pow(2, 53)}h`; - const decoded = TimeDuration.decode(payload); - const message = pipe(decoded, foldLeftRight); + test('it should NOT validate an TimeDuration with a valid time unit but unsafe integer', () => { + const payload = `${Math.pow(2, 53)}h`; + const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).decode(payload); + const message = foldLeftRight(decoded); - expect(getPaths(left(message.errors))).toEqual([ - `Invalid value "${Math.pow(2, 53)}h" supplied to "TimeDuration"`, - ]); - expect(message.schema).toEqual({}); + expect(getPaths(left(message.errors))).toEqual([ + `Invalid value "${Math.pow(2, 53)}h" supplied to "TimeDuration"`, + ]); + expect(message.schema).toEqual({}); + }); }); }); diff --git a/packages/kbn-securitysolution-io-ts-types/src/time_duration/index.ts b/packages/kbn-securitysolution-io-ts-types/src/time_duration/index.ts index 5549979ac68d..0b69c62e0d2f 100644 --- a/packages/kbn-securitysolution-io-ts-types/src/time_duration/index.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/time_duration/index.ts @@ -12,38 +12,60 @@ import { Either } from 'fp-ts/lib/Either'; /** * Types the TimeDuration as: * - A string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time - * - in the format {safe_integer}{timeUnit}, e.g. "30s", "1m", "2h" + * - in the format {safe_integer}{timeUnit}, e.g. "30s", "1m", "2h", "7d" */ -export const TimeDuration = new t.Type( - 'TimeDuration', - t.string.is, - (input, context): Either => { - if (typeof input === 'string' && input.trim() !== '') { - try { - const inputLength = input.length; - const unit = input.trim().at(-1); - const time = parseFloat(input.trim().substring(0, inputLength - 1)); - if (!Number.isInteger(time)) { - return t.failure(input, context); - } - if ( - time >= 1 && - Number.isSafeInteger(time) && - (unit === 's' || unit === 'm' || unit === 'h') - ) { - return t.success(input); - } else { +type TimeUnits = 's' | 'm' | 'h' | 'd' | 'w' | 'y'; +interface TimeDurationWithAllowedDurations { + allowedDurations: Array<[number, TimeUnits]>; + allowedUnits?: never; +} +interface TimeDurationWithAllowedUnits { + allowedUnits: TimeUnits[]; + allowedDurations?: never; +} + +type TimeDurationType = TimeDurationWithAllowedDurations | TimeDurationWithAllowedUnits; + +const isTimeSafe = (time: number) => time >= 1 && Number.isSafeInteger(time); + +export const TimeDuration = ({ allowedUnits, allowedDurations }: TimeDurationType) => { + return new t.Type( + 'TimeDuration', + t.string.is, + (input, context): Either => { + if (typeof input === 'string' && input.trim() !== '') { + try { + const inputLength = input.length; + const time = Number(input.trim().substring(0, inputLength - 1)); + const unit = input.trim().at(-1); + if (!isTimeSafe(time)) { + return t.failure(input, context); + } + if (allowedDurations) { + for (const [allowedTime, allowedUnit] of allowedDurations) { + if (!isTimeSafe(allowedTime)) { + return t.failure(allowedDurations, context); + } + if (allowedTime === time && allowedUnit === unit) { + return t.success(input); + } + } + return t.failure(input, context); + } else if (allowedUnits.includes(unit as TimeUnits)) { + return t.success(input); + } else { + return t.failure(input, context); + } + } catch (error) { return t.failure(input, context); } - } catch (error) { + } else { return t.failure(input, context); } - } else { - return t.failure(input, context); - } - }, - t.identity -); + }, + t.identity + ); +}; export type TimeDurationC = typeof TimeDuration; diff --git a/renovate.json b/renovate.json index 6eed31366c40..4075b2452bea 100644 --- a/renovate.json +++ b/renovate.json @@ -125,7 +125,7 @@ ], "reviewers": ["team:kibana-security"], "matchBaseBranches": ["main"], - "labels": ["Team:Security", "release_note:skip", "auto-backport"], + "labels": ["Team:Security", "release_note:skip", "backport:all-open"], "enabled": true }, { diff --git a/src/core/server/http_resources/http_resources_service.ts b/src/core/server/http_resources/http_resources_service.ts index 7e95dc9e302d..5db20bf45e40 100644 --- a/src/core/server/http_resources/http_resources_service.ts +++ b/src/core/server/http_resources/http_resources_service.ts @@ -18,7 +18,7 @@ import type { InternalHttpServiceSetup, InternalHttpServicePreboot, } from '@kbn/core-http-server-internal'; -import { RequestHandlerContext } from '..'; +import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; import { InternalRenderingServicePreboot, InternalRenderingServiceSetup } from '../rendering'; import { InternalHttpResourcesSetup, diff --git a/src/core/server/http_resources/types.ts b/src/core/server/http_resources/types.ts index 397f03098dd1..246a7394c3ad 100644 --- a/src/core/server/http_resources/types.ts +++ b/src/core/server/http_resources/types.ts @@ -15,7 +15,7 @@ import type { KibanaResponseFactory, RequestHandler, } from '@kbn/core-http-server'; -import type { RequestHandlerContext } from '..'; +import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; /** * Allows to configure HTTP response parameters diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 6e72b1d2623c..020975c15fb1 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -42,7 +42,6 @@ import type { ExecutionContextStart, } from '@kbn/core-execution-context-server'; import type { - RequestHandlerContextBase, IRouter, RequestHandler, KibanaResponseFactory, @@ -69,12 +68,11 @@ import type { CoreUsageDataStart, CoreUsageDataSetup } from '@kbn/core-usage-dat import type { I18nServiceSetup } from '@kbn/core-i18n-server'; import type { StatusServiceSetup } from '@kbn/core-status-server'; import type { UiSettingsServiceSetup, UiSettingsServiceStart } from '@kbn/core-ui-settings-server'; - +import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; import { HttpResources } from './http_resources'; -import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; -import type { CoreRequestHandlerContext } from './core_route_handler_context'; -import type { PrebootCoreRequestHandlerContext } from './preboot_core_route_handler_context'; +import { PluginsServiceSetup, PluginsServiceStart } from './plugins'; +export type { PluginOpaqueId } from '@kbn/core-base-common'; export type { CoreUsageStats, CoreUsageData, @@ -462,33 +460,13 @@ export type { AnalyticsServicePreboot, AnalyticsServiceStart, } from '@kbn/core-analytics-server'; - -export type { CoreRequestHandlerContext } from './core_route_handler_context'; - -/** - * Base context passed to a route handler, containing the `core` context part. - * - * @public - */ -export interface RequestHandlerContext extends RequestHandlerContextBase { - core: Promise; -} - -/** - * @internal - */ -export interface PrebootRequestHandlerContext extends RequestHandlerContextBase { - core: Promise; -} - -/** - * Mixin allowing plugins to define their own request handler contexts. - * - * @public - */ -export type CustomRequestHandlerContext = RequestHandlerContext & { - [Key in keyof T]: T[Key] extends Promise ? T[Key] : Promise; -}; +export type { + RequestHandlerContext, + CoreRequestHandlerContext, + CustomRequestHandlerContext, + PrebootRequestHandlerContext, + PrebootCoreRequestHandlerContext, +} from '@kbn/core-http-request-handler-context-server'; /** * Context passed to the `setup` method of `preboot` plugins. @@ -599,7 +577,6 @@ export type { HttpResources, PluginsServiceSetup, PluginsServiceStart, - PluginOpaqueId, }; /** diff --git a/src/core/server/server.ts b/src/core/server/server.ts index aa747a1023b3..e333e8b81a40 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -58,16 +58,21 @@ import { import { CoreUsageDataService } from '@kbn/core-usage-data-server-internal'; import { StatusService, statusConfig } from '@kbn/core-status-server-internal'; import { UiSettingsService, uiSettingsConfig } from '@kbn/core-ui-settings-server-internal'; +import { + CoreRouteHandlerContext, + PrebootCoreRouteHandlerContext, +} from '@kbn/core-http-request-handler-context-server-internal'; +import type { + RequestHandlerContext, + PrebootRequestHandlerContext, +} from '@kbn/core-http-request-handler-context-server'; import { CoreApp } from './core_app'; import { HttpResourcesService } from './http_resources'; import { RenderingService } from './rendering'; import { PluginsService, config as pluginsConfig } from './plugins'; import { InternalCorePreboot, InternalCoreSetup, InternalCoreStart } from './internal_types'; -import { CoreRouteHandlerContext } from './core_route_handler_context'; -import { PrebootCoreRouteHandlerContext } from './preboot_core_route_handler_context'; import { DiscoveredPlugins } from './plugins'; -import type { RequestHandlerContext, PrebootRequestHandlerContext } from '.'; const coreId = Symbol('core'); const rootConfigPath = ''; diff --git a/src/dev/build/tasks/bundle_fleet_packages.ts b/src/dev/build/tasks/bundle_fleet_packages.ts index 4ba5e79d2992..30cfc1d22b0e 100644 --- a/src/dev/build/tasks/bundle_fleet_packages.ts +++ b/src/dev/build/tasks/bundle_fleet_packages.ts @@ -15,6 +15,10 @@ import { Task, read, downloadToDisk, unzipBuffer, createZipFile } from '../lib'; const BUNDLED_PACKAGES_DIR = 'x-pack/plugins/fleet/target/bundled_packages'; +// APM needs to directly request its versions from Package Storage v2 - this should +// be removed when Package Storage v2 is in production +const PACKAGE_STORAGE_V2_URL = 'https://epr-v2.ea-web.elastic.dev'; + interface FleetPackage { name: string; version: string; @@ -64,7 +68,12 @@ export const BundleFleetPackages: Task = { } const archivePath = `${fleetPackage.name}-${versionToWrite}.zip`; - const archiveUrl = `${eprUrl}/epr/${fleetPackage.name}/${fleetPackage.name}-${fleetPackage.version}.zip`; + let archiveUrl = `${eprUrl}/epr/${fleetPackage.name}/${fleetPackage.name}-${fleetPackage.version}.zip`; + + // Point APM to package storage v2 + if (fleetPackage.name === 'apm') { + archiveUrl = `${PACKAGE_STORAGE_V2_URL}/epr/${fleetPackage.name}/${fleetPackage.name}-${fleetPackage.version}.zip`; + } const destination = build.resolvePath(BUNDLED_PACKAGES_DIR, archivePath); diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 1705c9ac2ec9..b4224e154def 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -43,6 +43,7 @@ export const storybookAliases = { security_solution: 'x-pack/plugins/security_solution/.storybook', shared_ux: 'packages/shared-ux/storybook/config', threat_intelligence: 'x-pack/plugins/threat_intelligence/.storybook', + triggers_actions_ui: 'x-pack/plugins/triggers_actions_ui/.storybook', ui_actions_enhanced: 'src/plugins/ui_actions_enhanced/.storybook', unified_search: 'src/plugins/unified_search/.storybook', }; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap index 0f64f4c0a477..3fd9966e7524 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap @@ -26,6 +26,7 @@ Object { "as": "partitionVis", "type": "render", "value": Object { + "canNavigateToLens": false, "params": Object { "listenOnChange": true, }, @@ -160,6 +161,7 @@ Object { "as": "partitionVis", "type": "render", "value": Object { + "canNavigateToLens": false, "params": Object { "listenOnChange": true, }, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts index 5b69fbc6194f..119d45f579eb 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts @@ -188,6 +188,7 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ visConfig, syncColors: handlers?.isSyncColorsEnabled?.() ?? false, visType: args.isDonut ? ChartTypes.DONUT : ChartTypes.PIE, + canNavigateToLens: Boolean(handlers?.variables?.canNavigateToLens), params: { listenOnChange: true, }, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts index 2f436de90e13..6a8fd2935ba5 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts @@ -108,6 +108,7 @@ export interface RenderValue { visType: ChartTypes; visConfig: PartitionVisParams; syncColors: boolean; + canNavigateToLens?: boolean; } export enum LabelPositions { diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx index 546b70d98c6e..4b6fe45e4df9 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx @@ -49,7 +49,11 @@ export const getPartitionVisRenderer: ( displayName: strings.getDisplayName(), help: strings.getHelpDescription(), reuseDomNode: true, - render: async (domNode, { visConfig, visData, visType, syncColors }, handlers) => { + render: async ( + domNode, + { visConfig, visData, visType, syncColors, canNavigateToLens }, + handlers + ) => { const { core, plugins } = getStartDeps(); handlers.onDestroy(() => { @@ -62,9 +66,12 @@ export const getPartitionVisRenderer: ( const visualizationType = extractVisualizationType(executionContext); if (containerType && visualizationType) { - plugins.usageCollection?.reportUiCounter(containerType, METRIC_TYPE.COUNT, [ + const events = [ `render_${visualizationType}_${visType}`, - ]); + canNavigateToLens ? `render_${visualizationType}_${visType}_convertable` : undefined, + ].filter((event): event is string => Boolean(event)); + + plugins.usageCollection?.reportUiCounter(containerType, METRIC_TYPE.COUNT, events); } handlers.done(); }; diff --git a/src/plugins/controls/public/services/plugin_services.stub.ts b/src/plugins/controls/public/services/plugin_services.stub.ts index 485891ff6ef9..08be260ce052 100644 --- a/src/plugins/controls/public/services/plugin_services.stub.ts +++ b/src/plugins/controls/public/services/plugin_services.stub.ts @@ -27,16 +27,15 @@ import { themeServiceFactory } from './theme/theme.story'; import { registry as stubRegistry } from './plugin_services.story'; export const providers: PluginServiceProviders = { - http: new PluginServiceProvider(httpServiceFactory), + controls: new PluginServiceProvider(controlsServiceFactory), data: new PluginServiceProvider(dataServiceFactory), - overlays: new PluginServiceProvider(overlaysServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), + http: new PluginServiceProvider(httpServiceFactory), + optionsList: new PluginServiceProvider(optionsListServiceFactory), + overlays: new PluginServiceProvider(overlaysServiceFactory), settings: new PluginServiceProvider(settingsServiceFactory), - unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), theme: new PluginServiceProvider(themeServiceFactory), - - controls: new PluginServiceProvider(controlsServiceFactory), - optionsList: new PluginServiceProvider(optionsListServiceFactory), + unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), }; export const pluginServices = new PluginServices(); diff --git a/src/plugins/controls/public/services/plugin_services.ts b/src/plugins/controls/public/services/plugin_services.ts index 4debd0e8c9eb..f1811063e39a 100644 --- a/src/plugins/controls/public/services/plugin_services.ts +++ b/src/plugins/controls/public/services/plugin_services.ts @@ -30,16 +30,15 @@ export const providers: PluginServiceProviders< ControlsServices, KibanaPluginServiceParams > = { - http: new PluginServiceProvider(httpServiceFactory), + controls: new PluginServiceProvider(controlsServiceFactory), data: new PluginServiceProvider(dataServiceFactory), - unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), - overlays: new PluginServiceProvider(overlaysServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), + http: new PluginServiceProvider(httpServiceFactory), + optionsList: new PluginServiceProvider(optionsListServiceFactory, ['data', 'http']), + overlays: new PluginServiceProvider(overlaysServiceFactory), settings: new PluginServiceProvider(settingsServiceFactory), theme: new PluginServiceProvider(themeServiceFactory), - - optionsList: new PluginServiceProvider(optionsListServiceFactory, ['data', 'http']), - controls: new PluginServiceProvider(controlsServiceFactory), + unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), }; export const pluginServices = new PluginServices(); diff --git a/src/plugins/custom_integrations/common/index.ts b/src/plugins/custom_integrations/common/index.ts index a3ab30a526ca..e52a5deac59e 100755 --- a/src/plugins/custom_integrations/common/index.ts +++ b/src/plugins/custom_integrations/common/index.ts @@ -23,6 +23,7 @@ export const INTEGRATION_CATEGORY_DISPLAY = { datastore: 'Datastore', elastic_stack: 'Elastic Stack', google_cloud: 'Google Cloud', + infrastructure: 'Infrastructure', kubernetes: 'Kubernetes', languages: 'Languages', message_queue: 'Message queue', diff --git a/src/plugins/custom_integrations/common/language_integrations.ts b/src/plugins/custom_integrations/common/language_integrations.ts index c9821281f285..9ba914c02fd0 100644 --- a/src/plugins/custom_integrations/common/language_integrations.ts +++ b/src/plugins/custom_integrations/common/language_integrations.ts @@ -145,4 +145,18 @@ export const languageIntegrations: LanguageIntegration[] = [ integrationsAppUrl: `/app/integrations/language_clients/java/overview`, exportLanguageUiComponent: false, }, + // Uncomment to show the sample language client card + README UI + // { + // id: 'sample', + // title: i18n.translate('customIntegrations.languageclients.SampleTitle', { + // defaultMessage: 'Sample Language Client', + // }), + // icon: 'es.svg', + // description: i18n.translate('customIntegrations.languageclients.SampleDescription', { + // defaultMessage: 'Sample language client', + // }), + // docUrlTemplate: '', + // integrationsAppUrl: `/app/integrations/language_clients/sample/overview`, + // exportLanguageUiComponent: true, + // }, ]; diff --git a/src/plugins/custom_integrations/public/components/fleet_integration/overview_component.tsx b/src/plugins/custom_integrations/public/components/fleet_integration/overview_component.tsx deleted file mode 100644 index c5b8584e1e4a..000000000000 --- a/src/plugins/custom_integrations/public/components/fleet_integration/overview_component.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { EuiTitle } from '@elastic/eui'; - -/* -Example of Overview component to be shown inside Integrations app -*/ -export interface ReadmeProps { - packageName: string; -} -export const OverviewComponent: React.FC = ({ packageName }) => { - return ( - -

{packageName} client - Overview

-
- ); -}; diff --git a/src/plugins/custom_integrations/public/components/fleet_integration/sample/sample_client_readme.tsx b/src/plugins/custom_integrations/public/components/fleet_integration/sample/sample_client_readme.tsx new file mode 100644 index 000000000000..c2ca0d62da68 --- /dev/null +++ b/src/plugins/custom_integrations/public/components/fleet_integration/sample/sample_client_readme.tsx @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; + +// eslint-disable-next-line @kbn/eslint/module_migration +import styled from 'styled-components'; +import cuid from 'cuid'; + +import { + EuiButton, + EuiCode, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiPageHeader, + EuiPageSection, + EuiSpacer, + EuiText, + EuiTitle, + EuiPanel, + EuiImage, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import icon from '../../../assets/language_clients/es.svg'; + +const CenterColumn = styled(EuiFlexItem)` + max-width: 740px; +`; + +const FixedHeader = styled.div` + width: 100%; + height: 196px; + border-bottom: 1px solid ${euiThemeVars.euiColorLightShade}; +`; + +const IconPanel = styled(EuiPanel)` + padding: ${(props) => props.theme.eui.euiSizeXL}; + width: ${(props) => + parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.euiSizeXL) * 2}px; + svg, + img { + height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + width: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + } + .euiFlexItem { + height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + justify-content: center; + } +`; + +const TopFlexGroup = styled(EuiFlexGroup)` + max-width: 1150px; + margin-left: auto; + margin-right: auto; + padding: calc(${euiThemeVars.euiSizeXL} * 2) ${euiThemeVars.euiSizeM} 0 ${euiThemeVars.euiSizeM}; +`; + +export const SampleClientReadme = () => { + const [apiKey, setApiKey] = useState(null); + + return ( + <> + + + + + + + + + +

+ +

+
+
+
+
+ + + + + + + + + + } + /> + + + + +

+ +

+
+ + + + + {`# Grab the sample language client from NPM and install it in your project \n`} + {`$ npm install @elastic/elasticsearch-sample`} + +
+ + + +

+ +

+
+ + + + + + + + + + setApiKey(cuid())} disabled={!!apiKey}> + Generate API key + + + + {apiKey && ( + + + {apiKey} + + + )} + +
+ + + +

+ +

+
+ + + elastic.config.json, + }} + /> + + + + + + {` +{ + "apiKey": "${apiKey || 'YOUR_API_KEY'} +} + + `} + +
+
+
+
+
+ + ); +}; diff --git a/src/plugins/custom_integrations/public/mocks.tsx b/src/plugins/custom_integrations/public/mocks.tsx index 038fdb178045..2503008ea90e 100644 --- a/src/plugins/custom_integrations/public/mocks.tsx +++ b/src/plugins/custom_integrations/public/mocks.tsx @@ -24,7 +24,7 @@ function createCustomIntegrationsStart(): jest.Mocked { const services = servicesFactory({ startPlugins: {}, coreStart: coreMock.createStart() }); return { - languageClientsUiComponents: new Map(), + languageClientsUiComponents: {}, ContextProvider: jest.fn(({ children }) => ( {children} diff --git a/src/plugins/custom_integrations/public/plugin.tsx b/src/plugins/custom_integrations/public/plugin.tsx index 90a796955a59..827d31ce3749 100755 --- a/src/plugins/custom_integrations/public/plugin.tsx +++ b/src/plugins/custom_integrations/public/plugin.tsx @@ -19,12 +19,10 @@ import { ROUTES_APPEND_CUSTOM_INTEGRATIONS, ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS, } from '../common'; -import { languageIntegrations } from '../common/language_integrations'; - -import { OverviewComponent } from './components/fleet_integration/overview_component'; import { CustomIntegrationsServicesProvider } from './services'; import { servicesFactory } from './services/kibana'; +import { SampleClientReadme } from './components/fleet_integration/sample/sample_client_readme'; export class CustomIntegrationsPlugin implements Plugin @@ -48,16 +46,7 @@ export class CustomIntegrationsPlugin ): CustomIntegrationsStart { const services = servicesFactory({ coreStart, startPlugins }); - const languageClientsUiComponents = new Map(); - - // Set the language clients components to render in Fleet plugin under Integrations app - // Export component only if the integration has exportLanguageUiComponent = true - languageIntegrations - .filter((int) => int.exportLanguageUiComponent) - .map((int) => { - const ReadmeComponent = () => ; - languageClientsUiComponents.set(`language_client.${int.id}`, ReadmeComponent); - }); + const languageClientsUiComponents = { sample: SampleClientReadme }; const ContextProvider: React.FC = ({ children }) => ( diff --git a/src/plugins/custom_integrations/public/types.ts b/src/plugins/custom_integrations/public/types.ts index db8acc3e2e07..60b8aefe23dd 100755 --- a/src/plugins/custom_integrations/public/types.ts +++ b/src/plugins/custom_integrations/public/types.ts @@ -15,7 +15,7 @@ export interface CustomIntegrationsSetup { export interface CustomIntegrationsStart { ContextProvider: React.FC; - languageClientsUiComponents: Map; + languageClientsUiComponents: Record; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts deleted file mode 100644 index 3a6475b60251..000000000000 --- a/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - ExtractDeps, - extractPanelsReferences, - InjectDeps, - injectPanelsReferences, -} from './embeddable_references'; -import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks'; -import { SavedDashboardPanel } from '../types'; -import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; - -const embeddablePersistableStateService = createEmbeddablePersistableStateServiceMock(); -const deps: InjectDeps & ExtractDeps = { - embeddablePersistableStateService, -}; - -test('inject/extract panel references', () => { - embeddablePersistableStateService.extract.mockImplementationOnce((state) => { - const { HARDCODED_ID, ...restOfState } = state as unknown as Record; - return { - state: restOfState as EmbeddableStateWithType, - references: [{ id: HARDCODED_ID as string, name: 'refName', type: 'type' }], - }; - }); - - embeddablePersistableStateService.inject.mockImplementationOnce((state, references) => { - const ref = references.find((r) => r.name === 'refName'); - return { - ...state, - HARDCODED_ID: ref!.id, - }; - }); - - const savedDashboardPanel: SavedDashboardPanel = { - type: 'search', - embeddableConfig: { - HARDCODED_ID: 'IMPORTANT_HARDCODED_ID', - }, - id: 'savedObjectId', - panelIndex: '123', - gridData: { - x: 0, - y: 0, - h: 15, - w: 15, - i: '123', - }, - version: '7.0.0', - }; - - const [{ panel: extractedPanel, references }] = extractPanelsReferences( - [savedDashboardPanel], - deps - ); - expect(extractedPanel.embeddableConfig).toEqual({}); - expect(references).toMatchInlineSnapshot(` - Array [ - Object { - "id": "IMPORTANT_HARDCODED_ID", - "name": "refName", - "type": "type", - }, - ] - `); - - const [injectedPanel] = injectPanelsReferences([extractedPanel], references, deps); - - expect(injectedPanel).toEqual(savedDashboardPanel); -}); diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.ts deleted file mode 100644 index 6664f70d3392..000000000000 --- a/src/plugins/dashboard/common/embeddable/embeddable_references.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { omit } from 'lodash'; -import { SavedObjectReference } from '@kbn/core/types'; -import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common/types'; -import { - convertSavedDashboardPanelToPanelState, - convertPanelStateToSavedDashboardPanel, -} from './embeddable_saved_object_converters'; -import { SavedDashboardPanel } from '../types'; - -export interface InjectDeps { - embeddablePersistableStateService: EmbeddablePersistableStateService; -} - -export function injectPanelsReferences( - panels: SavedDashboardPanel[], - references: SavedObjectReference[], - deps: InjectDeps -): SavedDashboardPanel[] { - const result: SavedDashboardPanel[] = []; - for (const panel of panels) { - const embeddableState = convertSavedDashboardPanelToPanelState(panel); - embeddableState.explicitInput = omit( - deps.embeddablePersistableStateService.inject( - { ...embeddableState.explicitInput, type: panel.type }, - references - ), - 'type' - ); - result.push(convertPanelStateToSavedDashboardPanel(embeddableState, panel.version)); - } - return result; -} - -export interface ExtractDeps { - embeddablePersistableStateService: EmbeddablePersistableStateService; -} - -export function extractPanelsReferences( - panels: SavedDashboardPanel[], - deps: ExtractDeps -): Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> { - const result: Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> = []; - - for (const panel of panels) { - const embeddable = convertSavedDashboardPanelToPanelState(panel); - const { state: embeddableInputWithExtractedReferences, references } = - deps.embeddablePersistableStateService.extract({ - ...embeddable.explicitInput, - type: embeddable.type, - }); - embeddable.explicitInput = omit(embeddableInputWithExtractedReferences, 'type'); - - const newPanel = convertPanelStateToSavedDashboardPanel(embeddable, panel.version); - result.push({ - panel: newPanel, - references, - }); - } - - return result; -} diff --git a/src/plugins/dashboard/common/index.ts b/src/plugins/dashboard/common/index.ts index 73e01693977d..81833f8a8f18 100644 --- a/src/plugins/dashboard/common/index.ts +++ b/src/plugins/dashboard/common/index.ts @@ -6,24 +6,28 @@ * Side Public License, v 1. */ -export type { GridData } from './embeddable/types'; -export type { - RawSavedDashboardPanel730ToLatest, - DashboardDoc730ToLatest, - DashboardDoc700To720, - DashboardDocPre700, -} from './bwc/types'; export type { + GridData, + DashboardPanelMap, + SavedDashboardPanel, + DashboardAttributes, + DashboardPanelState, DashboardContainerStateWithType, - SavedDashboardPanelTo60, - SavedDashboardPanel610, - SavedDashboardPanel620, - SavedDashboardPanel630, - SavedDashboardPanel640To720, - SavedDashboardPanel730ToLatest, } from './types'; -export { migratePanelsTo730 } from './migrate_to_730_panels'; +export { + injectReferences, + extractReferences, +} from './persistable_state/dashboard_saved_object_references'; + +export { createInject, createExtract } from './persistable_state/dashboard_container_references'; + +export { + convertPanelStateToSavedDashboardPanel, + convertSavedDashboardPanelToPanelState, + convertSavedPanelsToPanelMap, + convertPanelMapToSavedPanels, +} from './lib/dashboard_panel_converters'; export const UI_SETTINGS = { ENABLE_LABS_UI: 'labs:dashboard:enable_ui', diff --git a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts b/src/plugins/dashboard/common/lib/dashboard_panel_converters.test.ts similarity index 98% rename from src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts rename to src/plugins/dashboard/common/lib/dashboard_panel_converters.test.ts index 9ec93fa85fc5..2ebca116f3f1 100644 --- a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts +++ b/src/plugins/dashboard/common/lib/dashboard_panel_converters.test.ts @@ -9,7 +9,7 @@ import { convertSavedDashboardPanelToPanelState, convertPanelStateToSavedDashboardPanel, -} from './embeddable_saved_object_converters'; +} from './dashboard_panel_converters'; import { SavedDashboardPanel, DashboardPanelState } from '../types'; import { EmbeddableInput } from '@kbn/embeddable-plugin/common/types'; diff --git a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts b/src/plugins/dashboard/common/lib/dashboard_panel_converters.ts similarity index 75% rename from src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts rename to src/plugins/dashboard/common/lib/dashboard_panel_converters.ts index aa9519a5a48b..2652c7f9a40a 100644 --- a/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts +++ b/src/plugins/dashboard/common/lib/dashboard_panel_converters.ts @@ -8,7 +8,7 @@ import { omit } from 'lodash'; import { EmbeddableInput, SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common'; -import { DashboardPanelState, SavedDashboardPanel } from '../types'; +import { DashboardPanelMap, DashboardPanelState, SavedDashboardPanel } from '../types'; export function convertSavedDashboardPanelToPanelState< TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput @@ -42,3 +42,17 @@ export function convertPanelStateToSavedDashboardPanel( ...(panelState.panelRefName !== undefined && { panelRefName: panelState.panelRefName }), }; } + +export const convertSavedPanelsToPanelMap = (panels?: SavedDashboardPanel[]): DashboardPanelMap => { + const panelsMap: DashboardPanelMap = {}; + panels?.forEach((panel, idx) => { + panelsMap![panel.panelIndex ?? String(idx)] = convertSavedDashboardPanelToPanelState(panel); + }); + return panelsMap; +}; + +export const convertPanelMapToSavedPanels = (panels: DashboardPanelMap, version: string) => { + return Object.values(panels).map((panel) => + convertPanelStateToSavedDashboardPanel(panel, version) + ); +}; diff --git a/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.test.ts b/src/plugins/dashboard/common/persistable_state/dashboard_container_references.test.ts similarity index 99% rename from src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.test.ts rename to src/plugins/dashboard/common/persistable_state/dashboard_container_references.test.ts index ee13926486f8..47215e5e3200 100644 --- a/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.test.ts +++ b/src/plugins/dashboard/common/persistable_state/dashboard_container_references.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createExtract, createInject } from './dashboard_container_persistable_state'; +import { createExtract, createInject } from './dashboard_container_references'; import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks'; import { DashboardContainerStateWithType } from '../types'; diff --git a/src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts b/src/plugins/dashboard/common/persistable_state/dashboard_container_references.ts similarity index 100% rename from src/plugins/dashboard/common/embeddable/dashboard_container_persistable_state.ts rename to src/plugins/dashboard/common/persistable_state/dashboard_container_references.ts diff --git a/src/plugins/dashboard/common/saved_dashboard_references.test.ts b/src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.test.ts similarity index 98% rename from src/plugins/dashboard/common/saved_dashboard_references.test.ts rename to src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.test.ts index 84bfe6ea48d3..e28a429d6d00 100644 --- a/src/plugins/dashboard/common/saved_dashboard_references.test.ts +++ b/src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.test.ts @@ -11,9 +11,9 @@ import { injectReferences, InjectDeps, ExtractDeps, -} from './saved_dashboard_references'; +} from './dashboard_saved_object_references'; -import { createExtract, createInject } from './embeddable/dashboard_container_persistable_state'; +import { createExtract, createInject } from './dashboard_container_references'; import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks'; const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock(); diff --git a/src/plugins/dashboard/common/saved_dashboard_references.ts b/src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts similarity index 96% rename from src/plugins/dashboard/common/saved_dashboard_references.ts rename to src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts index e3a3193dd85a..a3126a381d94 100644 --- a/src/plugins/dashboard/common/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/persistable_state/dashboard_saved_object_references.ts @@ -6,23 +6,33 @@ * Side Public License, v 1. */ import semverGt from 'semver/functions/gt'; -import { SavedObjectAttributes, SavedObjectReference } from '@kbn/core/types'; -import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common/types'; + import { - PersistableControlGroupInput, RawControlGroupAttributes, + PersistableControlGroupInput, } from '@kbn/controls-plugin/common'; -import { DashboardContainerStateWithType, DashboardPanelState } from './types'; +import { SavedObjectAttributes, SavedObjectReference } from '@kbn/core/types'; +import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common/types'; + +import { + SavedDashboardPanel, + DashboardPanelState, + DashboardContainerStateWithType, +} from '../types'; import { convertPanelStateToSavedDashboardPanel, convertSavedDashboardPanelToPanelState, -} from './embeddable/embeddable_saved_object_converters'; -import { SavedDashboardPanel } from './types'; +} from '../lib/dashboard_panel_converters'; export interface ExtractDeps { embeddablePersistableStateService: EmbeddablePersistableStateService; } -export interface SavedObjectAttributesAndReferences { + +export interface InjectDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +interface SavedObjectAttributesAndReferences { attributes: SavedObjectAttributes; references: SavedObjectReference[]; } @@ -79,7 +89,7 @@ function panelStatesToPanels( let originalPanel = originalPanels.find((p) => p.panelIndex === id); if (!originalPanel) { - // Maybe original panel doesn't have a panel index and it's just straight up based on it's index + // Maybe original panel doesn't have a panel index and it's just straight up based on its index const numericId = parseInt(id, 10); originalPanel = isNaN(numericId) ? originalPanel : originalPanels[numericId]; } @@ -91,6 +101,45 @@ function panelStatesToPanels( }); } +export function injectReferences( + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: InjectDeps +): SavedObjectAttributes { + // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when + // importing objects without panelsJSON. At development time of this, there is no guarantee each saved + // object has panelsJSON in all previous versions of kibana. + if (typeof attributes.panelsJSON !== 'string') { + return attributes; + } + const parsedPanels = JSON.parse(attributes.panelsJSON); + // Same here, prevent failing saved object import if ever panels aren't an array. + if (!Array.isArray(parsedPanels)) { + return attributes; + } + + const { panels, state } = dashboardAttributesToState(attributes); + + const injectedState = deps.embeddablePersistableStateService.inject( + state, + references + ) as DashboardContainerStateWithType; + const injectedPanels = panelStatesToPanels(injectedState.panels, panels); + + const newAttributes = { + ...attributes, + panelsJSON: JSON.stringify(injectedPanels), + } as SavedObjectAttributes; + + if (injectedState.controlGroupInput) { + newAttributes.controlGroupInput = { + ...(attributes.controlGroupInput as SavedObjectAttributes), + panelsJSON: JSON.stringify(injectedState.controlGroupInput.panels), + }; + } + + return newAttributes; +} + export function extractReferences( { attributes, references = [] }: SavedObjectAttributesAndReferences, deps: ExtractDeps @@ -137,49 +186,6 @@ export function extractReferences( }; } -export interface InjectDeps { - embeddablePersistableStateService: EmbeddablePersistableStateService; -} - -export function injectReferences( - { attributes, references = [] }: SavedObjectAttributesAndReferences, - deps: InjectDeps -): SavedObjectAttributes { - // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when - // importing objects without panelsJSON. At development time of this, there is no guarantee each saved - // object has panelsJSON in all previous versions of kibana. - if (typeof attributes.panelsJSON !== 'string') { - return attributes; - } - const parsedPanels = JSON.parse(attributes.panelsJSON); - // Same here, prevent failing saved object import if ever panels aren't an array. - if (!Array.isArray(parsedPanels)) { - return attributes; - } - - const { panels, state } = dashboardAttributesToState(attributes); - - const injectedState = deps.embeddablePersistableStateService.inject( - state, - references - ) as DashboardContainerStateWithType; - const injectedPanels = panelStatesToPanels(injectedState.panels, panels); - - const newAttributes = { - ...attributes, - panelsJSON: JSON.stringify(injectedPanels), - } as SavedObjectAttributes; - - if (injectedState.controlGroupInput) { - newAttributes.controlGroupInput = { - ...(attributes.controlGroupInput as SavedObjectAttributes), - panelsJSON: JSON.stringify(injectedState.controlGroupInput.panels), - }; - } - - return newAttributes; -} - function pre730ExtractReferences( { attributes, references = [] }: SavedObjectAttributesAndReferences, deps: ExtractDeps diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts index 941f9437e54e..ff5a1cbc1755 100644 --- a/src/plugins/dashboard/common/types.ts +++ b/src/plugins/dashboard/common/types.ts @@ -11,28 +11,13 @@ import { EmbeddableStateWithType, PanelState, } from '@kbn/embeddable-plugin/common/types'; -import { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common/lib/saved_object_embeddable'; -import { PersistableControlGroupInput } from '@kbn/controls-plugin/common'; +import { Serializable } from '@kbn/utility-types'; import { - RawSavedDashboardPanelTo60, - RawSavedDashboardPanel610, - RawSavedDashboardPanel620, - RawSavedDashboardPanel630, - RawSavedDashboardPanel640To720, - RawSavedDashboardPanel730ToLatest, -} from './bwc/types'; - -import { GridData } from './embeddable/types'; - -export type PanelId = string; -export type SavedObjectId = string; - -export interface DashboardPanelState< - TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput -> extends PanelState { - readonly gridData: GridData; - panelRefName?: string; -} + PersistableControlGroupInput, + RawControlGroupAttributes, +} from '@kbn/controls-plugin/common'; +import { RefreshInterval } from '@kbn/data-plugin/common'; +import { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common/lib/saved_object_embeddable'; export interface DashboardCapabilities { showWriteControls: boolean; @@ -43,62 +28,77 @@ export interface DashboardCapabilities { } /** - * This should always represent the latest dashboard panel shape, after all possible migrations. + * The attributes of the dashboard saved object. This interface should be the + * source of truth for the latest dashboard attributes shape after all migrations. */ -export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; - -export type SavedDashboardPanel640To720 = Pick< - RawSavedDashboardPanel640To720, - Exclude -> & { - readonly id: string; - readonly type: string; -}; +export interface DashboardAttributes { + controlGroupInput?: RawControlGroupAttributes; + refreshInterval?: RefreshInterval; + timeRestore: boolean; + optionsJSON?: string; + useMargins?: boolean; + description: string; + panelsJSON: string; + timeFrom?: string; + version: number; + timeTo?: string; + title: string; + kibanaSavedObjectMeta: { + searchSourceJSON: string; + }; +} -export type SavedDashboardPanel630 = Pick< - RawSavedDashboardPanel630, - Exclude -> & { - readonly id: string; - readonly type: string; -}; +/** -------------------------------------------------------------------- + * Dashboard panel types + -----------------------------------------------------------------------*/ -export type SavedDashboardPanel620 = Pick< - RawSavedDashboardPanel620, - Exclude -> & { - readonly id: string; - readonly type: string; -}; +/** + * The dashboard panel format expected by the embeddable container. + */ +export interface DashboardPanelState< + TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +> extends PanelState { + readonly gridData: GridData; + panelRefName?: string; +} -export type SavedDashboardPanel610 = Pick< - RawSavedDashboardPanel610, - Exclude -> & { - readonly id: string; - readonly type: string; -}; +/** + * A saved dashboard panel parsed directly from the Dashboard Attributes panels JSON + */ +export interface SavedDashboardPanel { + embeddableConfig: { [key: string]: Serializable }; // parsed into the panel's explicitInput + id?: string; // the saved object id for by reference panels + type: string; // the embeddable type + panelRefName?: string; + gridData: GridData; + panelIndex: string; + version: string; + title?: string; +} -export type SavedDashboardPanelTo60 = Pick< - RawSavedDashboardPanelTo60, - Exclude -> & { - readonly id: string; - readonly type: string; -}; +/** + * Grid type for React Grid Layout + */ +export interface GridData { + w: number; + h: number; + x: number; + y: number; + i: string; +} -// id becomes optional starting in 7.3.0 -export type SavedDashboardPanel730ToLatest = Pick< - RawSavedDashboardPanel730ToLatest, - Exclude -> & { - readonly id?: string; - readonly type: string; -}; +export interface DashboardPanelMap { + [key: string]: DashboardPanelState; +} -// Making this interface because so much of the Container type from embeddable is tied up in public -// Once that is all available from common, we should be able to move the dashboard_container type to our common as well +/** -------------------------------------------------------------------- + * Dashboard container types + -----------------------------------------------------------------------*/ +/** + * Types below this line are copied here because so many important types are tied up in public. These types should be + * moved from public into common. + */ export interface DashboardContainerStateWithType extends EmbeddableStateWithType { panels: { [panelId: string]: DashboardPanelState; diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx index ac467e35729f..aa3419e37890 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { AddToLibraryAction } from '.'; import { DashboardContainer } from '../embeddable/dashboard_container'; import { getSampleDashboardInput } from '../test_helpers'; import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; @@ -27,6 +26,7 @@ import { CONTACT_CARD_EMBEDDABLE, } from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables'; import { pluginServices } from '../../services/plugin_services'; +import { AddToLibraryAction } from './add_to_library_action'; const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any); pluginServices.getServices().embeddable.getEmbeddableFactory = jest diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index 8c6577012161..0510d35519ff 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -18,8 +18,9 @@ import { import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { dashboardAddToLibraryAction } from '../../dashboard_strings'; -import { type DashboardPanelState, DASHBOARD_CONTAINER_TYPE, type DashboardContainer } from '..'; +import { type DashboardPanelState, type DashboardContainer } from '..'; import { pluginServices } from '../../services/plugin_services'; +import { DASHBOARD_CONTAINER_TYPE } from '../../dashboard_constants'; export const ACTION_ADD_TO_LIBRARY = 'saveToLibrary'; diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx index 5bb331e58fe3..0fb63049ebe3 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx @@ -12,7 +12,7 @@ import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helper import { coreMock } from '@kbn/core/public/mocks'; import { CoreStart } from '@kbn/core/public'; -import { ClonePanelAction } from '.'; +import { ClonePanelAction } from './clone_panel_action'; import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; import { ContactCardEmbeddable, diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index 02862e7c75e8..11a96733337f 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -27,9 +27,10 @@ import { placePanelBeside, IPanelPlacementBesideArgs, } from '../embeddable/panel/dashboard_panel_placement'; -import { dashboardClonePanelAction } from '../../dashboard_strings'; -import { type DashboardPanelState, DASHBOARD_CONTAINER_TYPE, type DashboardContainer } from '..'; import { pluginServices } from '../../services/plugin_services'; +import { dashboardClonePanelAction } from '../../dashboard_strings'; +import { DASHBOARD_CONTAINER_TYPE } from '../../dashboard_constants'; +import { type DashboardPanelState, type DashboardContainer } from '..'; export const ACTION_CLONE_PANEL = 'clonePanel'; diff --git a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_action.tsx b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_action.tsx index 8f602db5e452..cdd8d726e9fa 100644 --- a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_action.tsx +++ b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_action.tsx @@ -14,9 +14,10 @@ import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import { dashboardCopyToDashboardAction } from '../../dashboard_strings'; -import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; +import { DashboardContainer } from '../embeddable'; import { CopyToDashboardModal } from './copy_to_dashboard_modal'; import { pluginServices } from '../../services/plugin_services'; +import { DASHBOARD_CONTAINER_TYPE } from '../../dashboard_constants'; export const ACTION_COPY_TO_DASHBOARD = 'copyToDashboard'; diff --git a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx index 7f9a99ed2723..af91631d20b3 100644 --- a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx +++ b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_modal.tsx @@ -25,8 +25,8 @@ import { import { IEmbeddable, PanelNotFoundError } from '@kbn/embeddable-plugin/public'; import { LazyDashboardPicker, withSuspense } from '@kbn/presentation-util-plugin/public'; import { dashboardCopyToDashboardAction } from '../../dashboard_strings'; -import { createDashboardEditUrl, DashboardConstants, DashboardContainer } from '../..'; -import { DashboardPanelState } from '..'; +import { createDashboardEditUrl, DashboardConstants } from '../..'; +import { type DashboardContainer, DashboardPanelState } from '..'; import { pluginServices } from '../../services/plugin_services'; interface CopyToDashboardModalProps { diff --git a/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx b/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx index c5da566b90a6..79ab109ddce5 100644 --- a/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/expand_panel_action.tsx @@ -9,9 +9,9 @@ import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; -import type { DashboardContainerInput } from '../..'; +import { DashboardContainerInput, DASHBOARD_CONTAINER_TYPE } from '../..'; import { dashboardExpandPanelAction } from '../../dashboard_strings'; -import { DASHBOARD_CONTAINER_TYPE, type DashboardContainer } from '../embeddable'; +import { type DashboardContainer } from '../embeddable'; export const ACTION_EXPAND_PANEL = 'togglePanel'; diff --git a/src/plugins/dashboard/public/application/actions/filters_notification_badge.test.tsx b/src/plugins/dashboard/public/application/actions/filters_notification_badge.test.tsx index 275a5625e5e0..3b3fb5dde049 100644 --- a/src/plugins/dashboard/public/application/actions/filters_notification_badge.test.tsx +++ b/src/plugins/dashboard/public/application/actions/filters_notification_badge.test.tsx @@ -6,27 +6,26 @@ * Side Public License, v 1. */ -import { getSampleDashboardInput } from '../test_helpers'; -import { DashboardContainer } from '../embeddable/dashboard_container'; - -import { FiltersNotificationBadge } from '.'; -import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; -import { type Query, type AggregateQuery, Filter } from '@kbn/es-query'; import { - ErrorEmbeddable, - FilterableEmbeddable, IContainer, + ErrorEmbeddable, isErrorEmbeddable, + FilterableEmbeddable, } from '@kbn/embeddable-plugin/public'; - import { ContactCardEmbeddable, - ContactCardEmbeddableFactory, + CONTACT_CARD_EMBEDDABLE, ContactCardEmbeddableInput, ContactCardEmbeddableOutput, - CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, } from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables'; +import { type Query, type AggregateQuery, Filter } from '@kbn/es-query'; +import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; + +import { getSampleDashboardInput } from '../test_helpers'; import { pluginServices } from '../../services/plugin_services'; +import { DashboardContainer } from '../embeddable/dashboard_container'; +import { FiltersNotificationBadge } from './filters_notification_badge'; const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any); pluginServices.getServices().embeddable.getEmbeddableFactory = jest diff --git a/src/plugins/dashboard/public/application/actions/index.ts b/src/plugins/dashboard/public/application/actions/index.ts index 5c95e3c42ccc..a238ce05e101 100644 --- a/src/plugins/dashboard/public/application/actions/index.ts +++ b/src/plugins/dashboard/public/application/actions/index.ts @@ -6,23 +6,76 @@ * Side Public License, v 1. */ -export type { ExpandPanelActionContext } from './expand_panel_action'; -export { ExpandPanelAction, ACTION_EXPAND_PANEL } from './expand_panel_action'; -export type { ReplacePanelActionContext } from './replace_panel_action'; -export { ReplacePanelAction, ACTION_REPLACE_PANEL } from './replace_panel_action'; -export type { ClonePanelActionContext } from './clone_panel_action'; -export { ClonePanelAction, ACTION_CLONE_PANEL } from './clone_panel_action'; -export type { AddToLibraryActionContext } from './add_to_library_action'; -export { AddToLibraryAction, ACTION_ADD_TO_LIBRARY } from './add_to_library_action'; -export type { UnlinkFromLibraryActionContext } from './unlink_from_library_action'; -export { UnlinkFromLibraryAction, ACTION_UNLINK_FROM_LIBRARY } from './unlink_from_library_action'; -export type { CopyToDashboardActionContext } from './copy_to_dashboard_action'; -export { CopyToDashboardAction, ACTION_COPY_TO_DASHBOARD } from './copy_to_dashboard_action'; -export type { LibraryNotificationActionContext } from './library_notification_action'; -export { - LibraryNotificationAction, - ACTION_LIBRARY_NOTIFICATION, -} from './library_notification_action'; -export { FiltersNotificationBadge, BADGE_FILTERS_NOTIFICATION } from './filters_notification_badge'; -export type { ExportContext } from './export_csv_action'; -export { ExportCSVAction, ACTION_EXPORT_CSV } from './export_csv_action'; +import { + CONTEXT_MENU_TRIGGER, + PANEL_BADGE_TRIGGER, + PANEL_NOTIFICATION_TRIGGER, +} from '@kbn/embeddable-plugin/public'; +import { CoreStart } from '@kbn/core/public'; +import { getSavedObjectFinder } from '@kbn/saved-objects-plugin/public'; + +import { ExportCSVAction } from './export_csv_action'; +import { ClonePanelAction } from './clone_panel_action'; +import { DashboardStartDependencies } from '../../plugin'; +import { ExpandPanelAction } from './expand_panel_action'; +import { ReplacePanelAction } from './replace_panel_action'; +import { AddToLibraryAction } from './add_to_library_action'; +import { CopyToDashboardAction } from './copy_to_dashboard_action'; +import { UnlinkFromLibraryAction } from './unlink_from_library_action'; +import { FiltersNotificationBadge } from './filters_notification_badge'; +import { LibraryNotificationAction } from './library_notification_action'; + +interface BuildAllDashboardActionsProps { + core: CoreStart; + allowByValueEmbeddables?: boolean; + plugins: DashboardStartDependencies; +} + +export const buildAllDashboardActions = async ({ + core, + plugins, + allowByValueEmbeddables, +}: BuildAllDashboardActionsProps) => { + const { uiSettings } = core; + const { uiActions, share, presentationUtil } = plugins; + + const clonePanelAction = new ClonePanelAction(core.savedObjects); + uiActions.registerAction(clonePanelAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id); + + const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, uiSettings); + const changeViewAction = new ReplacePanelAction(SavedObjectFinder); + uiActions.registerAction(changeViewAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction.id); + + const panelLevelFiltersNotification = new FiltersNotificationBadge(); + uiActions.registerAction(panelLevelFiltersNotification); + uiActions.attachAction(PANEL_BADGE_TRIGGER, panelLevelFiltersNotification.id); + + const expandPanelAction = new ExpandPanelAction(); + uiActions.registerAction(expandPanelAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction.id); + + if (share) { + const ExportCSVPlugin = new ExportCSVAction(); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, ExportCSVPlugin); + } + + if (allowByValueEmbeddables) { + const addToLibraryAction = new AddToLibraryAction(); + uiActions.registerAction(addToLibraryAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id); + + const unlinkFromLibraryAction = new UnlinkFromLibraryAction(); + uiActions.registerAction(unlinkFromLibraryAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id); + + const libraryNotificationAction = new LibraryNotificationAction(unlinkFromLibraryAction); + uiActions.registerAction(libraryNotificationAction); + uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); + + const copyToDashboardAction = new CopyToDashboardAction(presentationUtil.ContextProvider); + uiActions.registerAction(copyToDashboardAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, copyToDashboardAction.id); + } +}; diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx index f1202de4ac1b..f30cba538b8d 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx @@ -6,11 +6,6 @@ * Side Public License, v 1. */ -import { getSampleDashboardInput } from '../test_helpers'; -import { DashboardContainer } from '../embeddable/dashboard_container'; - -import { LibraryNotificationAction, UnlinkFromLibraryAction } from '.'; -import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; import { ErrorEmbeddable, IContainer, @@ -25,7 +20,13 @@ import { ContactCardEmbeddableOutput, CONTACT_CARD_EMBEDDABLE, } from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables'; +import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; + +import { getSampleDashboardInput } from '../test_helpers'; import { pluginServices } from '../../services/plugin_services'; +import { DashboardContainer } from '../embeddable/dashboard_container'; +import { UnlinkFromLibraryAction } from './unlink_from_library_action'; +import { LibraryNotificationAction } from './library_notification_action'; const mockEmbeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any); pluginServices.getServices().embeddable.getEmbeddableFactory = jest diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx index a5abe8161e9a..a05b78994b31 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx @@ -17,10 +17,10 @@ import { import { KibanaThemeProvider, reactToUiComponent } from '@kbn/kibana-react-plugin/public'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; -import { UnlinkFromLibraryAction } from '.'; -import { LibraryNotificationPopover } from './library_notification_popover'; -import { dashboardLibraryNotification } from '../../dashboard_strings'; import { pluginServices } from '../../services/plugin_services'; +import { UnlinkFromLibraryAction } from './unlink_from_library_action'; +import { dashboardLibraryNotification } from '../../dashboard_strings'; +import { LibraryNotificationPopover } from './library_notification_popover'; export const ACTION_LIBRARY_NOTIFICATION = 'ACTION_LIBRARY_NOTIFICATION'; diff --git a/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx b/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx index 81d2f2a0557e..38c8452eadde 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx @@ -17,8 +17,10 @@ import { EuiPopoverTitle, EuiText, } from '@elastic/eui'; -import { LibraryNotificationActionContext, UnlinkFromLibraryAction } from '.'; + import { dashboardLibraryNotification } from '../../dashboard_strings'; +import { UnlinkFromLibraryAction } from './unlink_from_library_action'; +import { LibraryNotificationActionContext } from './library_notification_action'; export interface LibraryNotificationProps { context: LibraryNotificationActionContext; diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx index f39988842e3f..52f6a345a181 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx @@ -8,9 +8,10 @@ import { type IEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; -import { DASHBOARD_CONTAINER_TYPE, type DashboardContainer } from '../embeddable'; +import type { DashboardContainer } from '../embeddable'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; import { dashboardReplacePanelAction } from '../../dashboard_strings'; +import { DASHBOARD_CONTAINER_TYPE } from '../../dashboard_constants'; export const ACTION_REPLACE_PANEL = 'replacePanel'; diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx index 854383edd4e1..080c358a86fd 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx @@ -23,10 +23,10 @@ import { CONTACT_CARD_EMBEDDABLE, } from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables'; -import { UnlinkFromLibraryAction } from '.'; import { getSampleDashboardInput } from '../test_helpers'; -import { DashboardContainer } from '../embeddable/dashboard_container'; import { pluginServices } from '../../services/plugin_services'; +import { UnlinkFromLibraryAction } from './unlink_from_library_action'; +import { DashboardContainer } from '../embeddable/dashboard_container'; let container: DashboardContainer; let embeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable; diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx index e399411e77fe..b7c53a78becc 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx @@ -17,8 +17,9 @@ import { } from '@kbn/embeddable-plugin/public'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { dashboardUnlinkFromLibraryAction } from '../../dashboard_strings'; -import { type DashboardPanelState, DASHBOARD_CONTAINER_TYPE, type DashboardContainer } from '..'; +import { type DashboardPanelState, type DashboardContainer } from '..'; import { pluginServices } from '../../services/plugin_services'; +import { DASHBOARD_CONTAINER_TYPE } from '../../dashboard_constants'; export const ACTION_UNLINK_FROM_LIBRARY = 'unlinkFromLibrary'; diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 302cb4379422..b05944c99292 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -9,24 +9,23 @@ import { History } from 'history'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { EmbeddableRenderer, ViewMode } from '@kbn/embeddable-plugin/public'; import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; +import { EmbeddableRenderer, ViewMode } from '@kbn/embeddable-plugin/public'; import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; -import { useDashboardSelector } from './state'; -import { useDashboardAppState } from './hooks'; import { dashboardFeatureCatalog, getDashboardBreadcrumb, getDashboardTitle, leaveConfirmStrings, } from '../dashboard_strings'; -import { createDashboardEditUrl } from '../dashboard_constants'; -import { DashboardTopNav, isCompleteDashboardAppState } from './top_nav/dashboard_top_nav'; -import { DashboardEmbedSettings, DashboardRedirect } from '../types'; -import { DashboardAppNoDataPage } from './dashboard_app_no_data'; +import { useDashboardAppState } from './hooks'; +import { useDashboardSelector } from './state'; import { pluginServices } from '../services/plugin_services'; +import { DashboardAppNoDataPage } from './dashboard_app_no_data'; +import { DashboardEmbedSettings, DashboardRedirect } from '../types'; import { useDashboardMountContext } from './hooks/dashboard_mount_context'; +import { DashboardTopNav, isCompleteDashboardAppState } from './top_nav/dashboard_top_nav'; export interface DashboardAppProps { history: History; savedDashboardId?: string; @@ -43,13 +42,12 @@ export function DashboardApp({ const { onAppLeave } = useDashboardMountContext(); const { chrome: { setBreadcrumbs, setIsVisible }, + screenshotMode: { isScreenshotMode }, coreContext: { executionContext }, - data: { search }, embeddable: { getStateTransfer }, notifications: { toasts }, - screenshotMode: { isScreenshotMode }, settings: { uiSettings }, - spaces: { getLegacyUrlConflict }, + data: { search }, } = pluginServices.getServices(); const [showNoDataPage, setShowNoDataPage] = useState(false); @@ -160,17 +158,7 @@ export function DashboardApp({ dashboardAppState={dashboardAppState} /> - {dashboardAppState.savedDashboard.outcome === 'conflict' && - dashboardAppState.savedDashboard.id && - dashboardAppState.savedDashboard.aliasId - ? getLegacyUrlConflict?.({ - currentObjectId: dashboardAppState.savedDashboard.id, - otherObjectId: dashboardAppState.savedDashboard.aliasId, - otherObjectPath: `#${createDashboardEditUrl( - dashboardAppState.savedDashboard.aliasId - )}${history.location.search}`, - }) - : null} + {dashboardAppState.createConflictWarning?.()}
createKbnUrlStateStorage({ history, @@ -172,51 +162,48 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da }); const app = ( - // TODO: Remove KibanaContextProvider as part of https://github.com/elastic/kibana/pull/138774 - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ); diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 4f7483cf06f3..036c77fc6257 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -32,7 +32,7 @@ import type { Query } from '@kbn/es-query'; import type { RefreshInterval } from '@kbn/data-plugin/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; -import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; +import { DASHBOARD_CONTAINER_TYPE } from '../../dashboard_constants'; import { createPanelState } from './panel'; import { DashboardPanelState } from './types'; import { DashboardViewport } from './viewport/dashboard_viewport'; diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx index 27670ee10436..58a2c63492c0 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx @@ -7,16 +7,14 @@ */ import { i18n } from '@kbn/i18n'; -import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common'; - import { identity, pickBy } from 'lodash'; + import { ControlGroupContainer, ControlGroupInput, ControlGroupOutput, CONTROL_GROUP_TYPE, } from '@kbn/controls-plugin/public'; -import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common'; import { Container, ErrorEmbeddable, @@ -25,14 +23,13 @@ import { EmbeddableFactoryDefinition, } from '@kbn/embeddable-plugin/public'; +import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common'; +import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common'; + import { DashboardContainerInput } from '../..'; -import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; +import { createExtract, createInject } from '../../../common'; import type { DashboardContainer } from './dashboard_container'; -import { - createExtract, - createInject, -} from '../../../common/embeddable/dashboard_container_persistable_state'; -import { pluginServices } from '../../services/plugin_services'; +import { DASHBOARD_CONTAINER_TYPE } from '../../dashboard_constants'; export type DashboardContainerFactory = EmbeddableFactory< DashboardContainerInput, @@ -80,6 +77,7 @@ export class DashboardContainerFactoryDefinition initialInput: DashboardContainerInput, parent?: Container ): Promise => { + const { pluginServices } = await import('../../services/plugin_services'); const { embeddable: { getEmbeddableFactory }, } = pluginServices.getServices(); diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx index 64afdcdb2e60..7fda6eb1a3f3 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx @@ -22,9 +22,9 @@ import { DashboardContainer, DashboardLoadedInfo } from '../dashboard_container' import { GridData } from '../../../../common'; import { DashboardGridItem } from './dashboard_grid_item'; import { DashboardLoadedEventStatus, DashboardPanelState } from '../types'; -import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants'; +import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../../../dashboard_constants'; import { pluginServices } from '../../../services/plugin_services'; -import { dashboardLoadingErrorStrings } from '../../../dashboard_strings'; +import { dashboardSavedObjectErrorStrings } from '../../../dashboard_strings'; let lastValidGridSize = 0; @@ -153,7 +153,7 @@ class DashboardGridUi extends React.Component { } catch (error) { console.error(error); // eslint-disable-line no-console isLayoutInvalid = true; - toasts.addDanger(dashboardLoadingErrorStrings.getDashboardGridError(error.message)); + toasts.addDanger(dashboardSavedObjectErrorStrings.getDashboardGridError(error.message)); } this.setState({ layout, diff --git a/src/plugins/dashboard/public/application/embeddable/index.ts b/src/plugins/dashboard/public/application/embeddable/index.ts index ce8bb5b7169a..1979ae5ad7bf 100644 --- a/src/plugins/dashboard/public/application/embeddable/index.ts +++ b/src/plugins/dashboard/public/application/embeddable/index.ts @@ -13,11 +13,4 @@ export { createPanelState } from './panel'; export * from './types'; -export { - DASHBOARD_GRID_COLUMN_COUNT, - DEFAULT_PANEL_HEIGHT, - DEFAULT_PANEL_WIDTH, - DASHBOARD_CONTAINER_TYPE, -} from './dashboard_constants'; - export { createDashboardContainerByValueRenderer } from './dashboard_container_by_value_renderer'; diff --git a/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.test.ts b/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.test.ts index 10c3044ea912..4c926675e1e9 100644 --- a/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.test.ts +++ b/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.test.ts @@ -8,7 +8,7 @@ import { EmbeddableInput } from '@kbn/embeddable-plugin/public'; import { CONTACT_CARD_EMBEDDABLE } from '@kbn/embeddable-plugin/public/lib/test_samples'; -import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants'; +import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../dashboard_constants'; import { DashboardPanelState } from '../types'; import { createPanelState } from './create_panel_state'; diff --git a/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.ts b/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.ts index 5aa9066ea1eb..e5d4f69c914c 100644 --- a/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.ts +++ b/src/plugins/dashboard/public/application/embeddable/panel/create_panel_state.ts @@ -7,7 +7,7 @@ */ import { PanelState, EmbeddableInput } from '@kbn/embeddable-plugin/public'; -import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants'; +import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../dashboard_constants'; import { DashboardPanelState } from '../types'; import { IPanelPlacementArgs, diff --git a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts index 9d90b711a684..77b51874319b 100644 --- a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts +++ b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts @@ -8,8 +8,8 @@ import _ from 'lodash'; import { PanelNotFoundError } from '@kbn/embeddable-plugin/public'; -import { GridData } from '../../../../common'; -import { DashboardPanelState, DASHBOARD_GRID_COLUMN_COUNT } from '..'; +import { DashboardPanelState, GridData } from '../../../../common'; +import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants'; export type PanelPlacementMethod = ( args: PlacementArgs diff --git a/src/plugins/dashboard/public/application/embeddable/placeholder/index.ts b/src/plugins/dashboard/public/application/embeddable/placeholder/index.ts index af4327f0fcd9..1d1aba84e7c3 100644 --- a/src/plugins/dashboard/public/application/embeddable/placeholder/index.ts +++ b/src/plugins/dashboard/public/application/embeddable/placeholder/index.ts @@ -6,5 +6,6 @@ * Side Public License, v 1. */ -export * from './placeholder_embeddable'; -export * from './placeholder_embeddable_factory'; +export { PlaceholderEmbeddableFactory } from './placeholder_embeddable_factory'; + +export const PLACEHOLDER_EMBEDDABLE = 'placeholder'; diff --git a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable.tsx b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable.tsx index a7fa1e793ebf..de468d86c89f 100644 --- a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable.tsx +++ b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable.tsx @@ -13,9 +13,9 @@ import classNames from 'classnames'; import { EuiLoadingChart } from '@elastic/eui'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { Embeddable, type EmbeddableInput, type IContainer } from '@kbn/embeddable-plugin/public'; -import { pluginServices } from '../../../services/plugin_services'; -export const PLACEHOLDER_EMBEDDABLE = 'placeholder'; +import { PLACEHOLDER_EMBEDDABLE } from '.'; +import { pluginServices } from '../../../services/plugin_services'; export class PlaceholderEmbeddable extends Embeddable { public readonly type = PLACEHOLDER_EMBEDDABLE; diff --git a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts index 74ce8bf96edb..26cdddbf17d8 100644 --- a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts +++ b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts @@ -13,7 +13,7 @@ import { EmbeddableInput, IContainer, } from '@kbn/embeddable-plugin/public'; -import { PlaceholderEmbeddable, PLACEHOLDER_EMBEDDABLE } from './placeholder_embeddable'; +import { PLACEHOLDER_EMBEDDABLE } from '.'; export class PlaceholderEmbeddableFactory implements EmbeddableFactoryDefinition { public readonly type = PLACEHOLDER_EMBEDDABLE; @@ -29,6 +29,7 @@ export class PlaceholderEmbeddableFactory implements EmbeddableFactoryDefinition } public async create(initialInput: EmbeddableInput, parent?: IContainer) { + const { PlaceholderEmbeddable } = await import('./placeholder_embeddable'); return new PlaceholderEmbeddable(initialInput, parent); } diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index c462df50ef27..76a3ae7a053a 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -9,27 +9,20 @@ import React from 'react'; import { Provider } from 'react-redux'; import { createBrowserHistory } from 'history'; + +import type { Filter } from '@kbn/es-query'; +import { DataView } from '@kbn/data-views-plugin/public'; +import { EmbeddableFactory, ViewMode } from '@kbn/embeddable-plugin/public'; import { renderHook, act, RenderHookResult } from '@testing-library/react-hooks'; import { createKbnUrlStateStorage, defer } from '@kbn/kibana-utils-plugin/public'; -import { DataView } from '@kbn/data-views-plugin/public'; +import { DashboardAppState } from '../../types'; +import { getSampleDashboardInput } from '../test_helpers'; import { DashboardConstants } from '../../dashboard_constants'; -import { SavedObjectLoader } from '../../services/saved_object_loader'; -import { DashboardAppServices, DashboardAppState } from '../../types'; +import { pluginServices } from '../../services/plugin_services'; import { DashboardContainer } from '../embeddable/dashboard_container'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { dashboardStateStore, setDescription, setViewMode } from '../state'; import { useDashboardAppState, UseDashboardStateProps } from './use_dashboard_app_state'; -import { - getSampleDashboardInput, - getSavedDashboardMock, - makeDefaultServices, -} from '../test_helpers'; - -import type { Filter } from '@kbn/es-query'; -import { pluginServices } from '../../services/plugin_services'; -import { EmbeddableFactory, ViewMode } from '@kbn/embeddable-plugin/public'; -import { DashboardServices } from '../../services/types'; interface SetupEmbeddableFactoryReturn { finalizeEmbeddableCreation: () => void; @@ -40,7 +33,6 @@ interface SetupEmbeddableFactoryReturn { interface RenderDashboardStateHookReturn { embeddableFactoryResult: SetupEmbeddableFactoryReturn; renderHookResult: RenderHookResult, DashboardAppState>; - services: DashboardAppServices; props: UseDashboardStateProps; } @@ -55,28 +47,7 @@ const createDashboardAppStateProps = (): UseDashboardStateProps => ({ setShowNoDataPage: () => {}, }); -const createDashboardAppStateServices = () => { - const defaults = makeDefaultServices(); - - const defaultDataView = { id: 'foo', fields: [{ name: 'bar' }] } as DataView; - - (pluginServices.getServices().data.dataViews.getDefaultDataView as jest.Mock).mockResolvedValue( - defaultDataView - ); - (pluginServices.getServices().data.dataViews.getDefaultId as jest.Mock).mockResolvedValue( - defaultDataView.id - ); - (pluginServices.getServices().data.query.filterManager.getFilters as jest.Mock).mockReturnValue( - [] - ); - - return defaults; -}; - -const setupEmbeddableFactory = ( - services: DashboardAppServices, - id: string -): SetupEmbeddableFactoryReturn => { +const setupEmbeddableFactory = (id: string): SetupEmbeddableFactoryReturn => { const dashboardContainer = new DashboardContainer({ ...getSampleDashboardInput(), id }); const deferEmbeddableCreate = defer(); pluginServices.getServices().embeddable.getEmbeddableFactory = jest.fn().mockImplementation( @@ -100,15 +71,22 @@ const setupEmbeddableFactory = ( const renderDashboardAppStateHook = ({ partialProps, - partialServices, }: { partialProps?: Partial; - partialServices?: Partial; }): RenderDashboardStateHookReturn => { + const defaultDataView = { id: 'foo', fields: [{ name: 'bar' }] } as DataView; + (pluginServices.getServices().data.dataViews.getDefaultDataView as jest.Mock).mockResolvedValue( + defaultDataView + ); + (pluginServices.getServices().data.dataViews.getDefaultId as jest.Mock).mockResolvedValue( + defaultDataView.id + ); + (pluginServices.getServices().data.query.filterManager.getFilters as jest.Mock).mockReturnValue( + [] + ); + const props = { ...createDashboardAppStateProps(), ...(partialProps ?? {}) }; - const services = { ...createDashboardAppStateServices(), ...(partialServices ?? {}) }; - const embeddableFactoryResult = setupEmbeddableFactory(services, originalDashboardEmbeddableId); - const DashboardServicesProvider = pluginServices.getContextProvider(); + const embeddableFactoryResult = setupEmbeddableFactory(originalDashboardEmbeddableId); const renderHookResult = renderHook( (replaceProps: Partial) => { @@ -116,18 +94,11 @@ const renderDashboardAppStateHook = ({ }, { wrapper: ({ children }) => { - return ( - - {/* Can't get rid of KibanaContextProvider here yet because of saved dashboard tests below */} - - {children} - - - ); + return {children}; }, } ); - return { embeddableFactoryResult, renderHookResult, services, props }; + return { embeddableFactoryResult, renderHookResult, props }; }; describe('Dashboard container lifecycle', () => { @@ -146,7 +117,7 @@ describe('Dashboard container lifecycle', () => { }); test('Old dashboard container is destroyed when new dashboardId is given', async () => { - const { renderHookResult, embeddableFactoryResult, services } = renderDashboardAppStateHook({}); + const { renderHookResult, embeddableFactoryResult } = renderDashboardAppStateHook({}); const getResult = () => renderHookResult.result.current; // on initial render dashboard container is undefined @@ -158,7 +129,7 @@ describe('Dashboard container lifecycle', () => { expect(embeddableFactoryResult.dashboardDestroySpy).not.toBeCalled(); const newDashboardId = 'wow_a_new_dashboard_id'; - const embeddableFactoryNew = setupEmbeddableFactory(services, newDashboardId); + const embeddableFactoryNew = setupEmbeddableFactory(newDashboardId); renderHookResult.rerender({ savedDashboardId: newDashboardId }); embeddableFactoryNew.finalizeEmbeddableCreation(); @@ -170,7 +141,7 @@ describe('Dashboard container lifecycle', () => { }); test('Dashboard container is destroyed if dashboard id is changed before container is resolved', async () => { - const { renderHookResult, embeddableFactoryResult, services } = renderDashboardAppStateHook({}); + const { renderHookResult, embeddableFactoryResult } = renderDashboardAppStateHook({}); const getResult = () => renderHookResult.result.current; // on initial render dashboard container is undefined @@ -178,7 +149,7 @@ describe('Dashboard container lifecycle', () => { await act(() => Promise.resolve()); // wait for the original savedDashboard to be loaded... const newDashboardId = 'wow_a_new_dashboard_id'; - const embeddableFactoryNew = setupEmbeddableFactory(services, newDashboardId); + const embeddableFactoryNew = setupEmbeddableFactory(newDashboardId); renderHookResult.rerender({ savedDashboardId: newDashboardId }); await act(() => Promise.resolve()); // wait for the new savedDashboard to be loaded... @@ -199,38 +170,33 @@ describe('Dashboard container lifecycle', () => { // FLAKY: https://github.com/elastic/kibana/issues/105018 describe.skip('Dashboard initial state', () => { it('Extracts state from Dashboard Saved Object', async () => { + const savedTitle = 'testDash1'; + ( + pluginServices.getServices().dashboardSavedObject + .loadDashboardStateFromSavedObject as jest.Mock + ).mockResolvedValue({ title: savedTitle }); + const { renderHookResult, embeddableFactoryResult } = renderDashboardAppStateHook({}); const getResult = () => renderHookResult.result.current; - // saved dashboard isn't applied until after the dashboard embeddable has been created. - expect(getResult().savedDashboard).toBeUndefined(); - embeddableFactoryResult.finalizeEmbeddableCreation(); await renderHookResult.waitForNextUpdate(); - expect(getResult().savedDashboard).toBeDefined(); - expect(getResult().savedDashboard?.title).toEqual( - getResult().getLatestDashboardState?.().title - ); + expect(savedTitle).toEqual(getResult().getLatestDashboardState?.().title); }); it('Sets initial time range and filters from saved dashboard', async () => { - const savedDashboards = {} as SavedObjectLoader; - savedDashboards.get = jest.fn().mockImplementation((id?: string) => - Promise.resolve( - getSavedDashboardMock({ - getFilters: () => [{ meta: { test: 'filterMeTimbers' } } as unknown as Filter], - timeRestore: true, - timeFrom: 'now-13d', - timeTo: 'now', - id, - }) - ) - ); - const partialServices: Partial = { savedDashboards }; - const { renderHookResult, embeddableFactoryResult, services } = renderDashboardAppStateHook({ - partialServices, + ( + pluginServices.getServices().dashboardSavedObject + .loadDashboardStateFromSavedObject as jest.Mock + ).mockResolvedValue({ + filters: [{ meta: { test: 'filterMeTimbers' } } as unknown as Filter], + timeRestore: true, + timeFrom: 'now-13d', + timeTo: 'now', }); + + const { renderHookResult, embeddableFactoryResult } = renderDashboardAppStateHook({}); const getResult = () => renderHookResult.result.current; embeddableFactoryResult.finalizeEmbeddableCreation(); @@ -238,15 +204,13 @@ describe.skip('Dashboard initial state', () => { expect(getResult().getLatestDashboardState?.().timeRestore).toEqual(true); expect( - (services as DashboardAppServices & { data: DashboardServices['data'] }).data.query.timefilter - .timefilter.setTime + pluginServices.getServices().data.query.timefilter.timefilter.setTime ).toHaveBeenCalledWith({ from: 'now-13d', to: 'now', }); expect( - (services as DashboardAppServices & { data: DashboardServices['data'] }).data.query - .filterManager.setAppFilters + pluginServices.getServices().data.query.filterManager.setAppFilters ).toHaveBeenCalledWith([{ meta: { test: 'filterMeTimbers' } } as unknown as Filter]); }); diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 932bfdd016b3..850c6f575904 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -6,50 +6,45 @@ * Side Public License, v 1. */ +import { omit } from 'lodash'; import { History } from 'history'; import { debounceTime, switchMap } from 'rxjs/operators'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; import { ViewMode } from '@kbn/embeddable-plugin/public'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; -import { DashboardConstants } from '../..'; -import { getNewDashboardTitle } from '../../dashboard_strings'; -import { setDashboardState, useDashboardDispatch, useDashboardSelector } from '../state'; -import type { - DashboardBuildContext, - DashboardAppServices, - DashboardAppState, - DashboardState, -} from '../../types'; -import { DashboardAppLocatorParams } from '../../locator'; import { - loadDashboardHistoryLocationState, - tryDestroyDashboardContainer, - syncDashboardContainerInput, - savedObjectToDashboardState, + diffDashboardState, + syncDashboardUrlState, syncDashboardDataViews, - syncDashboardFilterState, - loadSavedDashboardState, buildDashboardContainer, - syncDashboardUrlState, - diffDashboardState, - areTimeRangesEqual, - areRefreshIntervalsEqual, + syncDashboardFilterState, + syncDashboardContainerInput, + tryDestroyDashboardContainer, + loadDashboardHistoryLocationState, } from '../lib'; -import { isDashboardAppInNoDataState } from '../dashboard_app_no_data'; +import { + dashboardStateLoadWasSuccessful, + LoadDashboardFromSavedObjectReturn, +} from '../../services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object'; +import { DashboardConstants } from '../..'; +import { DashboardAppLocatorParams } from '../../locator'; +import { dashboardSavedObjectErrorStrings, getNewDashboardTitle } from '../../dashboard_strings'; import { pluginServices } from '../../services/plugin_services'; import { useDashboardMountContext } from './dashboard_mount_context'; +import { isDashboardAppInNoDataState } from '../dashboard_app_no_data'; +import { setDashboardState, useDashboardDispatch, useDashboardSelector } from '../state'; +import type { DashboardBuildContext, DashboardAppState, DashboardState } from '../../types'; export interface UseDashboardStateProps { history: History; showNoDataPage: boolean; savedDashboardId?: string; isEmbeddedExternally: boolean; - setShowNoDataPage: (showNoData: boolean) => void; kbnUrlStateStorage: IKbnUrlStateStorage; + setShowNoDataPage: (showNoData: boolean) => void; } export const useDashboardAppState = ({ @@ -79,25 +74,23 @@ export const useDashboardAppState = ({ const [lastSavedState, setLastSavedState] = useState(); const $onLastSavedStateChange = useMemo(() => new Subject(), []); - const { - services: { savedDashboards }, - } = useKibana(); - /** * Unpack services and context */ const { scopedHistory } = useDashboardMountContext(); const { + embeddable, + notifications: { toasts }, chrome: { docTitle }, dashboardCapabilities, dashboardSessionStorage, + spaces: { redirectLegacyUrl }, data: { query, search, dataViews }, - embeddable, initializerContext: { kibanaVersion }, screenshotMode: { isScreenshotMode, getScreenshotContext }, - spaces: { redirectLegacyUrl }, - notifications, + dashboardSavedObject: { loadDashboardStateFromSavedObject }, } = pluginServices.getServices(); + const { getStateTransfer } = embeddable; /** @@ -120,7 +113,6 @@ export const useDashboardAppState = ({ */ const dashboardBuildContext: DashboardBuildContext = { history, - savedDashboards, kbnUrlStateStorage, isEmbeddedExternally, dispatchDashboardStateChange, @@ -149,33 +141,25 @@ export const useDashboardAppState = ({ /** * Load and unpack state from dashboard saved object. */ - const loadSavedDashboardResult = await loadSavedDashboardState({ - ...dashboardBuildContext, - savedDashboardId, - }); - if (canceled || !loadSavedDashboardResult) return; - const { savedDashboard, savedDashboardState } = loadSavedDashboardResult; - - // If the saved dashboard is an alias match, then we will redirect - if (savedDashboard.outcome === 'aliasMatch' && savedDashboard.id && savedDashboard.aliasId) { - // We want to keep the "query" params on our redirect. - // But, these aren't true query params, they are technically part of the hash - // So, to get the new path, we will just replace the current id in the hash - // with the alias id - const path = scopedHistory().location.hash.replace( - savedDashboard.id, - savedDashboard.aliasId - ); - const aliasPurpose = savedDashboard.aliasPurpose; - if (isScreenshotMode()) { - scopedHistory().replace(path); - } else { - await redirectLegacyUrl?.({ path, aliasPurpose }); - } - // Return so we don't run any more of the hook and let it rerun after the redirect that just happened + let loadSavedDashboardResult: LoadDashboardFromSavedObjectReturn; + try { + loadSavedDashboardResult = await loadDashboardStateFromSavedObject({ + getScopedHistory: scopedHistory, + id: savedDashboardId, + }); + } catch (error) { + // redirect back to landing page if dashboard could not be loaded. + toasts.addDanger(dashboardSavedObjectErrorStrings.getDashboardLoadError(error.message)); + history.push(DashboardConstants.LANDING_PAGE_PATH); + return; + } + if (canceled || !dashboardStateLoadWasSuccessful(loadSavedDashboardResult)) { return; } + const { dashboardState: savedDashboardState, createConflictWarning } = + loadSavedDashboardResult; + /** * Combine initial state from the saved object, session storage, and URL, then dispatch it to Redux. */ @@ -187,12 +171,11 @@ export const useDashboardAppState = ({ const { initialDashboardStateFromUrl, stopWatchingAppStateInUrl } = syncDashboardUrlState({ ...dashboardBuildContext, - savedDashboard, }); const printLayoutDetected = isScreenshotMode() && getScreenshotContext('layout') === 'print'; - const initialDashboardState = { + const initialDashboardState: DashboardState = { ...savedDashboardState, ...dashboardSessionStorageState, ...initialDashboardStateFromUrl, @@ -208,10 +191,9 @@ export const useDashboardAppState = ({ /** * Start syncing dashboard state with the Query, Filters and Timepicker from the Query Service. */ - const { applyFilters, stopSyncingDashboardFilterState } = syncDashboardFilterState({ + const { stopSyncingDashboardFilterState } = syncDashboardFilterState({ ...dashboardBuildContext, initialDashboardState, - savedDashboard, }); /** @@ -222,10 +204,9 @@ export const useDashboardAppState = ({ ...dashboardBuildContext, initialDashboardState, incomingEmbeddable, - savedDashboard, executionContext: { type: 'dashboard', - description: savedDashboard.title, + description: initialDashboardState.title, }, }); @@ -256,15 +237,13 @@ export const useDashboardAppState = ({ const stopSyncingContainerInput = syncDashboardContainerInput({ ...dashboardBuildContext, dashboardContainer, - savedDashboard, - applyFilters, }); /** * Any time the redux state, or the last saved state changes, compare them, set the unsaved * changes state, and and push the unsaved changes to session storage. */ - const { timefilter } = query.timefilter; + const lastSavedSubscription = combineLatest([ $onLastSavedStateChange, dashboardAppState.$onDashboardStateChange, @@ -281,31 +260,24 @@ export const useDashboardAppState = ({ newState: current, }).then((unsavedChanges) => { if (observer.closed) return; - const savedTimeChanged = - lastSaved.timeRestore && - (!areTimeRangesEqual( - { - from: savedDashboard?.timeFrom, - to: savedDashboard?.timeTo, - }, - timefilter.getTime() - ) || - !areRefreshIntervalsEqual( - savedDashboard?.refreshInterval, - timefilter.getRefreshInterval() - )); - /** * changes to the dashboard should only be considered 'unsaved changes' when * editing the dashboard */ const hasUnsavedChanges = - current.viewMode === ViewMode.EDIT && - (Object.keys(unsavedChanges).length > 0 || savedTimeChanged); + current.viewMode === ViewMode.EDIT && Object.keys(unsavedChanges).length > 0; setDashboardAppState((s) => ({ ...s, hasUnsavedChanges })); unsavedChanges.viewMode = current.viewMode; // always push view mode into session store. - dashboardSessionStorage.setState(savedDashboardId, unsavedChanges); + + /** + * Current behaviour expects time range not to be backed up. + * TODO: Revisit this. It seems like we should treat all state the same. + */ + dashboardSessionStorage.setState( + savedDashboardId, + omit(unsavedChanges, ['timeRange', 'refreshInterval']) + ); }); }); }) @@ -319,11 +291,7 @@ export const useDashboardAppState = ({ setLastSavedState(savedDashboardState); dashboardBuildContext.$checkForUnsavedChanges.next(undefined); const updateLastSavedState = () => { - setLastSavedState( - savedObjectToDashboardState({ - savedDashboard, - }) - ); + setLastSavedState(dashboardBuildContext.getLatestDashboardState()); }; /** @@ -332,10 +300,9 @@ export const useDashboardAppState = ({ docTitle.change(savedDashboardState.title || getNewDashboardTitle()); setDashboardAppState((s) => ({ ...s, - applyFilters, - savedDashboard, dashboardContainer, updateLastSavedState, + createConflictWarning, getLatestDashboardState: dashboardBuildContext.getLatestDashboardState, })); @@ -359,47 +326,43 @@ export const useDashboardAppState = ({ }, [ dashboardAppState.$triggerDashboardRefresh, dashboardAppState.$onDashboardStateChange, + loadDashboardStateFromSavedObject, dispatchDashboardStateChange, $onLastSavedStateChange, dashboardSessionStorage, dashboardCapabilities, isEmbeddedExternally, + getScreenshotContext, kbnUrlStateStorage, + setShowNoDataPage, + redirectLegacyUrl, savedDashboardId, + isScreenshotMode, getStateTransfer, - savedDashboards, + showNoDataPage, scopedHistory, - notifications, - dataViews, kibanaVersion, + dataViews, embeddable, docTitle, history, + toasts, search, query, - showNoDataPage, - setShowNoDataPage, - redirectLegacyUrl, - getScreenshotContext, - isScreenshotMode, ]); /** * rebuild reset to last saved state callback whenever last saved state changes */ const resetToLastSavedState = useCallback(() => { - if ( - !lastSavedState || - !dashboardAppState.savedDashboard || - !dashboardAppState.getLatestDashboardState - ) { + if (!lastSavedState || !dashboardAppState.getLatestDashboardState) { return; } if (dashboardAppState.getLatestDashboardState().timeRestore) { const { timefilter } = query.timefilter; - const { timeFrom: from, timeTo: to, refreshInterval } = dashboardAppState.savedDashboard; - if (from && to) timefilter.setTime({ from, to }); + const { timeRange, refreshInterval } = lastSavedState; + if (timeRange) timefilter.setTime(timeRange); if (refreshInterval) timefilter.setRefreshInterval(refreshInterval); } dispatchDashboardStateChange( diff --git a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts index 8ad2b7ddc52e..2d5304e002d5 100644 --- a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts @@ -16,8 +16,7 @@ import { isErrorEmbeddable, } from '@kbn/embeddable-plugin/public'; -import { DashboardSavedObject } from '../../saved_dashboards'; -import { DashboardContainer, DASHBOARD_CONTAINER_TYPE } from '../embeddable'; +import { DashboardContainer } from '../embeddable'; import { DashboardBuildContext, DashboardState, DashboardContainerInput } from '../../types'; import { enableDashboardSearchSessions, @@ -25,9 +24,9 @@ import { stateToDashboardContainerInput, } from '.'; import { pluginServices } from '../../services/plugin_services'; +import { DASHBOARD_CONTAINER_TYPE } from '../../dashboard_constants'; type BuildDashboardContainerProps = DashboardBuildContext & { - savedDashboard: DashboardSavedObject; initialDashboardState: DashboardState; incomingEmbeddable?: EmbeddablePackageState; executionContext?: KibanaExecutionContext; @@ -41,7 +40,6 @@ export const buildDashboardContainer = async ({ initialDashboardState, isEmbeddedExternally, incomingEmbeddable, - savedDashboard, history, executionContext, }: BuildDashboardContainerProps) => { @@ -55,7 +53,6 @@ export const buildDashboardContainer = async ({ // set up search session enableDashboardSearchSessions({ - savedDashboard, initialDashboardState, getLatestDashboardState, canStoreSearchSession, @@ -95,7 +92,6 @@ export const buildDashboardContainer = async ({ dashboardState: initialDashboardState, incomingEmbeddable, searchSessionId, - savedDashboard, executionContext, }); diff --git a/src/plugins/dashboard/public/application/lib/convert_dashboard_panels.ts b/src/plugins/dashboard/public/application/lib/convert_dashboard_panels.ts deleted file mode 100644 index 8e74245137f8..000000000000 --- a/src/plugins/dashboard/public/application/lib/convert_dashboard_panels.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - convertSavedDashboardPanelToPanelState, - convertPanelStateToSavedDashboardPanel, -} from '../../../common/embeddable/embeddable_saved_object_converters'; -import { pluginServices } from '../../services/plugin_services'; -import type { SavedDashboardPanel, DashboardPanelMap } from '../../types'; - -export const convertSavedPanelsToPanelMap = (panels?: SavedDashboardPanel[]): DashboardPanelMap => { - const panelsMap: DashboardPanelMap = {}; - panels?.forEach((panel, idx) => { - panelsMap![panel.panelIndex ?? String(idx)] = convertSavedDashboardPanelToPanelState(panel); - }); - return panelsMap; -}; - -export const convertPanelMapToSavedPanels = (panels: DashboardPanelMap) => { - const { - initializerContext: { kibanaVersion }, - } = pluginServices.getServices(); - - return Object.values(panels).map((panel) => - convertPanelStateToSavedDashboardPanel(panel, kibanaVersion) - ); -}; diff --git a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts index c9e954a081ca..14e0f4ac4c17 100644 --- a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts @@ -6,37 +6,21 @@ * Side Public License, v 1. */ -import _ from 'lodash'; +import { cloneDeep, omit } from 'lodash'; import type { KibanaExecutionContext } from '@kbn/core/public'; -import type { ControlGroupInput } from '@kbn/controls-plugin/public'; -import { type EmbeddablePackageState, ViewMode } from '@kbn/embeddable-plugin/public'; -import { - compareFilters, - COMPARE_ALL_OPTIONS, - Filter, - isFilterPinned, - TimeRange, -} from '@kbn/es-query'; import { mapAndFlattenFilters } from '@kbn/data-plugin/public'; +import { type EmbeddablePackageState } from '@kbn/embeddable-plugin/public'; +import { Filter, isFilterPinned, compareFilters, COMPARE_ALL_OPTIONS } from '@kbn/es-query'; -import type { DashboardSavedObject } from '../../saved_dashboards'; -import { getTagsFromSavedDashboard, migrateAppState } from '.'; -import { convertPanelStateToSavedDashboardPanel } from '../../../common/embeddable/embeddable_saved_object_converters'; -import type { DashboardState, RawDashboardState, DashboardContainerInput } from '../../types'; -import { convertSavedPanelsToPanelMap } from './convert_dashboard_panels'; -import { deserializeControlGroupFromDashboardSavedObject } from './dashboard_control_group'; import { pluginServices } from '../../services/plugin_services'; - -interface SavedObjectToDashboardStateProps { - savedDashboard: DashboardSavedObject; -} +import { convertPanelStateToSavedDashboardPanel } from '../../../common'; +import type { DashboardState, RawDashboardState, DashboardContainerInput } from '../../types'; interface StateToDashboardContainerInputProps { searchSessionId?: string; isEmbeddedExternally?: boolean; dashboardState: DashboardState; - savedDashboard: DashboardSavedObject; incomingEmbeddable?: EmbeddablePackageState; executionContext?: KibanaExecutionContext; } @@ -44,40 +28,6 @@ interface StateToDashboardContainerInputProps { interface StateToRawDashboardStateProps { state: DashboardState; } -/** - * Converts a dashboard saved object to a dashboard state by extracting raw state from the given Dashboard - * Saved Object migrating the panel states to the latest version, then converting each panel from a saved - * dashboard panel to a panel state. - */ -export const savedObjectToDashboardState = ({ - savedDashboard, -}: SavedObjectToDashboardStateProps): DashboardState => { - const { - dashboardCapabilities: { showWriteControls }, - } = pluginServices.getServices(); - - const rawState = migrateAppState({ - fullScreenMode: false, - title: savedDashboard.title, - query: savedDashboard.getQuery(), - filters: savedDashboard.getFilters(), - timeRestore: savedDashboard.timeRestore, - description: savedDashboard.description || '', - tags: getTagsFromSavedDashboard(savedDashboard), - panels: savedDashboard.panelsJSON ? JSON.parse(savedDashboard.panelsJSON) : [], - viewMode: savedDashboard.id || showWriteControls ? ViewMode.EDIT : ViewMode.VIEW, - options: savedDashboard.optionsJSON ? JSON.parse(savedDashboard.optionsJSON) : {}, - }); - - if (rawState.timeRestore) { - rawState.timeRange = { from: savedDashboard.timeFrom, to: savedDashboard.timeTo } as TimeRange; - } - - rawState.controlGroupInput = deserializeControlGroupFromDashboardSavedObject( - savedDashboard - ) as ControlGroupInput; - return { ...rawState, panels: convertSavedPanelsToPanelMap(rawState.panels) }; -}; /** * Converts a dashboard state object to dashboard container input @@ -85,7 +35,6 @@ export const savedObjectToDashboardState = ({ export const stateToDashboardContainerInput = ({ isEmbeddedExternally, searchSessionId, - savedDashboard, dashboardState, executionContext, }: StateToDashboardContainerInputProps): DashboardContainerInput => { @@ -111,7 +60,7 @@ export const stateToDashboardContainerInput = ({ filters: dashboardFilters, } = dashboardState; - const migratedDashboardFilters = mapAndFlattenFilters(_.cloneDeep(dashboardFilters)); + const migratedDashboardFilters = mapAndFlattenFilters(cloneDeep(dashboardFilters)); return { refreshConfig: timefilter.getRefreshInterval(), filters: filterManager @@ -124,7 +73,7 @@ export const stateToDashboardContainerInput = ({ ) ), isFullScreenMode: fullScreenMode, - id: savedDashboard.id || '', + id: dashboardState.savedObjectId ?? '', isEmbeddedExternally, ...(options || {}), controlGroupInput, @@ -136,7 +85,7 @@ export const stateToDashboardContainerInput = ({ query, title, timeRange: { - ..._.cloneDeep(timefilter.getTime()), + ...cloneDeep(timefilter.getTime()), }, timeslice, timeRestore, @@ -161,5 +110,5 @@ export const stateToRawDashboardState = ({ const savedDashboardPanels = Object.values(state.panels).map((panel) => convertPanelStateToSavedDashboardPanel(panel, kibanaVersion) ); - return { ..._.omit(state, 'panels'), panels: savedDashboardPanels }; + return { ...omit(state, 'panels'), panels: savedDashboardPanels }; }; diff --git a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts index a9f474ed85dd..4f44d0cf250d 100644 --- a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts +++ b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts @@ -14,16 +14,15 @@ import { debounceTime, distinctUntilChanged, distinctUntilKeyChanged } from 'rxj import { ControlGroupInput, - controlGroupInputToRawControlGroupAttributes, getDefaultControlGroupInput, persistableControlGroupInputIsEqual, - rawControlGroupAttributesToControlGroupInput, + controlGroupInputToRawControlGroupAttributes, } from '@kbn/controls-plugin/common'; import { ControlGroupContainer } from '@kbn/controls-plugin/public'; import { DashboardContainer } from '..'; import { DashboardState } from '../../types'; -import { DashboardContainerInput, DashboardSavedObject } from '../..'; +import { DashboardContainerInput } from '../..'; interface DiffChecks { [key: string]: (a?: unknown, b?: unknown) => boolean; @@ -169,32 +168,17 @@ export const syncDashboardControlGroup = async ({ }; }; -export const serializeControlGroupToDashboardSavedObject = ( - dashboardSavedObject: DashboardSavedObject, - dashboardState: DashboardState +export const serializeControlGroupInput = ( + controlGroupInput: DashboardState['controlGroupInput'] ) => { // only save to saved object if control group is not default if ( - persistableControlGroupInputIsEqual( - dashboardState.controlGroupInput, - getDefaultControlGroupInput() - ) + !controlGroupInput || + persistableControlGroupInputIsEqual(controlGroupInput, getDefaultControlGroupInput()) ) { - dashboardSavedObject.controlGroupInput = undefined; - return; + return undefined; } - if (dashboardState.controlGroupInput) { - dashboardSavedObject.controlGroupInput = controlGroupInputToRawControlGroupAttributes( - dashboardState.controlGroupInput - ); - } -}; - -export const deserializeControlGroupFromDashboardSavedObject = ( - dashboardSavedObject: DashboardSavedObject -): Omit | undefined => { - if (!dashboardSavedObject.controlGroupInput) return; - return rawControlGroupAttributesToControlGroupInput(dashboardSavedObject.controlGroupInput); + return controlGroupInputToRawControlGroupAttributes(controlGroupInput); }; export const combineDashboardFiltersWithControlGroupFilters = ( diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.test.ts b/src/plugins/dashboard/public/application/lib/dashboard_session_restoration.test.ts similarity index 89% rename from src/plugins/dashboard/public/application/lib/session_restoration.test.ts rename to src/plugins/dashboard/public/application/lib/dashboard_session_restoration.test.ts index aeb83dd8a6e4..56ee2ac55f44 100644 --- a/src/plugins/dashboard/public/application/lib/session_restoration.test.ts +++ b/src/plugins/dashboard/public/application/lib/dashboard_session_restoration.test.ts @@ -6,16 +6,13 @@ * Side Public License, v 1. */ -import { getSavedDashboardMock } from '../test_helpers'; -import { createSessionRestorationDataProvider, savedObjectToDashboardState } from '.'; +import { DashboardState } from '../../types'; +import { createSessionRestorationDataProvider } from '.'; import { pluginServices } from '../../services/plugin_services'; describe('createSessionRestorationDataProvider', () => { const searchSessionInfoProvider = createSessionRestorationDataProvider({ - getAppState: () => - savedObjectToDashboardState({ - savedDashboard: getSavedDashboardMock(), - }), + getAppState: () => ({ panels: {} } as unknown as DashboardState), getDashboardTitle: () => 'Dashboard', getDashboardId: () => 'Id', }); diff --git a/src/plugins/dashboard/public/application/lib/dashboard_session_restoration.ts b/src/plugins/dashboard/public/application/lib/dashboard_session_restoration.ts index b2cedeee4ee0..113c39d0717b 100644 --- a/src/plugins/dashboard/public/application/lib/dashboard_session_restoration.ts +++ b/src/plugins/dashboard/public/application/lib/dashboard_session_restoration.ts @@ -17,12 +17,11 @@ import { import { getQueryParams } from '@kbn/kibana-utils-plugin/public'; import type { DashboardState } from '../../types'; -import type { DashboardSavedObject } from '../../saved_dashboards'; -import { DashboardAppLocatorParams, DashboardConstants } from '../..'; -import { getDashboardTitle } from '../../dashboard_strings'; -import { stateToRawDashboardState } from './convert_dashboard_state'; import { DASHBOARD_APP_LOCATOR } from '../../locator'; +import { getDashboardTitle } from '../../dashboard_strings'; import { pluginServices } from '../../services/plugin_services'; +import { DashboardAppLocatorParams, DashboardConstants } from '../..'; +import { stateToRawDashboardState } from './convert_dashboard_state'; export const getSearchSessionIdFromURL = (history: History): string | undefined => getQueryParams(history.location)[DashboardConstants.SEARCH_SESSION_ID] as string | undefined; @@ -52,10 +51,8 @@ export function enableDashboardSearchSessions({ canStoreSearchSession, initialDashboardState, getLatestDashboardState, - savedDashboard, }: { canStoreSearchSession: boolean; - savedDashboard: DashboardSavedObject; initialDashboardState: DashboardState; getLatestDashboardState: () => DashboardState; }) { @@ -63,13 +60,13 @@ export function enableDashboardSearchSessions({ const dashboardTitle = getDashboardTitle( initialDashboardState.title, initialDashboardState.viewMode, - !savedDashboard.id + !getLatestDashboardState().savedObjectId ); data.search.session.enableStorage( createSessionRestorationDataProvider({ getDashboardTitle: () => dashboardTitle, - getDashboardId: () => savedDashboard?.id || '', + getDashboardId: () => getLatestDashboardState().savedObjectId ?? '', getAppState: getLatestDashboardState, }), { @@ -106,7 +103,7 @@ function getLocatorParams({ return { timeRange: shouldRestoreSearchSession ? timefilter.getAbsoluteTime() : timefilter.getTime(), searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined, - panels: getDashboardId() ? undefined : appState.panels, + panels: getDashboardId() ? undefined : (appState.panels as DashboardAppLocatorParams['panels']), query: queryString.formatQuery(appState.query) as Query, filters: filterManager.getFilters(), savedQuery: appState.savedQuery, diff --git a/src/plugins/dashboard/public/application/lib/dashboard_tagging.ts b/src/plugins/dashboard/public/application/lib/dashboard_tagging.ts deleted file mode 100644 index 0a8ec17aeb2f..000000000000 --- a/src/plugins/dashboard/public/application/lib/dashboard_tagging.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { TagDecoratedSavedObject } from '@kbn/saved-objects-tagging-oss-plugin/public'; -import type { SavedObject } from '@kbn/saved-objects-plugin/public'; - -import { DashboardSavedObject } from '../..'; -import { pluginServices } from '../../services/plugin_services'; - -// TS is picky with type guards, we can't just inline `() => false` -function defaultTaggingGuard(_obj: SavedObject): _obj is TagDecoratedSavedObject { - return false; -} - -export const getTagsFromSavedDashboard = (savedDashboard: DashboardSavedObject) => { - const hasTaggingCapabilities = getHasTaggingCapabilitiesGuard(); - return hasTaggingCapabilities(savedDashboard) ? savedDashboard.getTags() : []; -}; - -export const getHasTaggingCapabilitiesGuard = () => { - const { - savedObjectsTagging: { hasTagDecoration }, - } = pluginServices.getServices(); - - return hasTagDecoration || defaultTaggingGuard; -}; diff --git a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts index ca913199a3ba..1c57d1bd2afa 100644 --- a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts @@ -6,14 +6,25 @@ * Side Public License, v 1. */ -import { xor, omit, isEmpty } from 'lodash'; import fastIsEqual from 'fast-deep-equal'; -import { compareFilters, COMPARE_ALL_OPTIONS, type Filter, isFilterPinned } from '@kbn/es-query'; +import { xor, omit, isEmpty, pick } from 'lodash'; + +import { + compareFilters, + COMPARE_ALL_OPTIONS, + type Filter, + isFilterPinned, + TimeRange, +} from '@kbn/es-query'; +import { RefreshInterval } from '@kbn/data-plugin/common'; import { IEmbeddable } from '@kbn/embeddable-plugin/public'; - import { persistableControlGroupInputIsEqual } from '@kbn/controls-plugin/common'; + import { DashboardContainerInput } from '../..'; -import { DashboardOptions, DashboardPanelMap, DashboardState } from '../../types'; +import { areTimesEqual } from './filter_utils'; +import { DashboardPanelMap } from '../embeddable'; +import { DashboardOptions, DashboardState } from '../../types'; +import { pluginServices } from '../../services/plugin_services'; const stateKeystoIgnore = [ 'expandedPanelId', @@ -96,15 +107,67 @@ export const diffDashboardState = async ({ newState.controlGroupInput ); + const timeStatediff = getTimeSettingsAreEqual({ + currentTimeRestore: newState.timeRestore, + lastSaved: { ...pick(originalState, ['timeRange', 'timeRestore', 'refreshInterval']) }, + }) + ? {} + : pick(newState, ['timeRange', 'timeRestore', 'refreshInterval']); + return { ...commonStateDiff, ...(panelsAreEqual ? {} : { panels: newState.panels }), ...(filtersAreEqual ? {} : { filters: newState.filters }), ...(optionsAreEqual ? {} : { options: newState.options }), ...(controlGroupIsEqual ? {} : { controlGroupInput: newState.controlGroupInput }), + ...timeStatediff, }; }; +interface TimeStateDiffArg { + timeRange?: TimeRange; + timeRestore?: boolean; + refreshInterval?: RefreshInterval; +} + +export const getTimeSettingsAreEqual = ({ + lastSaved, + currentTimeRestore, +}: { + lastSaved?: TimeStateDiffArg; + currentTimeRestore?: boolean; +}) => { + const { + data: { + query: { + timefilter: { timefilter }, + }, + }, + } = pluginServices.getServices(); + + if (currentTimeRestore !== lastSaved?.timeRestore) return false; + if (!currentTimeRestore) return true; + + const currentRange = timefilter.getTime(); + const lastRange = lastSaved?.timeRange ?? timefilter.getTimeDefaults(); + if ( + !areTimesEqual(currentRange.from, lastRange.from) || + !areTimesEqual(currentRange.to, lastRange.to) + ) { + return false; + } + + const currentInterval = timefilter.getRefreshInterval(); + const lastInterval = lastSaved?.refreshInterval ?? timefilter.getRefreshIntervalDefaults(); + if ( + currentInterval.pause !== lastInterval.pause || + currentInterval.value !== lastInterval.value + ) { + return false; + } + return true; +}; + const getFiltersAreEqual = ( filtersA: Filter[], filtersB: Filter[], diff --git a/src/plugins/dashboard/public/application/lib/filter_utils.ts b/src/plugins/dashboard/public/application/lib/filter_utils.ts index 9b9a1270fd3b..fb2762c7dc58 100644 --- a/src/plugins/dashboard/public/application/lib/filter_utils.ts +++ b/src/plugins/dashboard/public/application/lib/filter_utils.ts @@ -8,12 +8,7 @@ import _ from 'lodash'; import moment, { Moment } from 'moment'; -import type { Optional } from '@kbn/utility-types'; -import type { RefreshInterval } from '@kbn/data-plugin/public'; -import type { Filter, TimeRange } from '@kbn/es-query'; - -type TimeRangeCompare = Optional; -type RefreshIntervalCompare = Optional; +import type { Filter } from '@kbn/es-query'; /** * Converts the time to a utc formatted string. If the time is not valid (e.g. it might be in a relative format like @@ -32,22 +27,6 @@ export const convertTimeToUTCString = (time?: string | Moment): undefined | stri } }; -export const areTimeRangesEqual = (rangeA: TimeRangeCompare, rangeB: TimeRangeCompare): boolean => - areTimesEqual(rangeA.from, rangeB.from) && areTimesEqual(rangeA.to, rangeB.to); - -export const areRefreshIntervalsEqual = ( - refreshA?: RefreshIntervalCompare, - refreshB?: RefreshIntervalCompare -): boolean => refreshA?.pause === refreshB?.pause && refreshA?.value === refreshB?.value; - -/** - * Compares the two times, making sure they are in both compared in string format. Absolute times - * are sometimes stored as moment objects, but converted to strings when reloaded. Relative times are - * strings that are not convertible to moment objects. - * @param timeA {string|Moment} - * @param timeB {string|Moment} - * @returns {boolean} - */ export const areTimesEqual = (timeA?: string | Moment, timeB?: string | Moment) => { return convertTimeToUTCString(timeA) === convertTimeToUTCString(timeB); }; diff --git a/src/plugins/dashboard/public/application/lib/index.ts b/src/plugins/dashboard/public/application/lib/index.ts index 1b4ab12d2bc1..0f364a31061d 100644 --- a/src/plugins/dashboard/public/application/lib/index.ts +++ b/src/plugins/dashboard/public/application/lib/index.ts @@ -6,28 +6,24 @@ * Side Public License, v 1. */ -export * from './filter_utils'; -export { getDashboardIdFromUrl } from './url'; -export { saveDashboard } from './save_dashboard'; -export { migrateAppState } from './migrate_app_state'; -export { addHelpMenuToAppChrome } from './help_menu_util'; -export { diffDashboardState } from './diff_dashboard_state'; -export { getTagsFromSavedDashboard } from './dashboard_tagging'; -export { syncDashboardUrlState } from './sync_dashboard_url_state'; -export { loadSavedDashboardState } from './load_saved_dashboard_state'; -export { attemptLoadDashboardByTitle } from './load_dashboard_by_title'; -export { syncDashboardFilterState } from './sync_dashboard_filter_state'; -export { syncDashboardDataViews } from './sync_dashboard_data_views'; -export { syncDashboardContainerInput } from './sync_dashboard_container_input'; -export { loadDashboardHistoryLocationState } from './load_dashboard_history_location_state'; -export { buildDashboardContainer, tryDestroyDashboardContainer } from './build_dashboard_container'; export { - stateToDashboardContainerInput, - savedObjectToDashboardState, -} from './convert_dashboard_state'; + areTimesEqual, + convertTimeToUTCString, + cleanFiltersForSerialize, + cleanFiltersForComparison, +} from './filter_utils'; export { createSessionRestorationDataProvider, enableDashboardSearchSessions, getSearchSessionIdFromURL, getSessionURLObservable, } from './dashboard_session_restoration'; +export { addHelpMenuToAppChrome } from './help_menu_util'; +export { diffDashboardState } from './diff_dashboard_state'; +export { syncDashboardUrlState } from './sync_dashboard_url_state'; +export { syncDashboardDataViews } from './sync_dashboard_data_views'; +export { syncDashboardFilterState } from './sync_dashboard_filter_state'; +export { stateToDashboardContainerInput } from './convert_dashboard_state'; +export { syncDashboardContainerInput } from './sync_dashboard_container_input'; +export { loadDashboardHistoryLocationState } from './load_dashboard_history_location_state'; +export { buildDashboardContainer, tryDestroyDashboardContainer } from './build_dashboard_container'; diff --git a/src/plugins/dashboard/public/application/lib/load_dashboard_by_title.ts b/src/plugins/dashboard/public/application/lib/load_dashboard_by_title.ts deleted file mode 100644 index bff9f8600c0e..000000000000 --- a/src/plugins/dashboard/public/application/lib/load_dashboard_by_title.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { DashboardSavedObject } from '../..'; -import { pluginServices } from '../../services/plugin_services'; - -export async function attemptLoadDashboardByTitle( - title: string -): Promise<{ id: string } | undefined> { - const { - savedObjects: { client }, - } = pluginServices.getServices(); - - const results = await client.find({ - search: `"${title}"`, - searchFields: ['title'], - type: 'dashboard', - }); - // The search isn't an exact match, lets see if we can find a single exact match to use - const matchingDashboards = results.savedObjects.filter( - (dashboard) => dashboard.attributes.title.toLowerCase() === title.toLowerCase() - ); - if (matchingDashboards.length === 1) { - return { id: matchingDashboards[0].id }; - } -} diff --git a/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts b/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts index ce06ef443d69..9a7d1791c6c9 100644 --- a/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts +++ b/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { ForwardedDashboardState } from '../../locator'; import { DashboardState } from '../../types'; -import { convertSavedPanelsToPanelMap } from './convert_dashboard_panels'; +import { ForwardedDashboardState } from '../../locator'; +import { convertSavedPanelsToPanelMap } from '../../../common'; export const loadDashboardHistoryLocationState = ( state?: ForwardedDashboardState diff --git a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts deleted file mode 100644 index 6a7eba0884ab..000000000000 --- a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ViewMode } from '@kbn/embeddable-plugin/public'; -import { getDashboard60Warning, dashboardLoadingErrorStrings } from '../../dashboard_strings'; -import { savedObjectToDashboardState } from './convert_dashboard_state'; -import { DashboardState, DashboardBuildContext } from '../../types'; -import { DashboardConstants, DashboardSavedObject } from '../..'; -import { migrateLegacyQuery } from './migrate_legacy_query'; -import { cleanFiltersForSerialize } from './filter_utils'; -import { pluginServices } from '../../services/plugin_services'; - -interface LoadSavedDashboardStateReturn { - savedDashboardState: DashboardState; - savedDashboard: DashboardSavedObject; -} - -/** - * Loads, migrates, and returns state from a dashboard saved object. - */ -export const loadSavedDashboardState = async ({ - history, - savedDashboards, - savedDashboardId, -}: DashboardBuildContext & { savedDashboardId?: string }): Promise< - LoadSavedDashboardStateReturn | undefined -> => { - const { - dashboardCapabilities: { showWriteControls }, - data: { - query: { queryString }, - }, - notifications: { toasts }, - } = pluginServices.getServices(); - - // BWC - remove for 8.0 - if (savedDashboardId === 'create') { - history.replace({ - ...history.location, // preserve query, - pathname: DashboardConstants.CREATE_NEW_DASHBOARD_URL, - }); - - toasts.addWarning(getDashboard60Warning()); - return; - } - try { - const savedDashboard = (await savedDashboards.get({ - id: savedDashboardId, - useResolve: true, - })) as DashboardSavedObject; - const savedDashboardState = savedObjectToDashboardState({ - savedDashboard, - }); - - const isViewMode = !showWriteControls || Boolean(savedDashboard.id); - savedDashboardState.viewMode = isViewMode ? ViewMode.VIEW : ViewMode.EDIT; - savedDashboardState.filters = cleanFiltersForSerialize(savedDashboardState.filters); - savedDashboardState.query = migrateLegacyQuery( - savedDashboardState.query || queryString.getDefaultQuery() - ); - - return { savedDashboardState, savedDashboard }; - } catch (error) { - // E.g. a corrupt or deleted dashboard - toasts.addDanger(dashboardLoadingErrorStrings.getDashboardLoadError(error.message)); - history.push(DashboardConstants.LANDING_PAGE_PATH); - return; - } -}; diff --git a/src/plugins/dashboard/public/application/lib/migrate_app_state.test.ts b/src/plugins/dashboard/public/application/lib/migrate_app_state.test.ts deleted file mode 100644 index 578439070e97..000000000000 --- a/src/plugins/dashboard/public/application/lib/migrate_app_state.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pluginServices } from '../../services/plugin_services'; -import { SavedDashboardPanel } from '../../types'; -import { migrateAppState } from './migrate_app_state'; - -pluginServices.getServices().initializerContext.kibanaVersion = '8.0'; - -test('migrate app state from 6.0', async () => { - const appState = { - uiState: { - 'P-1': { vis: { defaultColors: { '0+-+100': 'rgb(0,104,55)' } } }, - }, - panels: [ - { - col: 1, - id: 'Visualization-MetricChart', - panelIndex: 1, - row: 1, - size_x: 6, - size_y: 3, - type: 'visualization', - }, - ], - }; - migrateAppState(appState as any); - expect(appState.uiState).toBeUndefined(); - - const newPanel = appState.panels[0] as unknown as SavedDashboardPanel; - - expect(newPanel.gridData.w).toBe(24); - expect(newPanel.gridData.h).toBe(15); - expect(newPanel.gridData.x).toBe(0); - expect(newPanel.gridData.y).toBe(0); - - expect((newPanel.embeddableConfig as any).vis.defaultColors['0+-+100']).toBe('rgb(0,104,55)'); -}); - -test('migrate sort from 6.1', async () => { - const appState = { - uiState: { - 'P-1': { vis: { defaultColors: { '0+-+100': 'rgb(0,104,55)' } } }, - }, - panels: [ - { - col: 1, - id: 'Visualization-MetricChart', - panelIndex: 1, - row: 1, - size_x: 6, - size_y: 3, - type: 'visualization', - sort: 'sort', - }, - ], - useMargins: false, - }; - migrateAppState(appState as any); - expect(appState.uiState).toBeUndefined(); - - const newPanel = appState.panels[0] as unknown as SavedDashboardPanel; - expect(newPanel.gridData.w).toBe(24); - expect(newPanel.gridData.h).toBe(15); - expect((newPanel as any).sort).toBeUndefined(); - - expect((newPanel.embeddableConfig as any).sort).toBe('sort'); - expect((newPanel.embeddableConfig as any).vis.defaultColors['0+-+100']).toBe('rgb(0,104,55)'); -}); - -test('migrates 6.0 even when uiState does not exist', async () => { - const appState = { - panels: [ - { - col: 1, - id: 'Visualization-MetricChart', - panelIndex: 1, - row: 1, - size_x: 6, - size_y: 3, - type: 'visualization', - sort: 'sort', - }, - ], - }; - migrateAppState(appState as any); - expect((appState as any).uiState).toBeUndefined(); - - const newPanel = appState.panels[0] as unknown as SavedDashboardPanel; - expect(newPanel.gridData.w).toBe(24); - expect(newPanel.gridData.h).toBe(15); - expect((newPanel as any).sort).toBeUndefined(); - - expect((newPanel.embeddableConfig as any).sort).toBe('sort'); -}); - -test('6.2 migration adjusts w & h without margins', async () => { - const appState = { - panels: [ - { - id: 'Visualization-MetricChart', - panelIndex: 1, - gridData: { - h: 3, - w: 7, - x: 2, - y: 5, - }, - type: 'visualization', - sort: 'sort', - version: '6.2.0', - }, - ], - useMargins: false, - }; - migrateAppState(appState as any); - expect((appState as any).uiState).toBeUndefined(); - - const newPanel = appState.panels[0] as unknown as SavedDashboardPanel; - expect(newPanel.gridData.w).toBe(28); - expect(newPanel.gridData.h).toBe(15); - expect(newPanel.gridData.x).toBe(8); - expect(newPanel.gridData.y).toBe(25); - expect((newPanel as any).sort).toBeUndefined(); - - expect((newPanel.embeddableConfig as any).sort).toBe('sort'); -}); - -test('6.2 migration adjusts w & h with margins', async () => { - const appState = { - panels: [ - { - id: 'Visualization-MetricChart', - panelIndex: 1, - gridData: { - h: 3, - w: 7, - x: 2, - y: 5, - }, - type: 'visualization', - sort: 'sort', - version: '6.2.0', - }, - ], - useMargins: true, - }; - migrateAppState(appState as any); - expect((appState as any).uiState).toBeUndefined(); - - const newPanel = appState.panels[0] as unknown as SavedDashboardPanel; - expect(newPanel.gridData.w).toBe(28); - expect(newPanel.gridData.h).toBe(12); - expect(newPanel.gridData.x).toBe(8); - expect(newPanel.gridData.y).toBe(20); - expect((newPanel as any).sort).toBeUndefined(); - - expect((newPanel.embeddableConfig as any).sort).toBe('sort'); -}); diff --git a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts b/src/plugins/dashboard/public/application/lib/migrate_app_state.ts deleted file mode 100644 index e077aab89ea8..000000000000 --- a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import semverSatisfies from 'semver/functions/satisfies'; - -import { i18n } from '@kbn/i18n'; -import { METRIC_TYPE } from '@kbn/analytics'; -import type { SerializableRecord } from '@kbn/utility-types'; - -import { RawDashboardState, SavedDashboardPanel } from '../../types'; -import type { - SavedDashboardPanelTo60, - SavedDashboardPanel730ToLatest, - SavedDashboardPanel610, - SavedDashboardPanel630, - SavedDashboardPanel640To720, - SavedDashboardPanel620, -} from '../../../common'; -import { migratePanelsTo730 } from '../../../common'; -import { pluginServices } from '../../services/plugin_services'; - -/** - * Attempts to migrate the state stored in the URL into the latest version of it. - * - * Once we hit a major version, we can remove support for older style URLs and get rid of this logic. - */ -export function migrateAppState( - appState: { [key: string]: any } & RawDashboardState -): RawDashboardState { - if (!appState.panels) { - throw new Error( - i18n.translate('dashboard.panel.invalidData', { - defaultMessage: 'Invalid data in url', - }) - ); - } - - const { - usageCollection: { reportUiCounter }, - initializerContext: { kibanaVersion }, - } = pluginServices.getServices(); - - const panelNeedsMigration = ( - appState.panels as Array< - | SavedDashboardPanelTo60 - | SavedDashboardPanel610 - | SavedDashboardPanel620 - | SavedDashboardPanel630 - | SavedDashboardPanel640To720 - | SavedDashboardPanel730ToLatest - > - ).some((panel) => { - if ((panel as { version?: string }).version === undefined) return true; - - const version = (panel as SavedDashboardPanel730ToLatest).version; - - if (reportUiCounter) { - // This will help us figure out when to remove support for older style URLs. - reportUiCounter('DashboardPanelVersionInUrl', METRIC_TYPE.LOADED, `${version}`); - } - - return semverSatisfies(version, '<7.3'); - }); - - if (panelNeedsMigration) { - appState.panels = migratePanelsTo730( - appState.panels as Array< - | SavedDashboardPanelTo60 - | SavedDashboardPanel610 - | SavedDashboardPanel620 - | SavedDashboardPanel630 - | SavedDashboardPanel640To720 - >, - kibanaVersion, - appState.useMargins as boolean, - appState.uiState as { [key: string]: SerializableRecord } - ) as SavedDashboardPanel[]; - delete appState.uiState; - } - - return appState; -} diff --git a/src/plugins/dashboard/public/application/lib/save_dashboard.ts b/src/plugins/dashboard/public/application/lib/save_dashboard.ts deleted file mode 100644 index f3347ca3f204..000000000000 --- a/src/plugins/dashboard/public/application/lib/save_dashboard.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; - -import { isFilterPinned } from '@kbn/es-query'; -import type { RefreshInterval } from '@kbn/data-plugin/public'; -import type { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public'; - -import { convertTimeToUTCString } from '.'; -import type { DashboardSavedObject } from '../../saved_dashboards'; -import { dashboardSaveToastStrings } from '../../dashboard_strings'; -import { getHasTaggingCapabilitiesGuard } from './dashboard_tagging'; -import type { DashboardRedirect, DashboardState } from '../../types'; -import { serializeControlGroupToDashboardSavedObject } from './dashboard_control_group'; -import { convertPanelStateToSavedDashboardPanel } from '../../../common/embeddable/embeddable_saved_object_converters'; -import { pluginServices } from '../../services/plugin_services'; - -export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { stayInEditMode?: boolean }; - -interface SaveDashboardProps { - redirectTo: DashboardRedirect; - currentState: DashboardState; - saveOptions: SavedDashboardSaveOpts; - savedDashboard: DashboardSavedObject; -} - -export const saveDashboard = async ({ - redirectTo, - saveOptions, - currentState, - savedDashboard, -}: SaveDashboardProps): Promise<{ id?: string; redirected?: boolean; error?: any }> => { - const { - data: { - query: { - timefilter: { timefilter }, - }, - }, - dashboardSessionStorage, - initializerContext: { kibanaVersion }, - notifications, - } = pluginServices.getServices(); - - const lastDashboardId = savedDashboard.id; - const hasTaggingCapabilities = getHasTaggingCapabilitiesGuard(); - - const { panels, title, tags, description, timeRestore, options } = currentState; - - const savedDashboardPanels = Object.values(panels).map((panel) => - convertPanelStateToSavedDashboardPanel(panel, kibanaVersion) - ); - - savedDashboard.title = title; - savedDashboard.description = description; - savedDashboard.timeRestore = timeRestore; - savedDashboard.optionsJSON = JSON.stringify(options); - savedDashboard.panelsJSON = JSON.stringify(savedDashboardPanels); - - // control group input - serializeControlGroupToDashboardSavedObject(savedDashboard, currentState); - - if (hasTaggingCapabilities(savedDashboard)) { - savedDashboard.setTags(tags); - } - - const { from, to } = timefilter.getTime(); - savedDashboard.timeFrom = savedDashboard.timeRestore ? convertTimeToUTCString(from) : undefined; - savedDashboard.timeTo = savedDashboard.timeRestore ? convertTimeToUTCString(to) : undefined; - - const timeRestoreObj: RefreshInterval = _.pick(timefilter.getRefreshInterval(), [ - 'display', - 'pause', - 'section', - 'value', - ]) as RefreshInterval; - savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined; - - // only save unpinned filters - const unpinnedFilters = savedDashboard.getFilters().filter((filter) => !isFilterPinned(filter)); - savedDashboard.searchSource.setField('filter', unpinnedFilters); - - try { - const newId = await savedDashboard.save(saveOptions); - if (newId) { - notifications.toasts.addSuccess({ - title: dashboardSaveToastStrings.getSuccessString(currentState.title), - 'data-test-subj': 'saveDashboardSuccess', - }); - - /** - * If the dashboard id has been changed, redirect to the new ID to keep the url param in sync. - */ - if (newId !== lastDashboardId) { - dashboardSessionStorage.clearState(lastDashboardId); - redirectTo({ - id: newId, - editMode: true, - useReplace: true, - destination: 'dashboard', - }); - return { redirected: true, id: newId }; - } - } - return { id: newId }; - } catch (error) { - notifications.toasts.addDanger( - dashboardSaveToastStrings.getFailureString(currentState.title, error.message), - { - 'data-test-subj': 'saveDashboardFailure', - } - ); - return { error }; - } -}; diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts index 67e098186f9e..9e97beaad276 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts @@ -10,12 +10,11 @@ import _ from 'lodash'; import { Subscription } from 'rxjs'; import { debounceTime, tap } from 'rxjs/operators'; -import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS } from '@kbn/es-query'; import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/public'; -import type { Query } from '@kbn/es-query'; import type { DashboardContainer } from '../embeddable'; -import { DashboardConstants, type DashboardSavedObject } from '../..'; +import { DashboardConstants } from '../..'; import { setControlGroupState, setExpandedPanelId, @@ -36,16 +35,13 @@ import { pluginServices } from '../../services/plugin_services'; type SyncDashboardContainerCommon = DashboardBuildContext & { dashboardContainer: DashboardContainer; - savedDashboard: DashboardSavedObject; }; type ApplyStateChangesToContainerProps = SyncDashboardContainerCommon & { force: boolean; }; -type ApplyContainerChangesToStateProps = SyncDashboardContainerCommon & { - applyFilters: (query: Query, filters: Filter[]) => void; -}; +type ApplyContainerChangesToStateProps = SyncDashboardContainerCommon; type SyncDashboardContainerProps = SyncDashboardContainerCommon & ApplyContainerChangesToStateProps; @@ -94,7 +90,6 @@ export const syncDashboardContainerInput = ( }; export const applyContainerChangesToState = ({ - applyFilters, dashboardContainer, getLatestDashboardState, dispatchDashboardStateChange, @@ -112,7 +107,6 @@ export const applyContainerChangesToState = ({ if (!compareFilters(input.filters, filterManager.getFilters(), COMPARE_ALL_OPTIONS)) { // Add filters modifies the object passed to it, hence the clone deep. filterManager.addFilters(_.cloneDeep(input.filters)); - applyFilters(latestState.query, input.filters); } if (!_.isEqual(input.panels, latestState.panels)) { @@ -144,7 +138,6 @@ export const applyContainerChangesToState = ({ export const applyStateChangesToContainer = ({ force, history, - savedDashboard, dashboardContainer, kbnUrlStateStorage, isEmbeddedExternally, @@ -161,7 +154,6 @@ export const applyStateChangesToContainer = ({ const currentDashboardStateAsInput = stateToDashboardContainerInput({ dashboardState: latestState, isEmbeddedExternally, - savedDashboard, }); const differences = diffDashboardContainerInput( dashboardContainer.getInput(), diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts index d02e674936ae..0bce899e22fc 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts @@ -8,25 +8,22 @@ import _ from 'lodash'; import { merge } from 'rxjs'; -import { debounceTime, finalize, map, switchMap, tap } from 'rxjs/operators'; +import { finalize, map, switchMap, tap } from 'rxjs/operators'; import { connectToQueryState, GlobalQueryStateFromUrl, - syncQueryStateWithUrl, + syncGlobalQueryStateWithUrl, waitUntilNextSessionCompletes$, } from '@kbn/data-plugin/public'; import type { Filter, Query } from '@kbn/es-query'; import { cleanFiltersForSerialize } from '.'; -import { setQuery } from '../state'; import type { DashboardBuildContext, DashboardState } from '../../types'; -import type { DashboardSavedObject } from '../../saved_dashboards'; import { setFiltersAndQuery } from '../state/dashboard_state_slice'; import { pluginServices } from '../../services/plugin_services'; type SyncDashboardFilterStateProps = DashboardBuildContext & { initialDashboardState: DashboardState; - savedDashboard: DashboardSavedObject; }; /** @@ -35,7 +32,6 @@ type SyncDashboardFilterStateProps = DashboardBuildContext & { * and the dashboard Redux store. */ export const syncDashboardFilterState = ({ - savedDashboard, kbnUrlStateStorage, initialDashboardState, $checkForUnsavedChanges, @@ -46,25 +42,17 @@ export const syncDashboardFilterState = ({ const { data: { query: queryService, search }, } = pluginServices.getServices(); - const { filterManager, queryString, timefilter } = queryService; + const { queryString, timefilter } = queryService; const { timefilter: timefilterService } = timefilter; // apply initial dashboard filter state. applyDashboardFilterState({ currentDashboardState: initialDashboardState, kbnUrlStateStorage, - savedDashboard, }); - // this callback will be used any time new filters and query need to be applied. - const applyFilters = (query: Query, filters: Filter[]) => { - savedDashboard.searchSource.setField('query', query); - savedDashboard.searchSource.setField('filter', filters); - dispatchDashboardStateChange(setQuery(query)); - }; - // starts syncing `_g` portion of url with query services - const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( + const { stop: stopSyncingQueryServiceStateWithUrl } = syncGlobalQueryStateWithUrl( queryService, kbnUrlStateStorage ); @@ -81,7 +69,6 @@ export const syncDashboardFilterState = ({ set: ({ filters, query }) => { intermediateFilterState.filters = cleanFiltersForSerialize(filters ?? []) || []; intermediateFilterState.query = query || queryString.getDefaultQuery(); - applyFilters(intermediateFilterState.query, intermediateFilterState.filters); dispatchDashboardStateChange(setFiltersAndQuery(intermediateFilterState)); }, state$: $onDashboardStateChange.pipe( @@ -97,11 +84,6 @@ export const syncDashboardFilterState = ({ } ); - // apply filters when the filter manager changes - const filterManagerSubscription = merge(filterManager.getUpdates$(), queryString.getUpdates$()) - .pipe(debounceTime(100)) - .subscribe(() => applyFilters(queryString.getQuery() as Query, filterManager.getFilters())); - const timeRefreshSubscription = merge( timefilterService.getRefreshIntervalUpdate$(), timefilterService.getTimeUpdate$() @@ -127,26 +109,23 @@ export const syncDashboardFilterState = ({ .subscribe(); const stopSyncingDashboardFilterState = () => { - filterManagerSubscription.unsubscribe(); forceRefreshSubscription.unsubscribe(); timeRefreshSubscription.unsubscribe(); stopSyncingQueryServiceStateWithUrl(); stopSyncingAppFilters(); }; - return { applyFilters, stopSyncingDashboardFilterState }; + return { stopSyncingDashboardFilterState }; }; interface ApplyDashboardFilterStateProps { kbnUrlStateStorage: DashboardBuildContext['kbnUrlStateStorage']; currentDashboardState: DashboardState; - savedDashboard: DashboardSavedObject; } export const applyDashboardFilterState = ({ currentDashboardState, kbnUrlStateStorage, - savedDashboard, }: ApplyDashboardFilterStateProps) => { const { data: { @@ -155,13 +134,9 @@ export const applyDashboardFilterState = ({ } = pluginServices.getServices(); const { timefilter: timefilterService } = timefilter; - // apply filters to the query service and to the saved dashboard + // apply filters and query to the query service filterManager.setAppFilters(_.cloneDeep(currentDashboardState.filters)); - savedDashboard.searchSource.setField('filter', currentDashboardState.filters); - - // apply query to the query service and to the saved dashboard queryString.setQuery(currentDashboardState.query); - savedDashboard.searchSource.setField('query', currentDashboardState.query); /** * If a global time range is not set explicitly and the time range was saved with the dashboard, apply @@ -169,18 +144,11 @@ export const applyDashboardFilterState = ({ */ if (currentDashboardState.timeRestore) { const globalQueryState = kbnUrlStateStorage.get('_g'); - if (!globalQueryState?.time) { - if (savedDashboard.timeFrom && savedDashboard.timeTo) { - timefilterService.setTime({ - from: savedDashboard.timeFrom, - to: savedDashboard.timeTo, - }); - } + if (!globalQueryState?.time && currentDashboardState.timeRange) { + timefilterService.setTime(currentDashboardState.timeRange); } - if (!globalQueryState?.refreshInterval) { - if (savedDashboard.refreshInterval) { - timefilterService.setRefreshInterval(savedDashboard.refreshInterval); - } + if (!globalQueryState?.refreshInterval && currentDashboardState.refreshInterval) { + timefilterService.setRefreshInterval(currentDashboardState.refreshInterval); } } }; diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts index 947e3f5d69de..31101ae3679f 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts @@ -8,41 +8,52 @@ import _ from 'lodash'; import { debounceTime } from 'rxjs/operators'; +import semverSatisfies from 'semver/functions/satisfies'; import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/public'; -import { migrateAppState } from '.'; -import { DashboardSavedObject } from '../..'; + import { setDashboardState } from '../state'; import { migrateLegacyQuery } from './migrate_legacy_query'; -import { applyDashboardFilterState } from './sync_dashboard_filter_state'; +import { pluginServices } from '../../services/plugin_services'; import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants'; -import type { - DashboardBuildContext, - DashboardPanelMap, - DashboardState, - RawDashboardState, -} from '../../types'; -import { convertSavedPanelsToPanelMap } from './convert_dashboard_panels'; +import { applyDashboardFilterState } from './sync_dashboard_filter_state'; +import { dashboardSavedObjectErrorStrings } from '../../dashboard_strings'; +import { convertSavedPanelsToPanelMap, DashboardPanelMap } from '../../../common'; +import type { DashboardBuildContext, DashboardState, RawDashboardState } from '../../types'; -type SyncDashboardUrlStateProps = DashboardBuildContext & { savedDashboard: DashboardSavedObject }; +/** + * We no longer support loading panels from a version older than 7.3 in the URL. + * @returns whether or not there is a panel in the URL state saved with a version before 7.3 + */ +export const isPanelVersionTooOld = (panels: RawDashboardState['panels']) => { + for (const panel of panels) { + if (!panel.version || semverSatisfies(panel.version, '<7.3')) return true; + } + return false; +}; export const syncDashboardUrlState = ({ dispatchDashboardStateChange, getLatestDashboardState, kbnUrlStateStorage, - savedDashboard, -}: SyncDashboardUrlStateProps) => { +}: DashboardBuildContext) => { /** * Loads any dashboard state from the URL, and removes the state from the URL. */ const loadAndRemoveDashboardState = (): Partial => { + const { + notifications: { toasts }, + } = pluginServices.getServices(); const rawAppStateInUrl = kbnUrlStateStorage.get(DASHBOARD_STATE_STORAGE_KEY); if (!rawAppStateInUrl) return {}; - let panelsMap: DashboardPanelMap = {}; + let panelsMap: DashboardPanelMap | undefined; if (rawAppStateInUrl.panels && rawAppStateInUrl.panels.length > 0) { - const rawState = migrateAppState(rawAppStateInUrl); - panelsMap = convertSavedPanelsToPanelMap(rawState.panels); + if (isPanelVersionTooOld(rawAppStateInUrl.panels)) { + toasts.addWarning(dashboardSavedObjectErrorStrings.getPanelTooOldError()); + } else { + panelsMap = convertSavedPanelsToPanelMap(rawAppStateInUrl.panels); + } } const migratedQuery = rawAppStateInUrl.query @@ -58,7 +69,7 @@ export const syncDashboardUrlState = ({ return { ..._.omit(rawAppStateInUrl, ['panels', 'query']), ...(migratedQuery ? { query: migratedQuery } : {}), - ...(rawAppStateInUrl.panels ? { panels: panelsMap } : {}), + ...(panelsMap ? { panels: panelsMap } : {}), }; }; @@ -75,7 +86,6 @@ export const syncDashboardUrlState = ({ applyDashboardFilterState({ currentDashboardState: updatedDashboardState, kbnUrlStateStorage, - savedDashboard, }); if (Object.keys(stateFromUrl).length === 0) return; diff --git a/src/plugins/dashboard/public/application/lib/url.test.ts b/src/plugins/dashboard/public/application/lib/url.test.ts deleted file mode 100644 index fc7e51b8c2e3..000000000000 --- a/src/plugins/dashboard/public/application/lib/url.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getDashboardIdFromUrl } from './url'; - -test('getDashboardIdFromUrl', () => { - let url = - "http://localhost:5601/wev/app/dashboards#/create?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; - expect(getDashboardIdFromUrl(url)).toEqual(undefined); - - url = - "http://localhost:5601/wev/app/dashboards#/view/625357282?_a=(description:'',filters:!()&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))"; - expect(getDashboardIdFromUrl(url)).toEqual('625357282'); - - url = 'http://myserver.mydomain.com:5601/wev/app/dashboards#/view/777182'; - expect(getDashboardIdFromUrl(url)).toEqual('777182'); - - url = - "http://localhost:5601/app/dashboards#/create?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; - expect(getDashboardIdFromUrl(url)).toEqual(undefined); - - url = '/view/test/?_g=(refreshInterval:'; - expect(getDashboardIdFromUrl(url)).toEqual('test'); - - url = 'view/test/?_g=(refreshInterval:'; - expect(getDashboardIdFromUrl(url)).toEqual('test'); - - url = '/other-app/test/'; - expect(getDashboardIdFromUrl(url)).toEqual(undefined); -}); diff --git a/src/plugins/dashboard/public/application/lib/url.ts b/src/plugins/dashboard/public/application/lib/url.ts deleted file mode 100644 index 0ff2809a9266..000000000000 --- a/src/plugins/dashboard/public/application/lib/url.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/** - * Returns dashboard id from URL - * literally looks from id after `dashboard/` string and before `/`, `?` and end of string - * @param url to extract dashboardId from - * input: http://localhost:5601/lib/app/kibana#/dashboard?param1=x¶m2=y¶m3=z - * output: undefined - * input: http://localhost:5601/lib/app/kibana#/dashboard/39292992?param1=x¶m2=y¶m3=z - * output: 39292992 - */ -export function getDashboardIdFromUrl(url: string): string | undefined { - const [, dashboardId] = url.match(/view\/(.*?)(\/|\?|$)/) ?? [ - undefined, // full match - undefined, // group with dashboardId - ]; - return dashboardId ?? undefined; -} diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx index 5712b4c6ee2f..4a77249aeb39 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx @@ -9,19 +9,15 @@ import React from 'react'; import { mount } from 'enzyme'; -import { I18nProvider, FormattedRelative } from '@kbn/i18n-react'; -import { SimpleSavedObject } from '@kbn/core/public'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { TableListViewKibanaDependencies, TableListViewKibanaProvider, } from '@kbn/content-management-table-list'; +import { I18nProvider, FormattedRelative } from '@kbn/i18n-react'; +import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; -import { DashboardAppServices } from '../../types'; -import { DashboardListing, DashboardListingProps } from './dashboard_listing'; -import { makeDefaultServices } from '../test_helpers'; import { pluginServices } from '../../services/plugin_services'; +import { DashboardListing, DashboardListingProps } from './dashboard_listing'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../../services/dashboard_session_storage/dashboard_session_storage_service'; function makeDefaultProps(): DashboardListingProps { @@ -31,14 +27,7 @@ function makeDefaultProps(): DashboardListingProps { }; } -function mountWith({ - props: incomingProps, - services: incomingServices, -}: { - props?: DashboardListingProps; - services?: DashboardAppServices; -}) { - const services = incomingServices ?? makeDefaultServices(); +function mountWith({ props: incomingProps }: { props?: DashboardListingProps }) { const props = incomingProps ?? makeDefaultProps(); const wrappingComponent: React.FC<{ children: React.ReactNode; @@ -47,35 +36,32 @@ function mountWith({ return ( - {/* Can't get rid of KibanaContextProvider here yet because of 'call to action when no dashboards exist' tests below */} - - null, - }, + null, }, - } as unknown as TableListViewKibanaDependencies['savedObjectsTagging'] - } - FormattedRelative={FormattedRelative} - toMountPoint={() => () => () => undefined} - > - {children} - - + }, + } as unknown as TableListViewKibanaDependencies['savedObjectsTagging'] + } + FormattedRelative={FormattedRelative} + toMountPoint={() => () => () => undefined} + > + {children} + ); }; const component = mount(, { wrappingComponent }); - return { component, props, services }; + return { component, props }; } describe('after fetch', () => { @@ -89,14 +75,14 @@ describe('after fetch', () => { }); test('renders call to action when no dashboards exist', async () => { - const services = makeDefaultServices(); - services.savedDashboards.find = () => { - return Promise.resolve({ - total: 0, - hits: [], - }); - }; - const { component } = mountWith({ services }); + ( + pluginServices.getServices().dashboardSavedObject.findDashboards.findSavedObjects as jest.Mock + ).mockResolvedValue({ + total: 0, + hits: [], + }); + + const { component } = mountWith({}); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); // Ensure the state changes are reflected @@ -105,18 +91,18 @@ describe('after fetch', () => { }); test('renders call to action with continue when no dashboards exist but one is in progress', async () => { - const services = makeDefaultServices(); - services.savedDashboards.find = () => { - return Promise.resolve({ - total: 0, - hits: [], - }); - }; pluginServices.getServices().dashboardSessionStorage.getDashboardIdsWithUnsavedChanges = jest .fn() .mockReturnValueOnce([DASHBOARD_PANELS_UNSAVED_ID]) .mockReturnValue(['dashboardUnsavedOne', 'dashboardUnsavedTwo']); - const { component } = mountWith({ services }); + ( + pluginServices.getServices().dashboardSavedObject.findDashboards.findSavedObjects as jest.Mock + ).mockResolvedValue({ + total: 0, + hits: [], + }); + + const { component } = mountWith({}); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); // Ensure the state changes are reflected @@ -139,17 +125,9 @@ describe('after fetch', () => { const title = 'search by title'; const props = makeDefaultProps(); props.title = title; - pluginServices.getServices().savedObjects.client.find = () => { - return Promise.resolve({ - perPage: 10, - total: 2, - page: 0, - savedObjects: [ - { attributes: { title: `${title}_number1` }, id: 'hello there' } as SimpleSavedObject, - { attributes: { title: `${title}_number2` }, id: 'goodbye' } as SimpleSavedObject, - ], - }); - }; + ( + pluginServices.getServices().dashboardSavedObject.findDashboards.findByTitle as jest.Mock + ).mockResolvedValue(undefined); const { component } = mountWith({ props }); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); @@ -163,14 +141,9 @@ describe('after fetch', () => { const title = 'search by title'; const props = makeDefaultProps(); props.title = title; - pluginServices.getServices().savedObjects.client.find = () => { - return Promise.resolve({ - perPage: 10, - total: 1, - page: 0, - savedObjects: [{ attributes: { title }, id: 'you_found_me' } as SimpleSavedObject], - }); - }; + ( + pluginServices.getServices().dashboardSavedObject.findDashboards.findByTitle as jest.Mock + ).mockResolvedValue({ id: 'you_found_me' }); const { component } = mountWith({ props }); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index 40753c556a56..1e78b9430347 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -6,7 +6,10 @@ * Side Public License, v 1. */ +import useMount from 'react-use/lib/useMount'; import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + import { EuiLink, EuiButton, @@ -15,30 +18,29 @@ import { EuiFlexItem, EuiButtonEmpty, } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import type { SavedObjectsFindOptionsReference } from '@kbn/core/public'; -import useMount from 'react-use/lib/useMount'; -import type { SavedObjectReference } from '@kbn/core/types'; -import { useExecutionContext, useKibana } from '@kbn/kibana-react-plugin/public'; +import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; import { syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public'; +import type { SavedObjectsFindOptionsReference, SimpleSavedObject } from '@kbn/core/public'; import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { TableListView, type UserContentCommonSchema } from '@kbn/content-management-table-list'; -import { attemptLoadDashboardByTitle } from '../lib'; -import { DashboardAppServices, DashboardRedirect } from '../../types'; import { getDashboardBreadcrumb, - dashboardListingTable, + dashboardListingTableStrings, noItemsStrings, dashboardUnsavedListingStrings, getNewDashboardTitle, + dashboardSavedObjectErrorStrings, } from '../../dashboard_strings'; +import { DashboardConstants } from '../..'; +import { DashboardRedirect } from '../../types'; +import { pluginServices } from '../../services/plugin_services'; import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; -import { confirmCreateWithUnsaved, confirmDiscardUnsavedChanges } from './confirm_overlays'; import { getDashboardListItemLink } from './get_dashboard_list_item_link'; +import { confirmCreateWithUnsaved, confirmDiscardUnsavedChanges } from './confirm_overlays'; import { DashboardAppNoDataPage, isDashboardAppInNoDataState } from '../dashboard_app_no_data'; -import { pluginServices } from '../../services/plugin_services'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../../services/dashboard_session_storage/dashboard_session_storage_service'; +import { DashboardAttributes } from '../embeddable'; const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit'; const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage'; @@ -52,17 +54,18 @@ interface DashboardSavedObjectUserContent extends UserContentCommonSchema { } const toTableListViewSavedObject = ( - savedObject: Record + savedObject: SimpleSavedObject ): DashboardSavedObjectUserContent => { + const { title, description, timeRestore } = savedObject.attributes; return { - id: savedObject.id as string, - updatedAt: savedObject.updatedAt! as string, - references: savedObject.references as SavedObjectReference[], type: 'dashboard', + id: savedObject.id, + updatedAt: savedObject.updatedAt!, + references: savedObject.references, attributes: { - title: (savedObject.title as string) ?? '', - description: savedObject.description as string, - timeRestore: savedObject.timeRestore as boolean, + title, + description, + timeRestore, }, }; }; @@ -80,19 +83,16 @@ export const DashboardListing = ({ initialFilter, kbnUrlStateStorage, }: DashboardListingProps) => { - const { - services: { savedDashboards }, - } = useKibana(); - const { application, + data: { query }, + dashboardSessionStorage, + settings: { uiSettings }, + notifications: { toasts }, chrome: { setBreadcrumbs }, coreContext: { executionContext }, dashboardCapabilities: { showWriteControls }, - dashboardSessionStorage, - data: { query }, - savedObjects: { client }, - settings: { uiSettings }, + dashboardSavedObject: { findDashboards, savedObjectsClient }, } = pluginServices.getServices(); const [showNoDataPage, setShowNoDataPage] = useState(false); @@ -125,7 +125,7 @@ export const DashboardListing = ({ kbnUrlStateStorage ); if (title) { - attemptLoadDashboardByTitle(title).then((result) => { + findDashboards.findByTitle(title).then((result) => { if (!result) return; redirectTo({ destination: 'dashboard', @@ -138,7 +138,7 @@ export const DashboardListing = ({ return () => { stopSyncingQueryServiceStateWithUrl(); }; - }, [title, client, redirectTo, query, kbnUrlStateStorage]); + }, [title, redirectTo, query, kbnUrlStateStorage, findDashboards]); const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING); const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING); @@ -262,10 +262,11 @@ export const DashboardListing = ({ const fetchItems = useCallback( (searchTerm: string, references?: SavedObjectsFindOptionsReference[]) => { - return savedDashboards - .find(searchTerm, { - hasReference: references, + return findDashboards + .findSavedObjects({ + search: searchTerm, size: listingLimit, + hasReference: references, }) .then(({ total, hits }) => { return { @@ -274,16 +275,24 @@ export const DashboardListing = ({ }; }); }, - [listingLimit, savedDashboards] + [findDashboards, listingLimit] ); const deleteItems = useCallback( - (dashboards: Array<{ id: string }>) => { - dashboards.map((d) => dashboardSessionStorage.clearState(d.id)); + async (dashboardsToDelete: Array<{ id: string }>) => { + await Promise.all( + dashboardsToDelete.map(({ id }) => { + dashboardSessionStorage.clearState(id); + return savedObjectsClient.delete(DashboardConstants.DASHBOARD_SAVED_OBJECT_TYPE, id); + }) + ).catch((error) => { + toasts.addError(error, { + title: dashboardSavedObjectErrorStrings.getErrorDeletingDashboardToast(), + }); + }); setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()); - return savedDashboards.delete(dashboards.map((d) => d.id)); }, - [savedDashboards, dashboardSessionStorage] + [savedObjectsClient, dashboardSessionStorage, toasts] ); const editItem = useCallback( @@ -292,7 +301,7 @@ export const DashboardListing = ({ [redirectTo] ); - const { getEntityName, getTableListTitle, getEntityNamePlural } = dashboardListingTable; + const { getEntityName, getTableListTitle, getEntityNamePlural } = dashboardListingTableStrings; return ( <> {showNoDataPage && ( diff --git a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.test.tsx b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.test.tsx index 1feec9bbdc42..34a78c5181b9 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.test.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.test.tsx @@ -11,87 +11,46 @@ import { mount } from 'enzyme'; import { I18nProvider } from '@kbn/i18n-react'; import { waitFor } from '@testing-library/react'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { DashboardSavedObject } from '../..'; -import { DashboardAppServices } from '../../types'; -import { makeDefaultServices } from '../test_helpers'; -import { SavedObjectLoader } from '../../services/saved_object_loader'; import { DashboardUnsavedListing, DashboardUnsavedListingProps } from './dashboard_unsaved_listing'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../../services/dashboard_session_storage/dashboard_session_storage_service'; import { pluginServices } from '../../services/plugin_services'; -const mockedDashboards: { [key: string]: DashboardSavedObject } = { - dashboardUnsavedOne: { - id: `dashboardUnsavedOne`, - title: `Dashboard Unsaved One`, - } as DashboardSavedObject, - dashboardUnsavedTwo: { - id: `dashboardUnsavedTwo`, - title: `Dashboard Unsaved Two`, - } as DashboardSavedObject, - dashboardUnsavedThree: { - id: `dashboardUnsavedThree`, - title: `Dashboard Unsaved Three`, - } as DashboardSavedObject, -}; - -function makeServices(): DashboardAppServices { - const services = makeDefaultServices(); - const savedDashboards = {} as SavedObjectLoader; - savedDashboards.get = jest - .fn() - .mockImplementation((id: string) => Promise.resolve(mockedDashboards[id])); - return { - ...services, - savedDashboards, - }; -} - const makeDefaultProps = (): DashboardUnsavedListingProps => ({ redirectTo: jest.fn(), unsavedDashboardIds: ['dashboardUnsavedOne', 'dashboardUnsavedTwo', 'dashboardUnsavedThree'], refreshUnsavedDashboards: jest.fn(), }); -function mountWith({ - services: incomingServices, - props: incomingProps, -}: { - services?: DashboardAppServices; - props?: DashboardUnsavedListingProps; -}) { - const services = incomingServices ?? makeServices(); +function mountWith({ props: incomingProps }: { props?: DashboardUnsavedListingProps }) { const props = incomingProps ?? makeDefaultProps(); const wrappingComponent: React.FC<{ children: React.ReactNode; }> = ({ children }) => { - return ( - - {/* Only the old savedObjects service is used for `DashboardUnsavedListing`, so will need to wrap this in - `DashboardServicesProvider` instead once that is removed as part of https://github.com/elastic/kibana/pull/138774*/} - {children} - - ); + return {children}; }; const component = mount(, { wrappingComponent }); - return { component, props, services }; + return { component, props }; } describe('Unsaved listing', () => { it('Gets information for each unsaved dashboard', async () => { - const { services } = mountWith({}); + mountWith({}); await waitFor(() => { - expect(services.savedDashboards.get).toHaveBeenCalledTimes(3); + expect( + pluginServices.getServices().dashboardSavedObject.findDashboards.findByIds + ).toHaveBeenCalledTimes(1); }); }); - it('Does not attempt to get unsaved dashboard id', async () => { + it('Does not attempt to get newly created dashboard', async () => { const props = makeDefaultProps(); props.unsavedDashboardIds = ['dashboardUnsavedOne', DASHBOARD_PANELS_UNSAVED_ID]; - const { services } = mountWith({ props }); + mountWith({ props }); await waitFor(() => { - expect(services.savedDashboards.get).toHaveBeenCalledTimes(1); + expect( + pluginServices.getServices().dashboardSavedObject.findDashboards.findByIds + ).toHaveBeenCalledWith(['dashboardUnsavedOne']); }); }); @@ -146,14 +105,22 @@ describe('Unsaved listing', () => { }); it('removes unsaved changes from any dashboard which errors on fetch', async () => { - const services = makeServices(); + ( + pluginServices.getServices().dashboardSavedObject.findDashboards.findByIds as jest.Mock + ).mockResolvedValue([ + { + id: 'failCase1', + status: 'error', + error: { error: 'oh no', message: 'bwah', statusCode: 100 }, + }, + { + id: 'failCase2', + status: 'error', + error: { error: 'oh no', message: 'bwah', statusCode: 100 }, + }, + ]); + const props = makeDefaultProps(); - services.savedDashboards.get = jest.fn().mockImplementation((id: string) => { - if (id === 'failCase1' || id === 'failCase2') { - return Promise.reject(new Error()); - } - return Promise.resolve(mockedDashboards[id]); - }); props.unsavedDashboardIds = [ 'dashboardUnsavedOne', @@ -162,7 +129,7 @@ describe('Unsaved listing', () => { 'failCase1', 'failCase2', ]; - const { component } = mountWith({ services, props }); + const { component } = mountWith({ props }); waitFor(() => { component.update(); expect(pluginServices.getServices().dashboardSessionStorage.clearState).toHaveBeenCalledWith( diff --git a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx index a5c5b1b224a3..3aa862fe3026 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import React, { useCallback, useEffect, useState } from 'react'; - import { EuiButtonEmpty, EuiCallOut, @@ -17,14 +15,14 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; +import React, { useCallback, useEffect, useState } from 'react'; -import type { DashboardSavedObject } from '../..'; -import { dashboardUnsavedListingStrings, getNewDashboardTitle } from '../../dashboard_strings'; -import type { DashboardAppServices, DashboardRedirect } from '../../types'; -import { confirmDiscardUnsavedChanges } from './confirm_overlays'; +import type { DashboardRedirect } from '../../types'; import { pluginServices } from '../../services/plugin_services'; +import { confirmDiscardUnsavedChanges } from './confirm_overlays'; +import { dashboardUnsavedListingStrings, getNewDashboardTitle } from '../../dashboard_strings'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../../services/dashboard_session_storage/dashboard_session_storage_service'; +import { DashboardAttributes } from '../embeddable'; const DashboardUnsavedItem = ({ id, @@ -102,7 +100,7 @@ const DashboardUnsavedItem = ({ }; interface UnsavedItemMap { - [key: string]: DashboardSavedObject; + [key: string]: DashboardAttributes; } export interface DashboardUnsavedListingProps { @@ -117,10 +115,9 @@ export const DashboardUnsavedListing = ({ refreshUnsavedDashboards, }: DashboardUnsavedListingProps) => { const { - services: { savedDashboards }, - } = useKibana(); - - const { dashboardSessionStorage } = pluginServices.getServices(); + dashboardSessionStorage, + dashboardSavedObject: { savedObjectsClient, findDashboards }, + } = pluginServices.getServices(); const [items, setItems] = useState({}); @@ -146,28 +143,24 @@ export const DashboardUnsavedListing = ({ return; } let canceled = false; - const dashPromises = unsavedDashboardIds - .filter((id) => id !== DASHBOARD_PANELS_UNSAVED_ID) - .map((dashboardId) => { - return (savedDashboards.get(dashboardId) as Promise).catch( - () => dashboardId - ); - }); - Promise.all(dashPromises).then((dashboards: Array) => { + const existingDashboardsWithUnsavedChanges = unsavedDashboardIds.filter( + (id) => id !== DASHBOARD_PANELS_UNSAVED_ID + ); + findDashboards.findByIds(existingDashboardsWithUnsavedChanges).then((results) => { const dashboardMap = {}; if (canceled) { return; } let hasError = false; - const newItems = dashboards.reduce((map, dashboard) => { - if (typeof dashboard === 'string') { + const newItems = results.reduce((map, result) => { + if (result.status === 'error') { hasError = true; - dashboardSessionStorage.clearState(dashboard); + dashboardSessionStorage.clearState(result.id); return map; } return { ...map, - [dashboard.id || DASHBOARD_PANELS_UNSAVED_ID]: dashboard, + [result.id || DASHBOARD_PANELS_UNSAVED_ID]: result.attributes, }; }, dashboardMap); if (hasError) { @@ -179,7 +172,13 @@ export const DashboardUnsavedListing = ({ return () => { canceled = true; }; - }, [savedDashboards, dashboardSessionStorage, refreshUnsavedDashboards, unsavedDashboardIds]); + }, [ + refreshUnsavedDashboards, + dashboardSessionStorage, + unsavedDashboardIds, + savedObjectsClient, + findDashboards, + ]); return unsavedDashboardIds.length === 0 ? null : ( <> diff --git a/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts b/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts index f28d095c9b9b..669bde5c3eb6 100644 --- a/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts +++ b/src/plugins/dashboard/public/application/state/dashboard_state_slice.ts @@ -7,11 +7,14 @@ */ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { PersistableControlGroupInput } from '@kbn/controls-plugin/common'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { RefreshInterval } from '@kbn/data-plugin/common'; import type { Filter, Query, TimeRange } from '@kbn/es-query'; -import type { DashboardOptions, DashboardPanelMap, DashboardState } from '../../types'; +import { PersistableControlGroupInput } from '@kbn/controls-plugin/common'; + +import { DashboardPanelMap } from '../../../common'; +import type { DashboardOptions, DashboardState } from '../../types'; export const dashboardStateSlice = createSlice({ name: 'dashboardState', @@ -33,11 +36,15 @@ export const dashboardStateSlice = createSlice({ description: string; tags?: string[]; timeRestore: boolean; + timeRange?: TimeRange; + refreshInterval?: RefreshInterval; }> ) => { state.title = action.payload.title; state.description = action.payload.description; state.timeRestore = action.payload.timeRestore; + state.timeRange = action.payload.timeRange; + state.refreshInterval = action.payload.refreshInterval; if (action.payload.tags) { state.tags = action.payload.tags; } diff --git a/src/plugins/dashboard/public/application/test_helpers/get_saved_dashboard_mock.ts b/src/plugins/dashboard/public/application/test_helpers/get_saved_dashboard_mock.ts deleted file mode 100644 index 69da1dbbe56a..000000000000 --- a/src/plugins/dashboard/public/application/test_helpers/get_saved_dashboard_mock.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { DashboardSavedObject } from '../../saved_dashboards'; - -export function getSavedDashboardMock( - config?: Partial -): DashboardSavedObject { - const searchSource = dataPluginMock.createStartContract(); - - return { - id: '123', - title: 'my dashboard', - panelsJSON: '[]', - searchSource: searchSource.search.searchSource.create(), - copyOnSave: false, - timeRestore: false, - timeTo: 'now', - timeFrom: 'now-15m', - optionsJSON: '', - lastSavedTitle: '', - destroy: () => {}, - save: () => { - return Promise.resolve('123'); - }, - getQuery: () => ({ query: '', language: 'kuery' }), - getFilters: () => [], - ...config, - } as DashboardSavedObject; -} diff --git a/src/plugins/dashboard/public/application/test_helpers/index.ts b/src/plugins/dashboard/public/application/test_helpers/index.ts index 7c8ae86074a4..c4d149e8c10b 100644 --- a/src/plugins/dashboard/public/application/test_helpers/index.ts +++ b/src/plugins/dashboard/public/application/test_helpers/index.ts @@ -7,6 +7,4 @@ */ export { getSampleDashboardInput, getSampleDashboardPanel } from './get_sample_dashboard_input'; -export { getSavedDashboardMock } from './get_saved_dashboard_mock'; -export { makeDefaultServices } from './make_default_services'; export { setupIntersectionObserverMock } from './intersection_observer_mock'; diff --git a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts b/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts deleted file mode 100644 index 3ffe9dc3b70e..000000000000 --- a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - SavedObjectLoader, - type SavedObjectLoaderFindOptions, -} from '../../services/saved_object_loader'; -import { DashboardAppServices } from '../../types'; -import { getSavedDashboardMock } from './get_saved_dashboard_mock'; - -// TODO: Remove as part of https://github.com/elastic/kibana/pull/138774 -export function makeDefaultServices(): DashboardAppServices { - const savedDashboards = {} as SavedObjectLoader; - savedDashboards.find = (search: string, sizeOrOptions: number | SavedObjectLoaderFindOptions) => { - const size = typeof sizeOrOptions === 'number' ? sizeOrOptions : sizeOrOptions.size ?? 10; - const hits = []; - for (let i = 0; i < size; i++) { - hits.push({ - id: `dashboard${i}`, - title: `dashboard${i} - ${search} - title`, - description: `dashboard${i} desc`, - references: [], - timeRestore: true, - type: '', - url: '', - updatedAt: '', - panelsJSON: '', - lastSavedTitle: '', - }); - } - return Promise.resolve({ - total: size, - hits, - }); - }; - savedDashboards.get = jest - .fn() - .mockImplementation((id?: string) => Promise.resolve(getSavedDashboardMock({ id }))); - - return { - savedDashboards, - }; -} diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 86fd56c2ec6a..97630c48dc78 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -6,46 +6,32 @@ * Side Public License, v 1. */ -import { METRIC_TYPE } from '@kbn/analytics'; -import { Required } from '@kbn/utility-types'; -import { EuiHorizontalRule } from '@elastic/eui'; import UseUnmount from 'react-use/lib/useUnmount'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import type { OverlayRef } from '@kbn/core/public'; -import type { TopNavMenuProps } from '@kbn/navigation-plugin/public'; -import type { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public'; import { - AddFromLibraryButton, + withSuspense, LazyLabsFlyout, - PrimaryActionButton, + SolutionToolbar, QuickButtonGroup, QuickButtonProps, - SolutionToolbar, - withSuspense, + PrimaryActionButton, + AddFromLibraryButton, } from '@kbn/presentation-util-plugin/public'; -import type { SavedQuery } from '@kbn/data-plugin/common'; -import { isErrorEmbeddable, openAddPanelFlyout, ViewMode } from '@kbn/embeddable-plugin/public'; import { - getSavedObjectFinder, - type SaveResult, showSaveModal, + type SaveResult, + getSavedObjectFinder, } from '@kbn/saved-objects-plugin/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { Required } from '@kbn/utility-types'; +import { EuiHorizontalRule } from '@elastic/eui'; +import type { OverlayRef } from '@kbn/core/public'; +import type { SavedQuery } from '@kbn/data-plugin/common'; +import type { TopNavMenuProps } from '@kbn/navigation-plugin/public'; +import type { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public'; +import { isErrorEmbeddable, openAddPanelFlyout, ViewMode } from '@kbn/embeddable-plugin/public'; -import { saveDashboard } from '../lib'; -import { TopNavIds } from './top_nav_ids'; -import { EditorMenu } from './editor_menu'; -import { UI_SETTINGS } from '../../../common'; -import { DashboardSaveModal } from './save_modal'; -import { showCloneModal } from './show_clone_modal'; -import { ShowShareModal } from './show_share_modal'; -import { getTopNavConfig } from './get_top_nav_config'; -import { showOptionsPopover } from './show_options_popover'; -import { DashboardConstants } from '../../dashboard_constants'; -import { confirmDiscardUnsavedChanges } from '../listing/confirm_overlays'; -import type { DashboardAppState, DashboardSaveOptions, NavAction } from '../../types'; -import type { DashboardEmbedSettings, DashboardRedirect } from '../../types'; -import { getCreateVisualizationButtonTitle, unsavedChangesBadge } from '../../dashboard_strings'; import { setFullScreenMode, setHidePanelTitles, @@ -58,8 +44,21 @@ import { useDashboardDispatch, useDashboardSelector, } from '../state'; +import { TopNavIds } from './top_nav_ids'; +import { EditorMenu } from './editor_menu'; +import { UI_SETTINGS } from '../../../common'; +import { DashboardSaveModal } from './save_modal'; +import { showCloneModal } from './show_clone_modal'; +import { ShowShareModal } from './show_share_modal'; +import { getTopNavConfig } from './get_top_nav_config'; +import { showOptionsPopover } from './show_options_popover'; import { pluginServices } from '../../services/plugin_services'; +import { DashboardEmbedSettings, DashboardRedirect, DashboardState } from '../../types'; +import { confirmDiscardUnsavedChanges } from '../listing/confirm_overlays'; import { useDashboardMountContext } from '../hooks/dashboard_mount_context'; +import { DashboardConstants, getFullEditPath } from '../../dashboard_constants'; +import { DashboardAppState, DashboardSaveOptions, NavAction } from '../../types'; +import { getCreateVisualizationButtonTitle, unsavedChangesBadge } from '../../dashboard_strings'; export interface DashboardTopNavState { chromeIsVisible: boolean; @@ -70,18 +69,13 @@ export interface DashboardTopNavState { type CompleteDashboardAppState = Required< DashboardAppState, - 'getLatestDashboardState' | 'dashboardContainer' | 'savedDashboard' | 'applyFilters' + 'getLatestDashboardState' | 'dashboardContainer' >; export const isCompleteDashboardAppState = ( state: DashboardAppState ): state is CompleteDashboardAppState => { - return ( - Boolean(state.getLatestDashboardState) && - Boolean(state.dashboardContainer) && - Boolean(state.savedDashboard) && - Boolean(state.applyFilters) - ); + return Boolean(state.getLatestDashboardState) && Boolean(state.dashboardContainer); }; export interface DashboardTopNavProps { @@ -101,24 +95,28 @@ export function DashboardTopNav({ }: DashboardTopNavProps) { const { setHeaderActionMenu } = useDashboardMountContext(); const { + dashboardSavedObject: { + checkForDuplicateDashboardTitle, + saveDashboardStateToSavedObject, + savedObjectsClient, + }, chrome: { getIsVisible$: getChromeIsVisible$, recentlyAccessed: chromeRecentlyAccessed, docTitle, }, coreContext: { i18nContext }, - dashboardCapabilities, + share, + overlays, + notifications, + usageCollection, data: { query, search }, - embeddable: { getEmbeddableFactory, getEmbeddableFactories, getStateTransfer }, - initializerContext: { allowByValueEmbeddables }, navigation: { TopNavMenu }, - notifications, - overlays, - savedObjects, - savedObjectsTagging: { hasTagDecoration, hasApi }, settings: { uiSettings, theme }, - share, - usageCollection, + initializerContext: { allowByValueEmbeddables }, + dashboardCapabilities: { showWriteControls, saveQuery: showSaveQuery }, + savedObjectsTagging: { hasApi: hasSavedObjectsTagging }, + embeddable: { getEmbeddableFactory, getEmbeddableFactories, getStateTransfer }, visualizations: { get: getVisualization, getAliases: getVisTypeAliases }, } = pluginServices.getServices(); @@ -144,24 +142,18 @@ export function DashboardTopNav({ const visibleSubscription = getChromeIsVisible$().subscribe((chromeIsVisible) => { setState((s) => ({ ...s, chromeIsVisible })); }); - const { id, title, getFullEditPath } = dashboardAppState.savedDashboard; - if (id && title) { + const { savedObjectId, title, viewMode } = dashboardState; + if (savedObjectId && title) { chromeRecentlyAccessed.add( - getFullEditPath(dashboardState.viewMode === ViewMode.EDIT), + getFullEditPath(savedObjectId, viewMode === ViewMode.EDIT), title, - id + savedObjectId ); } return () => { visibleSubscription.unsubscribe(); }; - }, [ - getChromeIsVisible$, - chromeRecentlyAccessed, - allowByValueEmbeddables, - dashboardState.viewMode, - dashboardAppState.savedDashboard, - ]); + }, [allowByValueEmbeddables, chromeRecentlyAccessed, dashboardState, getChromeIsVisible$]); const addFromLibrary = useCallback(() => { if (!isErrorEmbeddable(dashboardAppState.dashboardContainer)) { @@ -173,7 +165,7 @@ export function DashboardTopNav({ getFactory: getEmbeddableFactory, notifications, overlays, - SavedObjectFinder: getSavedObjectFinder(savedObjects, uiSettings), + SavedObjectFinder: getSavedObjectFinder({ client: savedObjectsClient }, uiSettings), reportUiCounter: usageCollection.reportUiCounter, theme, }), @@ -181,14 +173,14 @@ export function DashboardTopNav({ } }, [ dashboardAppState.dashboardContainer, + usageCollection.reportUiCounter, getEmbeddableFactories, getEmbeddableFactory, + savedObjectsClient, notifications, - savedObjects, overlays, - theme, uiSettings, - usageCollection, + theme, ]); const createNewVisType = useCallback( @@ -258,48 +250,71 @@ export function DashboardTopNav({ onTitleDuplicate, isTitleDuplicateConfirmed, }: DashboardSaveOptions): Promise => { + const { + timefilter: { timefilter }, + } = query; + const saveOptions = { confirmOverwrite: false, isTitleDuplicateConfirmed, onTitleDuplicate, + saveAsCopy: newCopyOnSave, }; - const stateFromSaveModal = { + const stateFromSaveModal: Pick< + DashboardState, + 'title' | 'description' | 'timeRestore' | 'timeRange' | 'refreshInterval' | 'tags' + > = { title: newTitle, + tags: [] as string[], description: newDescription, timeRestore: newTimeRestore, - tags: [] as string[], + timeRange: newTimeRestore ? timefilter.getTime() : undefined, + refreshInterval: newTimeRestore ? timefilter.getRefreshInterval() : undefined, }; - if (hasApi && newTags) { - // remove `hasAPI` once the savedObjectsTagging service is optional + if (hasSavedObjectsTagging && newTags) { + // remove `hasSavedObjectsTagging` once the savedObjectsTagging service is optional stateFromSaveModal.tags = newTags; } - dashboardAppState.savedDashboard.copyOnSave = newCopyOnSave; - const saveResult = await saveDashboard({ + if ( + !(await checkForDuplicateDashboardTitle({ + title: newTitle, + onTitleDuplicate, + lastSavedTitle: currentState.title, + copyOnSave: newCopyOnSave, + isTitleDuplicateConfirmed, + })) + ) { + // do not save if title is duplicate and is unconfirmed + return {}; + } + + const saveResult = await saveDashboardStateToSavedObject({ redirectTo, saveOptions, - savedDashboard: dashboardAppState.savedDashboard, currentState: { ...currentState, ...stateFromSaveModal }, }); if (saveResult.id && !saveResult.redirected) { dispatchDashboardStateChange(setStateFromSaveModal(stateFromSaveModal)); - dashboardAppState.updateLastSavedState?.(); - docTitle.change(stateFromSaveModal.title); + setTimeout(() => { + /** + * set timeout so dashboard state subject can update with the new title before updating the last saved state. + * TODO: Remove this timeout once the last saved state is also handled in Redux. + **/ + dashboardAppState.updateLastSavedState?.(); + docTitle.change(stateFromSaveModal.title); + }, 1); } - return saveResult.id ? { id: saveResult.id } : { error: saveResult.error }; + return saveResult.id ? { id: saveResult.id } : { error: new Error(saveResult.error) }; }; - const lastDashboardId = dashboardAppState.savedDashboard.id; - const savedTags = hasTagDecoration?.(dashboardAppState.savedDashboard) - ? dashboardAppState.savedDashboard.getTags() - : []; - const currentTagsSet = new Set([...savedTags, ...currentState.tags]); + const lastDashboardId = currentState.savedObjectId; const dashboardSaveModal = ( {}} - tags={Array.from(currentTagsSet)} + tags={currentState.tags} title={currentState.title} timeRestore={currentState.timeRestore} description={currentState.description} @@ -309,24 +324,25 @@ export function DashboardTopNav({ closeAllFlyouts(); showSaveModal(dashboardSaveModal, i18nContext); }, [ + saveDashboardStateToSavedObject, + checkForDuplicateDashboardTitle, dispatchDashboardStateChange, - hasApi, - hasTagDecoration, + hasSavedObjectsTagging, dashboardAppState, - i18nContext, - docTitle, closeAllFlyouts, + i18nContext, redirectTo, + docTitle, + query, ]); const runQuickSave = useCallback(async () => { setState((s) => ({ ...s, isSaveInProgress: true })); const currentState = dashboardAppState.getLatestDashboardState(); - const saveResult = await saveDashboard({ + const saveResult = await saveDashboardStateToSavedObject({ redirectTo, currentState, saveOptions: {}, - savedDashboard: dashboardAppState.savedDashboard, }); if (saveResult.id && !saveResult.redirected) { dashboardAppState.updateLastSavedState?.(); @@ -336,7 +352,7 @@ export function DashboardTopNav({ if (!mounted) return; setState((s) => ({ ...s, isSaveInProgress: false })); }, DashboardConstants.CHANGE_CHECK_DEBOUNCE); - }, [dashboardAppState, redirectTo, mounted]); + }, [dashboardAppState, saveDashboardStateToSavedObject, redirectTo, mounted]); const runClone = useCallback(() => { const currentState = dashboardAppState.getLatestDashboardState(); @@ -345,22 +361,33 @@ export function DashboardTopNav({ isTitleDuplicateConfirmed: boolean, onTitleDuplicate: () => void ) => { - dashboardAppState.savedDashboard.copyOnSave = true; - const saveOptions = { - confirmOverwrite: false, - isTitleDuplicateConfirmed, - onTitleDuplicate, - }; - const saveResult = await saveDashboard({ + if ( + !(await checkForDuplicateDashboardTitle({ + title: newTitle, + onTitleDuplicate, + lastSavedTitle: currentState.title, + copyOnSave: true, + isTitleDuplicateConfirmed, + })) + ) { + // do not clone if title is duplicate and is unconfirmed + return {}; + } + + const saveResult = await saveDashboardStateToSavedObject({ redirectTo, - saveOptions, - savedDashboard: dashboardAppState.savedDashboard, + saveOptions: { saveAsCopy: true }, currentState: { ...currentState, title: newTitle }, }); return saveResult.id ? { id: saveResult.id } : { error: saveResult.error }; }; showCloneModal({ onClone, title: currentState.title }); - }, [dashboardAppState, redirectTo]); + }, [ + checkForDuplicateDashboardTitle, + saveDashboardStateToSavedObject, + dashboardAppState, + redirectTo, + ]); const showOptions = useCallback( (anchorElement: HTMLElement) => { @@ -394,7 +421,6 @@ export function DashboardTopNav({ ShowShareModal({ anchorElement, currentDashboardState: currentState, - savedDashboard: dashboardAppState.savedDashboard, isDirty: Boolean(dashboardAppState.hasUnsavedChanges), }); }, @@ -442,7 +468,7 @@ export function DashboardTopNav({ }); const getNavBarProps = (): TopNavMenuProps => { - const { hasUnsavedChanges, savedDashboard } = dashboardAppState; + const { hasUnsavedChanges } = dashboardAppState; const shouldShowNavBarComponent = (forceShow: boolean): boolean => (forceShow || state.chromeIsVisible) && !dashboardState.fullScreenMode; @@ -464,11 +490,11 @@ export function DashboardTopNav({ dashboardAppState.getLatestDashboardState().viewMode, dashboardTopNavActions, { - showWriteControls: dashboardCapabilities.showWriteControls, - isDirty: Boolean(dashboardAppState.hasUnsavedChanges), - isSaveInProgress: state.isSaveInProgress, - isNewDashboard: !savedDashboard.id, isLabsEnabled, + showWriteControls, + isSaveInProgress: state.isSaveInProgress, + isNewDashboard: !dashboardState.savedObjectId, + isDirty: Boolean(dashboardAppState.hasUnsavedChanges), } ); @@ -485,22 +511,22 @@ export function DashboardTopNav({ return { badges, - appName: 'dashboard', - config: showTopNavMenu ? topNav : undefined, - className: isFullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined, screenTitle, - showSearchBar, showQueryBar, + showSearchBar, + showFilterBar, + showSaveQuery, showQueryInput, showDatePicker, - showFilterBar, - setMenuMountPoint: embedSettings ? undefined : setHeaderActionMenu, - indexPatterns: dashboardAppState.dataViews, - showSaveQuery: dashboardCapabilities.saveQuery, + appName: 'dashboard', useDefaultBehaviors: true, + visible: printMode !== true, savedQuery: state.savedQuery, savedQueryId: dashboardState.savedQuery, - visible: printMode !== true, + indexPatterns: dashboardAppState.dataViews, + config: showTopNavMenu ? topNav : undefined, + setMenuMountPoint: embedSettings ? undefined : setHeaderActionMenu, + className: isFullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined, onQuerySubmit: (_payload, isUpdate) => { if (isUpdate === false) { dashboardAppState.$triggerDashboardRefresh.next({ force: true }); diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx b/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx index ad9d85926a94..7e76ef1789a3 100644 --- a/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx +++ b/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx @@ -10,10 +10,9 @@ import { Capabilities } from '@kbn/core/public'; import { DashboardState } from '../../types'; import { DashboardAppLocatorParams } from '../..'; +import { pluginServices } from '../../services/plugin_services'; import { stateToRawDashboardState } from '../lib/convert_dashboard_state'; -import { getSavedDashboardMock } from '../test_helpers'; import { showPublicUrlSwitch, ShowShareModal, ShowShareModalProps } from './show_share_modal'; -import { pluginServices } from '../../services/plugin_services'; describe('showPublicUrlSwitch', () => { test('returns false if "dashboard" app is not available', () => { @@ -75,9 +74,8 @@ describe('ShowShareModal', () => { .mockReturnValue(unsavedState); return { isDirty: true, - savedDashboard: getSavedDashboardMock(), anchorElement: document.createElement('div'), - currentDashboardState: { panels: {} } as unknown as DashboardState, + currentDashboardState: { panels: {} } as DashboardState, }; }; @@ -139,7 +137,7 @@ describe('ShowShareModal', () => { }); unsavedStateKeys.forEach((key) => { expect(shareLocatorParams[key]).toStrictEqual( - (rawDashboardState as Partial)[key] + (rawDashboardState as unknown as Partial)[key] ); }); }); diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx index f93061f56d6e..0f7d119427dd 100644 --- a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx +++ b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx @@ -9,28 +9,26 @@ import moment from 'moment'; import React, { ReactElement, useState } from 'react'; -import { EuiCheckboxGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { EuiCheckboxGroup } from '@elastic/eui'; import type { Capabilities } from '@kbn/core/public'; -import type { SerializableControlGroupInput } from '@kbn/controls-plugin/common'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import { setStateToKbnUrl, unhashUrl } from '@kbn/kibana-utils-plugin/public'; +import type { SerializableControlGroupInput } from '@kbn/controls-plugin/common'; -import type { DashboardSavedObject } from '../..'; -import { shareModalStrings } from '../../dashboard_strings'; -import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../../locator'; import type { DashboardState } from '../../types'; import { dashboardUrlParams } from '../dashboard_router'; -import { stateToRawDashboardState } from '../lib/convert_dashboard_state'; -import { convertPanelMapToSavedPanels } from '../lib/convert_dashboard_panels'; +import { shareModalStrings } from '../../dashboard_strings'; +import { convertPanelMapToSavedPanels } from '../../../common'; import { pluginServices } from '../../services/plugin_services'; +import { stateToRawDashboardState } from '../lib/convert_dashboard_state'; +import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../../locator'; const showFilterBarId = 'showFilterBar'; export interface ShowShareModalProps { isDirty: boolean; anchorElement: HTMLElement; - savedDashboard: DashboardSavedObject; currentDashboardState: DashboardState; } @@ -45,7 +43,6 @@ export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => export function ShowShareModal({ isDirty, anchorElement, - savedDashboard, currentDashboardState, }: ShowShareModalProps) { const { @@ -59,6 +56,7 @@ export function ShowShareModal({ }, }, share: { toggleShareContextMenu }, + initializerContext: { kibanaVersion }, } = pluginServices.getServices(); if (!toggleShareContextMenu) return; // TODO: Make this logic cleaner once share is an optional service @@ -124,7 +122,9 @@ export function ShowShareModal({ DashboardAppLocatorParams, 'options' | 'query' | 'savedQuery' | 'filters' | 'panels' | 'controlGroupInput' > = {}; - const unsavedDashboardState = dashboardSessionStorage.getState(savedDashboard.id); + const { savedObjectId, title } = currentDashboardState; + const unsavedDashboardState = dashboardSessionStorage.getState(savedObjectId); + if (unsavedDashboardState) { unsavedStateForLocator = { query: unsavedDashboardState.query, @@ -133,13 +133,16 @@ export function ShowShareModal({ savedQuery: unsavedDashboardState.savedQuery, controlGroupInput: unsavedDashboardState.controlGroupInput as SerializableControlGroupInput, panels: unsavedDashboardState.panels - ? convertPanelMapToSavedPanels(unsavedDashboardState.panels) + ? (convertPanelMapToSavedPanels( + unsavedDashboardState.panels, + kibanaVersion + ) as DashboardAppLocatorParams['panels']) : undefined, }; } const locatorParams: DashboardAppLocatorParams = { - dashboardId: savedDashboard.id, + dashboardId: savedObjectId, preserveSavedFilters: true, refreshInterval: undefined, // We don't share refresh interval externally viewMode: ViewMode.VIEW, // For share locators we always load the dashboard in view mode @@ -161,11 +164,11 @@ export function ShowShareModal({ { useHash: false, storeInHashQuery: true }, unhashUrl(window.location.href) ), - objectId: savedDashboard.id, + objectId: savedObjectId, objectType: 'dashboard', sharingData: { title: - savedDashboard.title || + title || i18n.translate('dashboard.share.defaultDashboardTitle', { defaultMessage: 'Dashboard [{date}]', values: { date: moment().toISOString(true) }, diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts index badc14ddaee6..9bbe97681032 100644 --- a/src/plugins/dashboard/public/dashboard_constants.ts +++ b/src/plugins/dashboard/public/dashboard_constants.ts @@ -6,9 +6,18 @@ * Side Public License, v 1. */ +import { ViewMode } from '@kbn/embeddable-plugin/common'; +import type { DashboardState } from './types'; + export const DASHBOARD_STATE_STORAGE_KEY = '_a'; export const GLOBAL_STATE_STORAGE_KEY = '_g'; +export const DASHBOARD_GRID_COLUMN_COUNT = 48; +export const DASHBOARD_GRID_HEIGHT = 20; +export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2; +export const DEFAULT_PANEL_HEIGHT = 15; +export const DASHBOARD_CONTAINER_TYPE = 'dashboard'; + export const DashboardConstants = { LANDING_PAGE_PATH: '/list', CREATE_NEW_DASHBOARD_URL: '/create', @@ -18,11 +27,37 @@ export const DashboardConstants = { ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', DASHBOARDS_ID: 'dashboards', DASHBOARD_ID: 'dashboard', + DASHBOARD_SAVED_OBJECT_TYPE: 'dashboard', SEARCH_SESSION_ID: 'searchSessionId', CHANGE_CHECK_DEBOUNCE: 100, CHANGE_APPLY_DEBOUNCE: 50, }; +export const defaultDashboardState: DashboardState = { + viewMode: ViewMode.EDIT, // new dashboards start in edit mode. + fullScreenMode: false, + timeRestore: false, + query: { query: '', language: 'kuery' }, + description: '', + filters: [], + panels: {}, + title: '', + tags: [], + options: { + useMargins: true, + syncColors: false, + syncTooltips: false, + hidePanelTitles: false, + }, +}; + +export const getFullPath = (aliasId?: string, id?: string) => + `/app/dashboards#${createDashboardEditUrl(aliasId || id)}`; + +export const getFullEditPath = (id?: string, editMode?: boolean) => { + return `/app/dashboards#${createDashboardEditUrl(id, editMode)}`; +}; + export function createDashboardEditUrl(id?: string, editMode?: boolean) { if (!id) { return `${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`; diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index 5679ac28f838..6474c7dc2bba 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -382,7 +382,7 @@ export const panelStorageErrorStrings = { }), }; -export const dashboardLoadingErrorStrings = { +export const dashboardSavedObjectErrorStrings = { getDashboardLoadError: (message: string) => i18n.translate('dashboard.loadingError.errorMessage', { defaultMessage: 'Error encountered while loading saved dashboard: {message}', @@ -393,6 +393,14 @@ export const dashboardLoadingErrorStrings = { defaultMessage: 'Unable to load dashboard: {message}', values: { message }, }), + getErrorDeletingDashboardToast: () => + i18n.translate('dashboard.deleteError.toastDescription', { + defaultMessage: 'Error encountered while deleting dashboard', + }), + getPanelTooOldError: () => + i18n.translate('dashboard.loadURLError.PanelTooOld', { + defaultMessage: 'Cannot load panels from a URL created in a version older than 7.3', + }), }; /* @@ -432,7 +440,7 @@ export const emptyScreenStrings = { /* Dashboard Listing Page */ -export const dashboardListingTable = { +export const dashboardListingTableStrings = { getEntityName: () => i18n.translate('dashboard.listing.table.entityName', { defaultMessage: 'dashboard', @@ -450,8 +458,8 @@ export const dashboardUnsavedListingStrings = { defaultMessage: 'You have unsaved changes in the following {dash}:', values: { dash: plural - ? dashboardListingTable.getEntityNamePlural() - : dashboardListingTable.getEntityName(), + ? dashboardListingTableStrings.getEntityNamePlural() + : dashboardListingTableStrings.getEntityName(), }, }), getLoadingTitle: () => diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index 7cb54db209c1..598940bbd666 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -9,11 +9,7 @@ import { PluginInitializerContext } from '@kbn/core/public'; import { DashboardPlugin } from './plugin'; -export { - DashboardContainer, - DashboardContainerFactoryDefinition, - DASHBOARD_CONTAINER_TYPE, -} from './application'; +export { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin'; @@ -23,7 +19,6 @@ export { cleanEmptyKeys, } from './locator'; -export type { DashboardSavedObject } from './saved_dashboards'; export type { SavedDashboardPanel, DashboardContainerInput } from './types'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts index e3cd3f159f73..a66015afcb00 100644 --- a/src/plugins/dashboard/public/locator.ts +++ b/src/plugins/dashboard/public/locator.ts @@ -94,7 +94,7 @@ export type DashboardAppLocatorParams = { /** * List of dashboard panels */ - panels?: SavedDashboardPanel[]; + panels?: Array; // used SerializableRecord here to force the GridData type to be read as serializable /** * Saved query ID diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 9d1df9d2acb1..a0d35e2c7be4 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -9,73 +9,56 @@ import { BehaviorSubject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; -import { Start as InspectorStartContract } from '@kbn/inspector-plugin/public'; -import type { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; -import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import { App, Plugin, - type CoreSetup, - type CoreStart, AppUpdater, ScopedHistory, + type CoreSetup, + type CoreStart, AppMountParameters, DEFAULT_APP_CATEGORIES, PluginInitializerContext, SavedObjectsClientContract, } from '@kbn/core/public'; -import { - CONTEXT_MENU_TRIGGER, - EmbeddableSetup, - EmbeddableStart, - PANEL_BADGE_TRIGGER, - PANEL_NOTIFICATION_TRIGGER, -} from '@kbn/embeddable-plugin/public'; import type { ScreenshotModePluginSetup, ScreenshotModePluginStart, } from '@kbn/screenshot-mode-plugin/public'; -import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; -import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/public'; -import { createKbnUrlTracker } from '@kbn/kibana-utils-plugin/public'; -import type { VisualizationsStart } from '@kbn/visualizations-plugin/public'; -import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { UsageCollectionSetup, UsageCollectionStart, } from '@kbn/usage-collection-plugin/public'; +import { APP_WRAPPER_CLASS } from '@kbn/core/public'; +import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/public'; +import { createKbnUrlTracker } from '@kbn/kibana-utils-plugin/public'; + +import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; +import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import type { SavedObjectsStart } from '@kbn/saved-objects-plugin/public'; +import type { VisualizationsStart } from '@kbn/visualizations-plugin/public'; +import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; +import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public'; import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public'; -import { getSavedObjectFinder, type SavedObjectsStart } from '@kbn/saved-objects-plugin/public'; -import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { - ClonePanelAction, - createDashboardContainerByValueRenderer, - DASHBOARD_CONTAINER_TYPE, - DashboardContainerFactory, + type DashboardContainerFactory, DashboardContainerFactoryDefinition, - ExpandPanelAction, - ReplacePanelAction, - UnlinkFromLibraryAction, - AddToLibraryAction, - LibraryNotificationAction, - CopyToDashboardAction, -} from './application'; -import { SavedObjectLoader } from './services/saved_object_loader'; -import { DashboardAppLocatorDefinition, DashboardAppLocator } from './locator'; -import { createSavedDashboardLoader } from './saved_dashboards'; -import { DashboardConstants } from './dashboard_constants'; -import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; -import { ExportCSVAction } from './application/actions/export_csv_action'; -import { dashboardFeatureCatalog } from './dashboard_strings'; -import { FiltersNotificationBadge } from './application/actions/filters_notification_badge'; + createDashboardContainerByValueRenderer, +} from './application/embeddable'; import type { DashboardMountContextProps } from './types'; +import { dashboardFeatureCatalog } from './dashboard_strings'; +import { type DashboardAppLocator, DashboardAppLocatorDefinition } from './locator'; +import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; +import { DashboardConstants, DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; export interface DashboardFeatureFlagConfig { allowByValueEmbeddables: boolean; @@ -118,7 +101,6 @@ export interface DashboardSetup { } export interface DashboardStart { - getSavedDashboardLoader: () => SavedObjectLoader; getDashboardContainerByValueRenderer: () => ReturnType< typeof createDashboardContainerByValueRenderer >; @@ -159,9 +141,14 @@ export class DashboardPlugin new DashboardAppLocatorDefinition({ useHashedUrl: core.uiSettings.get('state:storeInSessionStorage'), getDashboardFilterFields: async (dashboardId: string) => { - const [, , selfStart] = await core.getStartServices(); - const dashboard = await selfStart.getSavedDashboardLoader().get(dashboardId); - return dashboard?.searchSource?.getField('filter') ?? []; + const { pluginServices } = await import('./services/plugin_services'); + const { + dashboardSavedObject: { loadDashboardStateFromSavedObject }, + } = pluginServices.getServices(); + return ( + (await loadDashboardStateFromSavedObject({ id: dashboardId })).dashboardState + ?.filters ?? [] + ); }, }) ); @@ -305,58 +292,16 @@ export class DashboardPlugin } public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart { - const { uiSettings } = core; - const { uiActions, share, presentationUtil } = plugins; - this.startDashboardKibanaServices(core, plugins, this.initializerContext).then(() => { - const clonePanelAction = new ClonePanelAction(core.savedObjects); - uiActions.registerAction(clonePanelAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id); - - const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, uiSettings); - const changeViewAction = new ReplacePanelAction(SavedObjectFinder); - uiActions.registerAction(changeViewAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction.id); - - const panelLevelFiltersNotification = new FiltersNotificationBadge(); - uiActions.registerAction(panelLevelFiltersNotification); - uiActions.attachAction(PANEL_BADGE_TRIGGER, panelLevelFiltersNotification.id); - - if (share) { - const ExportCSVPlugin = new ExportCSVAction(); - uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, ExportCSVPlugin); - } - - if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) { - const addToLibraryAction = new AddToLibraryAction(); - uiActions.registerAction(addToLibraryAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id); - - const unlinkFromLibraryAction = new UnlinkFromLibraryAction(); - uiActions.registerAction(unlinkFromLibraryAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id); - - const libraryNotificationAction = new LibraryNotificationAction(unlinkFromLibraryAction); - uiActions.registerAction(libraryNotificationAction); - uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); - - const copyToDashboardAction = new CopyToDashboardAction(presentationUtil.ContextProvider); - uiActions.registerAction(copyToDashboardAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, copyToDashboardAction.id); - } - }); - - const expandPanelAction = new ExpandPanelAction(); // this action does't rely on any services - uiActions.registerAction(expandPanelAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction.id); - - const savedDashboardLoader = createSavedDashboardLoader({ - savedObjectsClient: core.savedObjects.client, - savedObjects: plugins.savedObjects, - embeddableStart: plugins.embeddable, + this.startDashboardKibanaServices(core, plugins, this.initializerContext).then(async () => { + const { buildAllDashboardActions } = await import('./application/actions'); + buildAllDashboardActions({ + core, + plugins, + allowByValueEmbeddables: this.dashboardFeatureFlagConfig?.allowByValueEmbeddables, + }); }); return { - getSavedDashboardLoader: () => savedDashboardLoader, getDashboardContainerByValueRenderer: () => { const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory(DASHBOARD_CONTAINER_TYPE); diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts deleted file mode 100644 index c23b6eb2a87e..000000000000 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { assign, cloneDeep } from 'lodash'; -import { SavedObjectsClientContract } from '@kbn/core/public'; -import type { ResolvedSimpleSavedObject } from '@kbn/core/public'; -import { SavedObjectAttributes, SavedObjectReference } from '@kbn/core/types'; -import { RawControlGroupAttributes } from '@kbn/controls-plugin/common'; -import { EmbeddableStart } from '@kbn/embeddable-plugin/public'; -import { ISearchSource } from '@kbn/data-plugin/common'; -import { RefreshInterval } from '@kbn/data-plugin/public'; -import { Query, Filter } from '@kbn/es-query'; -import type { SavedObject, SavedObjectsStart } from '@kbn/saved-objects-plugin/public'; - -import { createDashboardEditUrl } from '../dashboard_constants'; -import { extractReferences, injectReferences } from '../../common/saved_dashboard_references'; - -import { DashboardOptions } from '../types'; - -export interface DashboardSavedObject extends SavedObject { - id?: string; - timeRestore: boolean; - timeTo?: string; - timeFrom?: string; - description?: string; - panelsJSON: string; - optionsJSON?: string; - // TODO: write a migration to rid of this, it's only around for bwc. - uiStateJSON?: string; - lastSavedTitle: string; - refreshInterval?: RefreshInterval; - searchSource: ISearchSource; - getQuery(): Query; - getFilters(): Filter[]; - getFullEditPath: (editMode?: boolean) => string; - outcome?: ResolvedSimpleSavedObject['outcome']; - aliasId?: ResolvedSimpleSavedObject['alias_target_id']; - aliasPurpose?: ResolvedSimpleSavedObject['alias_purpose']; - - controlGroupInput?: Omit; -} - -const defaults = { - title: '', - hits: 0, - description: '', - panelsJSON: '[]', - optionsJSON: JSON.stringify({ - // for BWC reasons we can't default dashboards that already exist without this setting to true. - useMargins: true, - syncColors: false, - syncTooltips: false, - hidePanelTitles: false, - } as DashboardOptions), - version: 1, - timeRestore: false, - timeTo: undefined, - timeFrom: undefined, - refreshInterval: undefined, -}; - -// Used only by the savedDashboards service, usually no reason to change this -export function createSavedDashboardClass( - savedObjectStart: SavedObjectsStart, - embeddableStart: EmbeddableStart, - savedObjectsClient: SavedObjectsClientContract -): new (id: string) => DashboardSavedObject { - class SavedDashboard extends savedObjectStart.SavedObjectClass { - // save these objects with the 'dashboard' type - public static type = 'dashboard'; - - // if type:dashboard has no mapping, we push this mapping into ES - public static mapping = { - title: 'text', - hits: 'integer', - description: 'text', - panelsJSON: 'text', - optionsJSON: 'text', - version: 'integer', - timeRestore: 'boolean', - timeTo: 'keyword', - timeFrom: 'keyword', - refreshInterval: { - type: 'object', - properties: { - display: { type: 'keyword' }, - pause: { type: 'boolean' }, - section: { type: 'integer' }, - value: { type: 'integer' }, - }, - }, - controlGroupInput: { - type: 'object', - properties: { - controlStyle: { type: 'keyword' }, - panelsJSON: { type: 'text' }, - }, - }, - }; - public static fieldOrder = ['title', 'description']; - public static searchSource = true; - public showInRecentlyAccessed = true; - - public outcome?: ResolvedSimpleSavedObject['outcome']; - public aliasId?: ResolvedSimpleSavedObject['alias_target_id']; - public aliasPurpose?: ResolvedSimpleSavedObject['alias_purpose']; - - constructor(arg: { id: string; useResolve: boolean } | string) { - super({ - type: SavedDashboard.type, - mapping: SavedDashboard.mapping, - searchSource: SavedDashboard.searchSource, - extractReferences: (opts: { - attributes: SavedObjectAttributes; - references: SavedObjectReference[]; - }) => extractReferences(opts, { embeddablePersistableStateService: embeddableStart }), - injectReferences: (so: DashboardSavedObject, references: SavedObjectReference[]) => { - const newAttributes = injectReferences( - { attributes: so._serialize().attributes, references }, - { - embeddablePersistableStateService: embeddableStart, - } - ); - Object.assign(so, newAttributes); - }, - - // if this is null/undefined then the SavedObject will be assigned the defaults - id: typeof arg === 'object' ? arg.id : arg, - - // default values that will get assigned if the doc is new - defaults, - }); - - const id: string = typeof arg === 'object' ? arg.id : arg; - const useResolve = typeof arg === 'object' ? arg.useResolve : false; - - this.getFullPath = () => `/app/dashboards#${createDashboardEditUrl(this.aliasId || this.id)}`; - - // Overwrite init if we want to use resolve - if (useResolve || true) { - this.init = async () => { - const esType = SavedDashboard.type; - // ensure that the esType is defined - if (!esType) throw new Error('You must define a type name to use SavedObject objects.'); - - if (!id) { - // just assign the defaults and be done - assign(this, defaults); - await this.hydrateIndexPattern!(); - - return this; - } - - const { - outcome, - alias_target_id: aliasId, - alias_purpose: aliasPurpose, - saved_object: resp, - } = await savedObjectsClient.resolve(esType, id); - - const respMapped = { - _id: resp.id, - _type: resp.type, - _source: cloneDeep(resp.attributes), - references: resp.references, - found: !!resp._version, - }; - - this.outcome = outcome; - this.aliasId = aliasId; - this.aliasPurpose = aliasPurpose; - await this.applyESResp(respMapped); - - return this; - }; - } - } - - getQuery() { - return this.searchSource!.getOwnField('query') || { query: '', language: 'kuery' }; - } - - getFilters() { - return this.searchSource!.getOwnField('filter') || []; - } - - getFullEditPath = (editMode?: boolean) => { - return `/app/dashboards#${createDashboardEditUrl(this.id, editMode)}`; - }; - } - - // Unfortunately this throws a typescript error without the casting. I think it's due to the - // convoluted way SavedObjects are created. - return SavedDashboard as unknown as new (id: string) => DashboardSavedObject; -} diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts deleted file mode 100644 index a154fdad9656..000000000000 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { SavedObjectsClientContract } from '@kbn/core/public'; -import { EmbeddableStart } from '@kbn/embeddable-plugin/public'; -import type { SavedObjectsStart } from '@kbn/saved-objects-plugin/public'; - -import { SavedObjectLoader } from '../services/saved_object_loader'; -import { createSavedDashboardClass } from './saved_dashboard'; - -interface Services { - savedObjectsClient: SavedObjectsClientContract; - savedObjects: SavedObjectsStart; - embeddableStart: EmbeddableStart; -} - -/** - * @param services - */ -export function createSavedDashboardLoader({ - savedObjects, - savedObjectsClient, - embeddableStart, -}: Services) { - const SavedDashboard = createSavedDashboardClass( - savedObjects, - embeddableStart, - savedObjectsClient - ); - return new SavedObjectLoader(SavedDashboard, savedObjectsClient); -} diff --git a/src/plugins/dashboard/public/services/dashboard_saved_object/dashboard_saved_object.stub.ts b/src/plugins/dashboard/public/services/dashboard_saved_object/dashboard_saved_object.stub.ts new file mode 100644 index 000000000000..c23a76746404 --- /dev/null +++ b/src/plugins/dashboard/public/services/dashboard_saved_object/dashboard_saved_object.stub.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { savedObjectsServiceMock } from '@kbn/core/public/mocks'; +import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import { DashboardAttributes } from '../../application'; +import { FindDashboardSavedObjectsResponse } from './lib/find_dashboard_saved_objects'; + +import { DashboardSavedObjectService } from './types'; +import { LoadDashboardFromSavedObjectReturn } from './lib/load_dashboard_state_from_saved_object'; + +type DashboardSavedObjectServiceFactory = PluginServiceFactory; + +export const dashboardSavedObjectServiceFactory: DashboardSavedObjectServiceFactory = () => { + const { client: savedObjectsClient } = savedObjectsServiceMock.createStartContract(); + return { + loadDashboardStateFromSavedObject: jest.fn().mockImplementation(() => + Promise.resolve({ + dashboardState: {}, + } as LoadDashboardFromSavedObjectReturn) + ), + saveDashboardStateToSavedObject: jest.fn(), + findDashboards: { + findSavedObjects: jest.fn().mockImplementation(({ search, size }) => { + const sizeToUse = size ?? 10; + const hits: FindDashboardSavedObjectsResponse['hits'] = []; + for (let i = 0; i < sizeToUse; i++) { + hits.push({ + type: 'dashboard', + id: `dashboard${i}`, + attributes: { + description: `dashboard${i} desc`, + title: `dashboard${i} - ${search} - title`, + }, + } as FindDashboardSavedObjectsResponse['hits'][0]); + } + return Promise.resolve({ + total: sizeToUse, + hits, + }); + }), + findByIds: jest.fn().mockImplementation(() => + Promise.resolve([ + { + id: `dashboardUnsavedOne`, + status: 'success', + attributes: { + title: `Dashboard Unsaved One`, + } as unknown as DashboardAttributes, + }, + { + id: `dashboardUnsavedTwo`, + status: 'success', + attributes: { + title: `Dashboard Unsaved Two`, + } as unknown as DashboardAttributes, + }, + { + id: `dashboardUnsavedThree`, + status: 'success', + attributes: { + title: `Dashboard Unsaved Three`, + } as unknown as DashboardAttributes, + }, + ]) + ), + findByTitle: jest.fn(), + }, + checkForDuplicateDashboardTitle: jest.fn(), + savedObjectsClient, + }; +}; diff --git a/src/plugins/dashboard/public/services/dashboard_saved_object/dashboard_saved_object_service.ts b/src/plugins/dashboard/public/services/dashboard_saved_object/dashboard_saved_object_service.ts new file mode 100644 index 000000000000..7fb558309936 --- /dev/null +++ b/src/plugins/dashboard/public/services/dashboard_saved_object/dashboard_saved_object_service.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; + +import type { DashboardStartDependencies } from '../../plugin'; +import { checkForDuplicateDashboardTitle } from './lib/check_for_duplicate_dashboard_title'; +import { + findDashboardIdByTitle, + findDashboardSavedObjects, + findDashboardSavedObjectsByIds, +} from './lib/find_dashboard_saved_objects'; +import { loadDashboardStateFromSavedObject } from './lib/load_dashboard_state_from_saved_object'; +import { saveDashboardStateToSavedObject } from './lib/save_dashboard_state_to_saved_object'; +import type { DashboardSavedObjectRequiredServices, DashboardSavedObjectService } from './types'; + +export type DashboardSavedObjectServiceFactory = KibanaPluginServiceFactory< + DashboardSavedObjectService, + DashboardStartDependencies, + DashboardSavedObjectRequiredServices +>; + +export const dashboardSavedObjectServiceFactory: DashboardSavedObjectServiceFactory = ( + { coreStart }, + requiredServices +) => { + const { + savedObjects: { client: savedObjectsClient }, + } = coreStart; + + return { + loadDashboardStateFromSavedObject: ({ id, getScopedHistory }) => + loadDashboardStateFromSavedObject({ + id, + getScopedHistory, + savedObjectsClient, + ...requiredServices, + }), + saveDashboardStateToSavedObject: ({ currentState, redirectTo, saveOptions }) => + saveDashboardStateToSavedObject({ + redirectTo, + saveOptions, + currentState, + savedObjectsClient, + ...requiredServices, + }), + findDashboards: { + findSavedObjects: ({ hasReference, search, size }) => + findDashboardSavedObjects({ hasReference, search, size, savedObjectsClient }), + findByIds: (ids) => findDashboardSavedObjectsByIds(savedObjectsClient, ids), + findByTitle: (title) => findDashboardIdByTitle(title, savedObjectsClient), + }, + checkForDuplicateDashboardTitle: (props) => + checkForDuplicateDashboardTitle(props, savedObjectsClient), + savedObjectsClient, + }; +}; diff --git a/src/plugins/dashboard/public/services/dashboard_saved_object/lib/check_for_duplicate_dashboard_title.ts b/src/plugins/dashboard/public/services/dashboard_saved_object/lib/check_for_duplicate_dashboard_title.ts new file mode 100644 index 000000000000..e03345e78c41 --- /dev/null +++ b/src/plugins/dashboard/public/services/dashboard_saved_object/lib/check_for_duplicate_dashboard_title.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectsClientContract } from '@kbn/core/public'; + +import { DashboardConstants } from '../../..'; +import type { DashboardAttributes } from '../../../application'; + +export interface DashboardDuplicateTitleCheckProps { + title: string; + copyOnSave: boolean; + lastSavedTitle: string; + onTitleDuplicate: () => void; + isTitleDuplicateConfirmed: boolean; +} + +/** + * check for an existing dashboard with the same title in ES + * returns Promise when there is no duplicate, or runs the provided onTitleDuplicate + * function when the title already exists + */ +export async function checkForDuplicateDashboardTitle( + { + title, + copyOnSave, + lastSavedTitle, + onTitleDuplicate, + isTitleDuplicateConfirmed, + }: DashboardDuplicateTitleCheckProps, + savedObjectsClient: SavedObjectsClientContract +): Promise { + // Don't check for duplicates if user has already confirmed save with duplicate title + if (isTitleDuplicateConfirmed) { + return true; + } + + // Don't check if the user isn't updating the title, otherwise that would become very annoying to have + // to confirm the save every time, except when copyOnSave is true, then we do want to check. + if (title === lastSavedTitle && !copyOnSave) { + return true; + } + const response = await savedObjectsClient.find({ + perPage: 10, + fields: ['title'], + search: `"${title}"`, + searchFields: ['title'], + type: DashboardConstants.DASHBOARD_SAVED_OBJECT_TYPE, + }); + const duplicate = response.savedObjects.find( + (obj) => obj.get('title').toLowerCase() === title.toLowerCase() + ); + if (!duplicate) { + return true; + } + onTitleDuplicate?.(); + return false; +} diff --git a/src/plugins/dashboard/public/services/dashboard_saved_object/lib/find_dashboard_saved_objects.ts b/src/plugins/dashboard/public/services/dashboard_saved_object/lib/find_dashboard_saved_objects.ts new file mode 100644 index 000000000000..c24511f56d3e --- /dev/null +++ b/src/plugins/dashboard/public/services/dashboard_saved_object/lib/find_dashboard_saved_objects.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + SavedObjectError, + SavedObjectsClientContract, + SavedObjectsFindOptionsReference, + SimpleSavedObject, +} from '@kbn/core/public'; + +import { DashboardConstants } from '../../..'; +import type { DashboardAttributes } from '../../../application'; + +export interface FindDashboardSavedObjectsArgs { + hasReference?: SavedObjectsFindOptionsReference[]; + savedObjectsClient: SavedObjectsClientContract; + search: string; + size: number; +} + +export interface FindDashboardSavedObjectsResponse { + total: number; + hits: Array>; +} + +export async function findDashboardSavedObjects({ + savedObjectsClient, + hasReference, + search, + size, +}: FindDashboardSavedObjectsArgs): Promise { + const { total, savedObjects } = await savedObjectsClient.find({ + type: DashboardConstants.DASHBOARD_SAVED_OBJECT_TYPE, + search: search ? `${search}*` : undefined, + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND' as 'AND', + perPage: size, + hasReference, + page: 1, + }); + return { + total, + hits: savedObjects, + }; +} + +export type FindDashboardBySavedObjectIdsResult = { id: string } & ( + | { status: 'success'; attributes: DashboardAttributes } + | { status: 'error'; error: SavedObjectError } +); + +export async function findDashboardSavedObjectsByIds( + savedObjectsClient: SavedObjectsClientContract, + ids: string[] +): Promise { + const { savedObjects } = await savedObjectsClient.bulkGet( + ids.map((id) => ({ id, type: DashboardConstants.DASHBOARD_SAVED_OBJECT_TYPE })) + ); + + return savedObjects.map((savedObjectResult) => { + if (savedObjectResult.error) + return { status: 'error', error: savedObjectResult.error, id: savedObjectResult.id }; + const { attributes, id } = savedObjectResult; + return { + id, + status: 'success', + attributes: attributes as DashboardAttributes, + }; + }); +} + +export async function findDashboardIdByTitle( + title: string, + savedObjectsClient: SavedObjectsClientContract +): Promise<{ id: string } | undefined> { + const results = await savedObjectsClient.find({ + search: `"${title}"`, + searchFields: ['title'], + type: 'dashboard', + }); + // The search isn't an exact match, lets see if we can find a single exact match to use + const matchingDashboards = results.savedObjects.filter( + (dashboard) => dashboard.attributes.title.toLowerCase() === title.toLowerCase() + ); + if (matchingDashboards.length === 1) { + return { id: matchingDashboards[0].id }; + } +} diff --git a/src/plugins/dashboard/public/services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object.ts b/src/plugins/dashboard/public/services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object.ts new file mode 100644 index 000000000000..963814013dc3 --- /dev/null +++ b/src/plugins/dashboard/public/services/dashboard_saved_object/lib/load_dashboard_state_from_saved_object.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { ReactElement } from 'react'; + +import { Filter } from '@kbn/es-query'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public'; +import { rawControlGroupAttributesToControlGroupInput } from '@kbn/controls-plugin/common'; +import { parseSearchSourceJSON, injectSearchSourceReferences } from '@kbn/data-plugin/public'; +import { SavedObjectAttributes, SavedObjectsClientContract, ScopedHistory } from '@kbn/core/public'; + +import { migrateLegacyQuery } from '../../../application/lib/migrate_legacy_query'; + +import { + DashboardConstants, + defaultDashboardState, + createDashboardEditUrl, +} from '../../../dashboard_constants'; +import type { DashboardAttributes } from '../../../application'; +import { DashboardSavedObjectRequiredServices } from '../types'; +import { DashboardOptions, DashboardState } from '../../../types'; +import { cleanFiltersForSerialize } from '../../../application/lib'; +import { convertSavedPanelsToPanelMap, injectReferences } from '../../../../common'; + +export type LoadDashboardFromSavedObjectProps = DashboardSavedObjectRequiredServices & { + id?: string; + getScopedHistory?: () => ScopedHistory; + savedObjectsClient: SavedObjectsClientContract; +}; + +export interface LoadDashboardFromSavedObjectReturn { + redirectedToAlias?: boolean; + dashboardState?: DashboardState; + createConflictWarning?: () => ReactElement | undefined; +} + +type SuccessfulLoadDashboardFromSavedObjectReturn = LoadDashboardFromSavedObjectReturn & { + dashboardState: DashboardState; +}; + +export const dashboardStateLoadWasSuccessful = ( + incoming?: LoadDashboardFromSavedObjectReturn +): incoming is SuccessfulLoadDashboardFromSavedObjectReturn => { + return Boolean(incoming && incoming?.dashboardState && !incoming.redirectedToAlias); +}; + +export const loadDashboardStateFromSavedObject = async ({ + savedObjectsTagging, + savedObjectsClient, + getScopedHistory, + screenshotMode, + embeddable, + spaces, + data, + id, +}: LoadDashboardFromSavedObjectProps): Promise => { + const { + search: dataSearchService, + query: { queryString }, + } = data; + + /** + * This is a newly created dashboard, so there is no saved object state to load. + */ + if (!id) return { dashboardState: defaultDashboardState }; + + /** + * Load the saved object + */ + const { + outcome, + alias_purpose: aliasPurpose, + alias_target_id: aliasId, + saved_object: rawDashboardSavedObject, + } = await savedObjectsClient.resolve( + DashboardConstants.DASHBOARD_SAVED_OBJECT_TYPE, + id + ); + if (!rawDashboardSavedObject._version) { + throw new SavedObjectNotFound(DashboardConstants.DASHBOARD_SAVED_OBJECT_TYPE, id); + } + + /** + * Inject saved object references back into the saved object attributes + */ + const { references, attributes: rawAttributes } = rawDashboardSavedObject; + const attributes = (() => { + if (!references || references.length === 0) return rawAttributes; + return injectReferences( + { references, attributes: rawAttributes as unknown as SavedObjectAttributes }, + { + embeddablePersistableStateService: embeddable, + } + ) as unknown as DashboardAttributes; + })(); + + /** + * Handle saved object resolve alias outcome by redirecting + */ + const scopedHistory = getScopedHistory?.(); + if (scopedHistory && outcome === 'aliasMatch' && id && aliasId) { + const path = scopedHistory.location.hash.replace(id, aliasId); + if (screenshotMode.isScreenshotMode()) { + scopedHistory.replace(path); + } else { + await spaces.redirectLegacyUrl?.({ path, aliasPurpose }); + } + return { redirectedToAlias: true }; + } + + /** + * Create conflict warning component if there is a saved object id conflict + */ + const createConflictWarning = + scopedHistory && outcome === 'conflict' && aliasId + ? () => + spaces.getLegacyUrlConflict?.({ + currentObjectId: id, + otherObjectId: aliasId, + otherObjectPath: `#${createDashboardEditUrl(aliasId)}${scopedHistory.location.search}`, + }) + : undefined; + + /** + * Create search source and pull filters and query from it. + */ + const searchSourceJSON = attributes.kibanaSavedObjectMeta.searchSourceJSON; + const searchSource = await (async () => { + if (!searchSourceJSON) { + return await dataSearchService.searchSource.create(); + } + try { + let searchSourceValues = parseSearchSourceJSON(searchSourceJSON); + searchSourceValues = injectSearchSourceReferences(searchSourceValues as any, references); + return await dataSearchService.searchSource.create(searchSourceValues); + } catch (error: any) { + return await dataSearchService.searchSource.create(); + } + })(); + + const filters = cleanFiltersForSerialize((searchSource?.getOwnField('filter') as Filter[]) ?? []); + + const query = migrateLegacyQuery( + searchSource?.getOwnField('query') || queryString.getDefaultQuery() // TODO SAVED DASHBOARDS determine if migrateLegacyQuery is still needed + ); + + const { + refreshInterval, + description, + timeRestore, + optionsJSON, + panelsJSON, + timeFrom, + timeTo, + title, + } = attributes; + + const timeRange = + timeRestore && timeFrom && timeTo + ? { + from: timeFrom, + to: timeTo, + } + : undefined; + + /** + * Parse panels and options from JSON + */ + const options: DashboardOptions = optionsJSON ? JSON.parse(optionsJSON) : undefined; + const panels = convertSavedPanelsToPanelMap(panelsJSON ? JSON.parse(panelsJSON) : []); + + return { + createConflictWarning, + dashboardState: { + ...defaultDashboardState, + + savedObjectId: id, + refreshInterval, + timeRestore, + description, + timeRange, + options, + filters, + panels, + title, + query, + + viewMode: ViewMode.VIEW, // dashboards loaded from saved object default to view mode. If it was edited recently, the view mode from session storage will override this. + tags: savedObjectsTagging.getTagIdsFromReferences?.(references) ?? [], + + controlGroupInput: + attributes.controlGroupInput && + rawControlGroupAttributesToControlGroupInput(attributes.controlGroupInput), + }, + }; +}; diff --git a/src/plugins/dashboard/public/services/dashboard_saved_object/lib/save_dashboard_state_to_saved_object.ts b/src/plugins/dashboard/public/services/dashboard_saved_object/lib/save_dashboard_state_to_saved_object.ts new file mode 100644 index 000000000000..11c6988d22f9 --- /dev/null +++ b/src/plugins/dashboard/public/services/dashboard_saved_object/lib/save_dashboard_state_to_saved_object.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pick } from 'lodash'; + +import { isFilterPinned } from '@kbn/es-query'; +import { SavedObjectsClientContract } from '@kbn/core/public'; +import { SavedObjectAttributes } from '@kbn/core-saved-objects-common'; + +import { extractSearchSourceReferences, RefreshInterval } from '@kbn/data-plugin/public'; +import { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public'; + +import type { DashboardAttributes } from '../../../application'; +import { DashboardSavedObjectRequiredServices } from '../types'; +import { DashboardConstants } from '../../../dashboard_constants'; +import { convertTimeToUTCString } from '../../../application/lib'; +import { DashboardRedirect, DashboardState } from '../../../types'; +import { dashboardSaveToastStrings } from '../../../dashboard_strings'; +import { convertPanelMapToSavedPanels, extractReferences } from '../../../../common'; +import { serializeControlGroupInput } from '../../../application/lib/dashboard_control_group'; + +export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { saveAsCopy?: boolean }; + +export type SaveDashboardProps = DashboardSavedObjectRequiredServices & { + currentState: DashboardState; + redirectTo: DashboardRedirect; + saveOptions: SavedDashboardSaveOpts; + savedObjectsClient: SavedObjectsClientContract; +}; + +export interface SaveDashboardReturn { + id?: string; + error?: string; + redirected?: boolean; +} + +export const saveDashboardStateToSavedObject = async ({ + data, + redirectTo, + embeddable, + saveOptions, + currentState, + savedObjectsClient, + savedObjectsTagging, + dashboardSessionStorage, + notifications: { toasts }, + initializerContext: { kibanaVersion }, +}: SaveDashboardProps): Promise => { + const { + search: dataSearchService, + query: { + timefilter: { timefilter }, + }, + } = data; + + const { + tags, + query, + title, + panels, + filters, + options, + timeRestore, + description, + savedObjectId, + controlGroupInput, + } = currentState; + + /** + * Stringify filters and query into search source JSON + */ + const { searchSourceJSON, searchSourceReferences } = await (async () => { + const searchSource = await dataSearchService.searchSource.create(); + searchSource.setField( + 'filter', // save only unpinned filters + filters.filter((filter) => !isFilterPinned(filter)) + ); + searchSource.setField('query', query); + + const rawSearchSourceFields = searchSource.getSerializedFields(); + const [fields, references] = extractSearchSourceReferences(rawSearchSourceFields); + return { searchSourceReferences: references, searchSourceJSON: JSON.stringify(fields) }; + })(); + + /** + * Stringify options and panels + */ + const optionsJSON = JSON.stringify(options); + const panelsJSON = JSON.stringify(convertPanelMapToSavedPanels(panels, kibanaVersion)); + + /** + * Parse global time filter settings + */ + const { from, to } = timefilter.getTime(); + const timeFrom = timeRestore ? convertTimeToUTCString(from) : undefined; + const timeTo = timeRestore ? convertTimeToUTCString(to) : undefined; + const refreshInterval = timeRestore + ? (pick(timefilter.getRefreshInterval(), [ + 'display', + 'pause', + 'section', + 'value', + ]) as RefreshInterval) + : undefined; + + const rawDashboardAttributes: DashboardAttributes = { + controlGroupInput: serializeControlGroupInput(controlGroupInput), + kibanaSavedObjectMeta: { searchSourceJSON }, + refreshInterval, + timeRestore, + optionsJSON, + description, + panelsJSON, + timeFrom, + title, + timeTo, + version: 1, // todo - where does version come from? Why is it needed? + }; + + /** + * Extract references from raw attributes and tags into the references array. + */ + const { attributes, references: dashboardReferences } = extractReferences( + { + attributes: rawDashboardAttributes as unknown as SavedObjectAttributes, + references: searchSourceReferences, + }, + { embeddablePersistableStateService: embeddable } + ); + const references = savedObjectsTagging.updateTagsReferences + ? savedObjectsTagging.updateTagsReferences(dashboardReferences, tags) + : dashboardReferences; + + /** + * Save the saved object using the saved objects client + */ + const idToSaveTo = saveOptions.saveAsCopy ? undefined : savedObjectId; + try { + const { id: newId } = await savedObjectsClient.create( + DashboardConstants.DASHBOARD_SAVED_OBJECT_TYPE, + attributes, + { + id: idToSaveTo, + overwrite: true, + references, + } + ); + + if (newId) { + toasts.addSuccess({ + title: dashboardSaveToastStrings.getSuccessString(currentState.title), + 'data-test-subj': 'saveDashboardSuccess', + }); + + /** + * If the dashboard id has been changed, redirect to the new ID to keep the url param in sync. + */ + if (newId !== savedObjectId) { + dashboardSessionStorage.clearState(savedObjectId); + redirectTo({ + id: newId, + editMode: true, + useReplace: true, + destination: 'dashboard', + }); + return { redirected: true, id: newId }; + } + } + return { id: newId }; + } catch (error) { + toasts.addDanger({ + title: dashboardSaveToastStrings.getFailureString(currentState.title, error.message), + 'data-test-subj': 'saveDashboardFailure', + }); + return { error }; + } +}; diff --git a/src/plugins/dashboard/public/services/dashboard_saved_object/types.ts b/src/plugins/dashboard/public/services/dashboard_saved_object/types.ts new file mode 100644 index 000000000000..dd817c751aa8 --- /dev/null +++ b/src/plugins/dashboard/public/services/dashboard_saved_object/types.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SavedObjectsClientContract } from '@kbn/core/public'; + +import { DashboardDataService } from '../data/types'; +import { DashboardSpacesService } from '../spaces/types'; +import { DashboardEmbeddableService } from '../embeddable/types'; +import { DashboardNotificationsService } from '../notifications/types'; +import { DashboardScreenshotModeService } from '../screenshot_mode/types'; +import { DashboardInitializerContextService } from '../initializer_context/types'; +import { DashboardSavedObjectsTaggingService } from '../saved_objects_tagging/types'; +import { DashboardSessionStorageServiceType } from '../dashboard_session_storage/types'; + +import { + LoadDashboardFromSavedObjectProps, + LoadDashboardFromSavedObjectReturn, +} from './lib/load_dashboard_state_from_saved_object'; +import { + SaveDashboardProps, + SaveDashboardReturn, +} from './lib/save_dashboard_state_to_saved_object'; +import { + FindDashboardBySavedObjectIdsResult, + FindDashboardSavedObjectsArgs, + FindDashboardSavedObjectsResponse, +} from './lib/find_dashboard_saved_objects'; +import { DashboardDuplicateTitleCheckProps } from './lib/check_for_duplicate_dashboard_title'; + +export interface DashboardSavedObjectRequiredServices { + screenshotMode: DashboardScreenshotModeService; + embeddable: DashboardEmbeddableService; + spaces: DashboardSpacesService; + data: DashboardDataService; + initializerContext: DashboardInitializerContextService; + notifications: DashboardNotificationsService; + savedObjectsTagging: DashboardSavedObjectsTaggingService; + dashboardSessionStorage: DashboardSessionStorageServiceType; +} + +export interface DashboardSavedObjectService { + loadDashboardStateFromSavedObject: ( + props: Pick + ) => Promise; + + saveDashboardStateToSavedObject: ( + props: Pick + ) => Promise; + findDashboards: { + findSavedObjects: ( + props: Pick + ) => Promise; + findByIds: (ids: string[]) => Promise; + findByTitle: (title: string) => Promise<{ id: string } | undefined>; + }; + checkForDuplicateDashboardTitle: (meta: DashboardDuplicateTitleCheckProps) => Promise; + savedObjectsClient: SavedObjectsClientContract; +} diff --git a/src/plugins/dashboard/public/services/embeddable/embeddable.stub.ts b/src/plugins/dashboard/public/services/embeddable/embeddable.stub.ts index 18f952d4620e..a1a6b2973664 100644 --- a/src/plugins/dashboard/public/services/embeddable/embeddable.stub.ts +++ b/src/plugins/dashboard/public/services/embeddable/embeddable.stub.ts @@ -16,9 +16,13 @@ export const embeddableServiceFactory: EmbeddableServiceFactory = () => { const pluginMock = embeddablePluginMock.createStartContract(); return { - getEmbeddableFactory: pluginMock.getEmbeddableFactory, getEmbeddableFactories: pluginMock.getEmbeddableFactories, + getEmbeddableFactory: pluginMock.getEmbeddableFactory, getStateTransfer: pluginMock.getStateTransfer, + getAllMigrations: pluginMock.getAllMigrations, EmbeddablePanel: pluginMock.EmbeddablePanel, + telemetry: pluginMock.telemetry, + extract: pluginMock.extract, + inject: pluginMock.inject, }; }; diff --git a/src/plugins/dashboard/public/services/embeddable/embeddable_service.ts b/src/plugins/dashboard/public/services/embeddable/embeddable_service.ts index 258c11f697bc..c796bbde2d7d 100644 --- a/src/plugins/dashboard/public/services/embeddable/embeddable_service.ts +++ b/src/plugins/dashboard/public/services/embeddable/embeddable_service.ts @@ -6,7 +6,10 @@ * Side Public License, v 1. */ +import { pick } from 'lodash'; + import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; + import type { DashboardStartDependencies } from '../../plugin'; import type { DashboardEmbeddableService } from './types'; @@ -15,14 +18,16 @@ export type EmbeddableServiceFactory = KibanaPluginServiceFactory< DashboardStartDependencies >; export const embeddableServiceFactory: EmbeddableServiceFactory = ({ startPlugins }) => { - const { - embeddable: { getEmbeddableFactory, getEmbeddableFactories, getStateTransfer, EmbeddablePanel }, - } = startPlugins; + const { embeddable } = startPlugins; - return { - getEmbeddableFactory, - getEmbeddableFactories, - getStateTransfer, - EmbeddablePanel, - }; + return pick(embeddable, [ + 'getEmbeddableFactory', + 'getEmbeddableFactories', + 'getStateTransfer', + 'EmbeddablePanel', + 'getAllMigrations', + 'telemetry', + 'extract', + 'inject', + ]); }; diff --git a/src/plugins/dashboard/public/services/embeddable/types.ts b/src/plugins/dashboard/public/services/embeddable/types.ts index ef24db7c2e62..40d0ae02bfa7 100644 --- a/src/plugins/dashboard/public/services/embeddable/types.ts +++ b/src/plugins/dashboard/public/services/embeddable/types.ts @@ -8,9 +8,14 @@ import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; -export interface DashboardEmbeddableService { - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; - getStateTransfer: EmbeddableStart['getStateTransfer']; - EmbeddablePanel: EmbeddableStart['EmbeddablePanel']; -} +export type DashboardEmbeddableService = Pick< + EmbeddableStart, + | 'getEmbeddableFactories' + | 'getEmbeddableFactory' + | 'getAllMigrations' + | 'getStateTransfer' + | 'EmbeddablePanel' + | 'telemetry' + | 'extract' + | 'inject' +>; diff --git a/src/plugins/dashboard/public/services/plugin_services.stub.ts b/src/plugins/dashboard/public/services/plugin_services.stub.ts index c703b8b6767a..b8c39909dd61 100644 --- a/src/plugins/dashboard/public/services/plugin_services.stub.ts +++ b/src/plugins/dashboard/public/services/plugin_services.stub.ts @@ -29,7 +29,6 @@ import { initializerContextServiceFactory } from './initializer_context/initiali import { navigationServiceFactory } from './navigation/navigation.stub'; import { notificationsServiceFactory } from './notifications/notifications.stub'; import { overlaysServiceFactory } from './overlays/overlays.stub'; -import { savedObjectsServiceFactory } from './saved_objects/saved_objects.stub'; import { savedObjectsTaggingServiceFactory } from './saved_objects_tagging/saved_objects_tagging.stub'; import { screenshotModeServiceFactory } from './screenshot_mode/screenshot_mode.stub'; import { settingsServiceFactory } from './settings/settings.stub'; @@ -38,8 +37,10 @@ import { usageCollectionServiceFactory } from './usage_collection/usage_collecti import { spacesServiceFactory } from './spaces/spaces.stub'; import { urlForwardingServiceFactory } from './url_forwarding/url_fowarding.stub'; import { visualizationsServiceFactory } from './visualizations/visualizations.stub'; +import { dashboardSavedObjectServiceFactory } from './dashboard_saved_object/dashboard_saved_object.stub'; export const providers: PluginServiceProviders = { + dashboardSavedObject: new PluginServiceProvider(dashboardSavedObjectServiceFactory), analytics: new PluginServiceProvider(analyticsServiceFactory), application: new PluginServiceProvider(applicationServiceFactory), chrome: new PluginServiceProvider(chromeServiceFactory), @@ -55,7 +56,6 @@ export const providers: PluginServiceProviders = { navigation: new PluginServiceProvider(navigationServiceFactory), notifications: new PluginServiceProvider(notificationsServiceFactory), overlays: new PluginServiceProvider(overlaysServiceFactory), - savedObjects: new PluginServiceProvider(savedObjectsServiceFactory), savedObjectsTagging: new PluginServiceProvider(savedObjectsTaggingServiceFactory), screenshotMode: new PluginServiceProvider(screenshotModeServiceFactory), settings: new PluginServiceProvider(settingsServiceFactory), diff --git a/src/plugins/dashboard/public/services/plugin_services.ts b/src/plugins/dashboard/public/services/plugin_services.ts index 421b5e75c482..b4ee1b566a8a 100644 --- a/src/plugins/dashboard/public/services/plugin_services.ts +++ b/src/plugins/dashboard/public/services/plugin_services.ts @@ -30,7 +30,6 @@ import { navigationServiceFactory } from './navigation/navigation_service'; import { notificationsServiceFactory } from './notifications/notifications_service'; import { overlaysServiceFactory } from './overlays/overlays_service'; import { screenshotModeServiceFactory } from './screenshot_mode/screenshot_mode_service'; -import { savedObjectsServiceFactory } from './saved_objects/saved_objects_service'; import { savedObjectsTaggingServiceFactory } from './saved_objects_tagging/saved_objects_tagging_service'; import { settingsServiceFactory } from './settings/settings_service'; import { shareServiceFactory } from './share/share_services'; @@ -39,17 +38,29 @@ import { urlForwardingServiceFactory } from './url_forwarding/url_forwarding_ser import { visualizationsServiceFactory } from './visualizations/visualizations_service'; import { usageCollectionServiceFactory } from './usage_collection/usage_collection_service'; import { analyticsServiceFactory } from './analytics/analytics_service'; +import { dashboardSavedObjectServiceFactory } from './dashboard_saved_object/dashboard_saved_object_service'; const providers: PluginServiceProviders = { + dashboardSavedObject: new PluginServiceProvider(dashboardSavedObjectServiceFactory, [ + 'dashboardSessionStorage', + 'savedObjectsTagging', + 'initializerContext', + 'screenshotMode', + 'notifications', + 'embeddable', + 'spaces', + 'data', + ]), + dashboardSessionStorage: new PluginServiceProvider(dashboardSessionStorageServiceFactory, [ + 'notifications', + 'spaces', + ]), + analytics: new PluginServiceProvider(analyticsServiceFactory), application: new PluginServiceProvider(applicationServiceFactory), chrome: new PluginServiceProvider(chromeServiceFactory), coreContext: new PluginServiceProvider(coreContextServiceFactory), dashboardCapabilities: new PluginServiceProvider(dashboardCapabilitiesServiceFactory), - dashboardSessionStorage: new PluginServiceProvider(dashboardSessionStorageServiceFactory, [ - 'notifications', - 'spaces', - ]), data: new PluginServiceProvider(dataServiceFactory), dataViewEditor: new PluginServiceProvider(dataViewEditorServiceFactory), documentationLinks: new PluginServiceProvider(documentationLinksServiceFactory), @@ -59,7 +70,6 @@ const providers: PluginServiceProviders SavedObject; - public type: string; - public lowercaseType: string; - public loaderProperties: Record; - - constructor( - SavedObjectClass: any, - private readonly savedObjectsClient: SavedObjectsClientContract - ) { - this.type = SavedObjectClass.type; - this.Class = SavedObjectClass; - this.lowercaseType = this.type.toLowerCase(); - - this.loaderProperties = { - name: `${this.lowercaseType}s`, - noun: upperFirst(this.type), - nouns: `${this.lowercaseType}s`, - }; - } - - /** - * Retrieve a saved object by id or create new one. - * Returns a promise that completes when the object finishes - * initializing. - * @param opts - * @returns {Promise} - */ - async get(opts?: Record | string) { - // can accept object as argument in accordance to SavedVis class - // see src/plugins/saved_objects/public/saved_object/saved_object_loader.ts - // @ts-ignore - const obj = new this.Class(opts); - return obj.init(); - } - - urlFor(id: string) { - return `#/${this.lowercaseType}/${encodeURIComponent(id)}`; - } - - async delete(ids: string | string[]) { - const idsUsed = !Array.isArray(ids) ? [ids] : ids; - - const deletions = idsUsed.map((id) => { - // @ts-ignore - const savedObject = new this.Class(id); - return savedObject.delete(); - }); - await Promise.all(deletions); - } - - /** - * Updates source to contain an id, url and references fields, and returns the updated - * source object. - * @param source - * @param id - * @param references - * @returns {source} The modified source object, with an id and url field. - */ - mapHitSource( - source: Record, - id: string, - references: SavedObjectReference[] = [], - updatedAt?: string - ): Record { - return { - ...source, - id, - url: this.urlFor(id), - references, - updatedAt, - }; - } - - /** - * Updates hit.attributes to contain an id and url field, and returns the updated - * attributes object. - * @param hit - * @returns {hit.attributes} The modified hit.attributes object, with an id and url field. - */ - mapSavedObjectApiHits({ - attributes, - id, - references = [], - updatedAt, - }: { - attributes: Record; - id: string; - references?: SavedObjectReference[]; - updatedAt?: string; - }) { - return this.mapHitSource(attributes, id, references, updatedAt); - } - - /** - * TODO: Rather than use a hardcoded limit, implement pagination. See - * https://github.com/elastic/kibana/issues/8044 for reference. - * - * @param search - * @param size - * @param fields - * @returns {Promise} - */ - private findAll( - search: string = '', - { size = 100, fields, hasReference }: SavedObjectLoaderFindOptions - ) { - return this.savedObjectsClient - .find>({ - type: this.lowercaseType, - search: search ? `${search}*` : undefined, - perPage: size, - page: 1, - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', - fields, - hasReference, - } as SavedObjectsFindOptions) - .then((resp) => { - return { - total: resp.total, - hits: resp.savedObjects.map((savedObject) => this.mapSavedObjectApiHits(savedObject)), - }; - }); - } - - find(search: string = '', sizeOrOptions: number | SavedObjectLoaderFindOptions = 100) { - const options: SavedObjectLoaderFindOptions = - typeof sizeOrOptions === 'number' - ? { - size: sizeOrOptions, - } - : sizeOrOptions; - - return this.findAll(search, options).then((resp) => { - return { - total: resp.total, - hits: resp.hits.filter((savedObject) => !savedObject.error), - }; - }); - } -} diff --git a/src/plugins/dashboard/public/services/saved_objects/saved_objects.stub.ts b/src/plugins/dashboard/public/services/saved_objects/saved_objects.stub.ts deleted file mode 100644 index f26e36392603..000000000000 --- a/src/plugins/dashboard/public/services/saved_objects/saved_objects.stub.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { savedObjectsServiceMock } from '@kbn/core-saved-objects-browser-mocks'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { DashboardSavedObjectsService } from './types'; - -type SavedObjectsServiceFactory = PluginServiceFactory; - -export const savedObjectsServiceFactory: SavedObjectsServiceFactory = () => { - const pluginMock = savedObjectsServiceMock.createStartContract(); - - return { - client: pluginMock.client, - }; -}; diff --git a/src/plugins/dashboard/public/services/saved_objects/saved_objects_service.ts b/src/plugins/dashboard/public/services/saved_objects/saved_objects_service.ts deleted file mode 100644 index 3fff4d9e1c36..000000000000 --- a/src/plugins/dashboard/public/services/saved_objects/saved_objects_service.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import type { DashboardStartDependencies } from '../../plugin'; -import type { DashboardSavedObjectsService } from './types'; - -export type SavedObjectsServiceFactory = KibanaPluginServiceFactory< - DashboardSavedObjectsService, - DashboardStartDependencies ->; - -export const savedObjectsServiceFactory: SavedObjectsServiceFactory = ({ coreStart }) => { - const { - savedObjects: { client }, - } = coreStart; - - return { - client, - }; -}; diff --git a/src/plugins/dashboard/public/services/saved_objects_tagging/saved_objects_tagging.stub.ts b/src/plugins/dashboard/public/services/saved_objects_tagging/saved_objects_tagging.stub.ts index d526eedbc1e4..1a1bcd6ca93b 100644 --- a/src/plugins/dashboard/public/services/saved_objects_tagging/saved_objects_tagging.stub.ts +++ b/src/plugins/dashboard/public/services/saved_objects_tagging/saved_objects_tagging.stub.ts @@ -20,9 +20,10 @@ export const savedObjectsTaggingServiceFactory: SavedObjectsTaggingServiceFactor // I'm not defining components so that I don't have to update the snapshot of `save_modal.test` // However, if it's ever necessary, it can be done via: `components: pluginMock.components`, - getSearchBarFilter: pluginMock.getSearchBarFilter, - getTableColumnDefinition: pluginMock.getTableColumnDefinition, hasTagDecoration: pluginMock.hasTagDecoration, parseSearchQuery: pluginMock.parseSearchQuery, + getSearchBarFilter: pluginMock.getSearchBarFilter, + getTagIdsFromReferences: pluginMock.getTagIdsFromReferences, + getTableColumnDefinition: pluginMock.getTableColumnDefinition, }; }; diff --git a/src/plugins/dashboard/public/services/saved_objects_tagging/saved_objects_tagging_service.ts b/src/plugins/dashboard/public/services/saved_objects_tagging/saved_objects_tagging_service.ts index 7e252ed79d7b..a100282b4cff 100644 --- a/src/plugins/dashboard/public/services/saved_objects_tagging/saved_objects_tagging_service.ts +++ b/src/plugins/dashboard/public/services/saved_objects_tagging/saved_objects_tagging_service.ts @@ -27,19 +27,23 @@ export const savedObjectsTaggingServiceFactory: SavedObjectsTaggingServiceFactor const { ui: { components, + parseSearchQuery, + hasTagDecoration, getSearchBarFilter, + updateTagsReferences, + getTagIdsFromReferences, getTableColumnDefinition, - hasTagDecoration, - parseSearchQuery, }, } = taggingApi; return { hasApi: true, components, - getSearchBarFilter, - getTableColumnDefinition, hasTagDecoration, parseSearchQuery, + getSearchBarFilter, + updateTagsReferences, + getTagIdsFromReferences, + getTableColumnDefinition, }; }; diff --git a/src/plugins/dashboard/public/services/saved_objects_tagging/types.ts b/src/plugins/dashboard/public/services/saved_objects_tagging/types.ts index 803db5ff46d9..ba08a5370934 100644 --- a/src/plugins/dashboard/public/services/saved_objects_tagging/types.ts +++ b/src/plugins/dashboard/public/services/saved_objects_tagging/types.ts @@ -12,9 +12,10 @@ export interface DashboardSavedObjectsTaggingService { hasApi: boolean; // remove this once the entire service is optional components?: SavedObjectsTaggingApi['ui']['components']; - getSearchBarFilter?: SavedObjectsTaggingApi['ui']['getSearchBarFilter']; - getTableColumnDefinition?: SavedObjectsTaggingApi['ui']['getTableColumnDefinition']; hasTagDecoration?: SavedObjectsTaggingApi['ui']['hasTagDecoration']; parseSearchQuery?: SavedObjectsTaggingApi['ui']['parseSearchQuery']; + getSearchBarFilter?: SavedObjectsTaggingApi['ui']['getSearchBarFilter']; + updateTagsReferences?: SavedObjectsTaggingApi['ui']['updateTagsReferences']; getTagIdsFromReferences?: SavedObjectsTaggingApi['ui']['getTagIdsFromReferences']; + getTableColumnDefinition?: SavedObjectsTaggingApi['ui']['getTableColumnDefinition']; } diff --git a/src/plugins/dashboard/public/services/types.ts b/src/plugins/dashboard/public/services/types.ts index 3309ce057597..5d14b59e8a12 100644 --- a/src/plugins/dashboard/public/services/types.ts +++ b/src/plugins/dashboard/public/services/types.ts @@ -15,6 +15,7 @@ import { DashboardApplicationService } from './application/types'; import { DashboardChromeService } from './chrome/types'; import { DashboardCoreContextService } from './core_context/types'; import { DashboardCapabilitiesService } from './dashboard_capabilities/types'; +import { DashboardSavedObjectService } from './dashboard_saved_object/types'; import { DashboardSessionStorageServiceType } from './dashboard_session_storage/types'; import { DashboardDataService } from './data/types'; import { DashboardDataViewEditorService } from './data_view_editor/types'; @@ -25,7 +26,6 @@ import { DashboardInitializerContextService } from './initializer_context/types' import { DashboardNavigationService } from './navigation/types'; import { DashboardNotificationsService } from './notifications/types'; import { DashboardOverlaysService } from './overlays/types'; -import { DashboardSavedObjectsService } from './saved_objects/types'; import { DashboardSavedObjectsTaggingService } from './saved_objects_tagging/types'; import { DashboardScreenshotModeService } from './screenshot_mode/types'; import { DashboardSettingsService } from './settings/types'; @@ -39,12 +39,14 @@ export type DashboardPluginServiceParams = KibanaPluginServiceParams void; -export interface SavedDashboardPanelMap { - [key: string]: SavedDashboardPanel; -} - -export interface DashboardPanelMap { - [key: string]: DashboardPanelState; -} /** * DashboardState contains all pieces of tracked state for an individual dashboard @@ -48,11 +41,13 @@ export interface DashboardState { description: string; savedQuery?: string; timeRestore: boolean; + timeRange?: TimeRange; + savedObjectId?: string; fullScreenMode: boolean; expandedPanelId?: string; options: DashboardOptions; panels: DashboardPanelMap; - timeRange?: TimeRange; + refreshInterval?: RefreshInterval; timeslice?: [number, number]; controlGroupInput?: PersistableControlGroupInput; @@ -95,19 +90,17 @@ export interface DashboardAppState { dataViews?: DataView[]; updateLastSavedState?: () => void; resetToLastSavedState?: () => void; - savedDashboard?: DashboardSavedObject; dashboardContainer?: DashboardContainer; + createConflictWarning?: () => ReactElement | undefined; getLatestDashboardState?: () => DashboardState; $triggerDashboardRefresh: Subject<{ force?: boolean }>; $onDashboardStateChange: BehaviorSubject; - applyFilters?: (query: Query, filters: Filter[]) => void; } /** * The shared services and tools used to build a dashboard from a saved object ID. */ -// TODO: Remove reference to DashboardAppServices as part of https://github.com/elastic/kibana/pull/138774 -export type DashboardBuildContext = Pick & { +export interface DashboardBuildContext { locatorState?: DashboardAppLocatorParams; history: History; isEmbeddedExternally: boolean; @@ -118,7 +111,7 @@ export type DashboardBuildContext = Pick; $onDashboardStateChange: BehaviorSubject; executionContext?: KibanaExecutionContext; -}; +} // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type DashboardOptions = { @@ -156,8 +149,3 @@ export interface DashboardMountContextProps { onAppLeave: AppMountParameters['onAppLeave']; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; } - -// TODO: Remove DashboardAppServices as part of https://github.com/elastic/kibana/pull/138774 -export interface DashboardAppServices { - savedDashboards: SavedObjectLoader; -} diff --git a/src/plugins/dashboard/server/embeddable/dashboard_container_embeddable_factory.ts b/src/plugins/dashboard/server/embeddable/dashboard_container_embeddable_factory.ts index 31c236da607a..df183f631a3e 100644 --- a/src/plugins/dashboard/server/embeddable/dashboard_container_embeddable_factory.ts +++ b/src/plugins/dashboard/server/embeddable/dashboard_container_embeddable_factory.ts @@ -8,10 +8,7 @@ import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common'; import { EmbeddableRegistryDefinition } from '@kbn/embeddable-plugin/server'; -import { - createExtract, - createInject, -} from '../../common/embeddable/dashboard_container_persistable_state'; +import { createExtract, createInject } from '../../common'; export const dashboardPersistableStateServiceFactory = ( persistableStateService: EmbeddablePersistableStateService diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts deleted file mode 100644 index b3625bec3e8a..000000000000 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Serializable } from '@kbn/utility-types'; -import { get, flow, mapValues } from 'lodash'; -import { - SavedObjectAttributes, - SavedObjectMigrationFn, - SavedObjectMigrationMap, -} from '@kbn/core/server'; - -import { EmbeddableSetup } from '@kbn/embeddable-plugin/server'; -import { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common'; -import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-plugin/common'; -import { - mergeMigrationFunctionMaps, - MigrateFunction, - MigrateFunctionsObject, -} from '@kbn/kibana-utils-plugin/common'; -import { - CONTROL_GROUP_TYPE, - rawControlGroupAttributesToSerializable, - serializableToRawControlGroupAttributes, -} from '@kbn/controls-plugin/common'; -import { migrations730 } from './migrations_730'; -import { SavedDashboardPanel } from '../../common/types'; -import { migrateMatchAllQuery } from './migrate_match_all_query'; -import { DashboardDoc700To720, DashboardDoc730ToLatest } from '../../common'; -import { injectReferences, extractReferences } from '../../common/saved_dashboard_references'; -import { - convertPanelStateToSavedDashboardPanel, - convertSavedDashboardPanelToPanelState, -} from '../../common/embeddable/embeddable_saved_object_converters'; -import { replaceIndexPatternReference } from './replace_index_pattern_reference'; - -function migrateIndexPattern(doc: DashboardDoc700To720) { - const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); - if (typeof searchSourceJSON !== 'string') { - return; - } - let searchSource; - try { - searchSource = JSON.parse(searchSourceJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return; - } - if (searchSource.index) { - searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; - doc.references.push({ - name: searchSource.indexRefName, - type: DATA_VIEW_SAVED_OBJECT_TYPE, - id: searchSource.index, - }); - delete searchSource.index; - } - if (searchSource.filter) { - searchSource.filter.forEach((filterRow: any, i: number) => { - if (!filterRow.meta || !filterRow.meta.index) { - return; - } - filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; - doc.references.push({ - name: filterRow.meta.indexRefName, - type: DATA_VIEW_SAVED_OBJECT_TYPE, - id: filterRow.meta.index, - }); - delete filterRow.meta.index; - }); - } - doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); -} - -const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => { - // Set new "references" attribute - doc.references = doc.references || []; - - // Migrate index pattern - migrateIndexPattern(doc as DashboardDoc700To720); - // Migrate panels - const panelsJSON = get(doc, 'attributes.panelsJSON'); - if (typeof panelsJSON !== 'string') { - return doc as DashboardDoc700To720; - } - let panels; - try { - panels = JSON.parse(panelsJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return doc as DashboardDoc700To720; - } - if (!Array.isArray(panels)) { - return doc as DashboardDoc700To720; - } - panels.forEach((panel, i) => { - if (!panel.type || !panel.id) { - return; - } - panel.panelRefName = `panel_${i}`; - doc.references!.push({ - name: `panel_${i}`, - type: panel.type, - id: panel.id, - }); - delete panel.type; - delete panel.id; - }); - doc.attributes.panelsJSON = JSON.stringify(panels); - return doc as DashboardDoc700To720; -}; - -/** - * In 7.8.0 we introduced dashboard drilldowns which are stored inside dashboard saved object as part of embeddable state - * In 7.11.0 we created an embeddable references/migrations system that allows to properly extract embeddable persistable state - * https://github.com/elastic/kibana/issues/71409 - * The idea of this migration is to inject all the embeddable panel references and then run the extraction again. - * As the result of the extraction: - * 1. In addition to regular `panel_` we will get new references which are extracted by `embeddablePersistableStateService` (dashboard drilldown references) - * 2. `panel_` references will be regenerated - * All other references like index-patterns are forwarded non touched - * @param deps - */ -function createExtractPanelReferencesMigration( - deps: DashboardSavedObjectTypeMigrationsDeps -): SavedObjectMigrationFn { - return (doc) => { - const references = doc.references ?? []; - - /** - * Remembering this because dashboard's extractReferences won't return those - * All other references like `panel_` will be overwritten - */ - const oldNonPanelReferences = references.filter((ref) => !ref.name.startsWith('panel_')); - - const injectedAttributes = injectReferences( - { - attributes: doc.attributes as unknown as SavedObjectAttributes, - references, - }, - { embeddablePersistableStateService: deps.embeddable } - ); - - const { attributes, references: newPanelReferences } = extractReferences( - { attributes: injectedAttributes, references: [] }, - { embeddablePersistableStateService: deps.embeddable } - ); - - return { - ...doc, - references: [...oldNonPanelReferences, ...newPanelReferences], - attributes, - }; - }; -} - -type ValueOrReferenceInput = SavedObjectEmbeddableInput & { - attributes?: Serializable; - savedVis?: Serializable; -}; - -/** - * Before 7.10, hidden panel titles were stored as a blank string on the title attribute. In 7.10, this was replaced - * with a usage of the existing hidePanelTitles key. Even though blank string titles still technically work - * in versions > 7.10, they are less explicit than using the hidePanelTitles key. This migration transforms all - * blank string titled panels to panels with the titles explicitly hidden. - */ -export const migrateExplicitlyHiddenTitles: SavedObjectMigrationFn = (doc) => { - const { attributes } = doc; - - // Skip if panelsJSON is missing - if (typeof attributes?.panelsJSON !== 'string') return doc; - - try { - const panels = JSON.parse(attributes.panelsJSON) as SavedDashboardPanel[]; - // Same here, prevent failing saved object import if ever panels aren't an array. - if (!Array.isArray(panels)) return doc; - - const newPanels: SavedDashboardPanel[] = []; - panels.forEach((panel) => { - // Convert each panel into the dashboard panel state - const originalPanelState = - convertSavedDashboardPanelToPanelState(panel); - newPanels.push( - convertPanelStateToSavedDashboardPanel( - { - ...originalPanelState, - explicitInput: { - ...originalPanelState.explicitInput, - ...(originalPanelState.explicitInput.title === '' && - !originalPanelState.explicitInput.hidePanelTitles - ? { hidePanelTitles: true } - : {}), - }, - }, - panel.version - ) - ); - }); - return { - ...doc, - attributes: { - ...attributes, - panelsJSON: JSON.stringify(newPanels), - }, - }; - } catch { - return doc; - } -}; - -// Runs the embeddable migrations on each panel -const migrateByValuePanels = - (migrate: MigrateFunction, version: string): SavedObjectMigrationFn => - (doc: any) => { - const { attributes } = doc; - - if (attributes?.controlGroupInput) { - const controlGroupInput = rawControlGroupAttributesToSerializable( - attributes.controlGroupInput - ); - const migratedControlGroupInput = migrate({ - ...controlGroupInput, - type: CONTROL_GROUP_TYPE, - }); - attributes.controlGroupInput = - serializableToRawControlGroupAttributes(migratedControlGroupInput); - } - - // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when - // importing objects without panelsJSON. At development time of this, there is no guarantee each saved - // object has panelsJSON in all previous versions of kibana. - if (typeof attributes?.panelsJSON !== 'string') { - return doc; - } - - const panels = JSON.parse(attributes.panelsJSON) as SavedDashboardPanel[]; - // Same here, prevent failing saved object import if ever panels aren't an array. - if (!Array.isArray(panels)) { - return doc; - } - const newPanels: SavedDashboardPanel[] = []; - panels.forEach((panel) => { - // Convert each panel into a state that can be passed to EmbeddablesSetup.migrate - const originalPanelState = - convertSavedDashboardPanelToPanelState(panel); - - // saved vis is used to store by value input for Visualize. This should eventually be renamed to `attributes` to align with Lens and Maps - if ( - originalPanelState.explicitInput.attributes || - originalPanelState.explicitInput.savedVis - ) { - // If this panel is by value, migrate the state using embeddable migrations - const migratedInput = migrate({ - ...originalPanelState.explicitInput, - type: originalPanelState.type, - }); - // Convert the embeddable state back into the panel shape - newPanels.push( - convertPanelStateToSavedDashboardPanel( - { - ...originalPanelState, - explicitInput: { ...migratedInput, id: migratedInput.id as string }, - }, - version - ) - ); - } else { - newPanels.push(panel); - } - }); - return { - ...doc, - attributes: { - ...attributes, - panelsJSON: JSON.stringify(newPanels), - }, - }; - }; - -export interface DashboardSavedObjectTypeMigrationsDeps { - embeddable: EmbeddableSetup; -} - -export const createDashboardSavedObjectTypeMigrations = ( - deps: DashboardSavedObjectTypeMigrationsDeps -): SavedObjectMigrationMap => { - const embeddableMigrations = mapValues( - deps.embeddable.getAllMigrations(), - migrateByValuePanels - ) as MigrateFunctionsObject; - - const dashboardMigrations = { - /** - * We need to have this migration twice, once with a version prior to 7.0.0 once with a version - * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already - * released. Thus a user who already had 7.0.0 installed already got the 7.0.0 migrations below running, - * so we need a version higher than that. But this fix was backported to the 6.7 release, meaning if we - * would only have the 7.0.1 migration in here a user on the 6.7 release will migrate their saved objects - * to the 7.0.1 state, and thus when updating their Kibana to 7.0, will never run the 7.0.0 migrations introduced - * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 - * only contained the 6.7.2 migration and not the 7.0.1 migration. - */ - '6.7.2': flow(migrateMatchAllQuery), - '7.0.0': flow(migrations700), - '7.3.0': flow(migrations730), - '7.9.3': flow(migrateMatchAllQuery), - '7.11.0': flow(createExtractPanelReferencesMigration(deps)), - '7.14.0': flow(replaceIndexPatternReference), - '7.17.3': flow(migrateExplicitlyHiddenTitles), - }; - - return mergeMigrationFunctionMaps(dashboardMigrations, embeddableMigrations); -}; diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard_saved_object.ts similarity index 97% rename from src/plugins/dashboard/server/saved_objects/dashboard.ts rename to src/plugins/dashboard/server/saved_objects/dashboard_saved_object.ts index 953852bee59c..b8474149ca87 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_saved_object.ts @@ -10,7 +10,7 @@ import { SavedObjectsType } from '@kbn/core/server'; import { createDashboardSavedObjectTypeMigrations, DashboardSavedObjectTypeMigrationsDeps, -} from './dashboard_migrations'; +} from './migrations/dashboard_saved_object_migrations'; export const createDashboardSavedObjectType = ({ migrationDeps, diff --git a/src/plugins/dashboard/server/saved_objects/index.ts b/src/plugins/dashboard/server/saved_objects/index.ts index af3de2dfca52..c16af55945f9 100644 --- a/src/plugins/dashboard/server/saved_objects/index.ts +++ b/src/plugins/dashboard/server/saved_objects/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { createDashboardSavedObjectType } from './dashboard'; +export { createDashboardSavedObjectType } from './dashboard_saved_object'; diff --git a/src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts b/src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts deleted file mode 100644 index cea39fc45b0f..000000000000 --- a/src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { SavedObjectUnsanitizedDoc } from '@kbn/core/server'; -import { DashboardDoc730ToLatest } from '../../common'; - -function isDoc( - doc: { [key: string]: unknown } | SavedObjectUnsanitizedDoc -): doc is SavedObjectUnsanitizedDoc { - return ( - typeof doc.id === 'string' && - typeof doc.type === 'string' && - doc.attributes !== null && - typeof doc.attributes === 'object' && - doc.references !== null && - typeof doc.references === 'object' - ); -} - -export function isDashboardDoc( - doc: { [key: string]: unknown } | DashboardDoc730ToLatest -): doc is DashboardDoc730ToLatest { - if (!isDoc(doc)) { - return false; - } - - if (typeof (doc as DashboardDoc730ToLatest).attributes.panelsJSON !== 'string') { - return false; - } - - return true; -} diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/migrations/dashboard_saved_object_migrations.test.ts similarity index 99% rename from src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts rename to src/plugins/dashboard/server/saved_objects/migrations/dashboard_saved_object_migrations.test.ts index 0cefab5104d7..1a2655c48183 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations/dashboard_saved_object_migrations.test.ts @@ -6,17 +6,15 @@ * Side Public License, v 1. */ -import { SavedObjectReference, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; +import { SerializableRecord } from '@kbn/utility-types'; import { savedObjectsServiceMock } from '@kbn/core/server/mocks'; import { createEmbeddableSetupMock } from '@kbn/embeddable-plugin/server/mocks'; -import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; -import { DashboardDoc730ToLatest } from '../../common'; -import { - createExtract, - createInject, -} from '../../common/embeddable/dashboard_container_persistable_state'; +import { SavedObjectReference, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; + +import { createExtract, createInject } from '../../../common'; import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; -import { SerializableRecord } from '@kbn/utility-types'; +import { createDashboardSavedObjectTypeMigrations } from './dashboard_saved_object_migrations'; +import { DashboardDoc730ToLatest } from './migrate_to_730/types'; const embeddableSetupMock = createEmbeddableSetupMock(); const extract = createExtract(embeddableSetupMock); diff --git a/src/plugins/dashboard/server/saved_objects/migrations/dashboard_saved_object_migrations.ts b/src/plugins/dashboard/server/saved_objects/migrations/dashboard_saved_object_migrations.ts new file mode 100644 index 000000000000..2f93c038065b --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/migrations/dashboard_saved_object_migrations.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { flow, mapValues } from 'lodash'; + +import { + mergeMigrationFunctionMaps, + MigrateFunctionsObject, +} from '@kbn/kibana-utils-plugin/common'; +import { EmbeddableSetup } from '@kbn/embeddable-plugin/server'; +import { SavedObjectMigrationFn, SavedObjectMigrationMap } from '@kbn/core/server'; + +import { migrations730, migrations700 } from './migrate_to_730'; +import { migrateMatchAllQuery } from './migrate_match_all_query'; +import { migrateExplicitlyHiddenTitles } from './migrate_hidden_titles'; +import { replaceIndexPatternReference } from './migrate_index_pattern_reference'; +import { migrateByValueDashboardPanels } from './migrate_by_value_dashboard_panels'; +import { createExtractPanelReferencesMigration } from './migrate_extract_panel_references'; + +export interface DashboardSavedObjectTypeMigrationsDeps { + embeddable: EmbeddableSetup; +} + +export const createDashboardSavedObjectTypeMigrations = ( + deps: DashboardSavedObjectTypeMigrationsDeps +): SavedObjectMigrationMap => { + const embeddableMigrations = mapValues( + deps.embeddable.getAllMigrations(), + migrateByValueDashboardPanels + ) as MigrateFunctionsObject; + + const dashboardMigrations = { + '6.7.2': flow(migrateMatchAllQuery), + '7.0.0': flow(migrations700), + '7.3.0': flow(migrations730), + '7.9.3': flow(migrateMatchAllQuery), + '7.11.0': flow(createExtractPanelReferencesMigration(deps)), + '7.14.0': flow(replaceIndexPatternReference), + '7.17.3': flow(migrateExplicitlyHiddenTitles), + }; + + return mergeMigrationFunctionMaps(dashboardMigrations, embeddableMigrations); +}; diff --git a/src/plugins/dashboard/server/saved_objects/migrations/migrate_by_value_dashboard_panels.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_by_value_dashboard_panels.ts new file mode 100644 index 000000000000..3bad12b53710 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_by_value_dashboard_panels.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + CONTROL_GROUP_TYPE, + rawControlGroupAttributesToSerializable, + serializableToRawControlGroupAttributes, +} from '@kbn/controls-plugin/common'; +import { Serializable } from '@kbn/utility-types'; +import { SavedObjectMigrationFn } from '@kbn/core/server'; +import { MigrateFunction } from '@kbn/kibana-utils-plugin/common'; +import { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common'; + +import { + convertPanelStateToSavedDashboardPanel, + convertSavedDashboardPanelToPanelState, + SavedDashboardPanel, +} from '../../../common'; + +type ValueOrReferenceInput = SavedObjectEmbeddableInput & { + attributes?: Serializable; + savedVis?: Serializable; +}; + +// Runs the embeddable migrations on each panel +export const migrateByValueDashboardPanels = + (migrate: MigrateFunction, version: string): SavedObjectMigrationFn => + (doc: any) => { + const { attributes } = doc; + + if (attributes?.controlGroupInput) { + const controlGroupInput = rawControlGroupAttributesToSerializable( + attributes.controlGroupInput + ); + const migratedControlGroupInput = migrate({ + ...controlGroupInput, + type: CONTROL_GROUP_TYPE, + }); + attributes.controlGroupInput = + serializableToRawControlGroupAttributes(migratedControlGroupInput); + } + + // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when + // importing objects without panelsJSON. At development time of this, there is no guarantee each saved + // object has panelsJSON in all previous versions of kibana. + if (typeof attributes?.panelsJSON !== 'string') { + return doc; + } + + const panels = JSON.parse(attributes.panelsJSON) as SavedDashboardPanel[]; + // Same here, prevent failing saved object import if ever panels aren't an array. + if (!Array.isArray(panels)) { + return doc; + } + const newPanels: SavedDashboardPanel[] = []; + panels.forEach((panel) => { + // Convert each panel into a state that can be passed to EmbeddablesSetup.migrate + const originalPanelState = + convertSavedDashboardPanelToPanelState(panel); + + // saved vis is used to store by value input for Visualize. This should eventually be renamed to `attributes` to align with Lens and Maps + if ( + originalPanelState.explicitInput.attributes || + originalPanelState.explicitInput.savedVis + ) { + // If this panel is by value, migrate the state using embeddable migrations + const migratedInput = migrate({ + ...originalPanelState.explicitInput, + type: originalPanelState.type, + }); + // Convert the embeddable state back into the panel shape + newPanels.push( + convertPanelStateToSavedDashboardPanel( + { + ...originalPanelState, + explicitInput: { ...migratedInput, id: migratedInput.id as string }, + }, + version + ) + ); + } else { + newPanels.push(panel); + } + }); + return { + ...doc, + attributes: { + ...attributes, + panelsJSON: JSON.stringify(newPanels), + }, + }; + }; diff --git a/src/plugins/dashboard/server/saved_objects/migrations/migrate_extract_panel_references.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_extract_panel_references.ts new file mode 100644 index 000000000000..5a8de73af988 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_extract_panel_references.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SavedObjectAttributes, SavedObjectMigrationFn } from '@kbn/core/server'; + +import { DashboardAttributes, extractReferences, injectReferences } from '../../../common'; +import { DashboardSavedObjectTypeMigrationsDeps } from './dashboard_saved_object_migrations'; + +/** + * In 7.8.0 we introduced dashboard drilldowns which are stored inside dashboard saved object as part of embeddable state + * In 7.11.0 we created an embeddable references/migrations system that allows to properly extract embeddable persistable state + * https://github.com/elastic/kibana/issues/71409 + * The idea of this migration is to inject all the embeddable panel references and then run the extraction again. + * As the result of the extraction: + * 1. In addition to regular `panel_` we will get new references which are extracted by `embeddablePersistableStateService` (dashboard drilldown references) + * 2. `panel_` references will be regenerated + * All other references like index-patterns are forwarded non touched + * @param deps + */ +export function createExtractPanelReferencesMigration( + deps: DashboardSavedObjectTypeMigrationsDeps +): SavedObjectMigrationFn { + return (doc) => { + const references = doc.references ?? []; + + /** + * Remembering this because dashboard's extractReferences won't return those + * All other references like `panel_` will be overwritten + */ + const oldNonPanelReferences = references.filter((ref) => !ref.name.startsWith('panel_')); + + const injectedAttributes = injectReferences( + { + attributes: doc.attributes as unknown as SavedObjectAttributes, + references, + }, + { embeddablePersistableStateService: deps.embeddable } + ); + + const { attributes, references: newPanelReferences } = extractReferences( + { attributes: injectedAttributes, references: [] }, + { embeddablePersistableStateService: deps.embeddable } + ); + + return { + ...doc, + references: [...oldNonPanelReferences, ...newPanelReferences], + attributes, + }; + }; +} diff --git a/src/plugins/dashboard/server/saved_objects/migrations/migrate_hidden_titles.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_hidden_titles.ts new file mode 100644 index 000000000000..8a9a91723120 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_hidden_titles.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SavedObjectMigrationFn } from '@kbn/core/server'; +import { EmbeddableInput } from '@kbn/embeddable-plugin/common'; + +import { + convertSavedDashboardPanelToPanelState, + convertPanelStateToSavedDashboardPanel, + SavedDashboardPanel, +} from '../../../common'; + +/** + * Before 7.10, hidden panel titles were stored as a blank string on the title attribute. In 7.10, this was replaced + * with a usage of the existing hidePanelTitles key. Even though blank string titles still technically work + * in versions > 7.10, they are less explicit than using the hidePanelTitles key. This migration transforms all + * blank string titled panels to panels with the titles explicitly hidden. + */ +export const migrateExplicitlyHiddenTitles: SavedObjectMigrationFn = (doc) => { + const { attributes } = doc; + + // Skip if panelsJSON is missing + if (typeof attributes?.panelsJSON !== 'string') return doc; + + try { + const panels = JSON.parse(attributes.panelsJSON) as SavedDashboardPanel[]; + // Same here, prevent failing saved object import if ever panels aren't an array. + if (!Array.isArray(panels)) return doc; + + const newPanels: SavedDashboardPanel[] = []; + panels.forEach((panel) => { + // Convert each panel into the dashboard panel state + const originalPanelState = convertSavedDashboardPanelToPanelState(panel); + newPanels.push( + convertPanelStateToSavedDashboardPanel( + { + ...originalPanelState, + explicitInput: { + ...originalPanelState.explicitInput, + ...(originalPanelState.explicitInput.title === '' && + !originalPanelState.explicitInput.hidePanelTitles + ? { hidePanelTitles: true } + : {}), + }, + }, + panel.version + ) + ); + }); + return { + ...doc, + attributes: { + ...attributes, + panelsJSON: JSON.stringify(newPanels), + }, + }; + } catch { + return doc; + } +}; diff --git a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.test.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_index_pattern_reference.test.ts similarity index 94% rename from src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.test.ts rename to src/plugins/dashboard/server/saved_objects/migrations/migrate_index_pattern_reference.test.ts index 13db82790c9d..a9682bdb8719 100644 --- a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_index_pattern_reference.test.ts @@ -7,7 +7,7 @@ */ import type { SavedObjectMigrationContext, SavedObjectMigrationFn } from '@kbn/core/server'; -import { replaceIndexPatternReference } from './replace_index_pattern_reference'; +import { replaceIndexPatternReference } from './migrate_index_pattern_reference'; describe('replaceIndexPatternReference', () => { const savedObjectMigrationContext = null as unknown as SavedObjectMigrationContext; diff --git a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_index_pattern_reference.ts similarity index 100% rename from src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts rename to src/plugins/dashboard/server/saved_objects/migrations/migrate_index_pattern_reference.ts diff --git a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.test.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_match_all_query.test.ts similarity index 100% rename from src/plugins/dashboard/server/saved_objects/migrate_match_all_query.test.ts rename to src/plugins/dashboard/server/saved_objects/migrations/migrate_match_all_query.test.ts diff --git a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_match_all_query.ts similarity index 100% rename from src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts rename to src/plugins/dashboard/server/saved_objects/migrations/migrate_match_all_query.ts index 147aa47a7a6e..a7c1f0ff6bdb 100644 --- a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_match_all_query.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { SavedObjectMigrationFn } from '@kbn/core/server'; import { get } from 'lodash'; import { DEFAULT_QUERY_LANGUAGE } from '@kbn/data-plugin/common'; +import { SavedObjectMigrationFn } from '@kbn/core/server'; /** * This migration script is related to: diff --git a/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/index.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/index.ts new file mode 100644 index 000000000000..2cf081358323 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { migrations730 } from './migrations_730'; +export { migrations700 } from './migrations_700'; diff --git a/src/plugins/dashboard/common/migrate_to_730_panels.test.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrate_to_730_panels.test.ts similarity index 99% rename from src/plugins/dashboard/common/migrate_to_730_panels.test.ts rename to src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrate_to_730_panels.test.ts index bdcd3bf8cedc..4eacf9b93d85 100644 --- a/src/plugins/dashboard/common/migrate_to_730_panels.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrate_to_730_panels.test.ts @@ -8,13 +8,14 @@ import { migratePanelsTo730 } from './migrate_to_730_panels'; import { + SavedDashboardPanel730ToLatest, + RawSavedDashboardPanel640To720, RawSavedDashboardPanelTo60, RawSavedDashboardPanel630, - RawSavedDashboardPanel640To720, RawSavedDashboardPanel610, RawSavedDashboardPanel620, -} from './bwc/types'; -import { SavedDashboardPanelTo60, SavedDashboardPanel730ToLatest } from './types'; + SavedDashboardPanelTo60, +} from './types'; test('6.0 migrates uiState, sort, scales, and gridData', async () => { const uiState = { diff --git a/src/plugins/dashboard/common/migrate_to_730_panels.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrate_to_730_panels.ts similarity index 98% rename from src/plugins/dashboard/common/migrate_to_730_panels.ts rename to src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrate_to_730_panels.ts index f40240bd7247..531a0715038d 100644 --- a/src/plugins/dashboard/common/migrate_to_730_panels.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrate_to_730_panels.ts @@ -6,25 +6,25 @@ * Side Public License, v 1. */ +import uuid from 'uuid'; +import semverSatisfies from 'semver/functions/satisfies'; + import { i18n } from '@kbn/i18n'; import type { SerializableRecord } from '@kbn/utility-types'; -import semverSatisfies from 'semver/functions/satisfies'; -import uuid from 'uuid'; + import { - GridData, - SavedDashboardPanelTo60, SavedDashboardPanel620, SavedDashboardPanel630, SavedDashboardPanel610, -} from '.'; -import { - RawSavedDashboardPanelTo60, + SavedDashboardPanelTo60, RawSavedDashboardPanel630, - RawSavedDashboardPanel640To720, - RawSavedDashboardPanel730ToLatest, RawSavedDashboardPanel610, RawSavedDashboardPanel620, -} from './bwc/types'; + RawSavedDashboardPanelTo60, + RawSavedDashboardPanel640To720, + RawSavedDashboardPanel730ToLatest, +} from './types'; +import { GridData } from '../../../../common'; const PANEL_HEIGHT_SCALE_FACTOR = 5; const PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS = 4; @@ -266,7 +266,6 @@ export function migratePanelsTo730( | RawSavedDashboardPanel620 | RawSavedDashboardPanel630 | RawSavedDashboardPanel640To720 - // We run these on post processed panels too for url BWC | SavedDashboardPanelTo60 | SavedDashboardPanel610 | SavedDashboardPanel620 diff --git a/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_700.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_700.ts new file mode 100644 index 000000000000..d1954e8266d8 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_700.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { get } from 'lodash'; + +import { SavedObjectMigrationFn } from '@kbn/core/server'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; + +import { DashboardDoc700To720 } from './types'; + +function migrateIndexPattern(doc: DashboardDoc700To720) { + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + if (typeof searchSourceJSON !== 'string') { + return; + } + let searchSource; + try { + searchSource = JSON.parse(searchSourceJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return; + } + if (searchSource.index) { + searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + doc.references.push({ + name: searchSource.indexRefName, + type: DATA_VIEW_SAVED_OBJECT_TYPE, + id: searchSource.index, + }); + delete searchSource.index; + } + if (searchSource.filter) { + searchSource.filter.forEach((filterRow: any, i: number) => { + if (!filterRow.meta || !filterRow.meta.index) { + return; + } + filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + doc.references.push({ + name: filterRow.meta.indexRefName, + type: DATA_VIEW_SAVED_OBJECT_TYPE, + id: filterRow.meta.index, + }); + delete filterRow.meta.index; + }); + } + doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); +} + +export const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => { + // Set new "references" attribute + doc.references = doc.references || []; + + // Migrate index pattern + migrateIndexPattern(doc as DashboardDoc700To720); + // Migrate panels + const panelsJSON = get(doc, 'attributes.panelsJSON'); + if (typeof panelsJSON !== 'string') { + return doc as DashboardDoc700To720; + } + let panels; + try { + panels = JSON.parse(panelsJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc as DashboardDoc700To720; + } + if (!Array.isArray(panels)) { + return doc as DashboardDoc700To720; + } + panels.forEach((panel, i) => { + if (!panel.type || !panel.id) { + return; + } + panel.panelRefName = `panel_${i}`; + doc.references!.push({ + name: `panel_${i}`, + type: panel.type, + id: panel.id, + }); + delete panel.type; + delete panel.id; + }); + doc.attributes.panelsJSON = JSON.stringify(panels); + return doc as DashboardDoc700To720; +}; diff --git a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.test.ts similarity index 96% rename from src/plugins/dashboard/server/saved_objects/migrations_730.test.ts rename to src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.test.ts index 2fc999b06749..6314546b5933 100644 --- a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.test.ts @@ -7,12 +7,17 @@ */ import { savedObjectsServiceMock } from '@kbn/core/server/mocks'; -import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; -import { migrations730 } from './migrations_730'; -import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; -import { RawSavedDashboardPanel730ToLatest } from '../../common'; import { createEmbeddableSetupMock } from '@kbn/embeddable-plugin/server/mocks'; +import { + DashboardDocPre700, + DashboardDoc700To720, + DashboardDoc730ToLatest, + RawSavedDashboardPanel730ToLatest, +} from './types'; +import { migrations730 } from './migrations_730'; +import { createDashboardSavedObjectTypeMigrations } from '../dashboard_saved_object_migrations'; + const mockContext = savedObjectsServiceMock.createMigrationContext(); const migrations = createDashboardSavedObjectTypeMigrations({ embeddable: createEmbeddableSetupMock(), diff --git a/src/plugins/dashboard/server/saved_objects/migrations_730.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.ts similarity index 69% rename from src/plugins/dashboard/server/saved_objects/migrations_730.ts rename to src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.ts index 3cc34d8513a4..dcd1c2f2cb87 100644 --- a/src/plugins/dashboard/server/saved_objects/migrations_730.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/migrations_730.ts @@ -7,11 +7,38 @@ */ import { inspect } from 'util'; -import { SavedObjectMigrationContext } from '@kbn/core/server'; -import { DashboardDoc730ToLatest } from '../../common'; -import { isDashboardDoc } from './is_dashboard_doc'; +import { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; + import { moveFiltersToQuery } from './move_filters_to_query'; -import { migratePanelsTo730, DashboardDoc700To720 } from '../../common'; +import { migratePanelsTo730 } from './migrate_to_730_panels'; +import { DashboardDoc730ToLatest, DashboardDoc700To720 } from './types'; + +function isDoc( + doc: { [key: string]: unknown } | SavedObjectUnsanitizedDoc +): doc is SavedObjectUnsanitizedDoc { + return ( + typeof doc.id === 'string' && + typeof doc.type === 'string' && + doc.attributes !== null && + typeof doc.attributes === 'object' && + doc.references !== null && + typeof doc.references === 'object' + ); +} + +export function isDashboardDoc( + doc: { [key: string]: unknown } | DashboardDoc730ToLatest +): doc is DashboardDoc730ToLatest { + if (!isDoc(doc)) { + return false; + } + + if (typeof (doc as DashboardDoc730ToLatest).attributes.panelsJSON !== 'string') { + return false; + } + + return true; +} export const migrations730 = (doc: DashboardDoc700To720, { log }: SavedObjectMigrationContext) => { if (!isDashboardDoc(doc)) { diff --git a/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/move_filters_to_query.test.ts similarity index 100% rename from src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts rename to src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/move_filters_to_query.test.ts diff --git a/src/plugins/dashboard/server/saved_objects/move_filters_to_query.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/move_filters_to_query.ts similarity index 100% rename from src/plugins/dashboard/server/saved_objects/move_filters_to_query.ts rename to src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/move_filters_to_query.ts diff --git a/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/readme.md b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/readme.md new file mode 100644 index 000000000000..50f1b3283ca3 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/readme.md @@ -0,0 +1,3 @@ +## Legacy Pre 7.3 Migrations + +This folder contains legacy migrations that migrate dashboard saved object from any previous version into Kibana 7.3.0. The migrations in this folder need to be able to handle state from any older version of dashboard from as early as 5.0 because Saved Object Migrations did not exist, and in-place migrations were used instead. After 7.3.0, saved object migrations are in place, so it can be assumed that any saved migration that is registered there will receive state from the version before. diff --git a/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/types.ts b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/types.ts new file mode 100644 index 000000000000..2257b05c0a64 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/migrations/migrate_to_730/types.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Serializable } from '@kbn/utility-types'; +import { SavedObjectReference } from '@kbn/core/server'; + +import type { + GridData, + DashboardAttributes as CurrentDashboardAttributes, // Dashboard attributes from common are the source of truth for the current version. +} from '../../../../common'; + +interface SavedObjectAttributes { + kibanaSavedObjectMeta: { + searchSourceJSON: string; + }; +} + +interface Doc { + references: SavedObjectReference[]; + attributes: Attributes; + id: string; + type: string; +} + +interface DocPre700 { + attributes: Attributes; + id: string; + type: string; +} + +interface DashboardAttributesTo720 extends SavedObjectAttributes { + panelsJSON: string; + description: string; + uiStateJSON?: string; + version: number; + timeRestore: boolean; + useMargins?: boolean; + title: string; + optionsJSON?: string; +} + +export type DashboardDoc730ToLatest = Doc; + +export type DashboardDoc700To720 = Doc; + +export type DashboardDocPre700 = DocPre700; + +// Note that these types are prefixed with `Raw` because there are some post processing steps +// that happen before the saved objects even reach the client. Namely, injecting type and id +// parameters back into the panels, where the raw saved objects actually have them stored elsewhere. +// +// Ideally, everywhere in the dashboard code would use references at the top level instead of +// embedded in the panels. The reason this is stored at the top level is so the references can be uniformly +// updated across all saved object types that have references. + +// Starting in 7.3 we introduced the possibility of embeddables existing without an id +// parameter. If there was no id, then type remains on the panel. So it either will have a name, +// or a type property. +export type RawSavedDashboardPanel730ToLatest = Pick< + RawSavedDashboardPanel640To720, + Exclude +> & { + // Should be either type, and not name (not backed by a saved object), or name but not type (backed by a + // saved object and type and id are stored on references). Had trouble with oring the two types + // because of optional properties being marked as required: https://github.com/microsoft/TypeScript/issues/20722 + readonly type?: string; + readonly name?: string; + + panelIndex: string; + panelRefName?: string; +}; + +// NOTE!! +// All of these types can actually exist in 7.2! The names are pretty confusing because we did +// in place migrations for so long. For example, `RawSavedDashboardPanelTo60` is what a panel +// created in 6.0 will look like after it's been migrated up to 7.2, *not* what it would look like in 6.0. +// That's why it actually doesn't have id or type, but has a name property, because that was a migration +// added in 7.0. + +// Hopefully since we finally have a formal saved object migration system and we can do less in place +// migrations, this will be easier to understand moving forward. + +// Starting in 6.4 we added an in-place edit on panels to remove columns and sort properties and put them +// inside the embeddable config (https://github.com/elastic/kibana/pull/17446). +// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in +// this shape in v 7.2. +export type RawSavedDashboardPanel640To720 = Pick< + RawSavedDashboardPanel630, + Exclude +>; + +// In 6.3.0 we expanded the number of grid columns and rows: https://github.com/elastic/kibana/pull/16763 +// We added in-place migrations to multiply older x,y,h,w numbers. Note the typescript shape here is the same +// because it's just multiplying existing fields. +// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in 7.2 +// that need to be modified. +export type RawSavedDashboardPanel630 = RawSavedDashboardPanel620; + +// In 6.2 we added an inplace migration, moving uiState into each panel's new embeddableConfig property. +// Source: https://github.com/elastic/kibana/pull/14949 +export type RawSavedDashboardPanel620 = RawSavedDashboardPanel610 & { + embeddableConfig: { [key: string]: Serializable }; + version: string; +}; + +// In 6.1 we switched from an angular grid to react grid layout (https://github.com/elastic/kibana/pull/13853) +// This used gridData instead of size_x, size_y, row and col. We also started tracking the version this panel +// was created in to make future migrations easier. +// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in +// this shape in v 7.2. +export type RawSavedDashboardPanel610 = Pick< + RawSavedDashboardPanelTo60, + Exclude +> & { gridData: GridData; version: string }; + +export interface RawSavedDashboardPanelTo60 { + readonly columns?: string[]; + readonly sort?: string; + readonly size_x?: number; + readonly size_y?: number; + readonly row: number; + readonly col: number; + panelIndex?: number | string; // earlier versions allowed this to be number or string. Some very early versions seem to be missing this entirely + readonly name: string; + + // This is where custom panel titles are stored prior to Embeddable API v2 + title?: string; +} + +export type SavedDashboardPanel640To720 = Pick< + RawSavedDashboardPanel640To720, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel630 = Pick< + RawSavedDashboardPanel630, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel620 = Pick< + RawSavedDashboardPanel620, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel610 = Pick< + RawSavedDashboardPanel610, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanelTo60 = Pick< + RawSavedDashboardPanelTo60, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +// id becomes optional starting in 7.3.0 +export type SavedDashboardPanel730ToLatest = Pick< + RawSavedDashboardPanel730ToLatest, + Exclude +> & { + readonly id?: string; + readonly type: string; +}; diff --git a/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts b/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts index ea981be3515f..25a4986208d3 100644 --- a/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts +++ b/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SavedDashboardPanel730ToLatest } from '../../common'; +import { SavedDashboardPanel } from '../../common'; import { getEmptyDashboardData, collectPanelsByType } from './dashboard_telemetry'; import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks'; @@ -18,7 +18,7 @@ const visualizationType1ByValue = { }, }, type: 'visualization', -} as unknown as SavedDashboardPanel730ToLatest; +} as unknown as SavedDashboardPanel; const visualizationType2ByValue = { embeddableConfig: { @@ -27,7 +27,7 @@ const visualizationType2ByValue = { }, }, type: 'visualization', -} as unknown as SavedDashboardPanel730ToLatest; +} as unknown as SavedDashboardPanel; const visualizationType2ByReference = { ...visualizationType2ByValue, @@ -41,7 +41,7 @@ const lensTypeAByValue = { visualizationType: 'a', }, }, -} as unknown as SavedDashboardPanel730ToLatest; +} as unknown as SavedDashboardPanel; const lensTypeAByReference = { ...lensTypeAByValue, @@ -60,7 +60,7 @@ const lensXYSeriesA = { }, }, }, -} as unknown as SavedDashboardPanel730ToLatest; +} as unknown as SavedDashboardPanel; const lensXYSeriesB = { type: 'lens', @@ -90,7 +90,7 @@ const lensXYSeriesB = { }, }, }, -} as unknown as SavedDashboardPanel730ToLatest; +} as unknown as SavedDashboardPanel; const embeddablePersistableStateService = createEmbeddablePersistableStateServiceMock(); diff --git a/src/plugins/dashboard/server/usage/dashboard_telemetry.ts b/src/plugins/dashboard/server/usage/dashboard_telemetry.ts index ce41c5083468..1e8a0192ec38 100644 --- a/src/plugins/dashboard/server/usage/dashboard_telemetry.ts +++ b/src/plugins/dashboard/server/usage/dashboard_telemetry.ts @@ -16,7 +16,7 @@ import { } from '@kbn/controls-plugin/common'; import { initializeControlGroupTelemetry } from '@kbn/controls-plugin/server'; import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; -import { SavedDashboardPanel730ToLatest } from '../../common'; +import type { SavedDashboardPanel } from '../../common'; import { TASK_ID, DashboardTelemetryTaskState } from './dashboard_telemetry_collection_task'; export interface DashboardCollectorData { panels: { @@ -55,7 +55,7 @@ export const getEmptyPanelTypeData = () => ({ }); export const collectPanelsByType = ( - panels: SavedDashboardPanel730ToLatest[], + panels: SavedDashboardPanel[], collectorData: DashboardCollectorData, embeddableService: EmbeddablePersistableStateService ) => { diff --git a/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts b/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts index 823b5fadaaea..1ca13b430858 100644 --- a/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts +++ b/src/plugins/dashboard/server/usage/dashboard_telemetry_collection_task.ts @@ -14,17 +14,13 @@ import { TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; import { EmbeddableSetup } from '@kbn/embeddable-plugin/server'; -import { SavedDashboardPanel730ToLatest } from '../../common'; -import { - injectReferences, - SavedObjectAttributesAndReferences, -} from '../../common/saved_dashboard_references'; import { controlsCollectorFactory, collectPanelsByType, getEmptyDashboardData, DashboardCollectorData, } from './dashboard_telemetry'; +import { injectReferences, SavedDashboardPanel } from '../../common'; // This task is responsible for running daily and aggregating all the Dashboard telemerty data // into a single document. This is an effort to make sure the load of fetching/parsing all of the @@ -32,6 +28,11 @@ import { const TELEMETRY_TASK_TYPE = 'dashboard_telemetry'; export const TASK_ID = `Dashboard-${TELEMETRY_TASK_TYPE}`; +interface SavedObjectAttributesAndReferences { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; +} + export interface DashboardTelemetryTaskState { runs: number; telemetry: DashboardCollectorData; @@ -102,7 +103,7 @@ export function dashboardTaskRunner(logger: Logger, core: CoreSetup, embeddable: try { const panels = JSON.parse( attributes.panelsJSON as string - ) as unknown as SavedDashboardPanel730ToLatest[]; + ) as unknown as SavedDashboardPanel[]; collectPanelsByType(panels, dashboardData, embeddable); } catch (e) { diff --git a/src/plugins/dashboard/server/usage/find_by_value_embeddables.test.ts b/src/plugins/dashboard/server/usage/find_by_value_embeddables.test.ts index 8a3cdd71539f..c5e8da8acbd4 100644 --- a/src/plugins/dashboard/server/usage/find_by_value_embeddables.test.ts +++ b/src/plugins/dashboard/server/usage/find_by_value_embeddables.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SavedDashboardPanel730ToLatest } from '../../common'; +import type { SavedDashboardPanel } from '../../common'; import { findByValueEmbeddables } from './find_by_value_embeddables'; const visualizationByValue = { @@ -14,18 +14,18 @@ const visualizationByValue = { value: 'visualization-by-value', }, type: 'visualization', -} as unknown as SavedDashboardPanel730ToLatest; +} as unknown as SavedDashboardPanel; const mapByValue = { embeddableConfig: { value: 'map-by-value', }, type: 'map', -} as unknown as SavedDashboardPanel730ToLatest; +} as unknown as SavedDashboardPanel; const embeddableByRef = { panelRefName: 'panel_ref_1', -} as unknown as SavedDashboardPanel730ToLatest; +} as unknown as SavedDashboardPanel; describe('findByValueEmbeddables', () => { it('finds the by value embeddables for the given type', async () => { diff --git a/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts b/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts index 694fe2007f84..502ba828944d 100644 --- a/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts +++ b/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts @@ -7,7 +7,7 @@ */ import { ISavedObjectsRepository, SavedObjectAttributes } from '@kbn/core/server'; -import { SavedDashboardPanel730ToLatest } from '../../common'; +import type { SavedDashboardPanel } from '../../common'; export const findByValueEmbeddables = async ( savedObjectClient: Pick, @@ -22,7 +22,7 @@ export const findByValueEmbeddables = async ( try { return JSON.parse( dashboard.attributes.panelsJSON as string - ) as unknown as SavedDashboardPanel730ToLatest[]; + ) as unknown as SavedDashboardPanel[]; } catch (exception) { return []; } diff --git a/src/plugins/data_views/server/rest_api_routes/update_data_view.ts b/src/plugins/data_views/server/rest_api_routes/update_data_view.ts index 0e9f0983ac64..1ac504ac652b 100644 --- a/src/plugins/data_views/server/rest_api_routes/update_data_view.ts +++ b/src/plugins/data_views/server/rest_api_routes/update_data_view.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { IRouter, StartServicesAccessor } from '@kbn/core/server'; -import { DataViewsService } from '../../common/data_views'; +import { DataViewsService, DataView } from '../../common/data_views'; import { DataViewSpec } from '../../common/types'; import { handleErrors } from './util/handle_errors'; import { fieldSpecSchema, runtimeFieldSchema, serializedFieldFormatSchema } from './util/schemas'; @@ -71,46 +71,46 @@ export const updateDataView = async ({ name, } = spec; - let changeCount = 0; + let isChanged = false; let doRefreshFields = false; if (title !== undefined && title !== dataView.title) { - changeCount++; + isChanged = true; dataView.title = title; } if (timeFieldName !== undefined && timeFieldName !== dataView.timeFieldName) { - changeCount++; + isChanged = true; dataView.timeFieldName = timeFieldName; } if (sourceFilters !== undefined) { - changeCount++; + isChanged = true; dataView.sourceFilters = sourceFilters; } if (fieldFormats !== undefined) { - changeCount++; + isChanged = true; dataView.fieldFormatMap = fieldFormats; } if (type !== undefined) { - changeCount++; + isChanged = true; dataView.type = type; } if (typeMeta !== undefined) { - changeCount++; + isChanged = true; dataView.typeMeta = typeMeta; } if (name !== undefined) { - changeCount++; + isChanged = true; dataView.name = name; } if (fields !== undefined) { - changeCount++; + isChanged = true; doRefreshFields = true; dataView.fields.replaceAll( Object.values(fields || {}).map((field) => ({ @@ -122,19 +122,19 @@ export const updateDataView = async ({ } if (runtimeFieldMap !== undefined) { - changeCount++; + isChanged = true; dataView.replaceAllRuntimeFields(runtimeFieldMap); } - if (changeCount < 1) { - throw new Error('Index pattern change set is empty.'); - } - - await dataViewsService.updateSavedObject(dataView); + if (isChanged) { + const result = (await dataViewsService.updateSavedObject(dataView)) as DataView; - if (doRefreshFields && refreshFields) { - await dataViewsService.refreshFields(dataView); + if (doRefreshFields && refreshFields) { + await dataViewsService.refreshFields(dataView); + } + return result; } + return dataView; }; diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 75a95035bb89..a5e03084a697 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -488,6 +488,63 @@ describe('Execution', () => { expect(spy.fn).toHaveBeenCalledTimes(0); }); + + test('continues execution when error state is gone', async () => { + testScheduler.run(({ cold, expectObservable, flush }) => { + const a = 1; + const b = 2; + const c = 3; + const observable$ = cold('abc|', { a, b, c }); + const flakyFn = jest + .fn() + .mockImplementationOnce((value) => value) + .mockImplementationOnce(() => { + throw new Error('Some error.'); + }) + .mockImplementationOnce((value) => value); + const spyFn = jest.fn((value) => value); + + const executor = createUnitTestExecutor(); + executor.registerFunction({ + name: 'observable', + args: {}, + help: '', + fn: () => observable$, + }); + executor.registerFunction({ + name: 'flaky', + args: {}, + help: '', + fn: (value) => flakyFn(value), + }); + executor.registerFunction({ + name: 'spy', + args: {}, + help: '', + fn: (value) => spyFn(value), + }); + + const result = executor.run('observable | flaky | spy', null, {}); + + expectObservable(result).toBe('abc|', { + a: { partial: true, result: a }, + b: { + partial: true, + result: { + type: 'error', + error: expect.objectContaining({ message: '[flaky] > Some error.' }), + }, + }, + c: { partial: false, result: c }, + }); + + flush(); + + expect(spyFn).toHaveBeenCalledTimes(2); + expect(spyFn).toHaveBeenNthCalledWith(1, a); + expect(spyFn).toHaveBeenNthCalledWith(2, c); + }); + }); }); describe('state', () => { diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index b4ef83389ce2..68cebaa65569 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -295,87 +295,86 @@ export class Execution< } invokeChain( - chainArr: ExpressionAstFunction[], + [head, ...tail]: ExpressionAstFunction[], input: unknown - ): Observable { + ): Observable { + if (!head) { + return of(input as ChainOutput); + } + return of(input).pipe( - ...(chainArr.map((link) => - switchMap((currentInput) => { - const { function: fnName, arguments: fnArgs } = link; - const fn = getByAlias( - this.state.get().functions, - fnName, - this.execution.params.namespace - ); + switchMap((currentInput) => { + const { function: fnName, arguments: fnArgs } = head; + const fn = getByAlias(this.state.get().functions, fnName, this.execution.params.namespace); + + if (!fn) { + throw createError({ + name: 'fn not found', + message: i18n.translate('expressions.execution.functionNotFound', { + defaultMessage: `Function {fnName} could not be found.`, + values: { + fnName, + }, + }), + }); + } - if (!fn) { - throw createError({ - name: 'fn not found', - message: i18n.translate('expressions.execution.functionNotFound', { - defaultMessage: `Function {fnName} could not be found.`, - values: { - fnName, - }, - }), - }); - } + if (fn.disabled) { + throw createError({ + name: 'fn is disabled', + message: i18n.translate('expressions.execution.functionDisabled', { + defaultMessage: `Function {fnName} is disabled.`, + values: { + fnName, + }, + }), + }); + } - if (fn.disabled) { - throw createError({ - name: 'fn is disabled', - message: i18n.translate('expressions.execution.functionDisabled', { - defaultMessage: `Function {fnName} is disabled.`, - values: { - fnName, - }, - }), - }); - } + if (fn.deprecated) { + this.logger?.warn(`Function '${fnName}' is deprecated`); + } - if (fn.deprecated) { - this.logger?.warn(`Function '${fnName}' is deprecated`); - } + if (this.execution.params.debug) { + head.debug = { + args: {}, + duration: 0, + fn: fn.name, + input: currentInput, + success: true, + }; + } - if (this.execution.params.debug) { - link.debug = { - args: {}, - duration: 0, - fn: fn.name, - input: currentInput, - success: true, - }; - } + const timeStart = this.execution.params.debug ? now() : 0; + + // `resolveArgs` returns an object because the arguments themselves might + // actually have `then` or `subscribe` methods which would be treated as a `Promise` + // or an `Observable` accordingly. + return this.resolveArgs(fn, currentInput, fnArgs).pipe( + tap((args) => this.execution.params.debug && Object.assign(head.debug, { args })), + switchMap((args) => this.invokeFunction(fn, currentInput, args)), + switchMap((output) => (getType(output) === 'error' ? throwError(output) : of(output))), + tap((output) => this.execution.params.debug && Object.assign(head.debug, { output })), + switchMap((output) => this.invokeChain(tail, output)), + catchError((rawError) => { + const error = createError(rawError); + error.error.message = `[${fnName}] > ${error.error.message}`; + + if (this.execution.params.debug) { + Object.assign(head.debug, { error, rawError, success: false }); + } - const timeStart = this.execution.params.debug ? now() : 0; - - // `resolveArgs` returns an object because the arguments themselves might - // actually have `then` or `subscribe` methods which would be treated as a `Promise` - // or an `Observable` accordingly. - return this.resolveArgs(fn, currentInput, fnArgs).pipe( - tap((args) => this.execution.params.debug && Object.assign(link.debug, { args })), - switchMap((args) => this.invokeFunction(fn, currentInput, args)), - switchMap((output) => (getType(output) === 'error' ? throwError(output) : of(output))), - tap((output) => this.execution.params.debug && Object.assign(link.debug, { output })), - catchError((rawError) => { - const error = createError(rawError); - error.error.message = `[${fnName}] > ${error.error.message}`; - - if (this.execution.params.debug) { - Object.assign(link.debug, { error, rawError, success: false }); - } - - return throwError(error); - }), - finalize(() => { - if (this.execution.params.debug) { - Object.assign(link.debug, { duration: now() - timeStart }); - } - }) - ); - }) - ) as Parameters['pipe']>), + return of(error); + }), + finalize(() => { + if (this.execution.params.debug) { + Object.assign(head.debug, { duration: now() - timeStart }); + } + }) + ); + }), catchError((error) => of(error)) - ) as Observable; + ); } invokeFunction( diff --git a/src/plugins/vis_types/pie/kibana.json b/src/plugins/vis_types/pie/kibana.json index abed576cc673..4c5ee6b50579 100644 --- a/src/plugins/vis_types/pie/kibana.json +++ b/src/plugins/vis_types/pie/kibana.json @@ -3,8 +3,8 @@ "version": "kibana", "ui": true, "server": true, - "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection", "expressionPartitionVis"], - "requiredBundles": ["visDefaultEditor"], + "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection", "expressionPartitionVis", "dataViews"], + "requiredBundles": ["visDefaultEditor", "kibanaUtils"], "extraPublicDirs": ["common/index"], "owner": { "name": "Vis Editors", diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts new file mode 100644 index 000000000000..0a10a5bd7c0c --- /dev/null +++ b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getConfiguration } from '.'; +import { samplePieVis } from '../../sample_vis.test.mocks'; + +describe('getConfiguration', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should return correct configuration', () => { + samplePieVis.uiState.get.mockReturnValueOnce(undefined); + expect( + getConfiguration('test1', samplePieVis as any, { + metrics: ['metric-1'], + buckets: ['bucket-1'], + }) + ).toEqual({ + layers: [ + { + categoryDisplay: undefined, + emptySizeRatio: undefined, + layerId: 'test1', + layerType: 'data', + legendDisplay: 'show', + legendMaxLines: 1, + legendPosition: 'right', + legendSize: 'large', + metric: 'metric-1', + nestedLegend: true, + numberDisplay: 'percent', + percentDecimals: 2, + primaryGroups: ['bucket-1'], + secondaryGroups: [], + showValuesInLegend: true, + truncateLegend: true, + }, + ], + shape: 'donut', + palette: undefined, + }); + }); + + test('should return legendDisplay = show if uiState contains truthy value', () => { + samplePieVis.uiState.get.mockReturnValueOnce(true); + expect( + getConfiguration( + 'test1', + { ...samplePieVis, params: { ...samplePieVis.params, legendDisplay: 'hide' } } as any, + { + metrics: ['metric-1'], + buckets: ['bucket-1'], + } + ) + ).toEqual({ + layers: [expect.objectContaining({ legendDisplay: 'show' })], + shape: 'donut', + palette: undefined, + }); + }); + + test('should return legendDisplay = hide if uiState contains falsy value', () => { + samplePieVis.uiState.get.mockReturnValueOnce(false); + expect( + getConfiguration( + 'test1', + { ...samplePieVis, params: { ...samplePieVis.params, legendDisplay: 'show' } } as any, + { + metrics: ['metric-1'], + buckets: ['bucket-1'], + } + ) + ).toEqual({ + layers: [expect.objectContaining({ legendDisplay: 'hide' })], + shape: 'donut', + palette: undefined, + }); + }); + + test('should return value of legendDisplay if uiState contains undefined value', () => { + samplePieVis.uiState.get.mockReturnValueOnce(undefined); + const legendDisplay = 'show'; + expect( + getConfiguration( + 'test1', + { ...samplePieVis, params: { ...samplePieVis.params, legendDisplay } } as any, + { + metrics: ['metric-1'], + buckets: ['bucket-1'], + } + ) + ).toEqual({ + layers: [expect.objectContaining({ legendDisplay })], + shape: 'donut', + palette: undefined, + }); + }); +}); diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts new file mode 100644 index 000000000000..9a3420581c1f --- /dev/null +++ b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { LegendDisplay, PartitionVisParams } from '@kbn/expression-partition-vis-plugin/common'; +import { + CategoryDisplayTypes, + NumberDisplayTypes, + PartitionVisConfiguration, +} from '@kbn/visualizations-plugin/common/convert_to_lens'; +import { Vis } from '@kbn/visualizations-plugin/public'; + +const getLayers = ( + layerId: string, + vis: Vis, + metrics: string[], + buckets: string[] +): PartitionVisConfiguration['layers'] => { + const legendOpen = vis.uiState.get('vis.legendOpen'); + const legendDisplayFromUiState = + legendOpen !== undefined ? (legendOpen ? LegendDisplay.SHOW : LegendDisplay.HIDE) : undefined; + + const showValuesInLegend = + vis.params.labels.values ?? + vis.params.showValuesInLegend ?? + vis.type.visConfig.defaults.showValuesInLegend; + + return [ + { + layerId, + layerType: 'data' as const, + primaryGroups: buckets, + secondaryGroups: [], + metric: metrics[0], + numberDisplay: + showValuesInLegend === false + ? NumberDisplayTypes.HIDDEN + : vis.params.labels.valuesFormat ?? vis.type.visConfig.defaults.labels.valuesFormat, + categoryDisplay: vis.params.labels.show + ? vis.params.labels.position ?? vis.type.visConfig.defaults.labels.position + : CategoryDisplayTypes.HIDE, + legendDisplay: + legendDisplayFromUiState ?? + vis.params.legendDisplay ?? + vis.type.visConfig.defaults.legendDisplay, + legendPosition: vis.params.legendPosition ?? vis.type.visConfig.defaults.legendPosition, + showValuesInLegend, + nestedLegend: vis.params.nestedLegend ?? vis.type.visConfig.defaults.nestedLegend, + percentDecimals: + vis.params.labels.percentDecimals ?? vis.type.visConfig.defaults.labels.percentDecimals, + emptySizeRatio: vis.params.emptySizeRatio ?? vis.type.visConfig.defaults.emptySizeRatio, + legendMaxLines: vis.params.maxLegendLines ?? vis.type.visConfig.defaults.maxLegendLines, + legendSize: vis.params.legendSize ?? vis.type.visConfig.defaults.legendSize, + truncateLegend: vis.params.truncateLegend ?? vis.type.visConfig.defaults.truncateLegend, + }, + ]; +}; + +export const getConfiguration = ( + layerId: string, + vis: Vis, + { + metrics, + buckets, + }: { + metrics: string[]; + buckets: string[]; + } +): PartitionVisConfiguration => { + return { + shape: vis.params.isDonut ? 'donut' : 'pie', + layers: getLayers(layerId, vis, metrics, buckets), + palette: vis.params.palette, + }; +}; diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/index.test.ts b/src/plugins/vis_types/pie/public/convert_to_lens/index.test.ts new file mode 100644 index 000000000000..c1e39d741f84 --- /dev/null +++ b/src/plugins/vis_types/pie/public/convert_to_lens/index.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { convertToLens } from '.'; +import { samplePieVis } from '../sample_vis.test.mocks'; + +const mockGetColumnsFromVis = jest.fn(); +const mockGetConfiguration = jest.fn().mockReturnValue({}); + +jest.mock('../services', () => ({ + getDataViewsStart: jest.fn(() => ({ get: () => ({}), getDefault: () => ({}) })), +})); + +jest.mock('@kbn/visualizations-plugin/public', () => ({ + convertToLensModule: Promise.resolve({ + getColumnsFromVis: jest.fn(() => mockGetColumnsFromVis()), + }), + getDataViewByIndexPatternId: jest.fn(() => ({ id: 'index-pattern' })), +})); + +jest.mock('./configurations', () => ({ + getConfiguration: jest.fn(() => mockGetConfiguration()), +})); + +describe('convertToLens', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return null if getColumnsFromVis returns null', async () => { + mockGetColumnsFromVis.mockReturnValue(null); + const result = await convertToLens(samplePieVis as any, {} as any); + expect(mockGetColumnsFromVis).toBeCalledTimes(1); + expect(result).toBeNull(); + }); + + test('should return null if more than three split slice levels', async () => { + mockGetColumnsFromVis.mockReturnValue({ + buckets: ['1', '2', '3', '4'], + }); + const result = await convertToLens(samplePieVis as any, {} as any); + expect(mockGetColumnsFromVis).toBeCalledTimes(1); + expect(result).toBeNull(); + }); + + test('should return null if no one split slices', async () => { + mockGetColumnsFromVis.mockReturnValue({ + buckets: [], + }); + const result = await convertToLens(samplePieVis as any, {} as any); + expect(mockGetColumnsFromVis).toBeCalledTimes(1); + expect(result).toBeNull(); + }); + + test('should state for valid vis', async () => { + mockGetColumnsFromVis.mockReturnValue({ + buckets: ['2'], + columns: [{ columnId: '2' }, { columnId: '1' }], + }); + const result = await convertToLens(samplePieVis as any, {} as any); + expect(mockGetColumnsFromVis).toBeCalledTimes(1); + expect(mockGetConfiguration).toBeCalledTimes(1); + expect(result?.type).toEqual('lnsPie'); + expect(result?.layers.length).toEqual(1); + expect(result?.layers[0]).toEqual( + expect.objectContaining({ + columnOrder: [], + columns: [{ columnId: '2' }, { columnId: '1' }], + }) + ); + }); +}); diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/index.ts b/src/plugins/vis_types/pie/public/convert_to_lens/index.ts new file mode 100644 index 000000000000..5b1973507c7d --- /dev/null +++ b/src/plugins/vis_types/pie/public/convert_to_lens/index.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Column, ColumnWithMeta } from '@kbn/visualizations-plugin/common'; +import { + convertToLensModule, + getDataViewByIndexPatternId, +} from '@kbn/visualizations-plugin/public'; +import uuid from 'uuid'; +import { getDataViewsStart } from '../services'; +import { getConfiguration } from './configurations'; +import { ConvertPieToLensVisualization } from './types'; + +export const isColumnWithMeta = (column: Column): column is ColumnWithMeta => { + if ((column as ColumnWithMeta).meta) { + return true; + } + return false; +}; + +export const excludeMetaFromColumn = (column: Column) => { + if (isColumnWithMeta(column)) { + const { meta, ...rest } = column; + return rest; + } + return column; +}; + +export const convertToLens: ConvertPieToLensVisualization = async (vis, timefilter) => { + if (!timefilter) { + return null; + } + + const dataViews = getDataViewsStart(); + const dataView = await getDataViewByIndexPatternId(vis.data.indexPattern?.id, dataViews); + + if (!dataView) { + return null; + } + + const { getColumnsFromVis } = await convertToLensModule; + const result = getColumnsFromVis(vis, timefilter, dataView, { + buckets: [], + splits: ['segment'], + unsupported: ['split_row', 'split_column'], + }); + + if (result === null) { + return null; + } + + // doesn't support more than three split slice levels + // doesn't support pie without at least one split slice + if (result.buckets.length > 3 || !result.buckets.length) { + return null; + } + + const layerId = uuid(); + + const indexPatternId = dataView.id!; + return { + type: 'lnsPie', + layers: [ + { + indexPatternId, + layerId, + columns: result.columns.map(excludeMetaFromColumn), + columnOrder: [], + }, + ], + configuration: getConfiguration(layerId, vis, result), + indexPatternIds: [indexPatternId], + }; +}; diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/types.ts b/src/plugins/vis_types/pie/public/convert_to_lens/types.ts new file mode 100644 index 000000000000..b190a4c89130 --- /dev/null +++ b/src/plugins/vis_types/pie/public/convert_to_lens/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { TimefilterContract } from '@kbn/data-plugin/public'; +import { PartitionVisParams } from '@kbn/expression-partition-vis-plugin/common'; +import { + NavigateToLensContext, + PartitionVisConfiguration, +} from '@kbn/visualizations-plugin/common'; +import { Vis } from '@kbn/visualizations-plugin/public'; + +export type ConvertPieToLensVisualization = ( + vis: Vis, + timefilter?: TimefilterContract +) => Promise | null>; diff --git a/src/plugins/vis_types/pie/public/plugin.ts b/src/plugins/vis_types/pie/public/plugin.ts index 480cf0c49db6..b4a8c0e3a2a6 100644 --- a/src/plugins/vis_types/pie/public/plugin.ts +++ b/src/plugins/vis_types/pie/public/plugin.ts @@ -6,13 +6,15 @@ * Side Public License, v 1. */ -import { CoreSetup, DocLinksStart, ThemeServiceStart } from '@kbn/core/public'; +import { CoreSetup, CoreStart, DocLinksStart, ThemeServiceStart } from '@kbn/core/public'; import { VisualizationsSetup } from '@kbn/visualizations-plugin/public'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { LEGACY_PIE_CHARTS_LIBRARY } from '../common'; import { pieVisType } from './vis_type'; +import { setDataViewsStart } from './services'; /** @internal */ export interface VisTypePieSetupDependencies { @@ -21,6 +23,11 @@ export interface VisTypePieSetupDependencies { usageCollection: UsageCollectionSetup; } +/** @internal */ +export interface VisTypePieStartDependencies { + dataViews: DataViewsPublicPluginStart; +} + /** @internal */ export interface VisTypePiePluginStartDependencies { data: DataPublicPluginStart; @@ -53,5 +60,7 @@ export class VisTypePiePlugin { return {}; } - start() {} + start(core: CoreStart, { dataViews }: VisTypePieStartDependencies) { + setDataViewsStart(dataViews); + } } diff --git a/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts index 3525b7b6fbc0..035432de9ad2 100644 --- a/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts @@ -9,6 +9,8 @@ import { LegendDisplay } from '@kbn/expression-partition-vis-plugin/common'; import { LegendSize } from '@kbn/visualizations-plugin/common'; +const mockUiStateGet = jest.fn().mockReturnValue(() => false); + export const samplePieVis = { type: { name: 'pie', @@ -1353,5 +1355,6 @@ export const samplePieVis = { vis: { legendOpen: false, }, + get: mockUiStateGet, }, }; diff --git a/src/plugins/vis_types/pie/public/services.ts b/src/plugins/vis_types/pie/public/services.ts new file mode 100644 index 000000000000..736ad70d4941 --- /dev/null +++ b/src/plugins/vis_types/pie/public/services.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createGetterSetter } from '@kbn/kibana-utils-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; + +export const [getDataViewsStart, setDataViewsStart] = + createGetterSetter('dataViews'); diff --git a/src/plugins/vis_types/pie/public/vis_type/pie.ts b/src/plugins/vis_types/pie/public/vis_type/pie.ts index 6d48507cf47a..8d4b7b6828e3 100644 --- a/src/plugins/vis_types/pie/public/vis_type/pie.ts +++ b/src/plugins/vis_types/pie/public/vis_type/pie.ts @@ -21,6 +21,7 @@ import { DEFAULT_PERCENT_DECIMALS } from '../../common'; import { PieTypeProps } from '../types'; import { toExpressionAst } from '../to_ast'; import { getPieOptions } from '../editor/components'; +import { convertToLens } from '../convert_to_lens'; export const getPieVisTypeDefinition = ({ showElasticChartsOptions = false, @@ -123,4 +124,10 @@ export const getPieVisTypeDefinition = ({ }, hierarchicalData: true, requiresSearch: true, + navigateToLens: async (vis, timefilter) => (vis ? convertToLens(vis, timefilter) : null), + getExpressionVariables: async (vis, timeFilter) => { + return { + canNavigateToLens: Boolean(vis?.params ? await convertToLens(vis, timeFilter) : null), + }; + }, }); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts index 435335fe9dd2..309f066b18f2 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts @@ -40,7 +40,7 @@ describe('convertTSVBtoLensConfiguration', () => { test('should return null for a not supported chart', async () => { const metricModel = { ...model, - type: 'metric', + type: 'markdown', } as Panel; const triggerOptions = await convertTSVBtoLensConfiguration(metricModel); expect(triggerOptions).toBeNull(); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts index 5b92c0ab2166..a64118a1cb50 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts @@ -21,6 +21,10 @@ const getConvertFnByType = (type: PANEL_TYPES) => { const { convertToLens } = await import('./top_n'); return convertToLens; }, + [PANEL_TYPES.METRIC]: async () => { + const { convertToLens } = await import('./metric'); + return convertToLens; + }, }; return convertionFns[type]?.(); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.ts new file mode 100644 index 000000000000..d1f24485d764 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { MetricVisConfiguration } from '@kbn/visualizations-plugin/common'; +import { Metric, Panel, Series } from '../../../../../common/types'; +import { Column, Layer } from '../../convert'; +import { getSeriesAgg } from '../../series'; +import { getPalette } from './palette'; + +const getMetricWithCollapseFn = (series: Series | undefined) => { + if (!series) { + return; + } + const { metrics, seriesAgg } = getSeriesAgg(series.metrics); + const visibleMetric = metrics[metrics.length - 1]; + return { metric: visibleMetric, collapseFn: seriesAgg }; +}; + +const findMetricColumn = (metric: Metric | undefined, columns: Column[]) => { + if (!metric) { + return; + } + + return columns.find((column) => 'meta' in column && column.meta.metricId === metric.id); +}; + +export const getConfigurationForMetric = ( + model: Panel, + layer: Layer, + bucket?: Column +): MetricVisConfiguration | null => { + const [primarySeries, secondarySeries] = model.series.filter(({ hidden }) => !hidden); + + const primaryMetricWithCollapseFn = getMetricWithCollapseFn(primarySeries); + + if (!primaryMetricWithCollapseFn || !primaryMetricWithCollapseFn.metric) { + return null; + } + + const secondaryMetricWithCollapseFn = getMetricWithCollapseFn(secondarySeries); + const primaryColumn = findMetricColumn(primaryMetricWithCollapseFn.metric, layer.columns); + const secondaryColumn = findMetricColumn(secondaryMetricWithCollapseFn?.metric, layer.columns); + + if (primaryMetricWithCollapseFn.collapseFn && secondaryMetricWithCollapseFn?.collapseFn) { + return null; + } + + const palette = getPalette(model.background_color_rules ?? []); + if (palette === null) { + return null; + } + + return { + layerId: layer.layerId, + layerType: 'data', + metricAccessor: primaryColumn?.columnId, + secondaryMetricAccessor: secondaryColumn?.columnId, + breakdownByAccessor: bucket?.columnId, + palette, + collapseFn: primaryMetricWithCollapseFn.collapseFn ?? secondaryMetricWithCollapseFn?.collapseFn, + }; +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts new file mode 100644 index 000000000000..827dc15ff171 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getPalette } from './palette'; + +describe('getPalette', () => { + const invalidRules = [ + { id: 'some-id-0' }, + { id: 'some-id-1', value: 10 }, + { id: 'some-id-2', operator: 'gte' }, + { id: 'some-id-3', color: '#000' }, + { id: 'some-id-4', background_color: '#000' }, + ]; + test('should return undefined if no filled rules was provided', () => { + expect(getPalette([])).toBeUndefined(); + expect(getPalette(invalidRules)).toBeUndefined(); + }); + + test('should return undefined if only one valid rule is provided and it is not lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'gt', value: 100, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); + + test('should return custom palette if only one valid rule is provided and it is lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, + ]) + ).toEqual({ + name: 'custom', + params: { + colorStops: [{ color: '#000000', stop: 100 }], + continuity: 'below', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 100, + rangeMin: -Infinity, + rangeType: 'number', + reverse: false, + steps: 1, + stops: [{ color: '#000000', stop: 100 }], + }, + type: 'palette', + }); + }); + + test('should return undefined if more than two types of rules', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, + { id: 'some-id-7', operator: 'lt', value: 200, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); + + test('should return undefined if two types of rules and last rule is not lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'gte', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lt', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); + + test('should return undefined if all rules are lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'lte', value: 150, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); + + test('should return undefined if two types of rules and all except last one are lt and last one is not lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lt', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'gte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'lt', value: 150, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); + + test('should return custom palette if two types of rules and all except last one is lt and last one is lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lt', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'lt', value: 150, background_color: '#000' }, + ]) + ).toEqual({ + name: 'custom', + params: { + colorStops: [ + { color: '#000000', stop: -Infinity }, + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + ], + continuity: 'below', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 200, + rangeMin: -Infinity, + rangeType: 'number', + reverse: false, + steps: 4, + stops: [ + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + { color: '#000000', stop: 200 }, + ], + }, + type: 'palette', + }); + }); + + test('should return custom palette if last one is lte and all previous are gte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'gte', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, + ]) + ).toEqual({ + name: 'custom', + params: { + colorStops: [ + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + ], + continuity: 'none', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 200, + rangeMin: 100, + rangeType: 'number', + reverse: false, + steps: 2, + stops: [ + { color: '#000000', stop: 150 }, + { color: '#000000', stop: 200 }, + ], + }, + type: 'palette', + }); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts new file mode 100644 index 000000000000..55741c57595e --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import color from 'color'; +import { ColorStop, CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; +import { uniqBy } from 'lodash'; +import { Panel } from '../../../../../common/types'; + +const Operators = { + GTE: 'gte', + GT: 'gt', + LTE: 'lte', + LT: 'lt', +} as const; + +type ColorStopsWithMinMax = Pick< + CustomPaletteParams, + 'colorStops' | 'stops' | 'steps' | 'rangeMax' | 'rangeMin' | 'continuity' +>; + +const getColorStopsWithMinMaxForAllGteOrWithLte = ( + rules: Exclude, + tailOperator: string +): ColorStopsWithMinMax => { + const lastRule = rules[rules.length - 1]; + const lastRuleColor = (lastRule.background_color ?? lastRule.color)!; + + const colorStops = rules.reduce((colors, rule, index, rulesArr) => { + const rgbColor = (rule.background_color ?? rule.color)!; + if (index === rulesArr.length - 1 && tailOperator === Operators.LTE) { + return colors; + } + // if last operation is LTE, color of gte should be replaced by lte + if (index === rulesArr.length - 2 && tailOperator === Operators.LTE) { + return [ + ...colors, + { + color: color(lastRuleColor).hex(), + stop: rule.value!, + }, + ]; + } + return [ + ...colors, + { + color: color(rgbColor).hex(), + stop: rule.value!, + }, + ]; + }, []); + + const stops = colorStops.reduce((prevStops, colorStop, index, colorStopsArr) => { + if (index === colorStopsArr.length - 1) { + return [ + ...prevStops, + { + color: colorStop.color, + stop: tailOperator === Operators.LTE ? lastRule.value! : colorStop.stop + 1, + }, + ]; + } + return [...prevStops, { color: colorStop.color, stop: colorStopsArr[index + 1].stop }]; + }, []); + + const [rule] = rules; + return { + rangeMin: rule.value, + rangeMax: tailOperator === Operators.LTE ? lastRule.value : Infinity, + colorStops, + stops, + steps: colorStops.length, + continuity: tailOperator === Operators.LTE ? 'none' : 'above', + }; +}; + +const getColorStopsWithMinMaxForLtWithLte = ( + rules: Exclude +): ColorStopsWithMinMax => { + const lastRule = rules[rules.length - 1]; + const colorStops = rules.reduce((colors, rule, index, rulesArr) => { + if (index === 0) { + return [{ color: color((rule.background_color ?? rule.color)!).hex(), stop: -Infinity }]; + } + const rgbColor = (rule.background_color ?? rule.color)!; + return [ + ...colors, + { + color: color(rgbColor).hex(), + stop: rulesArr[index - 1].value!, + }, + ]; + }, []); + + const stops = colorStops.reduce((prevStops, colorStop, index, colorStopsArr) => { + if (index === colorStopsArr.length - 1) { + return [ + ...prevStops, + { + color: colorStop.color, + stop: lastRule.value!, + }, + ]; + } + return [...prevStops, { color: colorStop.color, stop: colorStopsArr[index + 1].stop }]; + }, []); + + return { + rangeMin: -Infinity, + rangeMax: lastRule.value, + colorStops, + stops, + steps: colorStops.length + 1, + continuity: 'below', + }; +}; + +const getColorStopWithMinMaxForLte = ( + rule: Exclude[number] +): ColorStopsWithMinMax => { + const colorStop = { + color: color((rule.background_color ?? rule.color)!).hex(), + stop: rule.value!, + }; + return { + rangeMin: -Infinity, + rangeMax: rule.value!, + colorStops: [colorStop], + stops: [colorStop], + steps: 1, + continuity: 'below', + }; +}; + +const getCustomPalette = ( + colorStopsWithMinMax: ColorStopsWithMinMax +): PaletteOutput => { + return { + name: 'custom', + params: { + continuity: 'all', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: Infinity, + rangeMin: -Infinity, + rangeType: 'number', + reverse: false, + ...colorStopsWithMinMax, + }, + type: 'palette', + }; +}; + +export const getPalette = ( + rules: Exclude +): PaletteOutput | null | undefined => { + const validRules = + rules.filter( + ({ operator, color: textColor, value, background_color: bColor }) => + operator && (bColor ?? textColor) && value !== undefined + ) ?? []; + + validRules.sort((rule1, rule2) => { + return rule1.value! - rule2.value!; + }); + + const kindOfRules = uniqBy(validRules, 'operator'); + + if (!kindOfRules.length) { + return; + } + + // lnsMetric is supporting lte only, if one rule is defined + if (validRules.length === 1) { + if (validRules[0].operator !== Operators.LTE) { + return; + } + return getCustomPalette(getColorStopWithMinMaxForLte(validRules[0])); + } + + const headRules = validRules.slice(0, -1); + const tailRule = validRules[validRules.length - 1]; + const kindOfHeadRules = uniqBy(headRules, 'operator'); + + if ( + kindOfHeadRules.length > 1 || + (kindOfHeadRules[0].operator !== tailRule.operator && tailRule.operator !== Operators.LTE) + ) { + return; + } + + const [rule] = kindOfHeadRules; + + if (rule.operator === Operators.LTE) { + return; + } + + if (rule.operator === Operators.LT) { + if (tailRule.operator !== Operators.LTE) { + return; + } + return getCustomPalette(getColorStopsWithMinMaxForLtWithLte(validRules)); + } + + if (rule.operator === Operators.GTE) { + return getCustomPalette( + getColorStopsWithMinMaxForAllGteOrWithLte(validRules, tailRule.operator!) + ); + } +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts index 36f05c440bdc..e03701e6ea15 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts @@ -17,7 +17,7 @@ export { export { convertToCumulativeSumColumns } from './cumulative_sum'; export { convertFilterRatioToFormulaColumn } from './filter_ratio'; export { convertToLastValueColumn } from './last_value'; -export { convertToStaticValueColumn } from './static_value'; +export { convertToStaticValueColumn, convertStaticValueToFormulaColumn } from './static_value'; export { convertToFiltersColumn } from './filters'; export { convertToDateHistogramColumn } from './date_histogram'; export { convertToTermsColumn } from './terms'; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts index c4400f72b289..e03a9d782136 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts @@ -7,9 +7,10 @@ */ import { StaticValueParams } from '@kbn/visualizations-plugin/common/convert_to_lens'; -import { CommonColumnsConverterArgs, StaticValueColumn } from './types'; +import { CommonColumnsConverterArgs, FormulaColumn, StaticValueColumn } from './types'; import type { Metric } from '../../../../common/types'; import { createColumn, getFormat } from './column'; +import { createFormulaColumn } from './formula'; export const convertToStaticValueParams = ({ value }: Metric): StaticValueParams => ({ value, @@ -37,3 +38,22 @@ export const convertToStaticValueColumn = ( }, }; }; + +export const convertStaticValueToFormulaColumn = ( + { series, metrics, dataView }: CommonColumnsConverterArgs, + { + visibleSeriesCount = 0, + reducedTimeRange, + }: { visibleSeriesCount?: number; reducedTimeRange?: string } = {} +): FormulaColumn | null => { + // Lens support reference lines only when at least one layer data exists + if (visibleSeriesCount === 1) { + return null; + } + const currentMetric = metrics[metrics.length - 1]; + return createFormulaColumn(currentMetric.value ?? '', { + series, + metric: currentMetric, + dataView, + }); +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts index d21291dc2622..e5b862a0fe70 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts @@ -85,7 +85,7 @@ export type MovingAverageColumn = GenericColumnWithMeta; export type StaticValueColumn = GenericColumnWithMeta; -type ColumnsWithoutMeta = FiltersColumn | TermsColumn | DateHistogramColumn; +export type ColumnsWithoutMeta = FiltersColumn | TermsColumn | DateHistogramColumn; export type AnyColumnWithReferences = GenericColumnWithMeta; type CommonColumns = Exclude; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts index 933e7e344b7f..76d15793f451 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts @@ -62,7 +62,12 @@ export type SupportedMetrics = LocalSupportedMetrics & { [Key in UnsupportedSupportedMetrics]?: null; }; -const supportedPanelTypes: readonly PANEL_TYPES[] = [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N]; +const supportedPanelTypes: readonly PANEL_TYPES[] = [ + PANEL_TYPES.TIMESERIES, + PANEL_TYPES.TOP_N, + PANEL_TYPES.METRIC, +]; + const supportedTimeRangeModes: readonly TIME_RANGE_DATA_MODES[] = [ TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, TIME_RANGE_DATA_MODES.LAST_VALUE, diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.test.ts index 5ca1fe71a0ad..4461072c8df6 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.test.ts @@ -20,6 +20,7 @@ const mockConvertToCounterRateColumn = jest.fn(); const mockConvertOtherAggsToFormulaColumn = jest.fn(); const mockConvertToLastValueColumn = jest.fn(); const mockConvertToStaticValueColumn = jest.fn(); +const mockConvertStaticValueToFormulaColumn = jest.fn(); const mockConvertToStandartDeviationColumn = jest.fn(); const mockConvertMetricAggregationColumnWithoutSpecialParams = jest.fn(); @@ -32,6 +33,7 @@ jest.mock('../convert', () => ({ convertOtherAggsToFormulaColumn: jest.fn(() => mockConvertOtherAggsToFormulaColumn()), convertToLastValueColumn: jest.fn(() => mockConvertToLastValueColumn()), convertToStaticValueColumn: jest.fn(() => mockConvertToStaticValueColumn()), + convertStaticValueToFormulaColumn: jest.fn(() => mockConvertStaticValueToFormulaColumn()), convertToStandartDeviationColumn: jest.fn(() => mockConvertToStandartDeviationColumn()), convertMetricAggregationColumnWithoutSpecialParams: jest.fn(() => mockConvertMetricAggregationColumnWithoutSpecialParams() @@ -138,8 +140,18 @@ describe('getMetricsColumns', () => { mockConvertToLastValueColumn, ], [ - 'call convertToStaticValueColumn if metric type is static', + 'call convertStaticValueToFormulaColumn if metric type is static', [createSeries({ metrics: [{ type: TSVB_METRIC_TYPES.STATIC, id: '1' }] }), dataView, 1], + mockConvertStaticValueToFormulaColumn, + ], + [ + 'call convertToStaticValueColumn if metric type is static and isStaticValueColumnSupported is true', + [ + createSeries({ metrics: [{ type: TSVB_METRIC_TYPES.STATIC, id: '1' }] }), + dataView, + 1, + { isStaticValueColumnSupported: true }, + ], mockConvertToStaticValueColumn, ], [ diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.ts index 07294a3a61aa..8f7d4ded0d07 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.ts @@ -21,6 +21,7 @@ import { convertFilterRatioToFormulaColumn, convertToLastValueColumn, convertToStaticValueColumn, + convertStaticValueToFormulaColumn, convertMetricAggregationColumnWithoutSpecialParams, convertToCounterRateColumn, convertToStandartDeviationColumn, @@ -31,7 +32,10 @@ export const getMetricsColumns = ( series: Series, dataView: DataView, visibleSeriesCount: number, - reducedTimeRange?: string + { + isStaticValueColumnSupported = false, + reducedTimeRange, + }: { reducedTimeRange?: string; isStaticValueColumnSupported?: boolean } = {} ): Column[] | null => { const { metrics: validMetrics, seriesAgg } = getSeriesAgg( series.metrics as [Metric, ...Metric[]] @@ -117,10 +121,12 @@ export const getMetricsColumns = ( return getValidColumns(column); } case 'static': { - const column = convertToStaticValueColumn(columnsConverterArgs, { - visibleSeriesCount, - reducedTimeRange, - }); + const column = isStaticValueColumnSupported + ? convertToStaticValueColumn(columnsConverterArgs, { + visibleSeriesCount, + reducedTimeRange, + }) + : convertStaticValueToFormulaColumn(columnsConverterArgs); return getValidColumns(column); } case 'std_deviation': { diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.test.ts new file mode 100644 index 000000000000..9407599573d9 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.test.ts @@ -0,0 +1,272 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { METRIC_TYPES } from '@kbn/data-plugin/public'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { convertToLens } from '.'; +import { createPanel, createSeries } from '../lib/__mocks__'; + +const mockGetMetricsColumns = jest.fn(); +const mockGetBucketsColumns = jest.fn(); +const mockGetConfigurationForMetric = jest.fn(); +const mockIsValidMetrics = jest.fn(); +const mockGetDatasourceValue = jest + .fn() + .mockImplementation(() => Promise.resolve(stubLogstashDataView)); +const mockGetDataSourceInfo = jest.fn(); + +jest.mock('../../services', () => ({ + getDataViewsStart: jest.fn(() => mockGetDatasourceValue), +})); + +jest.mock('../lib/series', () => ({ + getMetricsColumns: jest.fn(() => mockGetMetricsColumns()), + getBucketsColumns: jest.fn(() => mockGetBucketsColumns()), +})); + +jest.mock('../lib/configurations/metric', () => ({ + getConfigurationForMetric: jest.fn(() => mockGetConfigurationForMetric()), +})); + +jest.mock('../lib/metrics', () => ({ + isValidMetrics: jest.fn(() => mockIsValidMetrics()), + getReducedTimeRange: jest.fn().mockReturnValue('10'), +})); + +jest.mock('../lib/datasource', () => ({ + getDataSourceInfo: jest.fn(() => mockGetDataSourceInfo()), +})); + +describe('convertToLens', () => { + const model = createPanel({ + series: [ + createSeries({ + metrics: [ + { id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }, + { id: 'some-id-1', type: METRIC_TYPES.COUNT }, + ], + }), + ], + }); + + const bucket = { + isBucketed: true, + isSplit: true, + operationType: 'terms', + params: { + exclude: [], + excludeIsRegex: true, + include: [], + includeIsRegex: true, + orderAgg: { + columnId: 'some-id-0', + dataType: 'number', + isBucketed: true, + isSplit: false, + operationType: 'average', + params: {}, + sourceField: 'bytes', + }, + orderBy: { columnId: 'some-id-0', type: 'column' }, + orderDirection: 'asc', + otherBucket: false, + parentFormat: { id: 'terms' }, + secondaryFields: [], + size: 3, + }, + sourceField: 'bytes', + }; + + const bucket2 = { + isBucketed: true, + isSplit: true, + operationType: 'terms', + params: { + exclude: [], + excludeIsRegex: true, + include: [], + includeIsRegex: true, + orderAgg: { + columnId: 'some-id-1', + dataType: 'number', + isBucketed: true, + isSplit: false, + operationType: 'average', + params: {}, + sourceField: 'bytes', + }, + orderBy: { columnId: 'some-id-1', type: 'column' }, + orderDirection: 'desc', + otherBucket: false, + parentFormat: { id: 'terms' }, + secondaryFields: [], + size: 10, + }, + sourceField: 'bytes', + }; + + const metric = { + meta: { metricId: 'some-id-0' }, + operationType: 'last_value', + params: { showArrayValues: false, sortField: '@timestamp' }, + reducedTimeRange: '10m', + }; + + beforeEach(() => { + mockIsValidMetrics.mockReturnValue(true); + mockGetDataSourceInfo.mockReturnValue({ + indexPatternId: 'test-index-pattern', + timeField: 'timeField', + indexPattern: { id: 'test-index-pattern' }, + }); + mockGetMetricsColumns.mockReturnValue([{}]); + mockGetBucketsColumns.mockReturnValue([{}]); + mockGetConfigurationForMetric.mockReturnValue({}); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return null for invalid metrics', async () => { + mockIsValidMetrics.mockReturnValue(null); + const result = await convertToLens(model); + expect(result).toBeNull(); + expect(mockIsValidMetrics).toBeCalledTimes(1); + }); + + test('should return null for invalid or unsupported metrics', async () => { + mockGetMetricsColumns.mockReturnValue(null); + const result = await convertToLens(model); + expect(result).toBeNull(); + expect(mockGetMetricsColumns).toBeCalledTimes(1); + }); + + test('should return null for invalid or unsupported buckets', async () => { + mockGetBucketsColumns.mockReturnValue(null); + const result = await convertToLens(model); + expect(result).toBeNull(); + expect(mockGetBucketsColumns).toBeCalledTimes(1); + }); + + test('should return state for valid model', async () => { + const result = await convertToLens(model); + expect(result).toBeDefined(); + expect(result?.type).toBe('lnsMetric'); + expect(mockGetBucketsColumns).toBeCalledTimes(model.series.length); + expect(mockGetConfigurationForMetric).toBeCalledTimes(1); + }); + + test('should skip hidden series', async () => { + const result = await convertToLens( + createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: true, + }), + ], + }) + ); + expect(result).toBeDefined(); + expect(result?.type).toBe('lnsMetric'); + expect(mockIsValidMetrics).toBeCalledTimes(0); + }); + + test('should return null if multiple indexPatterns are provided', async () => { + mockGetDataSourceInfo.mockReturnValueOnce({ + indexPatternId: 'test-index-pattern-1', + timeField: 'timeField', + indexPattern: { id: 'test-index-pattern-1' }, + }); + + const result = await convertToLens( + createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + ], + }) + ); + expect(result).toBeNull(); + }); + + test('should return null if visible series is 2 and bucket is 1', async () => { + mockGetBucketsColumns.mockReturnValueOnce([bucket]); + mockGetBucketsColumns.mockReturnValueOnce([]); + mockGetMetricsColumns.mockReturnValueOnce([metric]); + + const result = await convertToLens( + createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + ], + }) + ); + expect(result).toBeNull(); + }); + + test('should return null if visible series is 2 and two not unique buckets', async () => { + mockGetBucketsColumns.mockReturnValueOnce([bucket]); + mockGetBucketsColumns.mockReturnValueOnce([bucket2]); + mockGetMetricsColumns.mockReturnValueOnce([metric]); + + const result = await convertToLens( + createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + ], + }) + ); + expect(result).toBeNull(); + }); + + test('should return state if visible series is 2 and two unique buckets', async () => { + mockGetBucketsColumns.mockReturnValueOnce([bucket]); + mockGetBucketsColumns.mockReturnValueOnce([bucket]); + mockGetMetricsColumns.mockReturnValueOnce([metric]); + + const result = await convertToLens( + createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + ], + }) + ); + expect(result).toBeDefined(); + expect(result?.type).toBe('lnsMetric'); + expect(mockGetConfigurationForMetric).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.ts new file mode 100644 index 000000000000..25f55b5a1c44 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import uuid from 'uuid'; +import { DataView, parseTimeShift } from '@kbn/data-plugin/common'; +import { getIndexPatternIds } from '@kbn/visualizations-plugin/common/convert_to_lens'; +import { PANEL_TYPES } from '../../../common/enums'; +import { getDataViewsStart } from '../../services'; +import { getDataSourceInfo } from '../lib/datasource'; +import { getMetricsColumns, getBucketsColumns } from '../lib/series'; +import { getConfigurationForMetric as getConfiguration } from '../lib/configurations/metric'; +import { getReducedTimeRange, isValidMetrics } from '../lib/metrics'; +import { ConvertTsvbToLensVisualization } from '../types'; +import { ColumnsWithoutMeta, Layer as ExtendedLayer } from '../lib/convert'; +import { excludeMetaFromLayers, getUniqueBuckets } from './utils'; + +const MAX_SERIES = 2; +const MAX_BUCKETS = 2; + +export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeRange) => { + const dataViews = getDataViewsStart(); + const seriesNum = model.series.filter((series) => !series.hidden).length; + + const indexPatternIds = new Set(); + const visibleSeries = model.series.filter(({ hidden }) => !hidden); + let currentIndexPattern: DataView | null = null; + for (const series of visibleSeries) { + const datasourceInfo = await getDataSourceInfo( + model.index_pattern, + model.time_field, + Boolean(series.override_index_pattern), + series.series_index_pattern, + series.series_time_field, + dataViews + ); + + if (!datasourceInfo) { + return null; + } + + const { indexPatternId, indexPattern } = datasourceInfo; + indexPatternIds.add(indexPatternId); + currentIndexPattern = indexPattern; + } + + if (indexPatternIds.size > 1) { + return null; + } + + const [indexPatternId] = indexPatternIds.values(); + + const buckets = []; + const metrics = []; + + // handle multiple layers/series + for (const series of visibleSeries) { + // not valid time shift + if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') { + return null; + } + + if (!isValidMetrics(series.metrics, PANEL_TYPES.METRIC, series.time_range_mode)) { + return null; + } + + const reducedTimeRange = getReducedTimeRange(model, series, timeRange); + + // handle multiple metrics + const metricsColumns = getMetricsColumns(series, currentIndexPattern!, seriesNum, { + reducedTimeRange, + }); + if (metricsColumns === null) { + return null; + } + + const bucketsColumns = getBucketsColumns( + model, + series, + metricsColumns, + currentIndexPattern!, + false + ); + + if (bucketsColumns === null) { + return null; + } + + buckets.push(...bucketsColumns); + metrics.push(...metricsColumns); + } + + let uniqueBuckets = buckets; + if (visibleSeries.length === MAX_SERIES && buckets.length) { + if (buckets.length !== MAX_BUCKETS) { + return null; + } + + uniqueBuckets = getUniqueBuckets(buckets as ColumnsWithoutMeta[]); + if (uniqueBuckets.length !== 1) { + return null; + } + } + + const [bucket] = uniqueBuckets; + + const extendedLayer: ExtendedLayer = { + indexPatternId: indexPatternId as string, + layerId: uuid(), + columns: [...metrics, ...(bucket ? [bucket] : [])], + columnOrder: [], + }; + + const configuration = getConfiguration(model, extendedLayer, bucket); + if (!configuration) { + return null; + } + + const layers = Object.values(excludeMetaFromLayers({ 0: extendedLayer })); + return { + type: 'lnsMetric', + layers, + configuration, + indexPatternIds: getIndexPatternIds(layers), + }; +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.test.ts new file mode 100644 index 000000000000..8f880dfe95c5 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Column, DateHistogramColumn, TermsColumn } from '../lib/convert'; +import { getUniqueBuckets } from './utils'; + +describe('getUniqueBuckets', () => { + const bucket2: TermsColumn = { + columnId: '12', + dataType: 'string', + isBucketed: true, + isSplit: true, + operationType: 'terms', + params: { + exclude: [], + excludeIsRegex: true, + include: [], + includeIsRegex: true, + orderAgg: { + columnId: 'some-id-1', + dataType: 'number', + isBucketed: true, + isSplit: false, + operationType: 'average', + params: {}, + sourceField: 'bytes', + }, + orderBy: { columnId: 'some-id-1', type: 'column' }, + orderDirection: 'desc', + otherBucket: false, + parentFormat: { id: 'terms' }, + secondaryFields: [], + size: 10, + }, + sourceField: 'bytes', + }; + + it('should return unique buckets', () => { + expect(getUniqueBuckets([bucket2, bucket2])).toEqual([bucket2]); + }); + + it('should ignore columnIds', () => { + const bucketWithOtherColumnIds = { + ...bucket2, + columnId: '22', + params: { + ...bucket2.params, + orderAgg: { ...bucket2.params.orderAgg, columnId: '---1' } as Column, + orderBy: { ...bucket2.params.orderBy, columnId: '---2' } as { + type: 'column'; + columnId: string; + }, + }, + }; + expect(getUniqueBuckets([bucket2, bucketWithOtherColumnIds])).toEqual([bucket2]); + }); + + it('should respect differences of terms', () => { + const bucketWithOtherColumnIds = { + ...bucket2, + columnId: '22', + params: { + ...bucket2.params, + orderAgg: { ...bucket2.params.orderAgg, columnId: '---1' } as Column, + orderBy: { ...bucket2.params.orderBy, columnId: '---2' } as { + type: 'column'; + columnId: string; + }, + }, + sourceField: 'name', + }; + expect(getUniqueBuckets([bucket2, bucketWithOtherColumnIds])).toEqual([ + bucket2, + bucketWithOtherColumnIds, + ]); + }); + + it('should respect differences of other buckets', () => { + const bucket: DateHistogramColumn = { + dataType: 'number', + isBucketed: true, + isSplit: false, + operationType: 'date_histogram', + params: { dropPartials: false, includeEmptyRows: true, interval: 'auto' }, + sourceField: 'field1', + columnId: '1', + }; + const bucket1 = { + ...bucket, + columnId: '22', + }; + expect(getUniqueBuckets([bucket, bucket1])).toEqual([bucket]); + const bucket3 = { + ...bucket, + field: 'some-other-field', + }; + expect(getUniqueBuckets([bucket, bucket3])).toEqual([bucket, bucket3]); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.ts new file mode 100644 index 000000000000..8df1b0f40f8b --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { uniqWith } from 'lodash'; +import deepEqual from 'react-fast-compare'; +import { Layer, Operations, TermsColumn } from '@kbn/visualizations-plugin/common/convert_to_lens'; +import { Layer as ExtendedLayer, excludeMetaFromColumn, ColumnsWithoutMeta } from '../lib/convert'; + +export const excludeMetaFromLayers = ( + layers: Record +): Record => { + const newLayers: Record = {}; + Object.entries(layers).forEach(([layerId, layer]) => { + const columns = layer.columns.map(excludeMetaFromColumn); + newLayers[layerId] = { ...layer, columns }; + }); + + return newLayers; +}; + +const excludeColumnIdsFromBucket = (bucket: ColumnsWithoutMeta) => { + const { columnId, ...restBucket } = bucket; + if (bucket.operationType === Operations.TERMS) { + const { orderBy, orderAgg, ...restParams } = bucket.params; + let orderByWithoutColumn: Omit = orderBy; + if ('columnId' in orderBy) { + const { columnId: orderByColumnId, ...restOrderBy } = orderBy; + orderByWithoutColumn = restOrderBy; + } + + let orderAggWithoutColumn: Omit | undefined = + orderAgg; + if (orderAgg) { + const { columnId: cId, ...restOrderAgg } = orderAgg; + orderAggWithoutColumn = restOrderAgg; + } + + return { + ...restBucket, + params: { + ...restParams, + orderBy: orderByWithoutColumn, + orderAgg: orderAggWithoutColumn, + }, + }; + } + return restBucket; +}; + +export const getUniqueBuckets = (buckets: ColumnsWithoutMeta[]) => + uniqWith(buckets, (bucket1, bucket2) => { + if (bucket1.operationType !== bucket2.operationType) { + return false; + } + + const bucketWithoutColumnIds1 = excludeColumnIdsFromBucket(bucket1); + const bucketWithoutColumnIds2 = excludeColumnIdsFromBucket(bucket2); + + return deepEqual(bucketWithoutColumnIds1, bucketWithoutColumnIds2); + }); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts index 4610310c1b37..8cbbbf0f9e73 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts @@ -86,7 +86,9 @@ export const convertToLens: ConvertTsvbToLensVisualization = async (model: Panel return null; } // handle multiple metrics - const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum); + const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, { + isStaticValueColumnSupported: true, + }); if (metricsColumns === null) { return null; } diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts index 630a9e5cf4c9..020aaec28f57 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts @@ -65,7 +65,9 @@ export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeR const reducedTimeRange = getReducedTimeRange(model, series, timeRange); // handle multiple metrics - const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, reducedTimeRange); + const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, { + reducedTimeRange, + }); if (!metricsColumns) { return null; } diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts index acee3c2a1e83..69a90c7864eb 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts @@ -6,14 +6,18 @@ * Side Public License, v 1. */ -import { NavigateToLensContext, XYConfiguration } from '@kbn/visualizations-plugin/common'; +import { + MetricVisConfiguration, + NavigateToLensContext, + XYConfiguration, +} from '@kbn/visualizations-plugin/common'; import { TimeRange } from '@kbn/data-plugin/common'; import type { Panel } from '../../common/types'; export type ConvertTsvbToLensVisualization = ( model: Panel, timeRange?: TimeRange -) => Promise | null>; +) => Promise | null>; export interface Filter { kql?: string | { [key: string]: any } | undefined; diff --git a/src/plugins/visualizations/common/convert_to_lens/constants.ts b/src/plugins/visualizations/common/convert_to_lens/constants.ts index fa92b881033c..12ed815bc724 100644 --- a/src/plugins/visualizations/common/convert_to_lens/constants.ts +++ b/src/plugins/visualizations/common/convert_to_lens/constants.ts @@ -36,6 +36,70 @@ export const OperationsWithReferences = { export const Operations = { ...OperationsWithSourceField, ...OperationsWithReferences } as const; +export const PartitionChartTypes = { + PIE: 'pie', + DONUT: 'donut', + TREEMAP: 'treemap', + MOSAIC: 'mosaic', + WAFFLE: 'waffle', +} as const; + +export const CategoryDisplayTypes = { + DEFAULT: 'default', + INSIDE: 'inside', + HIDE: 'hide', +} as const; + +export const NumberDisplayTypes = { + HIDDEN: 'hidden', + PERCENT: 'percent', + VALUE: 'value', +} as const; + +export const LegendDisplayTypes = { + DEFAULT: 'default', + SHOW: 'show', + HIDE: 'hide', +} as const; + +export const LayerTypes = { + DATA: 'data', + REFERENCELINE: 'referenceLine', + ANNOTATIONS: 'annotations', +} as const; + +export const XYCurveTypes = { + LINEAR: 'LINEAR', + CURVE_MONOTONE_X: 'CURVE_MONOTONE_X', + CURVE_STEP_AFTER: 'CURVE_STEP_AFTER', +} as const; + +export const YAxisModes = { + AUTO: 'auto', + LEFT: 'left', + RIGHT: 'right', + BOTTOM: 'bottom', +} as const; + +export const SeriesTypes = { + BAR: 'bar', + LINE: 'line', + AREA: 'area', + BAR_STACKED: 'bar_stacked', + AREA_STACKED: 'area_stacked', + BAR_HORIZONTAL: 'bar_horizontal', + BAR_PERCENTAGE_STACKED: 'bar_percentage_stacked', + BAR_HORIZONTAL_STACKED: 'bar_horizontal_stacked', + AREA_PERCENTAGE_STACKED: 'area_percentage_stacked', + BAR_HORIZONTAL_PERCENTAGE_STACKED: 'bar_horizontal_percentage_stacked', +} as const; + +export const FillTypes = { + NONE: 'none', + ABOVE: 'above', + BELOW: 'below', +} as const; + export const RANGE_MODES = { Range: 'range', Histogram: 'histogram', diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/buckets/index.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/buckets/index.test.ts new file mode 100644 index 000000000000..f0a8e4d32f7c --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/buckets/index.test.ts @@ -0,0 +1,267 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { BUCKET_TYPES, METRIC_TYPES } from '@kbn/data-plugin/common'; +import { convertBucketToColumns } from '.'; +import { DateHistogramColumn, FiltersColumn, RangeColumn, TermsColumn } from '../../types'; +import { AggBasedColumn, SchemaConfig } from '../../..'; + +const mockConvertToDateHistogramColumn = jest.fn(); +const mockConvertToFiltersColumn = jest.fn(); +const mockConvertToTermsColumn = jest.fn(); +const mockConvertToRangeColumn = jest.fn(); + +jest.mock('../convert', () => ({ + convertToDateHistogramColumn: jest.fn(() => mockConvertToDateHistogramColumn()), + convertToFiltersColumn: jest.fn(() => mockConvertToFiltersColumn()), + convertToTermsColumn: jest.fn(() => mockConvertToTermsColumn()), + convertToRangeColumn: jest.fn(() => mockConvertToRangeColumn()), +})); + +describe('convertBucketToColumns', () => { + const field = stubLogstashDataView.fields[0].name; + const dateField = stubLogstashDataView.fields.find((f) => f.type === 'date')!.name; + const bucketAggs: SchemaConfig[] = [ + { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: BUCKET_TYPES.FILTERS, + aggParams: { + filters: [], + }, + }, + { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: BUCKET_TYPES.DATE_HISTOGRAM, + aggParams: { + field, + }, + }, + { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: BUCKET_TYPES.TERMS, + aggParams: { + field, + orderBy: '_key', + }, + }, + { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: BUCKET_TYPES.TERMS, + aggParams: { + field: dateField, + orderBy: '_key', + }, + }, + { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: BUCKET_TYPES.HISTOGRAM, + aggParams: { + field, + interval: '1h', + }, + }, + { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: BUCKET_TYPES.RANGE, + aggParams: { + field, + }, + }, + { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: BUCKET_TYPES.DATE_RANGE, + aggParams: { + field, + }, + }, + ]; + const aggs: Array> = [ + { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG, + aggParams: { + field, + }, + }, + ]; + const metricColumns: AggBasedColumn[] = [ + { + columnId: 'column-1', + operationType: 'average', + isBucketed: false, + isSplit: false, + sourceField: field, + dataType: 'number', + params: {}, + meta: { + aggId: '1', + }, + }, + ]; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test.each< + [ + string, + Parameters, + () => void, + Partial | null + ] + >([ + [ + 'null if bucket agg type is not supported', + [{ dataView: stubLogstashDataView, agg: bucketAggs[6], aggs, metricColumns }], + () => {}, + null, + ], + [ + 'null if bucket agg does not have aggParams', + [ + { + dataView: stubLogstashDataView, + agg: { ...bucketAggs[0], aggParams: undefined }, + aggs, + metricColumns, + }, + ], + () => {}, + null, + ], + [ + 'filters column if bucket agg is valid filters agg', + [{ dataView: stubLogstashDataView, agg: bucketAggs[0], aggs, metricColumns }], + () => { + mockConvertToFiltersColumn.mockReturnValue({ + operationType: 'filters', + }); + }, + { + operationType: 'filters', + }, + ], + [ + 'date histogram column if bucket agg is valid date histogram agg', + [{ dataView: stubLogstashDataView, agg: bucketAggs[1], aggs, metricColumns }], + () => { + mockConvertToDateHistogramColumn.mockReturnValue({ + operationType: 'date_histogram', + }); + }, + { + operationType: 'date_histogram', + }, + ], + [ + 'date histogram column if bucket agg is valid terms agg with date field', + [{ dataView: stubLogstashDataView, agg: bucketAggs[3], aggs, metricColumns }], + () => { + mockConvertToDateHistogramColumn.mockReturnValue({ + operationType: 'date_histogram', + }); + }, + { + operationType: 'date_histogram', + }, + ], + [ + 'terms column if bucket agg is valid terms agg with no date field', + [{ dataView: stubLogstashDataView, agg: bucketAggs[2], aggs, metricColumns }], + () => { + mockConvertToTermsColumn.mockReturnValue({ + operationType: 'terms', + }); + }, + { + operationType: 'terms', + }, + ], + [ + 'range column if bucket agg is valid histogram agg', + [{ dataView: stubLogstashDataView, agg: bucketAggs[4], aggs, metricColumns }], + () => { + mockConvertToRangeColumn.mockReturnValue({ + operationType: 'range', + }); + }, + { + operationType: 'range', + }, + ], + [ + 'range column if bucket agg is valid range agg', + [{ dataView: stubLogstashDataView, agg: bucketAggs[5], aggs, metricColumns }], + () => { + mockConvertToRangeColumn.mockReturnValue({ + operationType: 'range', + }); + }, + { + operationType: 'range', + }, + ], + ])('should return %s', (_, input, actions, expected) => { + actions(); + if (expected === null) { + expect(convertBucketToColumns(...input)).toBeNull(); + } else { + expect(convertBucketToColumns(...input)).toEqual(expect.objectContaining(expected)); + } + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/column.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/column.test.ts new file mode 100644 index 000000000000..74e9f2a57a9f --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/column.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { METRIC_TYPES } from '@kbn/data-plugin/public'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { SchemaConfig } from '../../..'; +import { createColumn } from './column'; +import { GeneralColumnWithMeta } from './types'; + +describe('createColumn', () => { + const field = stubLogstashDataView.fields[0]; + const aggId = `some-id`; + const customLabel = 'some-custom-label'; + const label = 'some label'; + const timeShift = '1h'; + + const agg: SchemaConfig = { + accessor: 0, + label, + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG, + aggId, + aggParams: { + field: field.name, + }, + }; + + const aggWithCustomLabel: SchemaConfig = { + ...agg, + aggParams: { + field: field.name, + customLabel, + }, + }; + + const aggWithTimeShift: SchemaConfig = { + ...agg, + aggParams: { + field: field.name, + timeShift, + }, + }; + + const extraColumnFields = { isBucketed: true, isSplit: true, reducedTimeRange: '1m' }; + + test.each<[string, Parameters, Partial]>([ + [ + 'with default params', + [agg, field], + { + dataType: 'number', + isBucketed: false, + isSplit: false, + label, + meta: { aggId }, + }, + ], + [ + 'with custom label', + [aggWithCustomLabel, field], + { + dataType: 'number', + isBucketed: false, + isSplit: false, + label: customLabel, + meta: { aggId }, + }, + ], + [ + 'with timeShift', + [aggWithTimeShift, field], + { + dataType: 'number', + isBucketed: false, + isSplit: false, + label, + meta: { aggId }, + timeShift, + }, + ], + [ + 'with extra column fields', + [agg, field, extraColumnFields], + { + dataType: 'number', + isBucketed: extraColumnFields.isBucketed, + isSplit: extraColumnFields.isSplit, + reducedTimeRange: extraColumnFields.reducedTimeRange, + label, + meta: { aggId }, + }, + ], + ])('should create column by agg %s', (_, input, expected) => { + expect(createColumn(...input)).toEqual(expect.objectContaining(expected)); + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/date_histogram.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/date_histogram.test.ts new file mode 100644 index 000000000000..897e5f876542 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/date_histogram.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { AggParamsDateHistogram } from '@kbn/data-plugin/common'; +import { convertToDateHistogramColumn } from './date_histogram'; +import { DateHistogramColumn } from './types'; +import { DataType } from '../../types'; + +describe('convertToDateHistogramColumn', () => { + const aggId = `some-id`; + const timeShift = '1h'; + const aggParams: AggParamsDateHistogram = { + interval: '1d', + drop_partials: true, + field: stubLogstashDataView.fields[0].name, + }; + + test.each< + [string, Parameters, Partial | null] + >([ + [ + 'date histogram column if field is provided', + [aggId, aggParams, stubLogstashDataView, false, false], + { + dataType: stubLogstashDataView.fields[0].type as DataType, + isBucketed: true, + isSplit: false, + timeShift: undefined, + sourceField: stubLogstashDataView.fields[0].name, + meta: { aggId }, + params: { + interval: '1d', + includeEmptyRows: true, + dropPartials: true, + }, + }, + ], + [ + 'null if field is not provided', + [aggId, { interval: '1d', field: undefined }, stubLogstashDataView, false, false], + null, + ], + [ + 'date histogram column with isSplit and timeShift if specified', + [aggId, { ...aggParams, timeShift }, stubLogstashDataView, true, false], + { + dataType: stubLogstashDataView.fields[0].type as DataType, + isBucketed: true, + isSplit: true, + sourceField: stubLogstashDataView.fields[0].name, + timeShift, + meta: { aggId }, + params: { + interval: '1d', + includeEmptyRows: true, + dropPartials: true, + }, + }, + ], + [ + 'date histogram column with dropEmptyRowsInDateHistogram if specified', + [aggId, aggParams, stubLogstashDataView, true, true], + { + dataType: stubLogstashDataView.fields[0].type as DataType, + isBucketed: true, + isSplit: true, + sourceField: stubLogstashDataView.fields[0].name, + timeShift: undefined, + meta: { aggId }, + params: { + interval: '1d', + includeEmptyRows: false, + dropPartials: true, + }, + }, + ], + ])('should return %s', (_, input, expected) => { + if (expected === null) { + expect(convertToDateHistogramColumn(...input)).toBeNull(); + } else { + expect(convertToDateHistogramColumn(...input)).toEqual(expect.objectContaining(expected)); + } + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/filters.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/filters.test.ts new file mode 100644 index 000000000000..1de1dbc9b312 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/filters.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AggParamsFilters } from '@kbn/data-plugin/common'; +import { convertToFiltersColumn } from './filters'; +import { FiltersColumn } from './types'; + +describe('convertToFiltersColumn', () => { + const aggId = `some-id`; + const timeShift = '1h'; + const filters = [{ input: { language: 'lucene', query: 'some other query' }, label: 'split' }]; + const aggParams: AggParamsFilters = { + filters, + }; + + test.each<[string, Parameters, Partial | null]>([ + [ + 'filters column if filters are provided', + [aggId, aggParams], + { + dataType: 'string', + isBucketed: true, + isSplit: false, + timeShift: undefined, + meta: { aggId }, + params: { filters: aggParams.filters! }, + }, + ], + ['null if filters are not provided', [aggId, {}], null], + [ + 'filters column with isSplit and timeShift if specified', + [aggId, { ...aggParams, timeShift }, true], + { + dataType: 'string', + isBucketed: true, + isSplit: true, + timeShift, + meta: { aggId }, + params: { filters: aggParams.filters! }, + }, + ], + ])('should return %s', (_, input, expected) => { + if (expected === null) { + expect(convertToFiltersColumn(...input)).toBeNull(); + } else { + expect(convertToFiltersColumn(...input)).toEqual(expect.objectContaining(expected)); + } + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/formula.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/formula.test.ts new file mode 100644 index 000000000000..bbde6a04f1a2 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/formula.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IAggConfig, METRIC_TYPES } from '@kbn/data-plugin/common'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { SchemaConfig } from '../../..'; +import { createFormulaColumn } from './formula'; + +describe('createFormulaColumn', () => { + const aggId = `some-id`; + const label = 'some label'; + const agg: SchemaConfig = { + accessor: 0, + label, + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.CUMULATIVE_SUM, + aggId, + aggParams: { + customMetric: { + id: 'some-id-metric', + enabled: true, + type: { name: METRIC_TYPES.AVG }, + params: { + field: stubLogstashDataView.fields[0], + }, + } as IAggConfig, + }, + }; + test('should return formula column', () => { + expect(createFormulaColumn('test-formula', agg)).toEqual( + expect.objectContaining({ + isBucketed: false, + isSplit: false, + meta: { + aggId, + }, + operationType: 'formula', + params: { + formula: 'test-formula', + }, + references: [], + }) + ); + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/last_value.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/last_value.test.ts new file mode 100644 index 000000000000..55ba1e8b5e09 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/last_value.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AggParamsTopHit, METRIC_TYPES } from '@kbn/data-plugin/common'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { SchemaConfig } from '../../..'; +import { convertToLastValueColumn } from './last_value'; +import { FiltersColumn } from './types'; + +const mockGetFieldNameFromField = jest.fn(); +const mockGetFieldByName = jest.fn(); +const mockGetLabel = jest.fn(); + +jest.mock('../utils', () => ({ + getFieldNameFromField: jest.fn(() => mockGetFieldNameFromField()), + getLabel: jest.fn(() => mockGetLabel()), +})); + +describe('convertToLastValueColumn', () => { + const dataView = stubLogstashDataView; + const sortField = dataView.fields[0]; + + const topHitAggParams: AggParamsTopHit = { + sortOrder: { + value: 'desc', + text: 'some text', + }, + sortField, + field: '', + aggregate: 'min', + size: 1, + }; + + const topHitAgg: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.TOP_HITS, + aggParams: topHitAggParams, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockGetFieldNameFromField.mockReturnValue(dataView.fields[0]); + mockGetFieldByName.mockReturnValue(dataView.fields[0]); + mockGetLabel.mockReturnValue('someLabel'); + dataView.getFieldByName = mockGetFieldByName; + }); + + test.each<[string, Parameters, Partial | null]>([ + [ + 'null if top hits size is more than 1', + [{ agg: { ...topHitAgg, aggParams: { ...topHitAgg.aggParams!, size: 2 } }, dataView }], + null, + ], + [ + 'null if top hits sord order is not desc', + [ + { + agg: { + ...topHitAgg, + aggParams: { + ...topHitAgg.aggParams!, + sortOrder: { ...topHitAgg.aggParams!.sortOrder!, value: 'asc' }, + }, + }, + dataView, + }, + ], + null, + ], + ])('should return %s', (_, input, expected) => { + if (expected === null) { + expect(convertToLastValueColumn(...input)).toBeNull(); + } else { + expect(convertToLastValueColumn(...input)).toEqual(expect.objectContaining(expected)); + } + }); + + test('should skip if top hit field is not specified', () => { + mockGetFieldNameFromField.mockReturnValue(null); + expect(convertToLastValueColumn({ agg: topHitAgg, dataView })).toBeNull(); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(0); + }); + + test('should skip if top hit field is not present in index pattern', () => { + mockGetFieldByName.mockReturnValue(null); + dataView.getFieldByName = mockGetFieldByName; + + expect(convertToLastValueColumn({ agg: topHitAgg, dataView })).toBeNull(); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(1); + expect(mockGetLabel).toBeCalledTimes(0); + }); + + test('should return top hit column if top hit field is not present in index pattern', () => { + expect(convertToLastValueColumn({ agg: topHitAgg, dataView })).toEqual( + expect.objectContaining({ + dataType: 'number', + label: 'someLabel', + operationType: 'last_value', + params: { showArrayValues: true, sortField: 'bytes' }, + sourceField: 'bytes', + }) + ); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(1); + expect(mockGetLabel).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/metric.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/metric.test.ts new file mode 100644 index 000000000000..3be17abc46ac --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/metric.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { METRIC_TYPES } from '@kbn/data-plugin/common'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { SchemaConfig } from '../../..'; +import { convertMetricAggregationColumnWithoutSpecialParams } from './metric'; +import { SUPPORTED_METRICS } from './supported_metrics'; + +const mockGetFieldByName = jest.fn(); + +describe('convertToLastValueColumn', () => { + const dataView = stubLogstashDataView; + + const agg: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG, + aggParams: { + field: dataView.fields[0].displayName, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockGetFieldByName.mockReturnValue(dataView.fields[0]); + dataView.getFieldByName = mockGetFieldByName; + }); + + test('should return null metric is not supported', () => { + expect( + convertMetricAggregationColumnWithoutSpecialParams(SUPPORTED_METRICS[METRIC_TYPES.TOP_HITS], { + agg, + dataView, + }) + ).toBeNull(); + }); + + test('should skip if field is not present and is required for the aggregation', () => { + mockGetFieldByName.mockReturnValue(null); + dataView.getFieldByName = mockGetFieldByName; + + expect( + convertMetricAggregationColumnWithoutSpecialParams(SUPPORTED_METRICS[METRIC_TYPES.AVG], { + agg, + dataView, + }) + ).toBeNull(); + expect(dataView.getFieldByName).toBeCalledTimes(1); + }); + + test('should return column if field is not present and is not required for the aggregation', () => { + mockGetFieldByName.mockReturnValue(null); + dataView.getFieldByName = mockGetFieldByName; + + expect( + convertMetricAggregationColumnWithoutSpecialParams(SUPPORTED_METRICS[METRIC_TYPES.COUNT], { + agg, + dataView, + }) + ).toEqual(expect.objectContaining({ operationType: 'count' })); + expect(dataView.getFieldByName).toBeCalledTimes(1); + }); + + test('should return column if field is present and is required for the aggregation', () => { + mockGetFieldByName.mockReturnValue(dataView.fields[0]); + dataView.getFieldByName = mockGetFieldByName; + + expect( + convertMetricAggregationColumnWithoutSpecialParams(SUPPORTED_METRICS[METRIC_TYPES.AVG], { + agg, + dataView, + }) + ).toEqual( + expect.objectContaining({ + dataType: 'number', + operationType: 'average', + }) + ); + expect(dataView.getFieldByName).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.test.ts new file mode 100644 index 000000000000..c28324533c83 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.test.ts @@ -0,0 +1,438 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { METRIC_TYPES } from '@kbn/data-plugin/common'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { FormulaColumn, AggBasedColumn } from './types'; +import { SchemaConfig } from '../../..'; +import { + convertToOtherParentPipelineAggColumns, + ParentPipelineAggColumn, + convertToCumulativeSumAggColumn, +} from './parent_pipeline'; + +const mockGetMetricFromParentPipelineAgg = jest.fn(); +const mockGetFormulaForPipelineAgg = jest.fn(); +const mockConvertMetricToColumns = jest.fn(); +const mockGetFieldByName = jest.fn(); +const mockConvertMetricAggregationColumnWithoutSpecialParams = jest.fn(); + +jest.mock('../utils', () => ({ + getMetricFromParentPipelineAgg: jest.fn(() => mockGetMetricFromParentPipelineAgg()), + getLabel: jest.fn(() => 'label'), + getFieldNameFromField: jest.fn(() => 'document'), +})); + +jest.mock('./metric', () => ({ + convertMetricAggregationColumnWithoutSpecialParams: jest.fn(() => + mockConvertMetricAggregationColumnWithoutSpecialParams() + ), +})); + +jest.mock('../metrics', () => ({ + getFormulaForPipelineAgg: jest.fn(() => mockGetFormulaForPipelineAgg()), + convertMetricToColumns: jest.fn(() => mockConvertMetricToColumns()), +})); + +describe('convertToOtherParentPipelineAggColumns', () => { + const field = stubLogstashDataView.fields[0].name; + const aggs: Array> = [ + { + aggId: '1', + aggType: METRIC_TYPES.AVG, + aggParams: { field }, + accessor: 0, + params: {}, + label: 'average', + format: {}, + }, + { + aggId: '1', + aggType: METRIC_TYPES.MOVING_FN, + aggParams: { metricAgg: '2' }, + accessor: 0, + params: {}, + label: 'Moving Average of Average', + format: {}, + }, + ]; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test.each< + [ + string, + Parameters, + () => void, + Partial | [Partial, Partial] | null + ] + >([ + [ + 'null if getMetricFromParentPipelineAgg returns null', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue(null); + }, + null, + ], + [ + 'null if cutom metric of parent pipeline agg is not supported', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.GEO_BOUNDS, + }); + }, + null, + ], + [ + 'null if cutom metric of parent pipeline agg is sibling pipeline agg', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.AVG_BUCKET, + }); + }, + null, + ], + [ + 'null if cannot build formula if cutom metric of parent pipeline agg is parent pipeline agg', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.MOVING_FN, + }); + mockGetFormulaForPipelineAgg.mockReturnValue(null); + }, + null, + ], + [ + 'formula column if cutom metric of parent pipeline agg is valid parent pipeline agg', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.MOVING_FN, + }); + mockGetFormulaForPipelineAgg.mockReturnValue('test-formula'); + }, + { + operationType: 'formula', + params: { + formula: 'test-formula', + }, + }, + ], + [ + 'null if cutom metric of parent pipeline agg is invalid not pipeline agg', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.AVG, + }); + mockConvertMetricToColumns.mockReturnValue(null); + }, + null, + ], + [ + 'parent pipeline and metric columns if cutom metric of parent pipeline agg is valid not pipeline agg', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.AVG, + }); + mockConvertMetricToColumns.mockReturnValue([ + { + columnId: 'test-id-1', + operationType: 'average', + sourceField: field, + }, + ]); + }, + [ + { operationType: 'moving_average', references: ['test-id-1'] }, + { + columnId: 'test-id-1', + operationType: 'average', + sourceField: field, + }, + ], + ], + ])('should return %s', (_, input, actions, expected) => { + actions(); + if (expected === null) { + expect(convertToOtherParentPipelineAggColumns(...input)).toBeNull(); + } else if (Array.isArray(expected)) { + expect(convertToOtherParentPipelineAggColumns(...input)).toEqual( + expected.map(expect.objectContaining) + ); + } else { + expect(convertToOtherParentPipelineAggColumns(...input)).toEqual( + expect.objectContaining(expected) + ); + } + }); +}); + +describe('convertToCumulativeSumAggColumn', () => { + const field = stubLogstashDataView.fields[0].name; + const aggs: Array> = [ + { + aggId: '1', + aggType: METRIC_TYPES.AVG, + aggParams: { field }, + accessor: 0, + params: {}, + label: 'average', + format: {}, + }, + { + aggId: '1', + aggType: METRIC_TYPES.CUMULATIVE_SUM, + aggParams: { metricAgg: '2' }, + accessor: 0, + params: {}, + label: 'Moving Average of Average', + format: {}, + }, + ]; + + beforeEach(() => { + mockGetFieldByName.mockReturnValue({ + aggregatable: true, + type: 'number', + sourceField: 'bytes', + }); + + stubLogstashDataView.getFieldByName = mockGetFieldByName; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test.each< + [ + string, + Parameters, + () => void, + Partial | [Partial, Partial] | null + ] + >([ + [ + 'null if cumulative sum does not have aggParams', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: { ...aggs[1], aggParams: undefined } as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue(null); + }, + null, + ], + [ + 'null if getMetricFromParentPipelineAgg returns null', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue(null); + }, + null, + ], + [ + 'null if cutom metric of parent pipeline agg is not supported', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.GEO_BOUNDS, + }); + }, + null, + ], + [ + 'null if cutom metric of parent pipeline agg is sibling pipeline agg', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.AVG_BUCKET, + }); + }, + null, + ], + [ + 'null if cannot build formula if cutom metric of parent pipeline agg is parent pipeline agg', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.MOVING_FN, + }); + mockGetFormulaForPipelineAgg.mockReturnValue(null); + }, + null, + ], + [ + 'formula column if cutom metric of parent pipeline agg is valid parent pipeline agg', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.MOVING_FN, + }); + mockGetFormulaForPipelineAgg.mockReturnValue('test-formula'); + }, + { + operationType: 'formula', + params: { + formula: 'test-formula', + }, + }, + ], + [ + 'null if cutom metric of parent pipeline agg is invalid sum or count agg', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.SUM, + }); + mockConvertMetricAggregationColumnWithoutSpecialParams.mockReturnValue(null); + }, + null, + ], + [ + 'cumulative sum and metric columns if cutom metric of parent pipeline agg is valid sum or count agg', + [ + { + dataView: stubLogstashDataView, + aggs, + agg: aggs[1] as SchemaConfig, + }, + ], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggId: '2-metric', + aggType: METRIC_TYPES.SUM, + }); + mockConvertMetricAggregationColumnWithoutSpecialParams.mockReturnValue({ + columnId: 'test-id-1', + operationType: 'sum', + sourceField: field, + }); + }, + [ + { operationType: 'cumulative_sum', references: ['test-id-1'] }, + { + columnId: 'test-id-1', + operationType: 'sum', + sourceField: field, + }, + ], + ], + ])('should return %s', (_, input, actions, expected) => { + actions(); + if (expected === null) { + expect(convertToCumulativeSumAggColumn(...input)).toBeNull(); + } else if (Array.isArray(expected)) { + expect(convertToCumulativeSumAggColumn(...input)).toEqual( + expected.map(expect.objectContaining) + ); + } else { + expect(convertToCumulativeSumAggColumn(...input)).toEqual(expect.objectContaining(expected)); + } + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.ts index c1fd75ae1926..ab41ceb259ad 100644 --- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.ts +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.ts @@ -122,6 +122,7 @@ export const convertToCumulativeSumAggColumn = ( { agg: metric as SchemaConfig, dataView }, reducedTimeRange ); + if (subMetric === null) { return null; } @@ -134,8 +135,8 @@ export const convertToCumulativeSumAggColumn = ( return [ { operationType: op.name, - references: [subMetric?.columnId], ...createColumn(agg), + references: [subMetric?.columnId], params: {}, timeShift: agg.aggParams?.timeShift, } as ParentPipelineAggColumn, diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile.test.ts new file mode 100644 index 000000000000..b4cf7f141e92 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile.test.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { METRIC_TYPES } from '@kbn/data-plugin/common'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { SchemaConfig } from '../../..'; +import { convertToPercentileColumn } from './percentile'; +import { PercentileColumn } from './types'; + +const mockGetFieldNameFromField = jest.fn(); +const mockGetFieldByName = jest.fn(); +const mockGetLabel = jest.fn(); +const mockGetLabelForPercentile = jest.fn(); + +jest.mock('../utils', () => ({ + getFieldNameFromField: jest.fn(() => mockGetFieldNameFromField()), + getLabel: jest.fn(() => mockGetLabel()), + getLabelForPercentile: jest.fn(() => mockGetLabelForPercentile()), +})); + +describe('convertToPercentileColumn', () => { + const dataView = stubLogstashDataView; + const field = dataView.fields[0].displayName; + const aggId = 'pr.10'; + const percentile = 10; + const percents = [percentile]; + + const agg: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.PERCENTILES, + aggParams: { field, percents }, + aggId, + }; + const singlePercentileRankAgg: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.SINGLE_PERCENTILE, + aggParams: { field, percentile }, + aggId, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockGetFieldNameFromField.mockReturnValue(dataView.fields[0]); + mockGetFieldByName.mockReturnValue(dataView.fields[0]); + mockGetLabel.mockReturnValue('someLabel'); + mockGetLabelForPercentile.mockReturnValue('someOtherLabel'); + dataView.getFieldByName = mockGetFieldByName; + }); + + test.each< + [string, Parameters, Partial | null] + >([ + ['null if no percents', [{ agg: { ...agg, aggId: 'pr' }, dataView }], null], + [ + 'null if no value', + [{ agg: { ...singlePercentileRankAgg, aggParams: undefined }, dataView }], + null, + ], + ['null if no aggId', [{ agg: { ...agg, aggId: undefined }, dataView }], null], + ['null if no aggParams', [{ agg: { ...agg, aggParams: undefined }, dataView }], null], + ['null if aggId is invalid', [{ agg: { ...agg, aggId: 'pr.invalid' }, dataView }], null], + [ + 'null if values are undefined', + [{ agg: { ...agg, aggParams: { percents: undefined, field } }, dataView }], + null, + ], + [ + 'null if values are empty', + [{ agg: { ...agg, aggParams: { percents: [], field } }, dataView }], + null, + ], + ])('should return %s', (_, input, expected) => { + if (expected === null) { + expect(convertToPercentileColumn(...input)).toBeNull(); + } else { + expect(convertToPercentileColumn(...input)).toEqual(expect.objectContaining(expected)); + } + }); + + test('should return null if field is not specified', () => { + mockGetFieldNameFromField.mockReturnValue(null); + expect(convertToPercentileColumn({ agg, dataView })).toBeNull(); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(0); + }); + + test('should return null if field absent at the index pattern', () => { + mockGetFieldByName.mockReturnValueOnce(null); + dataView.getFieldByName = mockGetFieldByName; + + expect(convertToPercentileColumn({ agg, dataView })).toBeNull(); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(1); + }); + + test('should return percentile rank column for percentiles', () => { + expect(convertToPercentileColumn({ agg, dataView })).toEqual( + expect.objectContaining({ + dataType: 'number', + label: 'someOtherLabel', + meta: { aggId: 'pr.10' }, + operationType: 'percentile', + params: { percentile: 10 }, + sourceField: 'bytes', + }) + ); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(1); + }); + + test('should return percentile rank column for single percentile', () => { + expect(convertToPercentileColumn({ agg: singlePercentileRankAgg, dataView })).toEqual( + expect.objectContaining({ + dataType: 'number', + label: 'someOtherLabel', + meta: { aggId: 'pr.10' }, + operationType: 'percentile', + params: { percentile: 10 }, + sourceField: 'bytes', + }) + ); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile_rank.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile_rank.test.ts new file mode 100644 index 000000000000..8a696d51d871 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile_rank.test.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { METRIC_TYPES } from '@kbn/data-plugin/common'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { SchemaConfig } from '../../..'; +import { convertToPercentileRankColumn } from './percentile_rank'; +import { PercentileRanksColumn } from './types'; + +const mockGetFieldNameFromField = jest.fn(); +const mockGetFieldByName = jest.fn(); +const mockGetLabel = jest.fn(); +const mockGetLabelForPercentile = jest.fn(); + +jest.mock('../utils', () => ({ + getFieldNameFromField: jest.fn(() => mockGetFieldNameFromField()), + getLabel: jest.fn(() => mockGetLabel()), + getLabelForPercentile: jest.fn(() => mockGetLabelForPercentile()), +})); + +describe('convertToPercentileRankColumn', () => { + const dataView = stubLogstashDataView; + const field = dataView.fields[0].displayName; + const aggId = 'pr.10'; + const value = 10; + const values = [value]; + + const agg: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.PERCENTILE_RANKS, + aggParams: { field, values }, + aggId, + }; + const singlePercentileRankAgg: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.SINGLE_PERCENTILE_RANK, + aggParams: { field, value }, + aggId, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockGetFieldNameFromField.mockReturnValue(dataView.fields[0]); + mockGetFieldByName.mockReturnValue(dataView.fields[0]); + mockGetLabel.mockReturnValue('someLabel'); + mockGetLabelForPercentile.mockReturnValue('someOtherLabel'); + dataView.getFieldByName = mockGetFieldByName; + }); + + test.each< + [ + string, + Parameters, + Partial | null + ] + >([ + ['null if no percents', [{ agg: { ...agg, aggId: 'pr' }, dataView }], null], + [ + 'null if no value', + [{ agg: { ...singlePercentileRankAgg, aggParams: undefined }, dataView }], + null, + ], + ['null if no aggId', [{ agg: { ...agg, aggId: undefined }, dataView }], null], + ['null if no aggParams', [{ agg: { ...agg, aggParams: undefined }, dataView }], null], + ['null if aggId is invalid', [{ agg: { ...agg, aggId: 'pr.invalid' }, dataView }], null], + [ + 'null if values are undefined', + [{ agg: { ...agg, aggParams: { values: undefined, field } }, dataView }], + null, + ], + [ + 'null if values are empty', + [{ agg: { ...agg, aggParams: { values: [], field } }, dataView }], + null, + ], + ])('should return %s', (_, input, expected) => { + if (expected === null) { + expect(convertToPercentileRankColumn(...input)).toBeNull(); + } else { + expect(convertToPercentileRankColumn(...input)).toEqual(expect.objectContaining(expected)); + } + }); + + test('should return null if field is not specified', () => { + mockGetFieldNameFromField.mockReturnValue(null); + expect(convertToPercentileRankColumn({ agg, dataView })).toBeNull(); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(0); + }); + + test('should return null if field absent at the index pattern', () => { + mockGetFieldByName.mockReturnValueOnce(null); + dataView.getFieldByName = mockGetFieldByName; + + expect(convertToPercentileRankColumn({ agg, dataView })).toBeNull(); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(1); + }); + + test('should return percentile rank column for percentile ranks', () => { + expect(convertToPercentileRankColumn({ agg, dataView })).toEqual( + expect.objectContaining({ + dataType: 'number', + label: 'someOtherLabel', + meta: { aggId: 'pr.10' }, + operationType: 'percentile_rank', + params: { value: 10 }, + sourceField: 'bytes', + }) + ); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(1); + }); + + test('should return percentile rank column for single percentile rank', () => { + expect(convertToPercentileRankColumn({ agg: singlePercentileRankAgg, dataView })).toEqual( + expect.objectContaining({ + dataType: 'number', + label: 'someOtherLabel', + meta: { aggId: 'pr.10' }, + operationType: 'percentile_rank', + params: { value: 10 }, + sourceField: 'bytes', + }) + ); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/range.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/range.test.ts new file mode 100644 index 000000000000..8f535c28c826 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/range.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { AggParamsRange, AggParamsHistogram } from '@kbn/data-plugin/common'; +import { convertToRangeColumn } from './range'; +import { RangeColumn } from './types'; +import { DataType } from '../../types'; +import { RANGE_MODES } from '../../constants'; + +describe('convertToRangeColumn', () => { + const aggId = `some-id`; + const ranges = [ + { + from: 1, + to: 1000, + label: '1', + }, + ]; + const aggParamsRange: AggParamsRange = { + field: stubLogstashDataView.fields[0].name, + ranges, + }; + const aggParamsHistogram: AggParamsHistogram = { + interval: '1d', + field: stubLogstashDataView.fields[0].name, + }; + + test.each<[string, Parameters, Partial | null]>([ + [ + 'range column if provide valid range agg', + [aggId, aggParamsRange, '', stubLogstashDataView], + { + dataType: stubLogstashDataView.fields[0].type as DataType, + isBucketed: true, + isSplit: false, + sourceField: stubLogstashDataView.fields[0].name, + meta: { aggId }, + params: { + type: RANGE_MODES.Range, + maxBars: 'auto', + ranges, + }, + }, + ], + [ + 'range column if provide valid histogram agg', + [aggId, aggParamsHistogram, '', stubLogstashDataView, true], + { + dataType: stubLogstashDataView.fields[0].type as DataType, + isBucketed: true, + isSplit: true, + sourceField: stubLogstashDataView.fields[0].name, + meta: { aggId }, + params: { + type: RANGE_MODES.Histogram, + maxBars: 'auto', + ranges: [], + }, + }, + ], + ])('should return %s', (_, input, expected) => { + if (expected === null) { + expect(convertToRangeColumn(...input)).toBeNull(); + } else { + expect(convertToRangeColumn(...input)).toEqual(expect.objectContaining(expected)); + } + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.test.ts new file mode 100644 index 000000000000..759620650b8a --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IAggConfig, METRIC_TYPES } from '@kbn/data-plugin/common'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { SchemaConfig } from '../../..'; +import { convertToSiblingPipelineColumns } from './sibling_pipeline'; + +const mockConvertMetricToColumns = jest.fn(); +const mockConvertToSchemaConfig = jest.fn(); + +jest.mock('../metrics', () => ({ + convertMetricToColumns: jest.fn(() => mockConvertMetricToColumns()), +})); + +jest.mock('../../../vis_schemas', () => ({ + convertToSchemaConfig: jest.fn(() => mockConvertToSchemaConfig()), +})); + +describe('convertToSiblingPipelineColumns', () => { + const dataView = stubLogstashDataView; + const aggId = 'agg-id-1'; + const agg: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG_BUCKET, + aggParams: { customMetric: {} as IAggConfig }, + aggId, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockConvertMetricToColumns.mockReturnValue([{}]); + mockConvertToSchemaConfig.mockReturnValue({}); + }); + + test('should return null if aggParams are not defined', () => { + expect( + convertToSiblingPipelineColumns({ agg: { ...agg, aggParams: undefined }, aggs: [], dataView }) + ).toBeNull(); + expect(mockConvertMetricToColumns).toBeCalledTimes(0); + }); + + test('should return null if customMetric is not defined', () => { + expect( + convertToSiblingPipelineColumns({ + agg: { ...agg, aggParams: { customMetric: undefined } }, + aggs: [], + dataView, + }) + ).toBeNull(); + expect(mockConvertMetricToColumns).toBeCalledTimes(0); + }); + + test('should return null if sibling agg is not supported', () => { + mockConvertMetricToColumns.mockReturnValue(null); + expect(convertToSiblingPipelineColumns({ agg, aggs: [], dataView })).toBeNull(); + expect(mockConvertToSchemaConfig).toBeCalledTimes(1); + expect(mockConvertMetricToColumns).toBeCalledTimes(1); + }); + + test('should return column', () => { + const column = { operationType: 'formula' }; + mockConvertMetricToColumns.mockReturnValue([column]); + expect(convertToSiblingPipelineColumns({ agg, aggs: [], dataView })).toEqual(column); + expect(mockConvertToSchemaConfig).toBeCalledTimes(1); + expect(mockConvertMetricToColumns).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/std_deviation.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/std_deviation.test.ts new file mode 100644 index 000000000000..cbb1f03a6dc2 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/std_deviation.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { METRIC_TYPES } from '@kbn/data-plugin/common'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { SchemaConfig } from '../../..'; +import { convertToStdDeviationFormulaColumns } from './std_deviation'; +import { FormulaColumn } from './types'; + +const mockGetFieldNameFromField = jest.fn(); +const mockGetFieldByName = jest.fn(); +const mockGetLabel = jest.fn(); + +jest.mock('../utils', () => ({ + getFieldNameFromField: jest.fn(() => mockGetFieldNameFromField()), + getLabel: jest.fn(() => mockGetLabel()), +})); + +describe('convertToStdDeviationFormulaColumns', () => { + const dataView = stubLogstashDataView; + const stdLowerAggId = 'agg-id.std_lower'; + const stdUpperAggId = 'agg-id.std_upper'; + const label = 'std label'; + const agg: SchemaConfig = { + accessor: 0, + label, + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.STD_DEV, + aggId: stdLowerAggId, + aggParams: { + field: dataView.fields[0].displayName, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockGetFieldNameFromField.mockReturnValue(dataView.fields[0].displayName); + mockGetFieldByName.mockReturnValue(dataView.fields[0]); + mockGetLabel.mockReturnValue('some label'); + dataView.getFieldByName = mockGetFieldByName; + }); + + test.each< + [string, Parameters, Partial | null] + >([['null if no aggId is passed', [{ agg: { ...agg, aggId: undefined }, dataView }], null]])( + 'should return %s', + (_, input, expected) => { + if (expected === null) { + expect(convertToStdDeviationFormulaColumns(...input)).toBeNull(); + } else { + expect(convertToStdDeviationFormulaColumns(...input)).toEqual( + expect.objectContaining(expected) + ); + } + } + ); + + test('should return null if field is not present', () => { + mockGetFieldNameFromField.mockReturnValue(null); + expect(convertToStdDeviationFormulaColumns({ agg, dataView })).toBeNull(); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(0); + }); + + test("should return null if field doesn't exist in dataView", () => { + mockGetFieldByName.mockReturnValue(null); + dataView.getFieldByName = mockGetFieldByName; + expect(convertToStdDeviationFormulaColumns({ agg, dataView })).toBeNull(); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(1); + }); + + test('should return null if agg id is invalid', () => { + expect( + convertToStdDeviationFormulaColumns({ agg: { ...agg, aggId: 'some-id' }, dataView }) + ).toBeNull(); + expect(mockGetFieldNameFromField).toBeCalledTimes(1); + expect(dataView.getFieldByName).toBeCalledTimes(1); + }); + + test('should return formula column for lower std deviation', () => { + expect( + convertToStdDeviationFormulaColumns({ agg: { ...agg, aggId: stdLowerAggId }, dataView }) + ).toEqual( + expect.objectContaining({ + label, + meta: { aggId: 'agg-id.std_lower' }, + operationType: 'formula', + params: { formula: 'average(bytes) - 2 * standard_deviation(bytes)' }, + }) + ); + }); + + test('should return formula column for upper std deviation', () => { + expect( + convertToStdDeviationFormulaColumns({ agg: { ...agg, aggId: stdUpperAggId }, dataView }) + ).toEqual( + expect.objectContaining({ + label, + meta: { aggId: 'agg-id.std_upper' }, + operationType: 'formula', + params: { formula: 'average(bytes) + 2 * standard_deviation(bytes)' }, + }) + ); + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/terms.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/terms.test.ts new file mode 100644 index 000000000000..d214ec74b09b --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/terms.test.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { AggParamsTerms, IAggConfig, METRIC_TYPES, BUCKET_TYPES } from '@kbn/data-plugin/common'; +import { convertToTermsColumn } from './terms'; +import { AggBasedColumn, TermsColumn } from './types'; +import { SchemaConfig } from '../../..'; + +const mockConvertMetricToColumns = jest.fn(); + +jest.mock('../metrics', () => ({ + convertMetricToColumns: jest.fn(() => mockConvertMetricToColumns()), +})); + +jest.mock('../../../vis_schemas', () => ({ + convertToSchemaConfig: jest.fn(() => ({})), +})); + +describe('convertToDateHistogramColumn', () => { + const aggId = `some-id`; + const aggParams: AggParamsTerms = { + field: stubLogstashDataView.fields[0].name, + orderBy: '_key', + order: { + value: 'asc', + text: '', + }, + size: 5, + }; + const aggs: Array> = [ + { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG, + aggParams: { + field: stubLogstashDataView.fields[0].name, + }, + }, + ]; + const metricColumns: AggBasedColumn[] = [ + { + columnId: 'column-1', + operationType: 'average', + isBucketed: false, + isSplit: false, + sourceField: stubLogstashDataView.fields[0].name, + dataType: 'number', + params: {}, + meta: { + aggId: '1', + }, + }, + ]; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test.each< + [string, Parameters, Partial | null, () => void] + >([ + [ + 'null if dataview does not include field from terms params', + [ + aggId, + { + agg: { aggParams: { ...aggParams, field: '' } } as SchemaConfig, + dataView: stubLogstashDataView, + aggs, + metricColumns, + }, + '', + false, + ], + null, + () => {}, + ], + [ + 'terms column with alphabetical orderBy', + [ + aggId, + { + agg: { aggParams } as SchemaConfig, + dataView: stubLogstashDataView, + aggs, + metricColumns, + }, + '', + false, + ], + { + operationType: 'terms', + sourceField: stubLogstashDataView.fields[0].name, + isBucketed: true, + params: { + size: 5, + include: [], + exclude: [], + parentFormat: { id: 'terms' }, + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + }, + }, + () => {}, + ], + [ + 'terms column with column orderBy if provided column for orderBy is exist', + [ + aggId, + { + agg: { aggParams: { ...aggParams, orderBy: '1' } } as SchemaConfig, + dataView: stubLogstashDataView, + aggs, + metricColumns, + }, + '', + false, + ], + { + operationType: 'terms', + sourceField: stubLogstashDataView.fields[0].name, + isBucketed: true, + params: { + size: 5, + include: [], + exclude: [], + parentFormat: { id: 'terms' }, + orderBy: { type: 'column', columnId: metricColumns[0].columnId }, + orderAgg: metricColumns[0], + orderDirection: 'asc', + }, + }, + () => {}, + ], + [ + 'null if provided column for orderBy is not exist', + [ + aggId, + { + agg: { aggParams: { ...aggParams, orderBy: '2' } } as SchemaConfig, + dataView: stubLogstashDataView, + aggs, + metricColumns, + }, + '', + false, + ], + null, + () => {}, + ], + [ + 'null if provided custom orderBy without orderAgg', + [ + aggId, + { + agg: { + aggParams: { ...aggParams, orderBy: 'custom', orderAgg: undefined }, + } as SchemaConfig, + dataView: stubLogstashDataView, + aggs, + metricColumns, + }, + '', + false, + ], + null, + () => {}, + ], + [ + 'null if provided custom orderBy and not valid orderAgg', + [ + aggId, + { + agg: { + aggParams: { ...aggParams, orderBy: 'custom', orderAgg: {} as IAggConfig }, + } as SchemaConfig, + dataView: stubLogstashDataView, + aggs, + metricColumns, + }, + '', + false, + ], + null, + () => { + mockConvertMetricToColumns.mockReturnValue(null); + }, + ], + [ + 'terms column with custom orderBy and prepared orderAgg', + [ + aggId, + { + agg: { + aggParams: { ...aggParams, orderBy: 'custom', orderAgg: {} as IAggConfig }, + } as SchemaConfig, + dataView: stubLogstashDataView, + aggs, + metricColumns, + }, + '', + false, + ], + { + operationType: 'terms', + sourceField: stubLogstashDataView.fields[0].name, + isBucketed: true, + params: { + size: 5, + include: [], + exclude: [], + parentFormat: { id: 'terms' }, + orderBy: { type: 'custom' }, + orderAgg: metricColumns[0], + orderDirection: 'asc', + }, + }, + () => { + mockConvertMetricToColumns.mockReturnValue(metricColumns); + }, + ], + ])('should return %s', (_, input, expected, actions) => { + actions(); + if (expected === null) { + expect(convertToTermsColumn(...input)).toBeNull(); + } else { + expect(convertToTermsColumn(...input)).toEqual(expect.objectContaining(expected)); + } + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/formula.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/formula.test.ts new file mode 100644 index 000000000000..95e128e22b09 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/formula.test.ts @@ -0,0 +1,474 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { DataViewField, IAggConfig, METRIC_TYPES } from '@kbn/data-plugin/common'; +import { SchemaConfig } from '../../..'; +import { getFormulaForPipelineAgg, getFormulaForAgg } from './formula'; + +const mockGetMetricFromParentPipelineAgg = jest.fn(); +const mockIsPercentileAgg = jest.fn(); +const mockIsPercentileRankAgg = jest.fn(); +const mockIsPipeline = jest.fn(); +const mockIsStdDevAgg = jest.fn(); +const mockGetFieldByName = jest.fn(); +const originalGetFieldByName = stubLogstashDataView.getFieldByName; + +jest.mock('../utils', () => ({ + getFieldNameFromField: jest.fn((field) => field), + getMetricFromParentPipelineAgg: jest.fn(() => mockGetMetricFromParentPipelineAgg()), + isPercentileAgg: jest.fn(() => mockIsPercentileAgg()), + isPercentileRankAgg: jest.fn(() => mockIsPercentileRankAgg()), + isPipeline: jest.fn(() => mockIsPipeline()), + isStdDevAgg: jest.fn(() => mockIsStdDevAgg()), +})); + +const dataView = stubLogstashDataView; + +const field = stubLogstashDataView.fields[0].name; +const aggs: Array> = [ + { + aggId: '1', + aggType: METRIC_TYPES.CUMULATIVE_SUM, + aggParams: { customMetric: {} as IAggConfig }, + accessor: 0, + params: {}, + label: 'cumulative sum', + format: {}, + }, + { + aggId: '2', + aggType: METRIC_TYPES.AVG_BUCKET, + aggParams: { customMetric: {} as IAggConfig }, + accessor: 0, + params: {}, + label: 'overall average', + format: {}, + }, + { + aggId: '3.10', + aggType: METRIC_TYPES.PERCENTILES, + aggParams: { percents: [0, 10], field }, + accessor: 0, + params: {}, + label: 'percentile', + format: {}, + }, + { + aggId: '4.5', + aggType: METRIC_TYPES.PERCENTILE_RANKS, + aggParams: { values: [0, 5], field }, + accessor: 0, + params: {}, + label: 'percintile rank', + format: {}, + }, + { + aggId: '5.std_upper', + aggType: METRIC_TYPES.STD_DEV, + aggParams: { field }, + accessor: 0, + params: {}, + label: 'std dev', + format: {}, + }, + { + aggId: '6', + aggType: METRIC_TYPES.AVG, + aggParams: { field }, + accessor: 0, + params: {}, + label: 'average', + format: {}, + }, +]; + +describe('getFormulaForPipelineAgg', () => { + afterEach(() => { + jest.clearAllMocks(); + dataView.getFieldByName = originalGetFieldByName; + }); + + test.each<[string, Parameters, () => void, string | null]>([ + [ + 'null if custom metric is invalid', + [{ agg: aggs[0] as SchemaConfig, aggs, dataView }], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue(null); + }, + null, + ], + [ + 'null if custom metric type is not supported', + [{ agg: aggs[0] as SchemaConfig, aggs, dataView }], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValue({ + aggType: METRIC_TYPES.GEO_BOUNDS, + }); + }, + null, + ], + [ + 'correct formula if agg is parent pipeline agg and custom metric is valid and supported pipeline agg', + [{ agg: aggs[0] as SchemaConfig, aggs, dataView }], + () => { + mockGetMetricFromParentPipelineAgg + .mockReturnValueOnce({ + aggType: METRIC_TYPES.MOVING_FN, + aggParams: {}, + aggId: '2', + }) + .mockReturnValueOnce({ + aggType: METRIC_TYPES.AVG, + aggParams: { + field, + }, + aggId: '3', + }); + }, + 'cumulative_sum(moving_average(average(bytes)))', + ], + [ + 'correct formula if agg is parent pipeline agg and custom metric is valid and supported not pipeline agg', + [{ agg: aggs[0] as SchemaConfig, aggs, dataView }], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({ + aggType: METRIC_TYPES.AVG, + aggParams: { + field, + }, + aggId: '2', + }); + }, + 'cumulative_sum(average(bytes))', + ], + [ + 'correct formula if agg is parent pipeline agg and custom metric is valid and supported percentile rank agg', + [{ agg: aggs[0] as SchemaConfig, aggs, dataView }], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({ + aggType: METRIC_TYPES.PERCENTILE_RANKS, + aggParams: { + field, + }, + aggId: '3.10', + }); + }, + 'cumulative_sum(percentile_rank(bytes, value=10))', + ], + [ + 'correct formula if agg is sibling pipeline agg and custom metric is valid and supported agg', + [{ agg: aggs[1] as SchemaConfig, aggs, dataView }], + () => { + mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({ + aggType: METRIC_TYPES.AVG, + aggParams: { + field, + }, + aggId: '3', + }); + }, + 'average(bytes)', + ], + ])('should return %s', (_, input, actions, expected) => { + actions(); + if (expected === null) { + expect(getFormulaForPipelineAgg(...input)).toBeNull(); + } else { + expect(getFormulaForPipelineAgg(...input)).toEqual(expected); + } + }); + + test('null if agg is sibling pipeline agg, custom metric is valid, agg is supported and field type is not supported', () => { + mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({ + aggType: METRIC_TYPES.AVG, + aggParams: { + field, + }, + aggId: '3', + }); + + const field1: DataViewField = { + name: 'bytes', + type: 'geo', + esTypes: ['long'], + aggregatable: true, + searchable: true, + count: 10, + readFromDocValues: true, + scripted: false, + isMapped: true, + } as DataViewField; + + mockGetFieldByName.mockReturnValueOnce(field1); + + dataView.getFieldByName = mockGetFieldByName; + const agg = getFormulaForPipelineAgg({ + agg: aggs[1] as SchemaConfig, + aggs, + dataView, + }); + expect(agg).toBeNull(); + }); + + test('null if agg is sibling pipeline agg, custom metric is valid, agg is supported, field type is supported and field is not aggregatable', () => { + mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({ + aggType: METRIC_TYPES.AVG, + aggParams: { + field, + }, + aggId: '3', + }); + + const field1: DataViewField = { + name: 'str', + type: 'string', + esTypes: ['text'], + aggregatable: false, + searchable: true, + count: 10, + readFromDocValues: true, + scripted: false, + isMapped: true, + } as DataViewField; + + mockGetFieldByName.mockReturnValueOnce(field1); + + dataView.getFieldByName = mockGetFieldByName; + const agg = getFormulaForPipelineAgg({ + agg: aggs[1] as SchemaConfig, + aggs, + dataView, + }); + expect(agg).toBeNull(); + }); +}); + +describe('getFormulaForAgg', () => { + beforeEach(() => { + mockIsPercentileAgg.mockReturnValue(false); + mockIsPipeline.mockReturnValue(false); + mockIsStdDevAgg.mockReturnValue(false); + mockIsPercentileRankAgg.mockReturnValue(false); + }); + + afterEach(() => { + jest.clearAllMocks(); + dataView.getFieldByName = originalGetFieldByName; + }); + + test.each<[string, Parameters, () => void, string | null]>([ + [ + 'null if agg type is not supported', + [ + { + agg: { ...aggs[0], aggType: METRIC_TYPES.GEO_BOUNDS, aggParams: { field } }, + aggs, + dataView, + }, + ], + () => {}, + null, + ], + [ + 'correct pipeline formula if agg is valid pipeline agg', + [{ agg: aggs[0], aggs, dataView }], + () => { + mockIsPipeline.mockReturnValue(true); + mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({ + aggType: METRIC_TYPES.AVG, + aggParams: { + field, + }, + aggId: '2', + }); + }, + 'cumulative_sum(average(bytes))', + ], + [ + 'correct percentile formula if agg is valid percentile agg', + [{ agg: aggs[2], aggs, dataView }], + () => { + mockIsPercentileAgg.mockReturnValue(true); + }, + 'percentile(bytes, percentile=10)', + ], + [ + 'correct percentile rank formula if agg is valid percentile rank agg', + [{ agg: aggs[3], aggs, dataView }], + () => { + mockIsPercentileRankAgg.mockReturnValue(true); + }, + 'percentile_rank(bytes, value=5)', + ], + [ + 'correct standart deviation formula if agg is valid standart deviation agg', + [{ agg: aggs[4], aggs, dataView }], + () => { + mockIsStdDevAgg.mockReturnValue(true); + }, + 'average(bytes) + 2 * standard_deviation(bytes)', + ], + [ + 'correct metric formula if agg is valid other metric agg', + [{ agg: aggs[5], aggs, dataView }], + () => {}, + 'average(bytes)', + ], + ])('should return %s', (_, input, actions, expected) => { + actions(); + if (expected === null) { + expect(getFormulaForAgg(...input)).toBeNull(); + } else { + expect(getFormulaForAgg(...input)).toEqual(expected); + } + }); + + test.each([ + [ + 'null if agg is valid pipeline agg', + aggs[0], + () => { + mockIsPipeline.mockReturnValue(true); + mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({ + aggType: METRIC_TYPES.AVG, + aggParams: { + field, + }, + aggId: '2', + }); + }, + ], + [ + 'null if percentile rank agg is valid percentile agg', + aggs[2], + () => { + mockIsPercentileAgg.mockReturnValue(true); + }, + ], + [ + 'null if agg is valid percentile rank agg', + aggs[3], + () => { + mockIsPercentileRankAgg.mockReturnValue(true); + }, + ], + [ + 'null if agg is valid standart deviation agg', + aggs[4], + () => { + mockIsStdDevAgg.mockReturnValue(true); + }, + ], + ['null if agg is valid other metric agg', aggs[5], () => {}], + ])('should return %s and field type is not supported', (_, agg, actions) => { + actions(); + const field1: DataViewField = { + name: 'bytes', + type: 'geo', + esTypes: ['long'], + aggregatable: true, + searchable: true, + count: 10, + readFromDocValues: true, + scripted: false, + isMapped: true, + } as DataViewField; + + mockGetFieldByName.mockReturnValueOnce(field1); + + dataView.getFieldByName = mockGetFieldByName; + const result = getFormulaForPipelineAgg({ + agg: agg as SchemaConfig< + | METRIC_TYPES.CUMULATIVE_SUM + | METRIC_TYPES.DERIVATIVE + | METRIC_TYPES.MOVING_FN + | METRIC_TYPES.AVG_BUCKET + | METRIC_TYPES.MAX_BUCKET + | METRIC_TYPES.MIN_BUCKET + | METRIC_TYPES.SUM_BUCKET + >, + aggs, + dataView, + }); + expect(result).toBeNull(); + }); + + test.each([ + [ + 'null if agg is valid pipeline agg', + aggs[0], + () => { + mockIsPipeline.mockReturnValue(true); + mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({ + aggType: METRIC_TYPES.AVG, + aggParams: { + field, + }, + aggId: '2', + }); + }, + ], + [ + 'null if percentile rank agg is valid percentile agg', + aggs[2], + () => { + mockIsPercentileAgg.mockReturnValue(true); + }, + ], + [ + 'null if agg is valid percentile rank agg', + aggs[3], + () => { + mockIsPercentileRankAgg.mockReturnValue(true); + }, + ], + [ + 'null if agg is valid standart deviation agg', + aggs[4], + () => { + mockIsStdDevAgg.mockReturnValue(true); + }, + ], + ['null if agg is valid other metric agg', aggs[5], () => {}], + ])( + 'should return %s, field type is supported and field is not aggregatable', + (_, agg, actions) => { + actions(); + const field1: DataViewField = { + name: 'str', + type: 'string', + esTypes: ['text'], + aggregatable: false, + searchable: true, + count: 10, + readFromDocValues: true, + scripted: false, + isMapped: true, + } as DataViewField; + + mockGetFieldByName.mockReturnValueOnce(field1); + + dataView.getFieldByName = mockGetFieldByName; + const result = getFormulaForPipelineAgg({ + agg: agg as SchemaConfig< + | METRIC_TYPES.CUMULATIVE_SUM + | METRIC_TYPES.DERIVATIVE + | METRIC_TYPES.MOVING_FN + | METRIC_TYPES.AVG_BUCKET + | METRIC_TYPES.MAX_BUCKET + | METRIC_TYPES.MIN_BUCKET + | METRIC_TYPES.SUM_BUCKET + >, + aggs, + dataView, + }); + expect(result).toBeNull(); + } + ); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/metrics.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/metrics.test.ts new file mode 100644 index 000000000000..659ab80ea03d --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/metrics.test.ts @@ -0,0 +1,368 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { METRIC_TYPES } from '@kbn/data-plugin/common'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { SchemaConfig } from '../../..'; +import { convertMetricToColumns } from './metrics'; + +const mockConvertMetricAggregationColumnWithoutSpecialParams = jest.fn(); +const mockConvertToOtherParentPipelineAggColumns = jest.fn(); +const mockConvertToPercentileColumn = jest.fn(); +const mockConvertToPercentileRankColumn = jest.fn(); +const mockConvertToSiblingPipelineColumns = jest.fn(); +const mockConvertToStdDeviationFormulaColumns = jest.fn(); +const mockConvertToLastValueColumn = jest.fn(); +const mockConvertToCumulativeSumAggColumn = jest.fn(); + +jest.mock('../convert', () => ({ + convertMetricAggregationColumnWithoutSpecialParams: jest.fn(() => + mockConvertMetricAggregationColumnWithoutSpecialParams() + ), + convertToOtherParentPipelineAggColumns: jest.fn(() => + mockConvertToOtherParentPipelineAggColumns() + ), + convertToPercentileColumn: jest.fn(() => mockConvertToPercentileColumn()), + convertToPercentileRankColumn: jest.fn(() => mockConvertToPercentileRankColumn()), + convertToSiblingPipelineColumns: jest.fn(() => mockConvertToSiblingPipelineColumns()), + convertToStdDeviationFormulaColumns: jest.fn(() => mockConvertToStdDeviationFormulaColumns()), + convertToLastValueColumn: jest.fn(() => mockConvertToLastValueColumn()), + convertToCumulativeSumAggColumn: jest.fn(() => mockConvertToCumulativeSumAggColumn()), +})); + +describe('convertMetricToColumns invalid cases', () => { + const dataView = stubLogstashDataView; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + beforeAll(() => { + mockConvertMetricAggregationColumnWithoutSpecialParams.mockReturnValue(null); + mockConvertToOtherParentPipelineAggColumns.mockReturnValue(null); + mockConvertToPercentileColumn.mockReturnValue(null); + mockConvertToPercentileRankColumn.mockReturnValue(null); + mockConvertToSiblingPipelineColumns.mockReturnValue(null); + mockConvertToStdDeviationFormulaColumns.mockReturnValue(null); + mockConvertToLastValueColumn.mockReturnValue(null); + mockConvertToCumulativeSumAggColumn.mockReturnValue(null); + }); + + test.each<[string, Parameters, null, jest.Mock | undefined]>([ + [ + 'null if agg is not supported', + [{ aggType: METRIC_TYPES.GEO_BOUNDS } as unknown as SchemaConfig, dataView, []], + null, + undefined, + ], + [ + 'null if supported agg AVG is not valid', + [{ aggType: METRIC_TYPES.AVG } as SchemaConfig, dataView, []], + null, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'null if supported agg MIN is not valid', + [{ aggType: METRIC_TYPES.MIN } as SchemaConfig, dataView, []], + null, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'null if supported agg MAX is not valid', + [{ aggType: METRIC_TYPES.MAX } as SchemaConfig, dataView, []], + null, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'null if supported agg SUM is not valid', + [{ aggType: METRIC_TYPES.SUM } as SchemaConfig, dataView, []], + null, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'null if supported agg COUNT is not valid', + [{ aggType: METRIC_TYPES.COUNT } as SchemaConfig, dataView, []], + null, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'null if supported agg CARDINALITY is not valid', + [{ aggType: METRIC_TYPES.CARDINALITY } as SchemaConfig, dataView, []], + null, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'null if supported agg VALUE_COUNT is not valid', + [{ aggType: METRIC_TYPES.VALUE_COUNT } as SchemaConfig, dataView, []], + null, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'null if supported agg MEDIAN is not valid', + [{ aggType: METRIC_TYPES.MEDIAN } as SchemaConfig, dataView, []], + null, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'null if supported agg STD_DEV is not valid', + [{ aggType: METRIC_TYPES.STD_DEV } as SchemaConfig, dataView, []], + null, + mockConvertToStdDeviationFormulaColumns, + ], + [ + 'null if supported agg PERCENTILES is not valid', + [{ aggType: METRIC_TYPES.PERCENTILES } as SchemaConfig, dataView, []], + null, + mockConvertToPercentileColumn, + ], + [ + 'null if supported agg SINGLE_PERCENTILE is not valid', + [{ aggType: METRIC_TYPES.SINGLE_PERCENTILE } as SchemaConfig, dataView, []], + null, + mockConvertToPercentileColumn, + ], + [ + 'null if supported agg PERCENTILE_RANKS is not valid', + [{ aggType: METRIC_TYPES.PERCENTILE_RANKS } as SchemaConfig, dataView, []], + null, + mockConvertToPercentileRankColumn, + ], + [ + 'null if supported agg SINGLE_PERCENTILE_RANK is not valid', + [{ aggType: METRIC_TYPES.SINGLE_PERCENTILE_RANK } as SchemaConfig, dataView, []], + null, + mockConvertToPercentileRankColumn, + ], + [ + 'null if supported agg TOP_HITS is not valid', + [{ aggType: METRIC_TYPES.TOP_HITS } as SchemaConfig, dataView, []], + null, + mockConvertToLastValueColumn, + ], + [ + 'null if supported agg TOP_METRICS is not valid', + [{ aggType: METRIC_TYPES.TOP_METRICS } as SchemaConfig, dataView, []], + null, + mockConvertToLastValueColumn, + ], + [ + 'null if supported agg CUMULATIVE_SUM is not valid', + [{ aggType: METRIC_TYPES.CUMULATIVE_SUM } as SchemaConfig, dataView, []], + null, + mockConvertToCumulativeSumAggColumn, + ], + [ + 'null if supported agg DERIVATIVE is not valid', + [{ aggType: METRIC_TYPES.DERIVATIVE } as SchemaConfig, dataView, []], + null, + mockConvertToOtherParentPipelineAggColumns, + ], + [ + 'null if supported agg MOVING_FN is not valid', + [{ aggType: METRIC_TYPES.MOVING_FN } as SchemaConfig, dataView, []], + null, + mockConvertToOtherParentPipelineAggColumns, + ], + [ + 'null if supported agg SUM_BUCKET is not valid', + [{ aggType: METRIC_TYPES.SUM_BUCKET } as SchemaConfig, dataView, []], + null, + mockConvertToSiblingPipelineColumns, + ], + [ + 'null if supported agg MIN_BUCKET is not valid', + [{ aggType: METRIC_TYPES.MIN_BUCKET } as SchemaConfig, dataView, []], + null, + mockConvertToSiblingPipelineColumns, + ], + [ + 'null if supported agg MAX_BUCKET is not valid', + [{ aggType: METRIC_TYPES.MAX_BUCKET } as SchemaConfig, dataView, []], + null, + mockConvertToSiblingPipelineColumns, + ], + [ + 'null if supported agg AVG_BUCKET is not valid', + [{ aggType: METRIC_TYPES.AVG_BUCKET } as SchemaConfig, dataView, []], + null, + mockConvertToSiblingPipelineColumns, + ], + [ + 'null if supported agg SERIAL_DIFF is not valid', + [{ aggType: METRIC_TYPES.SERIAL_DIFF } as SchemaConfig, dataView, []], + null, + undefined, + ], + ])('should return %s', (_, input, expected, mock) => { + expect(convertMetricToColumns(...input)).toBeNull(); + + if (mock) { + expect(mock).toBeCalledTimes(1); + } + }); +}); +describe('convertMetricToColumns valid cases', () => { + const dataView = stubLogstashDataView; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const result = [{}]; + + beforeAll(() => { + mockConvertMetricAggregationColumnWithoutSpecialParams.mockReturnValue(result); + mockConvertToOtherParentPipelineAggColumns.mockReturnValue(result); + mockConvertToPercentileColumn.mockReturnValue(result); + mockConvertToPercentileRankColumn.mockReturnValue(result); + mockConvertToSiblingPipelineColumns.mockReturnValue(result); + mockConvertToStdDeviationFormulaColumns.mockReturnValue(result); + mockConvertToLastValueColumn.mockReturnValue(result); + mockConvertToCumulativeSumAggColumn.mockReturnValue(result); + }); + + test.each<[string, Parameters, Array<{}>, jest.Mock]>([ + [ + 'array of columns if supported agg AVG is valid', + [{ aggType: METRIC_TYPES.AVG } as SchemaConfig, dataView, []], + result, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'array of columns if supported agg MIN is valid', + [{ aggType: METRIC_TYPES.MIN } as SchemaConfig, dataView, []], + result, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'array of columns if supported agg MAX is valid', + [{ aggType: METRIC_TYPES.MAX } as SchemaConfig, dataView, []], + result, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'array of columns if supported agg SUM is valid', + [{ aggType: METRIC_TYPES.SUM } as SchemaConfig, dataView, []], + result, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'array of columns if supported agg COUNT is valid', + [{ aggType: METRIC_TYPES.COUNT } as SchemaConfig, dataView, []], + result, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'array of columns if supported agg CARDINALITY is valid', + [{ aggType: METRIC_TYPES.CARDINALITY } as SchemaConfig, dataView, []], + result, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'array of columns if supported agg VALUE_COUNT is valid', + [{ aggType: METRIC_TYPES.VALUE_COUNT } as SchemaConfig, dataView, []], + result, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'array of columns if supported agg MEDIAN is valid', + [{ aggType: METRIC_TYPES.MEDIAN } as SchemaConfig, dataView, []], + result, + mockConvertMetricAggregationColumnWithoutSpecialParams, + ], + [ + 'array of columns if supported agg STD_DEV is valid', + [{ aggType: METRIC_TYPES.STD_DEV } as SchemaConfig, dataView, []], + result, + mockConvertToStdDeviationFormulaColumns, + ], + [ + 'array of columns if supported agg PERCENTILES is valid', + [{ aggType: METRIC_TYPES.PERCENTILES } as SchemaConfig, dataView, []], + result, + mockConvertToPercentileColumn, + ], + [ + 'array of columns if supported agg SINGLE_PERCENTILE is valid', + [{ aggType: METRIC_TYPES.SINGLE_PERCENTILE } as SchemaConfig, dataView, []], + result, + mockConvertToPercentileColumn, + ], + [ + 'array of columns if supported agg PERCENTILE_RANKS is valid', + [{ aggType: METRIC_TYPES.PERCENTILE_RANKS } as SchemaConfig, dataView, []], + result, + mockConvertToPercentileRankColumn, + ], + [ + 'array of columns if supported agg SINGLE_PERCENTILE_RANK is valid', + [{ aggType: METRIC_TYPES.SINGLE_PERCENTILE_RANK } as SchemaConfig, dataView, []], + result, + mockConvertToPercentileRankColumn, + ], + [ + 'array of columns if supported agg TOP_HITS is valid', + [{ aggType: METRIC_TYPES.TOP_HITS } as SchemaConfig, dataView, []], + result, + mockConvertToLastValueColumn, + ], + [ + 'array of columns if supported agg TOP_METRICS is valid', + [{ aggType: METRIC_TYPES.TOP_METRICS } as SchemaConfig, dataView, []], + result, + mockConvertToLastValueColumn, + ], + [ + 'array of columns if supported agg CUMULATIVE_SUM is valid', + [{ aggType: METRIC_TYPES.CUMULATIVE_SUM } as SchemaConfig, dataView, []], + result, + mockConvertToCumulativeSumAggColumn, + ], + [ + 'array of columns if supported agg DERIVATIVE is valid', + [{ aggType: METRIC_TYPES.DERIVATIVE } as SchemaConfig, dataView, []], + result, + mockConvertToOtherParentPipelineAggColumns, + ], + [ + 'array of columns if supported agg MOVING_FN is valid', + [{ aggType: METRIC_TYPES.MOVING_FN } as SchemaConfig, dataView, []], + result, + mockConvertToOtherParentPipelineAggColumns, + ], + [ + 'array of columns if supported agg SUM_BUCKET is valid', + [{ aggType: METRIC_TYPES.SUM_BUCKET } as SchemaConfig, dataView, []], + result, + mockConvertToSiblingPipelineColumns, + ], + [ + 'array of columns if supported agg MIN_BUCKET is valid', + [{ aggType: METRIC_TYPES.MIN_BUCKET } as SchemaConfig, dataView, []], + result, + mockConvertToSiblingPipelineColumns, + ], + [ + 'array of columns if supported agg MAX_BUCKET is valid', + [{ aggType: METRIC_TYPES.MAX_BUCKET } as SchemaConfig, dataView, []], + result, + mockConvertToSiblingPipelineColumns, + ], + [ + 'array of columns if supported agg AVG_BUCKET is valid', + [{ aggType: METRIC_TYPES.AVG_BUCKET } as SchemaConfig, dataView, []], + result, + mockConvertToSiblingPipelineColumns, + ], + ])('should return %s', (_, input, expected, mock) => { + expect(convertMetricToColumns(...input)).toEqual(expected.map(expect.objectContaining)); + if (mock) { + expect(mock).toBeCalledTimes(1); + } + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/percentage_formula.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/percentage_formula.test.ts new file mode 100644 index 000000000000..9855ce44b660 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/percentage_formula.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { METRIC_TYPES } from '@kbn/data-plugin/common'; +import { getPercentageColumnFormulaColumn } from './percentage_formula'; +import { FormulaColumn } from '../../types'; +import { SchemaConfig } from '../../..'; + +const mockGetFormulaForAgg = jest.fn(); +const mockCreateFormulaColumn = jest.fn(); + +jest.mock('./formula', () => ({ + getFormulaForAgg: jest.fn(() => mockGetFormulaForAgg()), +})); + +jest.mock('../convert', () => ({ + createFormulaColumn: jest.fn((formula) => mockCreateFormulaColumn(formula)), +})); + +describe('getPercentageColumnFormulaColumn', () => { + const dataView = stubLogstashDataView; + const field = stubLogstashDataView.fields[0].name; + const aggs: Array> = [ + { + aggId: '1', + aggType: METRIC_TYPES.AVG, + aggParams: { field }, + accessor: 0, + params: {}, + label: 'average', + format: {}, + }, + ]; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test.each< + [ + string, + Parameters, + () => void, + Partial | null + ] + >([ + [ + 'null if cannot build formula for provided agg', + [{ agg: aggs[0], aggs, dataView }], + () => { + mockGetFormulaForAgg.mockReturnValue(null); + }, + null, + ], + [ + 'null if cannot create formula column for provided arguments', + [{ agg: aggs[0], aggs, dataView }], + () => { + mockGetFormulaForAgg.mockReturnValue('test-formula'); + mockCreateFormulaColumn.mockReturnValue(null); + }, + null, + ], + [ + 'formula column if provided arguments are valid', + [{ agg: aggs[0], aggs, dataView }], + () => { + mockGetFormulaForAgg.mockReturnValue('test-formula'); + mockCreateFormulaColumn.mockImplementation((formula) => ({ + operationType: 'formula', + params: { formula }, + label: 'Average', + })); + }, + { + operationType: 'formula', + params: { + formula: `(test-formula) / overall_sum(test-formula)`, + format: { id: 'percent' }, + }, + label: `Average percentages`, + }, + ], + ])('should return %s', (_, input, actions, expected) => { + actions(); + if (expected === null) { + expect(getPercentageColumnFormulaColumn(...input)).toBeNull(); + } else { + expect(getPercentageColumnFormulaColumn(...input)).toEqual(expect.objectContaining(expected)); + } + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/utils.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/utils.test.ts new file mode 100644 index 000000000000..73118b6ad4f0 --- /dev/null +++ b/src/plugins/visualizations/common/convert_to_lens/lib/utils.test.ts @@ -0,0 +1,521 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { IAggConfig, METRIC_TYPES } from '@kbn/data-plugin/common'; +import { AggBasedColumn, ColumnWithMeta, Operations } from '../..'; +import { SchemaConfig } from '../../types'; +import { + getCustomBucketsFromSiblingAggs, + getFieldNameFromField, + getLabel, + getLabelForPercentile, + getMetricFromParentPipelineAgg, + getValidColumns, + isColumnWithMeta, + isMetricAggWithoutParams, + isPercentileAgg, + isPercentileRankAgg, + isPipeline, + isSchemaConfig, + isSiblingPipeline, + isStdDevAgg, +} from './utils'; + +describe('getLabel', () => { + const label = 'some label'; + const customLabel = 'some custom label'; + + const agg: SchemaConfig = { + accessor: 0, + label, + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG, + aggId: 'id', + aggParams: { field: 'some-field' }, + }; + + test('should return label', () => { + const { aggParams, ...aggWithoutAggParams } = agg; + expect(getLabel(aggWithoutAggParams)).toEqual(label); + expect(getLabel(agg)).toEqual(label); + expect(getLabel({ ...agg, aggParams: { ...aggParams!, customLabel: undefined } })).toEqual( + label + ); + }); + + test('should return customLabel', () => { + const aggParams = { ...agg.aggParams!, customLabel }; + const aggWithCustomLabel = { ...agg, aggParams }; + expect(getLabel(aggWithCustomLabel)).toEqual(customLabel); + }); +}); + +describe('getLabelForPercentile', () => { + const label = 'some label'; + const customLabel = 'some custom label'; + + const agg: SchemaConfig = { + accessor: 0, + label, + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.PERCENTILES, + aggId: 'id', + aggParams: { field: 'some-field' }, + }; + + test('should return empty string if no custom label is specified', () => { + const { aggParams, ...aggWithoutAggParams } = agg; + expect(getLabelForPercentile(aggWithoutAggParams)).toEqual(''); + expect(getLabel({ ...agg, aggParams: { ...aggParams!, customLabel: '' } })).toEqual(''); + }); + + test('should return label if custom label is specified', () => { + const aggParams = { ...agg.aggParams!, customLabel }; + const aggWithCustomLabel = { ...agg, aggParams }; + expect(getLabelForPercentile(aggWithCustomLabel)).toEqual(label); + }); +}); + +describe('getValidColumns', () => { + const dataView = stubLogstashDataView; + const columns: AggBasedColumn[] = [ + { + operationType: Operations.AVERAGE, + sourceField: dataView.fields[0].name, + columnId: 'some-id-0', + dataType: 'number', + params: {}, + meta: { aggId: 'aggId-0' }, + isSplit: false, + isBucketed: true, + }, + { + operationType: Operations.SUM, + sourceField: dataView.fields[0].name, + columnId: 'some-id-1', + dataType: 'number', + params: {}, + meta: { aggId: 'aggId-1' }, + isSplit: false, + isBucketed: true, + }, + ]; + test.each<[string, Parameters, AggBasedColumn[] | null]>([ + ['null if array contains null', [[null, ...columns]], null], + ['null if columns is null', [null], null], + ['null if columns is undefined', [undefined], null], + ['columns', [columns], columns], + ['columns if one column is passed', [columns[0]], [columns[0]]], + ])('should return %s', (_, input, expected) => { + if (expected === null) { + expect(getValidColumns(...input)).toBeNull(); + } else { + expect(getValidColumns(...input)).toEqual(expect.objectContaining(expected)); + } + }); +}); + +describe('getFieldNameFromField', () => { + test('should return null if no field is passed', () => { + expect(getFieldNameFromField(undefined)).toBeNull(); + }); + + test('should return field name if field is string', () => { + const fieldName = 'some-field-name'; + expect(getFieldNameFromField(fieldName)).toEqual(fieldName); + }); + + test('should return field name if field is DataViewField', () => { + const field = stubLogstashDataView.fields[0]; + expect(getFieldNameFromField(field)).toEqual(field.name); + }); +}); + +describe('isSchemaConfig', () => { + const iAggConfig = { + id: '', + enabled: false, + params: {}, + } as IAggConfig; + + const schemaConfig: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG, + }; + + test('should be false if is IAggConfig', () => { + expect(isSchemaConfig(iAggConfig)).toBeFalsy(); + }); + + test('should be false if is SchemaConfig', () => { + expect(isSchemaConfig(schemaConfig)).toBeTruthy(); + }); +}); + +describe('isColumnWithMeta', () => { + const column: AggBasedColumn = { + sourceField: '', + columnId: '', + operationType: 'terms', + isBucketed: false, + isSplit: false, + dataType: 'string', + } as AggBasedColumn; + + const columnWithMeta: ColumnWithMeta = { + sourceField: '', + columnId: '', + operationType: 'average', + isBucketed: false, + isSplit: false, + dataType: 'string', + params: {}, + meta: { aggId: 'some-agg-id' }, + }; + + test('should return false if column without meta', () => { + expect(isColumnWithMeta(column)).toBeFalsy(); + }); + + test('should return true if column with meta', () => { + expect(isColumnWithMeta(columnWithMeta)).toBeTruthy(); + }); +}); + +describe('isSiblingPipeline', () => { + const metric: Omit = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + }; + + test.each<[METRIC_TYPES, boolean]>([ + [METRIC_TYPES.AVG_BUCKET, true], + [METRIC_TYPES.SUM_BUCKET, true], + [METRIC_TYPES.MAX_BUCKET, true], + [METRIC_TYPES.MIN_BUCKET, true], + [METRIC_TYPES.CUMULATIVE_SUM, false], + ])('for %s should return %s', (aggType, expected) => { + expect(isSiblingPipeline({ ...metric, aggType } as SchemaConfig)).toBe( + expected + ); + }); +}); + +describe('isPipeline', () => { + const metric: Omit = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + }; + + test.each<[METRIC_TYPES, boolean]>([ + [METRIC_TYPES.AVG_BUCKET, true], + [METRIC_TYPES.SUM_BUCKET, true], + [METRIC_TYPES.MAX_BUCKET, true], + [METRIC_TYPES.MIN_BUCKET, true], + [METRIC_TYPES.CUMULATIVE_SUM, true], + [METRIC_TYPES.DERIVATIVE, true], + [METRIC_TYPES.MOVING_FN, true], + [METRIC_TYPES.AVG, false], + ])('for %s should return %s', (aggType, expected) => { + expect(isPipeline({ ...metric, aggType } as SchemaConfig)).toBe(expected); + }); +}); + +describe('isMetricAggWithoutParams', () => { + const metric: Omit = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + }; + + test.each<[METRIC_TYPES, boolean]>([ + [METRIC_TYPES.AVG, true], + [METRIC_TYPES.COUNT, true], + [METRIC_TYPES.MAX, true], + [METRIC_TYPES.MIN, true], + [METRIC_TYPES.SUM, true], + [METRIC_TYPES.MEDIAN, true], + [METRIC_TYPES.CARDINALITY, true], + [METRIC_TYPES.VALUE_COUNT, true], + [METRIC_TYPES.DERIVATIVE, false], + ])('for %s should return %s', (aggType, expected) => { + expect(isMetricAggWithoutParams({ ...metric, aggType } as SchemaConfig)).toBe( + expected + ); + }); +}); + +describe('isPercentileAgg', () => { + const metric: Omit = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + }; + + test.each<[METRIC_TYPES, boolean]>([ + [METRIC_TYPES.PERCENTILES, true], + [METRIC_TYPES.DERIVATIVE, false], + ])('for %s should return %s', (aggType, expected) => { + expect(isPercentileAgg({ ...metric, aggType } as SchemaConfig)).toBe(expected); + }); +}); + +describe('isPercentileRankAgg', () => { + const metric: Omit = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + }; + + test.each<[METRIC_TYPES, boolean]>([ + [METRIC_TYPES.PERCENTILE_RANKS, true], + [METRIC_TYPES.PERCENTILES, false], + ])('for %s should return %s', (aggType, expected) => { + expect(isPercentileRankAgg({ ...metric, aggType } as SchemaConfig)).toBe( + expected + ); + }); +}); + +describe('isStdDevAgg', () => { + const metric: Omit = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + }; + + test.each<[METRIC_TYPES, boolean]>([ + [METRIC_TYPES.STD_DEV, true], + [METRIC_TYPES.PERCENTILES, false], + ])('for %s should return %s', (aggType, expected) => { + expect(isStdDevAgg({ ...metric, aggType } as SchemaConfig)).toBe(expected); + }); +}); + +describe('getCustomBucketsFromSiblingAggs', () => { + const bucket1 = { + id: 'some-id', + params: { type: 'some-type' }, + type: 'type1', + enabled: true, + } as unknown as IAggConfig; + const serialize1 = () => bucket1; + + const bucket2 = { + id: 'some-id-1', + params: { type: 'some-type-1' }, + type: 'type2', + enabled: false, + } as unknown as IAggConfig; + const serialize2 = () => bucket2; + + const bucketWithSerialize1 = { ...bucket1, serialize: serialize1 } as unknown as IAggConfig; + const metric1: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG_BUCKET, + aggId: 'some-agg-id', + aggParams: { + customBucket: bucketWithSerialize1, + }, + }; + + const bucketWithSerialize2 = { ...bucket2, serialize: serialize2 } as unknown as IAggConfig; + const metric2: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG_BUCKET, + aggId: 'some-agg-id', + aggParams: { + customBucket: bucketWithSerialize2, + }, + }; + const bucket3 = { ...bucket1, id: 'other id' } as unknown as IAggConfig; + const serialize3 = () => bucket3; + + const bucketWithSerialize3 = { ...bucket3, serialize: serialize3 } as unknown as IAggConfig; + const metric3: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG_BUCKET, + aggId: 'some-agg-id', + aggParams: { + customBucket: bucketWithSerialize3, + }, + }; + + test("should filter out duplicated custom buckets, ignoring id's", () => { + expect(getCustomBucketsFromSiblingAggs([metric1, metric2, metric3])).toEqual([ + bucketWithSerialize1, + bucketWithSerialize2, + ]); + }); +}); + +const mockConvertToSchemaConfig = jest.fn(); + +jest.mock('../../vis_schemas', () => ({ + convertToSchemaConfig: jest.fn(() => mockConvertToSchemaConfig()), +})); + +describe('getMetricFromParentPipelineAgg', () => { + const metricAggId = 'agg-id-0'; + const aggId = 'agg-id-1'; + const plainAgg: SchemaConfig = { + accessor: 0, + label: 'some-label', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG, + aggId: metricAggId, + }; + const agg: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.AVG_BUCKET, + aggParams: { customMetric: {} as IAggConfig }, + aggId, + }; + + const parentPipelineAgg: SchemaConfig = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + aggType: METRIC_TYPES.CUMULATIVE_SUM, + aggParams: { metricAgg: 'custom' }, + aggId, + }; + + const metric = { aggType: METRIC_TYPES.CUMULATIVE_SUM }; + beforeEach(() => { + jest.clearAllMocks(); + }); + + beforeAll(() => { + mockConvertToSchemaConfig.mockReturnValue(metric); + }); + + test('should return null if aggParams are undefined', () => { + expect(getMetricFromParentPipelineAgg({ ...agg, aggParams: undefined }, [])).toBeNull(); + expect(mockConvertToSchemaConfig).toBeCalledTimes(0); + }); + + test('should return null if is sibling pipeline agg and custom metric is not defined', () => { + expect( + getMetricFromParentPipelineAgg({ ...agg, aggParams: { customMetric: undefined } }, []) + ).toBeNull(); + expect(mockConvertToSchemaConfig).toBeCalledTimes(0); + }); + + test('should return null if is parent pipeline agg, metricAgg is custom and custom metric is not defined', () => { + expect(getMetricFromParentPipelineAgg(parentPipelineAgg, [])).toBeNull(); + expect(mockConvertToSchemaConfig).toBeCalledTimes(0); + }); + + test('should return metric if is parent pipeline agg, metricAgg is equal to aggId and custom metric is not defined', () => { + const parentPipelineAggWithLink = { + ...parentPipelineAgg, + aggParams: { + metricAgg: metricAggId, + }, + }; + expect( + getMetricFromParentPipelineAgg(parentPipelineAggWithLink, [ + parentPipelineAggWithLink, + plainAgg, + ]) + ).toEqual(plainAgg); + expect(mockConvertToSchemaConfig).toBeCalledTimes(0); + }); + + test('should return metric if sibling pipeline agg with custom metric', () => { + expect(getMetricFromParentPipelineAgg(agg, [agg])).toEqual(metric); + expect(mockConvertToSchemaConfig).toBeCalledTimes(1); + }); + + test('should return metric if parent pipeline agg with custom metric', () => { + expect( + getMetricFromParentPipelineAgg( + { + ...parentPipelineAgg, + aggParams: { ...parentPipelineAgg.aggParams, customMetric: {} as IAggConfig }, + }, + [agg] + ) + ).toEqual(metric); + expect(mockConvertToSchemaConfig).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/utils.ts b/src/plugins/visualizations/common/convert_to_lens/lib/utils.ts index 39920e525b79..c4e5c5474bf0 100644 --- a/src/plugins/visualizations/common/convert_to_lens/lib/utils.ts +++ b/src/plugins/visualizations/common/convert_to_lens/lib/utils.ts @@ -150,7 +150,7 @@ export const isStdDevAgg = (metric: SchemaConfig): metric is SchemaConfig { +export const getCustomBucketsFromSiblingAggs = (metrics: SchemaConfig[]) => { return metrics.reduce((acc, metric) => { if ( isSiblingPipeline(metric) && diff --git a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts index 8c348fec4c2f..a6a771d07e7a 100644 --- a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts +++ b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts @@ -6,48 +6,32 @@ * Side Public License, v 1. */ -import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts'; +import { HorizontalAlignment, LayoutDirection, Position, VerticalAlignment } from '@elastic/charts'; import { $Values } from '@kbn/utility-types'; -import type { PaletteOutput } from '@kbn/coloring'; +import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; import { KibanaQueryOutput } from '@kbn/data-plugin/common'; import { LegendSize } from '../../constants'; - -export const XYCurveTypes = { - LINEAR: 'LINEAR', - CURVE_MONOTONE_X: 'CURVE_MONOTONE_X', - CURVE_STEP_AFTER: 'CURVE_STEP_AFTER', -} as const; - -export const YAxisModes = { - AUTO: 'auto', - LEFT: 'left', - RIGHT: 'right', - BOTTOM: 'bottom', -} as const; - -export const SeriesTypes = { - BAR: 'bar', - LINE: 'line', - AREA: 'area', - BAR_STACKED: 'bar_stacked', - AREA_STACKED: 'area_stacked', - BAR_HORIZONTAL: 'bar_horizontal', - BAR_PERCENTAGE_STACKED: 'bar_percentage_stacked', - BAR_HORIZONTAL_STACKED: 'bar_horizontal_stacked', - AREA_PERCENTAGE_STACKED: 'area_percentage_stacked', - BAR_HORIZONTAL_PERCENTAGE_STACKED: 'bar_horizontal_percentage_stacked', -} as const; - -export const FillTypes = { - NONE: 'none', - ABOVE: 'above', - BELOW: 'below', -} as const; +import { + CategoryDisplayTypes, + PartitionChartTypes, + NumberDisplayTypes, + LegendDisplayTypes, + FillTypes, + SeriesTypes, + YAxisModes, + XYCurveTypes, + LayerTypes, +} from '../constants'; export type FillType = $Values; export type SeriesType = $Values; export type YAxisMode = $Values; export type XYCurveType = $Values; +export type PartitionChartType = $Values; +export type CategoryDisplayType = $Values; +export type NumberDisplayType = $Values; +export type LegendDisplayType = $Values; +export type LayerType = $Values; export interface AxisExtentConfig { mode: 'full' | 'custom' | 'dataBounds'; @@ -199,4 +183,52 @@ export interface TableVisConfiguration { paging?: PagingState; } -export type Configuration = XYConfiguration | TableVisConfiguration; +export interface MetricVisConfiguration { + layerId: string; + layerType: 'data'; + metricAccessor?: string; + secondaryMetricAccessor?: string; + maxAccessor?: string; + breakdownByAccessor?: string; + // the dimensions can optionally be single numbers + // computed by collapsing all rows + collapseFn?: string; + subtitle?: string; + secondaryPrefix?: string; + progressDirection?: LayoutDirection; + color?: string; + palette?: PaletteOutput; + maxCols?: number; +} + +export interface PartitionLayerState { + layerId: string; + layerType: LayerType; + primaryGroups: string[]; + secondaryGroups?: string[]; + metric?: string; + collapseFns?: Record; + numberDisplay: NumberDisplayType; + categoryDisplay: CategoryDisplayType; + legendDisplay: LegendDisplayType; + legendPosition?: Position; + showValuesInLegend?: boolean; + nestedLegend?: boolean; + percentDecimals?: number; + emptySizeRatio?: number; + legendMaxLines?: number; + legendSize?: LegendSize; + truncateLegend?: boolean; +} + +export interface PartitionVisConfiguration { + shape: PartitionChartType; + layers: PartitionLayerState[]; + palette?: PaletteOutput; +} + +export type Configuration = + | XYConfiguration + | TableVisConfiguration + | PartitionVisConfiguration + | MetricVisConfiguration; diff --git a/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts b/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts index dccd579d95db..5b8b7832730b 100644 --- a/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts +++ b/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts @@ -40,7 +40,7 @@ jest.mock('../../common/convert_to_lens/lib/buckets', () => ({ })); jest.mock('../../common/convert_to_lens/lib/utils', () => ({ - getCutomBucketsFromSiblingAggs: jest.fn(() => mockGetCutomBucketsFromSiblingAggs()), + getCustomBucketsFromSiblingAggs: jest.fn(() => mockGetCutomBucketsFromSiblingAggs()), })); jest.mock('../vis_schemas', () => ({ diff --git a/src/plugins/visualizations/public/convert_to_lens/schemas.ts b/src/plugins/visualizations/public/convert_to_lens/schemas.ts index 372e434ca886..56108b1a1d63 100644 --- a/src/plugins/visualizations/public/convert_to_lens/schemas.ts +++ b/src/plugins/visualizations/public/convert_to_lens/schemas.ts @@ -11,7 +11,7 @@ import { METRIC_TYPES, TimefilterContract } from '@kbn/data-plugin/public'; import { AggBasedColumn, SchemaConfig } from '../../common'; import { convertMetricToColumns } from '../../common/convert_to_lens/lib/metrics'; import { convertBucketToColumns } from '../../common/convert_to_lens/lib/buckets'; -import { getCutomBucketsFromSiblingAggs } from '../../common/convert_to_lens/lib/utils'; +import { getCustomBucketsFromSiblingAggs } from '../../common/convert_to_lens/lib/utils'; import type { Vis } from '../types'; import { getVisSchemas, Schemas } from '../vis_schemas'; import { @@ -24,14 +24,26 @@ import { sortColumns, } from './utils'; +const areVisSchemasValid = (visSchemas: Schemas, unsupported: Array) => { + const usedUnsupportedSchemas = unsupported.filter( + (schema) => visSchemas[schema] && visSchemas[schema]?.length + ); + return !usedUnsupportedSchemas.length; +}; + export const getColumnsFromVis = ( vis: Vis, timefilter: TimefilterContract, dataView: DataView, - { splits, buckets }: { splits: Array; buckets: Array } = { - splits: [], - buckets: [], - }, + { + splits = [], + buckets = [], + unsupported = [], + }: { + splits?: Array; + buckets?: Array; + unsupported?: Array; + } = {}, config?: { dropEmptyRowsInDateHistogram?: boolean; } @@ -41,11 +53,11 @@ export const getColumnsFromVis = ( timeRange: timefilter.getAbsoluteTime(), }); - if (!isValidVis(visSchemas)) { + if (!isValidVis(visSchemas) || !areVisSchemasValid(visSchemas, unsupported)) { return null; } - const customBuckets = getCutomBucketsFromSiblingAggs(visSchemas.metric); + const customBuckets = getCustomBucketsFromSiblingAggs(visSchemas.metric); // doesn't support sibbling pipeline aggs with different bucket aggs if (customBuckets.length > 1) { @@ -111,8 +123,8 @@ export const getColumnsFromVis = ( const columnsWithoutReferenced = getColumnsWithoutReferenced(columns); return { - metrics: getColumnIds(metrics), - buckets: getColumnIds([...bucketColumns, ...splitBucketColumns, ...customBucketColumns]), + metrics: getColumnIds(columnsWithoutReferenced.filter((с) => !с.isBucketed)), + buckets: getColumnIds(columnsWithoutReferenced.filter((c) => c.isBucketed)), bucketCollapseFn: getBucketCollapseFn(visSchemas.metric, customBucketColumns), columnsWithoutReferenced, columns, diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx index 500a5e5b34d4..055b326b8f19 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx @@ -96,6 +96,7 @@ const TopNav = ({ [doReload] ); + const uiStateJSON = useMemo(() => vis.uiState.toJSON(), [vis.uiState]); useEffect(() => { const asyncGetTriggerContext = async () => { if (vis.type.navigateToLens) { @@ -107,7 +108,14 @@ const TopNav = ({ } }; asyncGetTriggerContext(); - }, [services.data.query.timefilter.timefilter, vis, vis.type, vis.params, vis.data.indexPattern]); + }, [ + services.data.query.timefilter.timefilter, + vis, + vis.type, + vis.params, + uiStateJSON?.vis, + vis.data.indexPattern, + ]); const displayEditInLensItem = Boolean(vis.type.navigateToLens && editInLensConfig); const config = useMemo(() => { diff --git a/test/api_integration/apis/data_views/data_views_crud/update_data_view/errors.ts b/test/api_integration/apis/data_views/data_views_crud/update_data_view/errors.ts index 54f61cba1cfb..670b003a3d24 100644 --- a/test/api_integration/apis/data_views/data_views_crud/update_data_view/errors.ts +++ b/test/api_integration/apis/data_views/data_views_crud/update_data_view/errors.ts @@ -53,7 +53,7 @@ export default function ({ getService }: FtrProviderContext) { ); }); - it('returns error when update patch is empty', async () => { + it('returns success when update patch is empty', async () => { const title1 = `foo-${Date.now()}-${Math.random()}*`; const response = await supertest.post(config.path).send({ [config.serviceKey]: { @@ -65,9 +65,7 @@ export default function ({ getService }: FtrProviderContext) { [config.serviceKey]: {}, }); - expect(response2.status).to.be(400); - expect(response2.body.statusCode).to.be(400); - expect(response2.body.message).to.be('Index pattern change set is empty.'); + expect(response2.status).to.be(200); }); }); }); diff --git a/test/functional/apps/dashboard/group3/bwc_shared_urls.ts b/test/functional/apps/dashboard/group3/bwc_shared_urls.ts index 35d13b715c14..0b06e28af0f1 100644 --- a/test/functional/apps/dashboard/group3/bwc_shared_urls.ts +++ b/test/functional/apps/dashboard/group3/bwc_shared_urls.ts @@ -12,9 +12,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header']); - const dashboardExpect = getService('dashboardExpect'); - const pieChart = getService('pieChart'); - const elasticChart = getService('elasticChart'); + const toasts = getService('toasts'); const browser = getService('browser'); const log = getService('log'); const queryBar = getService('queryBar'); @@ -42,11 +40,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { `legendOpen:!t))),` + `viewMode:edit)`; - const enableNewChartLibraryDebug = async () => { - await elasticChart.setNewChartUiDebugFlag(); - await queryBar.submitQuery(); - }; - describe('bwc shared urls', function describeIndexTests() { before(async function () { await PageObjects.dashboard.initTests(); @@ -81,13 +74,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug(`Navigating to ${url}`); await browser.get(url, true); await PageObjects.header.waitUntilLoadingHasFinished(); - await elasticChart.setNewChartUiDebugFlag(true); const query = await queryBar.getQueryString(); expect(query).to.equal('memory:>220000'); - await pieChart.expectEmptyPieChart(); - await dashboardExpect.panelCount(2); + const warningToast = await toasts.getToastElement(1); + expect(await warningToast.getVisibleText()).to.contain('Cannot load panels'); + await PageObjects.dashboard.waitForRenderComplete(); }); }); @@ -99,15 +92,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const url = `${kibanaLegacyBaseUrl}#/dashboard?${urlQuery}`; log.debug(`Navigating to ${url}`); await browser.get(url, true); - enableNewChartLibraryDebug(); await PageObjects.header.waitUntilLoadingHasFinished(); const query = await queryBar.getQueryString(); expect(query).to.equal('memory:>220000'); - await pieChart.expectPieSliceCount(5); - await dashboardExpect.panelCount(2); + const warningToast = await toasts.getToastElement(1); + expect(await warningToast.getVisibleText()).to.contain('Cannot load panels'); await PageObjects.dashboard.waitForRenderComplete(); - await dashboardExpect.selectedLegendColorCount('#F9D9F9', 5); }); it('loads a saved dashboard', async function () { @@ -120,15 +111,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug(`Navigating to ${url}`); await browser.get(url, true); await PageObjects.header.waitUntilLoadingHasFinished(); - enableNewChartLibraryDebug(); const query = await queryBar.getQueryString(); expect(query).to.equal('memory:>220000'); - await pieChart.expectPieSliceCount(5); - await dashboardExpect.panelCount(2); await PageObjects.dashboard.waitForRenderComplete(); - await dashboardExpect.selectedLegendColorCount('#F9D9F9', 5); }); it('loads a saved dashboard with query via dashboard_no_match', async function () { @@ -143,7 +130,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const query = await queryBar.getQueryString(); expect(query).to.equal('boop'); - await dashboardExpect.panelCount(2); await PageObjects.dashboard.waitForRenderComplete(); }); @@ -154,33 +140,26 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug(`Navigating to ${url}`); await browser.get(url, true); - await elasticChart.setNewChartUiDebugFlag(true); await PageObjects.header.waitUntilLoadingHasFinished(); - await dashboardExpect.selectedLegendColorCount('#000000', 5); }); it('back button works for old dashboards after state migrations', async () => { await PageObjects.dashboard.preserveCrossAppState(); const oldId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); await PageObjects.dashboard.waitForRenderComplete(); - await dashboardExpect.selectedLegendColorCount('#000000', 5); const url = `${kibanaLegacyBaseUrl}#/dashboard?${urlQuery}`; log.debug(`Navigating to ${url}`); await browser.get(url); - await elasticChart.setNewChartUiDebugFlag(true); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); - await dashboardExpect.selectedLegendColorCount('#F9D9F9', 5); await browser.goBack(); await PageObjects.header.waitUntilLoadingHasFinished(); const newId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); expect(newId).to.be.equal(oldId); await PageObjects.dashboard.waitForRenderComplete(); - await elasticChart.setNewChartUiDebugFlag(true); await queryBar.submitQuery(); - await dashboardExpect.selectedLegendColorCount('#000000', 5); }); }); }); diff --git a/test/functional/apps/dashboard/group3/dashboard_state.ts b/test/functional/apps/dashboard/group3/dashboard_state.ts index bc3f2ed2774a..2c79f1fd61d2 100644 --- a/test/functional/apps/dashboard/group3/dashboard_state.ts +++ b/test/functional/apps/dashboard/group3/dashboard_state.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import chroma from 'chroma-js'; -import { DEFAULT_PANEL_WIDTH } from '@kbn/dashboard-plugin/public/application/embeddable/dashboard_constants'; +import { DEFAULT_PANEL_WIDTH } from '@kbn/dashboard-plugin/public/dashboard_constants'; import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME } from '../../../page_objects/dashboard_page'; import { FtrProviderContext } from '../../../ftr_provider_context'; diff --git a/test/functional/apps/dashboard/group4/dashboard_empty.ts b/test/functional/apps/dashboard/group4/dashboard_empty.ts index fe5a74bebbc2..02437b068569 100644 --- a/test/functional/apps/dashboard/group4/dashboard_empty.ts +++ b/test/functional/apps/dashboard/group4/dashboard_empty.ts @@ -54,7 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); // create the new data view from the dashboards/create route in order to test that the dashboard is loaded properly as soon as the data view is created... - await PageObjects.common.navigateToUrl('dashboard', '/create'); + await PageObjects.common.navigateToApp('dashboard', { hash: '/create' }); const button = await testSubjects.find('createDataViewButton'); button.click(); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 7a579f4e4f84..bad1f6bdebf2 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -12,6 +12,7 @@ export const LINE_CHART_VIS_NAME = 'Visualization漢字 LineChart'; import expect from '@kbn/expect'; import { FtrService } from '../ftr_provider_context'; +import { CommonPageObject } from './common_page'; interface SaveDashboardOptions { /** @@ -430,6 +431,31 @@ export class DashboardPageObject extends FtrService { await this.switchToEditMode(); } + public async gotoDashboardURL({ + id, + args, + editMode, + }: { + id?: string; + editMode?: boolean; + args?: Parameters['navigateToActualUrl']>[2]; + } = {}) { + let dashboardLocation = `/create`; + if (id) { + const edit = editMode ? `?_a=(viewMode:edit)` : ''; + dashboardLocation = `/view/${id}${edit}`; + } + this.common.navigateToActualUrl('dashboard', dashboardLocation, args); + } + + public async gotoDashboardListingURL({ + args, + }: { + args?: Parameters['navigateToActualUrl']>[2]; + } = {}) { + await this.common.navigateToActualUrl('dashboard', '/list', args); + } + public async renameDashboard(dashboardName: string) { this.log.debug(`Naming dashboard ` + dashboardName); await this.testSubjects.click('dashboardRenameButton'); diff --git a/test/plugin_functional/test_suites/telemetry/telemetry.ts b/test/plugin_functional/test_suites/telemetry/telemetry.ts index 3b087c2705c1..1c68abd5426d 100644 --- a/test/plugin_functional/test_suites/telemetry/telemetry.ts +++ b/test/plugin_functional/test_suites/telemetry/telemetry.ts @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const browser = getService('browser'); const PageObjects = getPageObjects(['common']); - describe('Telemetry service', () => { + // FLAKY: https://github.com/elastic/kibana/issues/107034 + describe.skip('Telemetry service', () => { const checkCanSendTelemetry = (): Promise => { return browser.executeAsync((cb) => { (window as unknown as Record Promise>) diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index e6e3a24db521..475b88089581 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -163,6 +163,28 @@ describe('config validation', () => { `); }); + test('validates proxyUrl', () => { + const proxyUrl = 'https://test.com'; + const badProxyUrl = 'bad url'; + let validated: ActionsConfig; + + validated = configSchema.validate({ proxyUrl }); + expect(validated.proxyUrl).toEqual(proxyUrl); + expect(getValidatedConfig(mockLogger, validated).proxyUrl).toEqual(proxyUrl); + expect(mockLogger.warn.mock.calls).toMatchInlineSnapshot(`Array []`); + + validated = configSchema.validate({ proxyUrl: badProxyUrl }); + expect(validated.proxyUrl).toEqual(badProxyUrl); + expect(getValidatedConfig(mockLogger, validated).proxyUrl).toEqual(badProxyUrl); + expect(mockLogger.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "The confguration xpack.actions.proxyUrl: bad url is invalid.", + ], + ] + `); + }); + // Most of the customHostSettings tests are in ./lib/custom_host_settings.test.ts // but this one seemed more relevant for this test suite, since url is the one // required property. diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 4c8ca7ff9fff..76270a466ee8 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -127,6 +127,15 @@ export type ActionsConfig = TypeOf; export function getValidatedConfig(logger: Logger, originalConfig: ActionsConfig): ActionsConfig { const proxyBypassHosts = originalConfig.proxyBypassHosts; const proxyOnlyHosts = originalConfig.proxyOnlyHosts; + const proxyUrl = originalConfig.proxyUrl; + + if (proxyUrl) { + try { + new URL(proxyUrl); + } catch (err) { + logger.warn(`The confguration xpack.actions.proxyUrl: ${proxyUrl} is invalid.`); + } + } if (proxyBypassHosts && proxyOnlyHosts) { logger.warn( diff --git a/x-pack/plugins/aiops/public/components/log_categorization/information_text.tsx b/x-pack/plugins/aiops/public/components/log_categorization/information_text.tsx new file mode 100644 index 000000000000..bf80b55fee37 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/log_categorization/information_text.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; + +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +interface Props { + eventRateLength: number; + fieldSelected: boolean; + categoriesLength: number | null; + loading: boolean; +} + +export const InformationText: FC = ({ + eventRateLength, + fieldSelected, + categoriesLength, + loading, +}) => { + if (loading === true) { + return null; + } + return ( + <> + {eventRateLength === 0 ? ( + + + + } + titleSize="xs" + body={ +

+ +

+ } + data-test-subj="aiopsNoWindowParametersEmptyPrompt" + /> + ) : null} + + {eventRateLength > 0 && categoriesLength === null ? ( + + + + } + titleSize="xs" + body={ +

+ +

+ } + data-test-subj="aiopsNoWindowParametersEmptyPrompt" + /> + ) : null} + + {eventRateLength > 0 && categoriesLength !== null && categoriesLength === 0 ? ( + + + + } + titleSize="xs" + body={ +

+ +

+ } + data-test-subj="aiopsNoWindowParametersEmptyPrompt" + /> + ) : null} + + ); +}; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx index 845a4239f110..4eb6da0e58b9 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx @@ -40,6 +40,7 @@ import { useCategorizeRequest } from './use_categorize_request'; import type { EventRate, Category, SparkLinesPerCategory } from './use_categorize_request'; import { CategoryTable } from './category_table'; import { DocumentCountChart } from './document_count_chart'; +import { InformationText } from './information_text'; export interface LogCategorizationPageProps { dataView: DataView; @@ -160,6 +161,7 @@ export const LogCategorizationPage: FC = ({ docCount, })) ); + setCategories(null); setTotalCount(documentStats.totalCount); } }, [documentStats, earliest, latest, searchQueryLanguage, searchString, searchQuery]); @@ -210,6 +212,7 @@ export const LogCategorizationPage: FC = ({ ]); const onFieldChange = (value: EuiComboBoxOptionOption[] | undefined) => { + setCategories(null); setSelectedField(value && value.length ? value[0].label : undefined); }; @@ -313,8 +316,17 @@ export const LogCategorizationPage: FC = ({ ) : null} + {loading === true ? : null} - {categories !== null ? ( + + + + {selectedField !== undefined && categories !== null && categories.length > 0 ? ( { }); }); -describe('scheduledActionGroupOrSubgroupHasChanged()', () => { +describe('scheduledActionGroupHasChanged()', () => { test('should be false if no last scheduled and nothing scheduled', () => { const alert = new Alert('1'); - expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + expect(alert.scheduledActionGroupHasChanged()).toEqual(false); }); test('should be false if group does not change', () => { @@ -97,54 +97,13 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }, }); alert.scheduleActions('default'); - expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group and subgroup does not change', () => { - const alert = new Alert('1', { - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alert.scheduleActionsWithSubGroup('default', 'subgroup'); - expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group does not change and subgroup goes from undefined to defined', () => { - const alert = new Alert('1', { - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alert.scheduleActionsWithSubGroup('default', 'subgroup'); - expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group does not change and subgroup goes from defined to undefined', () => { - const alert = new Alert('1', { - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alert.scheduleActions('default'); - expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + expect(alert.scheduledActionGroupHasChanged()).toEqual(false); }); test('should be true if no last scheduled and has scheduled action', () => { const alert = new Alert('1'); alert.scheduleActions('default'); - expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + expect(alert.scheduledActionGroupHasChanged()).toEqual(true); }); test('should be true if group does change', () => { @@ -157,35 +116,7 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }, }); alert.scheduleActions('penguin'); - expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); - - test('should be true if group does change and subgroup does change', () => { - const alert = new Alert('1', { - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alert.scheduleActionsWithSubGroup('penguin', 'fish'); - expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); - - test('should be true if group does not change and subgroup does change', () => { - const alert = new Alert('1', { - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alert.scheduleActionsWithSubGroup('default', 'fish'); - expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + expect(alert.scheduledActionGroupHasChanged()).toEqual(true); }); }); @@ -296,137 +227,6 @@ describe('scheduleActions()', () => { }); }); -describe('scheduleActionsWithSubGroup()', () => { - test('makes hasScheduledActions() return true', () => { - const alert = new Alert('1', { - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alert - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alert.hasScheduledActions()).toEqual(true); - }); - - test('makes isThrottled() return true when throttled and subgroup is the same', () => { - const alert = new Alert('1', { - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alert - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alert.isThrottled('1m')).toEqual(true); - }); - - test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => { - const alert = new Alert('1', { - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alert - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alert.isThrottled('1m')).toEqual(true); - }); - - test('makes isThrottled() return false when throttled and subgroup is the different', () => { - const alert = new Alert('1', { - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'prev-subgroup', - }, - }, - }); - alert - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alert.isThrottled('1m')).toEqual(false); - }); - - test('make isThrottled() return false when throttled expired', () => { - const alert = new Alert('1', { - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(120000); - alert - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alert.isThrottled('1m')).toEqual(false); - }); - - test('makes getScheduledActionOptions() return given options', () => { - const alert = new Alert('1', { - state: { foo: true }, - meta: {}, - }); - alert - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alert.getScheduledActionOptions()).toEqual({ - actionGroup: 'default', - subgroup: 'subgroup', - context: { field: true }, - state: { otherField: true }, - }); - }); - - test('cannot schdule for execution twice', () => { - const alert = new Alert('1'); - alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(() => - alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); - - test('cannot schdule for execution twice with different subgroups', () => { - const alert = new Alert('1'); - alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(() => - alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); - - test('cannot schdule for execution twice whether there are subgroups', () => { - const alert = new Alert('1'); - alert.scheduleActions('default', { field: true }); - expect(() => - alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); -}); - describe('replaceState()', () => { test('replaces previous state', () => { const alert = new Alert('1', { diff --git a/x-pack/plugins/alerting/server/alert/alert.ts b/x-pack/plugins/alerting/server/alert/alert.ts index bf29cacf556c..e24b15de41db 100644 --- a/x-pack/plugins/alerting/server/alert/alert.ts +++ b/x-pack/plugins/alerting/server/alert/alert.ts @@ -23,7 +23,6 @@ interface ScheduledExecutionOptions< ActionGroupIds extends string = DefaultActionGroupId > { actionGroup: ActionGroupIds; - subgroup?: string; context: Context; state: State; } @@ -34,13 +33,7 @@ export type PublicAlert< ActionGroupIds extends string = DefaultActionGroupId > = Pick< Alert, - | 'getState' - | 'replaceState' - | 'scheduleActions' - | 'scheduleActionsWithSubGroup' - | 'setContext' - | 'getContext' - | 'hasContext' + 'getState' | 'replaceState' | 'scheduleActions' | 'setContext' | 'getContext' | 'hasContext' >; export class Alert< @@ -80,10 +73,6 @@ export class Alert< this.meta.lastScheduledActions, this.scheduledExecutionOptions ) && - this.scheduledActionSubgroupIsUnchanged( - this.meta.lastScheduledActions, - this.scheduledExecutionOptions - ) && this.meta.lastScheduledActions.date.getTime() + throttleMills > Date.now() ) { return true; @@ -91,7 +80,7 @@ export class Alert< return false; } - scheduledActionGroupOrSubgroupHasChanged(): boolean { + scheduledActionGroupHasChanged(): boolean { if (!this.meta.lastScheduledActions && this.scheduledExecutionOptions) { // it is considered a change when there are no previous scheduled actions // and new scheduled actions @@ -100,18 +89,11 @@ export class Alert< if (this.meta.lastScheduledActions && this.scheduledExecutionOptions) { // compare previous and new scheduled actions if both exist - return ( - !this.scheduledActionGroupIsUnchanged( - this.meta.lastScheduledActions, - this.scheduledExecutionOptions - ) || - !this.scheduledActionSubgroupIsUnchanged( - this.meta.lastScheduledActions, - this.scheduledExecutionOptions - ) + return !this.scheduledActionGroupIsUnchanged( + this.meta.lastScheduledActions, + this.scheduledExecutionOptions ); } - // no previous and no new scheduled actions return false; } @@ -123,15 +105,6 @@ export class Alert< return lastScheduledActions.group === scheduledExecutionOptions.actionGroup; } - private scheduledActionSubgroupIsUnchanged( - lastScheduledActions: NonNullable, - scheduledExecutionOptions: ScheduledExecutionOptions - ) { - return lastScheduledActions.subgroup && scheduledExecutionOptions.subgroup - ? lastScheduledActions.subgroup === scheduledExecutionOptions.subgroup - : true; - } - getLastScheduledActions() { return this.meta.lastScheduledActions; } @@ -168,22 +141,6 @@ export class Alert< return this; } - scheduleActionsWithSubGroup( - actionGroup: ActionGroupIds, - subgroup: string, - context: Context = {} as Context - ) { - this.ensureHasNoScheduledActions(); - this.setContext(context); - this.scheduledExecutionOptions = { - actionGroup, - subgroup, - context, - state: this.state, - }; - return this; - } - setContext(context: Context) { this.context = context; return this; @@ -200,8 +157,8 @@ export class Alert< return this; } - updateLastScheduledActions(group: ActionGroupIds, subgroup?: string) { - this.meta.lastScheduledActions = { group, subgroup, date: new Date() }; + updateLastScheduledActions(group: ActionGroupIds) { + this.meta.lastScheduledActions = { group, date: new Date() }; } /** diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 0c501f25a857..7138aabe9d26 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -35,6 +35,7 @@ export enum ReadOperations { Find = 'find', GetAuthorizedAlertsIndices = 'getAuthorizedAlertsIndices', RunSoon = 'runSoon', + GetRuleExecutionKPI = 'getRuleExecutionKPI', } export enum WriteOperations { diff --git a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts index d2040f8e63f3..56a862f2ad6c 100644 --- a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts @@ -121,14 +121,12 @@ describe('alertSummaryFromEventLog', () => { "alerts": Object { "alert-1": Object { "actionGroupId": undefined, - "actionSubgroup": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", }, "alert-2": Object { "actionGroupId": undefined, - "actionSubgroup": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", @@ -233,7 +231,6 @@ describe('alertSummaryFromEventLog', () => { "alerts": Object { "alert-1": Object { "actionGroupId": undefined, - "actionSubgroup": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", @@ -274,7 +271,6 @@ describe('alertSummaryFromEventLog', () => { "alerts": Object { "alert-1": Object { "actionGroupId": undefined, - "actionSubgroup": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", @@ -314,7 +310,6 @@ describe('alertSummaryFromEventLog', () => { "alerts": Object { "alert-1": Object { "actionGroupId": undefined, - "actionSubgroup": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", @@ -355,7 +350,6 @@ describe('alertSummaryFromEventLog', () => { "alerts": Object { "alert-1": Object { "actionGroupId": "action group A", - "actionSubgroup": undefined, "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": false, "status": "Active", @@ -396,7 +390,6 @@ describe('alertSummaryFromEventLog', () => { "alerts": Object { "alert-1": Object { "actionGroupId": undefined, - "actionSubgroup": undefined, "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": false, "status": "Active", @@ -437,7 +430,6 @@ describe('alertSummaryFromEventLog', () => { "alerts": Object { "alert-1": Object { "actionGroupId": "action group B", - "actionSubgroup": undefined, "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": false, "status": "Active", @@ -476,7 +468,6 @@ describe('alertSummaryFromEventLog', () => { "alerts": Object { "alert-1": Object { "actionGroupId": "action group A", - "actionSubgroup": undefined, "activeStartDate": undefined, "muted": false, "status": "Active", @@ -519,14 +510,12 @@ describe('alertSummaryFromEventLog', () => { "alerts": Object { "alert-1": Object { "actionGroupId": "action group A", - "actionSubgroup": undefined, "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": true, "status": "Active", }, "alert-2": Object { "actionGroupId": undefined, - "actionSubgroup": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", @@ -576,14 +565,12 @@ describe('alertSummaryFromEventLog', () => { "alerts": Object { "alert-1": Object { "actionGroupId": "action group B", - "actionSubgroup": undefined, "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": false, "status": "Active", }, "alert-2": Object { "actionGroupId": undefined, - "actionSubgroup": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", diff --git a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts index 54ac23bf94f2..d8e5f4dea9b4 100644 --- a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts @@ -87,14 +87,12 @@ export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams) case EVENT_LOG_ACTIONS.activeInstance: status.status = 'Active'; status.actionGroupId = event?.kibana?.alerting?.action_group_id; - status.actionSubgroup = event?.kibana?.alerting?.action_subgroup; break; case LEGACY_EVENT_LOG_ACTIONS.resolvedInstance: case EVENT_LOG_ACTIONS.recoveredInstance: status.status = 'OK'; status.activeStartDate = undefined; status.actionGroupId = undefined; - status.actionSubgroup = undefined; } } @@ -153,7 +151,6 @@ function getAlertStatus(alerts: Map, alertId: string): Aler status: 'OK', muted: false, actionGroupId: undefined, - actionSubgroup: undefined, activeStartDate: undefined, }; alerts.set(alertId, status); diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts index 91904901adcf..8fe081864b11 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts @@ -59,9 +59,8 @@ const contextWithName = { ...contextWithScheduleDelay, ruleName: 'my-super-cool- const alert = { action: EVENT_LOG_ACTIONS.activeInstance, id: 'aaabbb', - message: `.test-rule-type:123: 'my rule' active alert: 'aaabbb' in actionGroup: 'aGroup'; actionSubGroup: 'bSubGroup'`, + message: `.test-rule-type:123: 'my rule' active alert: 'aaabbb' in actionGroup: 'aGroup';`, group: 'aGroup', - subgroup: 'bSubgroup', state: { start: '2020-01-01T02:00:00.000Z', end: '2020-01-01T03:00:00.000Z', @@ -74,7 +73,6 @@ const action = { typeId: '.email', alertId: '123', alertGroup: 'aGroup', - alertSubgroup: 'bSubgroup', }; describe('AlertingEventLogger', () => { @@ -1006,14 +1004,13 @@ describe('createAlertRecord', () => { expect(record.event?.end).toEqual(alert.state.end); expect(record.event?.duration).toEqual(alert.state.duration); expect(record.message).toEqual( - `.test-rule-type:123: 'my rule' active alert: 'aaabbb' in actionGroup: 'aGroup'; actionSubGroup: 'bSubGroup'` + `.test-rule-type:123: 'my rule' active alert: 'aaabbb' in actionGroup: 'aGroup';` ); expect(record.kibana?.alert?.rule?.rule_type_id).toEqual(contextWithName.ruleType.id); expect(record.kibana?.alert?.rule?.consumer).toEqual(contextWithName.consumer); expect(record.kibana?.alert?.rule?.execution?.uuid).toEqual(contextWithName.executionId); expect(record.kibana?.alerting?.instance_id).toEqual(alert.id); expect(record.kibana?.alerting?.action_group_id).toEqual(alert.group); - expect(record.kibana?.alerting?.action_subgroup).toEqual(alert.subgroup); expect(record.kibana?.saved_objects).toEqual([ { id: contextWithName.ruleId, @@ -1059,14 +1056,13 @@ describe('createActionExecuteRecord', () => { expect(record.event?.kind).toEqual('alert'); expect(record.event?.category).toEqual([contextWithName.ruleType.producer]); expect(record.message).toEqual( - `alert: test:123: 'my-super-cool-rule' instanceId: '123' scheduled actionGroup(subgroup): 'aGroup(bSubgroup)' action: .email:abc` + `alert: test:123: 'my-super-cool-rule' instanceId: '123' scheduled actionGroup: 'aGroup' action: .email:abc` ); expect(record.kibana?.alert?.rule?.rule_type_id).toEqual(contextWithName.ruleType.id); expect(record.kibana?.alert?.rule?.consumer).toEqual(contextWithName.consumer); expect(record.kibana?.alert?.rule?.execution?.uuid).toEqual(contextWithName.executionId); expect(record.kibana?.alerting?.instance_id).toEqual(action.alertId); expect(record.kibana?.alerting?.action_group_id).toEqual(action.alertGroup); - expect(record.kibana?.alerting?.action_subgroup).toEqual(action.alertSubgroup); expect(record.kibana?.saved_objects).toEqual([ { id: contextWithName.ruleId, diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts index 96be872d81b8..18e4044172eb 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts @@ -47,7 +47,6 @@ interface AlertOpts { id: string; message: string; group?: string; - subgroup?: string; state?: AlertInstanceState; } @@ -56,7 +55,6 @@ interface ActionOpts { typeId: string; alertId: string; alertGroup?: string; - alertSubgroup?: string; } export class AlertingEventLogger { @@ -232,7 +230,6 @@ export function createAlertRecord(context: RuleContextOpts, alert: AlertOpts) { state: alert.state, instanceId: alert.id, group: alert.group, - subgroup: alert.subgroup, message: alert.message, savedObjects: [ { @@ -257,14 +254,7 @@ export function createActionExecuteRecord(context: RuleContextOpts, action: Acti action: EVENT_LOG_ACTIONS.executeAction, instanceId: action.alertId, group: action.alertGroup, - subgroup: action.alertSubgroup, - message: `alert: ${context.ruleType.id}:${context.ruleId}: '${context.ruleName}' instanceId: '${ - action.alertId - }' scheduled ${ - action.alertSubgroup - ? `actionGroup(subgroup): '${action.alertGroup}(${action.alertSubgroup})'` - : `actionGroup: '${action.alertGroup}'` - } action: ${action.typeId}:${action.id}`, + message: `alert: ${context.ruleType.id}:${context.ruleId}: '${context.ruleName}' instanceId: '${action.alertId}' scheduled actionGroup: '${action.alertGroup}' action: ${action.typeId}:${action.id}`, savedObjects: [ { id: context.ruleId, diff --git a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts index ba16b7c553e8..8f73ba3a5686 100644 --- a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts +++ b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts @@ -99,7 +99,6 @@ describe('createAlertEventLogRecordObject', () => { group: 'group 1', message: 'message text here', namespace: 'default', - subgroup: 'subgroup value', state: { start: '1970-01-01T00:00:00.000Z', end: '1970-01-01T00:05:00.000Z', @@ -136,7 +135,6 @@ describe('createAlertEventLogRecordObject', () => { }, alerting: { action_group_id: 'group 1', - action_subgroup: 'subgroup value', instance_id: 'test1', }, saved_objects: [ @@ -174,7 +172,6 @@ describe('createAlertEventLogRecordObject', () => { group: 'group 1', message: 'action execution start', namespace: 'default', - subgroup: 'subgroup value', state: { start: '1970-01-01T00:00:00.000Z', end: '1970-01-01T00:05:00.000Z', @@ -216,7 +213,6 @@ describe('createAlertEventLogRecordObject', () => { }, alerting: { action_group_id: 'group 1', - action_subgroup: 'subgroup value', instance_id: 'test1', }, saved_objects: [ diff --git a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts index cd7eda500d15..a0f229c0b46d 100644 --- a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts +++ b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts @@ -23,7 +23,6 @@ interface CreateAlertEventLogRecordParams { message?: string; state?: AlertInstanceState; group?: string; - subgroup?: string; namespace?: string; timestamp?: string; task?: { @@ -48,18 +47,16 @@ export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecor task, ruleId, group, - subgroup, namespace, consumer, spaceId, } = params; const alerting = - params.instanceId || group || subgroup + params.instanceId || group ? { alerting: { ...(params.instanceId ? { instance_id: params.instanceId } : {}), ...(group ? { action_group_id: group } : {}), - ...(subgroup ? { action_subgroup: subgroup } : {}), }, } : undefined; diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts index e48483785490..eb46ae67e2ef 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts @@ -13,6 +13,8 @@ import { formatSortForBucketSort, formatSortForTermSort, ExecutionUuidAggResult, + getExecutionKPIAggregation, + formatExecutionKPIResult, } from './get_execution_log_aggregation'; describe('formatSortForBucketSort', () => { @@ -1689,3 +1691,584 @@ describe('formatExecutionLogResult', () => { }); }); }); + +describe('getExecutionKPIAggregation', () => { + test('should correctly generate aggregation', () => { + expect(getExecutionKPIAggregation()).toEqual({ + excludeExecuteStart: { + filter: { + bool: { + must_not: [ + { + term: { + 'event.action': 'execute-start', + }, + }, + ], + }, + }, + aggs: { + executionUuid: { + terms: { + field: 'kibana.alert.rule.execution.uuid', + size: 1000, + }, + aggs: { + executionUuidSorted: { + bucket_sort: { + from: 0, + size: 1000, + gap_policy: 'insert_zeros', + }, + }, + actionExecution: { + filter: { + bool: { + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'actions', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + actionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + ruleExecution: { + filter: { + bool: { + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'alerting', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + numTriggeredActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_triggered_actions', + missing: 0, + }, + }, + numGeneratedActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_generated_actions', + missing: 0, + }, + }, + numActiveAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.active', + missing: 0, + }, + }, + numRecoveredAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.recovered', + missing: 0, + }, + }, + numNewAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.new', + missing: 0, + }, + }, + ruleExecutionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + minExecutionUuidBucket: { + bucket_selector: { + buckets_path: { + count: 'ruleExecution._count', + }, + script: { + source: 'params.count > 0', + }, + }, + }, + }, + }, + }, + }, + }); + }); + + test('should correctly generate aggregation with a defined filter in the form of a string', () => { + expect(getExecutionKPIAggregation('test:test')).toEqual({ + excludeExecuteStart: { + filter: { + bool: { + must_not: [ + { + term: { + 'event.action': 'execute-start', + }, + }, + ], + }, + }, + aggs: { + executionUuid: { + terms: { + field: 'kibana.alert.rule.execution.uuid', + size: 1000, + }, + aggs: { + executionUuidSorted: { + bucket_sort: { + from: 0, + size: 1000, + gap_policy: 'insert_zeros', + }, + }, + actionExecution: { + filter: { + bool: { + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'actions', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + actionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + ruleExecution: { + filter: { + bool: { + filter: { + bool: { + should: [ + { + match: { + test: 'test', + }, + }, + ], + minimum_should_match: 1, + }, + }, + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'alerting', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + numTriggeredActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_triggered_actions', + missing: 0, + }, + }, + numGeneratedActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_generated_actions', + missing: 0, + }, + }, + numActiveAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.active', + missing: 0, + }, + }, + numRecoveredAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.recovered', + missing: 0, + }, + }, + numNewAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.new', + missing: 0, + }, + }, + ruleExecutionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + minExecutionUuidBucket: { + bucket_selector: { + buckets_path: { + count: 'ruleExecution._count', + }, + script: { + source: 'params.count > 0', + }, + }, + }, + }, + }, + }, + }, + }); + }); + + test('should correctly generate aggregation with a defined filter in the form of a KueryNode', () => { + expect(getExecutionKPIAggregation(fromKueryExpression('test:test'))).toEqual({ + excludeExecuteStart: { + filter: { + bool: { + must_not: [ + { + term: { + 'event.action': 'execute-start', + }, + }, + ], + }, + }, + aggs: { + executionUuid: { + terms: { + field: 'kibana.alert.rule.execution.uuid', + size: 1000, + }, + aggs: { + executionUuidSorted: { + bucket_sort: { + from: 0, + size: 1000, + gap_policy: 'insert_zeros', + }, + }, + actionExecution: { + filter: { + bool: { + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'actions', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + actionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + ruleExecution: { + filter: { + bool: { + filter: { + bool: { + should: [ + { + match: { + test: 'test', + }, + }, + ], + minimum_should_match: 1, + }, + }, + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'alerting', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + numTriggeredActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_triggered_actions', + missing: 0, + }, + }, + numGeneratedActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_generated_actions', + missing: 0, + }, + }, + numActiveAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.active', + missing: 0, + }, + }, + numRecoveredAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.recovered', + missing: 0, + }, + }, + numNewAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.new', + missing: 0, + }, + }, + ruleExecutionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + minExecutionUuidBucket: { + bucket_selector: { + buckets_path: { + count: 'ruleExecution._count', + }, + script: { + source: 'params.count > 0', + }, + }, + }, + }, + }, + }, + }, + }); + }); +}); + +describe('formatExecutionKPIAggBuckets', () => { + test('should return empty results if aggregations are undefined', () => { + expect( + formatExecutionKPIResult({ + aggregations: undefined, + }) + ).toEqual({ + activeAlerts: 0, + erroredActions: 0, + failure: 0, + newAlerts: 0, + recoveredAlerts: 0, + success: 0, + triggeredActions: 0, + unknown: 0, + }); + }); + + test('should format results correctly', () => { + const results = { + aggregations: { + excludeExecuteStart: { + meta: {}, + doc_count: 875, + executionUuid: { + meta: {}, + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + ruleExecution: { + meta: {}, + doc_count: 3, + numTriggeredActions: { + value: 5.0, + }, + numGeneratedActions: { + value: 5.0, + }, + numActiveAlerts: { + value: 5.0, + }, + numNewAlerts: { + value: 5.0, + }, + numRecoveredAlerts: { + value: 0.0, + }, + ruleExecutionOutcomes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'success', + doc_count: 3, + }, + ], + }, + }, + actionExecution: { + meta: {}, + doc_count: 5, + actionOutcomes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'success', + doc_count: 5, + }, + ], + }, + }, + }, + { + ruleExecution: { + meta: {}, + doc_count: 2, + numTriggeredActions: { + value: 5.0, + }, + numGeneratedActions: { + value: 5.0, + }, + numActiveAlerts: { + value: 5.0, + }, + numNewAlerts: { + value: 5.0, + }, + numRecoveredAlerts: { + value: 0.0, + }, + ruleExecutionOutcomes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'success', + doc_count: 2, + }, + ], + }, + }, + actionExecution: { + meta: {}, + doc_count: 3, + actionOutcomes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'failure', + doc_count: 3, + }, + ], + }, + }, + }, + ], + }, + }, + }, + }; + + expect(formatExecutionKPIResult(results)).toEqual({ + success: 5, + unknown: 0, + failure: 0, + activeAlerts: 10, + newAlerts: 10, + recoveredAlerts: 0, + erroredActions: 3, + triggeredActions: 10, + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts index 0854488d5f29..fedc827a46c7 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts @@ -12,7 +12,7 @@ import { flatMap, get } from 'lodash'; import { AggregateEventsBySavedObjectResult } from '@kbn/event-log-plugin/server'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { parseDuration } from '.'; -import { IExecutionLog, IExecutionLogResult } from '../../common'; +import { IExecutionLog, IExecutionLogResult, EMPTY_EXECUTION_KPI_RESULT } from '../../common'; const DEFAULT_MAX_BUCKETS_LIMIT = 1000; // do not retrieve more than this number of executions @@ -51,6 +51,21 @@ interface IActionExecution buckets: Array<{ key: string; doc_count: number }>; } +interface IExecutionUuidKpiAggBucket extends estypes.AggregationsStringTermsBucketKeys { + actionExecution: { + doc_count: number; + actionOutcomes: IActionExecution; + }; + ruleExecution: { + doc_count: number; + numTriggeredActions: estypes.AggregationsSumAggregate; + numGeneratedActions: estypes.AggregationsSumAggregate; + numActiveAlerts: estypes.AggregationsSumAggregate; + numRecoveredAlerts: estypes.AggregationsSumAggregate; + numNewAlerts: estypes.AggregationsSumAggregate; + ruleExecutionOutcomes: IActionExecution; + }; +} interface IExecutionUuidAggBucket extends estypes.AggregationsStringTermsBucketKeys { timeoutMessage: estypes.AggregationsMultiBucketBase; ruleExecution: { @@ -76,12 +91,22 @@ export interface ExecutionUuidAggResult buckets: TBucket[]; } +export interface ExecutionUuidKPIAggResult + extends estypes.AggregationsAggregateBase { + buckets: TBucket[]; +} + interface ExcludeExecuteStartAggResult extends estypes.AggregationsAggregateBase { executionUuid: ExecutionUuidAggResult; executionUuidCardinality: { executionUuidCardinality: estypes.AggregationsCardinalityAggregate; }; } + +interface ExcludeExecuteStartKpiAggResult extends estypes.AggregationsAggregateBase { + executionUuid: ExecutionUuidKPIAggResult; +} + export interface IExecutionLogAggOptions { filter?: string | KueryNode; page: number; @@ -102,6 +127,115 @@ const ExecutionLogSortFields: Record = { num_new_alerts: 'ruleExecution>numNewAlerts', }; +export const getExecutionKPIAggregation = (filter?: IExecutionLogAggOptions['filter']) => { + const dslFilterQuery: estypes.QueryDslBoolQuery['filter'] = buildDslFilterQuery(filter); + + return { + excludeExecuteStart: { + filter: { + bool: { + must_not: [ + { + term: { + 'event.action': 'execute-start', + }, + }, + ], + }, + }, + aggs: { + executionUuid: { + // Bucket by execution UUID + terms: { + field: EXECUTION_UUID_FIELD, + size: DEFAULT_MAX_BUCKETS_LIMIT, + }, + aggs: { + executionUuidSorted: { + bucket_sort: { + from: 0, + size: 1000, + gap_policy: 'insert_zeros' as estypes.AggregationsGapPolicy, + }, + }, + actionExecution: { + filter: { + bool: { + must: [getProviderAndActionFilter('actions', 'execute')], + }, + }, + aggs: { + actionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + ruleExecution: { + filter: { + bool: { + ...(dslFilterQuery ? { filter: dslFilterQuery } : {}), + must: [getProviderAndActionFilter('alerting', 'execute')], + }, + }, + aggs: { + numTriggeredActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_triggered_actions', + missing: 0, + }, + }, + numGeneratedActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_generated_actions', + missing: 0, + }, + }, + numActiveAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.active', + missing: 0, + }, + }, + numRecoveredAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.recovered', + missing: 0, + }, + }, + numNewAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.new', + missing: 0, + }, + }, + ruleExecutionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + minExecutionUuidBucket: { + bucket_selector: { + buckets_path: { + count: 'ruleExecution._count', + }, + script: { + source: 'params.count > 0', + }, + }, + }, + }, + }, + }, + }, + }; +}; + export function getExecutionLogAggregation({ filter, page, @@ -130,13 +264,7 @@ export function getExecutionLogAggregation({ throw Boom.badRequest(`Invalid perPage field "${perPage}" - must be greater than 0`); } - let dslFilterQuery: estypes.QueryDslBoolQuery['filter']; - try { - const filterKueryNode = typeof filter === 'string' ? fromKueryExpression(filter) : filter; - dslFilterQuery = filter ? toElasticsearchQuery(filterKueryNode) : undefined; - } catch (err) { - throw Boom.badRequest(`Invalid kuery syntax for filter ${filter}`); - } + const dslFilterQuery: estypes.QueryDslBoolQuery['filter'] = buildDslFilterQuery(filter); return { excludeExecuteStart: { @@ -295,6 +423,15 @@ export function getExecutionLogAggregation({ }; } +function buildDslFilterQuery(filter: IExecutionLogAggOptions['filter']) { + try { + const filterKueryNode = typeof filter === 'string' ? fromKueryExpression(filter) : filter; + return filter ? toElasticsearchQuery(filterKueryNode) : undefined; + } catch (err) { + throw Boom.badRequest(`Invalid kuery syntax for filter ${filter}`); + } +} + function getProviderAndActionFilter(provider: string, action: string) { return { bool: { @@ -362,6 +499,52 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio }; } +function formatExecutionKPIAggBuckets(buckets: IExecutionUuidKpiAggBucket[]) { + const objToReturn = { + success: 0, + unknown: 0, + failure: 0, + activeAlerts: 0, + newAlerts: 0, + recoveredAlerts: 0, + erroredActions: 0, + triggeredActions: 0, + }; + + buckets.forEach((bucket) => { + const ruleExecutionOutcomes = bucket?.ruleExecution?.ruleExecutionOutcomes?.buckets ?? []; + const actionExecutionOutcomes = bucket?.actionExecution?.actionOutcomes?.buckets ?? []; + + const ruleExecutionCount = bucket?.ruleExecution?.doc_count ?? 0; + const successRuleExecution = + ruleExecutionOutcomes.find((subBucket) => subBucket?.key === 'success')?.doc_count ?? 0; + const failureRuleExecution = + ruleExecutionOutcomes.find((subBucket) => subBucket?.key === 'failure')?.doc_count ?? 0; + + objToReturn.success += successRuleExecution; + objToReturn.unknown += ruleExecutionCount - (successRuleExecution + failureRuleExecution); + objToReturn.failure += failureRuleExecution; + objToReturn.activeAlerts += bucket?.ruleExecution?.numActiveAlerts.value ?? 0; + objToReturn.newAlerts += bucket?.ruleExecution?.numNewAlerts.value ?? 0; + objToReturn.recoveredAlerts += bucket?.ruleExecution?.numRecoveredAlerts.value ?? 0; + objToReturn.erroredActions += + actionExecutionOutcomes.find((subBucket) => subBucket?.key === 'failure')?.doc_count ?? 0; + objToReturn.triggeredActions += bucket?.ruleExecution?.numTriggeredActions.value ?? 0; + }); + + return objToReturn; +} + +export function formatExecutionKPIResult(results: AggregateEventsBySavedObjectResult) { + const { aggregations } = results; + if (!aggregations || !aggregations.excludeExecuteStart) { + return EMPTY_EXECUTION_KPI_RESULT; + } + const aggs = aggregations.excludeExecuteStart as ExcludeExecuteStartKpiAggResult; + const buckets = aggs.executionUuid.buckets; + return formatExecutionKPIAggBuckets(buckets); +} + export function formatExecutionLogResult( results: AggregateEventsBySavedObjectResult ): IExecutionLogResult { diff --git a/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.test.ts b/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.test.ts new file mode 100644 index 000000000000..89b21547c892 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.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 { httpServiceMock } from '@kbn/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { rulesClientMock } from '../rules_client.mock'; +import { getGlobalExecutionKPIRoute } from './get_global_execution_kpi'; + +const rulesClient = rulesClientMock.create(); +jest.mock('../lib/license_api_access', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getGlobalExecutionKPIRoute', () => { + const dateString = new Date().toISOString(); + const mockedExecutionLog = { + success: 3, + unknown: 0, + failure: 0, + activeAlerts: 5, + newAlerts: 5, + recoveredAlerts: 0, + erroredActions: 0, + triggeredActions: 5, + }; + + it('gets global execution KPI', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getGlobalExecutionKPIRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/_global_execution_kpi"`); + + rulesClient.getGlobalExecutionKpiWithAuth.mockResolvedValue(mockedExecutionLog); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + query: { + date_start: dateString, + }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(rulesClient.getGlobalExecutionKpiWithAuth).toHaveBeenCalledTimes(1); + expect(rulesClient.getGlobalExecutionKpiWithAuth.mock.calls[0]).toEqual([ + { + dateStart: dateString, + }, + ]); + + expect(res.ok).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.ts b/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.ts new file mode 100644 index 000000000000..29937cc3d8c9 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { IRouter } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; +import { RewriteRequestCase, verifyAccessAndContext } from './lib'; +import { GetGlobalExecutionKPIParams } from '../rules_client'; +import { ILicenseState } from '../lib'; + +const querySchema = schema.object({ + date_start: schema.string(), + date_end: schema.maybe(schema.string()), + filter: schema.maybe(schema.string()), +}); + +const rewriteReq: RewriteRequestCase = ({ + date_start: dateStart, + date_end: dateEnd, + ...rest +}) => ({ + ...rest, + dateStart, + dateEnd, +}); + +export const getGlobalExecutionKPIRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/_global_execution_kpi`, + validate: { + query: querySchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const rulesClient = (await context.alerting).getRulesClient(); + return res.ok({ + body: await rulesClient.getGlobalExecutionKpiWithAuth(rewriteReq(req.query)), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.test.ts new file mode 100644 index 000000000000..db5033404788 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '@kbn/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { rulesClientMock } from '../rules_client.mock'; +import { getRuleExecutionKPIRoute } from './get_rule_execution_kpi'; + +const rulesClient = rulesClientMock.create(); +jest.mock('../lib/license_api_access', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getRuleExecutionKPIRoute', () => { + const dateString = new Date().toISOString(); + const mockedExecutionLog = { + success: 3, + unknown: 0, + failure: 0, + activeAlerts: 5, + newAlerts: 5, + recoveredAlerts: 0, + erroredActions: 0, + triggeredActions: 5, + }; + + it('gets rule execution KPI', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getRuleExecutionKPIRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_execution_kpi"`); + + rulesClient.getRuleExecutionKPI.mockResolvedValue(mockedExecutionLog); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { + id: '1', + }, + query: { + date_start: dateString, + }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(rulesClient.getRuleExecutionKPI).toHaveBeenCalledTimes(1); + expect(rulesClient.getRuleExecutionKPI.mock.calls[0]).toEqual([ + { + dateStart: dateString, + id: '1', + }, + ]); + + expect(res.ok).toHaveBeenCalled(); + }); + + it('returns NOT-FOUND when rule is not found', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getRuleExecutionKPIRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + rulesClient.getRuleExecutionKPI = jest + .fn() + .mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1')); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { + id: '1', + }, + query: {}, + }, + ['notFound'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot( + `[Error: Saved object [alert/1] not found]` + ); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.ts b/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.ts new file mode 100644 index 000000000000..11f7085c5329 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { IRouter } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; +import { RewriteRequestCase, verifyAccessAndContext } from './lib'; +import { GetRuleExecutionKPIParams } from '../rules_client'; +import { ILicenseState } from '../lib'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const querySchema = schema.object({ + date_start: schema.string(), + date_end: schema.maybe(schema.string()), + filter: schema.maybe(schema.string()), +}); + +const rewriteReq: RewriteRequestCase = ({ + date_start: dateStart, + date_end: dateEnd, + ...rest +}) => ({ + ...rest, + dateStart, + dateEnd, +}); + +export const getRuleExecutionKPIRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_execution_kpi`, + validate: { + params: paramSchema, + query: querySchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const rulesClient = (await context.alerting).getRulesClient(); + const { id } = req.params; + return res.ok({ + body: await rulesClient.getRuleExecutionKPI(rewriteReq({ id, ...req.query })), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index dca2e214d078..de9e2f112d9e 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -22,7 +22,9 @@ import { findRulesRoute, findInternalRulesRoute } from './find_rules'; import { getRuleAlertSummaryRoute } from './get_rule_alert_summary'; import { getRuleExecutionLogRoute } from './get_rule_execution_log'; import { getGlobalExecutionLogRoute } from './get_global_execution_logs'; +import { getGlobalExecutionKPIRoute } from './get_global_execution_kpi'; import { getActionErrorLogRoute } from './get_action_error_log'; +import { getRuleExecutionKPIRoute } from './get_rule_execution_kpi'; import { getRuleStateRoute } from './get_rule_state'; import { healthRoute } from './health'; import { resolveRuleRoute } from './resolve_rule'; @@ -63,6 +65,8 @@ export function defineRoutes(opts: RouteOptions) { getRuleExecutionLogRoute(router, licenseState); getGlobalExecutionLogRoute(router, licenseState); getActionErrorLogRoute(router, licenseState); + getRuleExecutionKPIRoute(router, licenseState); + getGlobalExecutionKPIRoute(router, licenseState); getRuleStateRoute(router, licenseState); healthRoute(router, licenseState, encryptedSavedObjects); ruleTypesRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index 7333ad59bbb7..2092b98e48c0 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -30,6 +30,8 @@ const createRulesClientMock = () => { listAlertTypes: jest.fn(), getAlertSummary: jest.fn(), getExecutionLogForRule: jest.fn(), + getRuleExecutionKPI: jest.fn(), + getGlobalExecutionKpiWithAuth: jest.fn(), getGlobalExecutionLogWithAuth: jest.fn(), getActionErrorLog: jest.fn(), getSpaceId: jest.fn(), diff --git a/x-pack/plugins/alerting/server/rules_client/audit_events.ts b/x-pack/plugins/alerting/server/rules_client/audit_events.ts index c29bc99aa6ed..30b759c895c4 100644 --- a/x-pack/plugins/alerting/server/rules_client/audit_events.ts +++ b/x-pack/plugins/alerting/server/rules_client/audit_events.ts @@ -26,7 +26,9 @@ export enum RuleAuditAction { BULK_EDIT = 'rule_bulk_edit', GET_EXECUTION_LOG = 'rule_get_execution_log', GET_GLOBAL_EXECUTION_LOG = 'rule_get_global_execution_log', + GET_GLOBAL_EXECUTION_KPI = 'rule_get_global_execution_kpi', GET_ACTION_ERROR_LOG = 'rule_get_action_error_log', + GET_RULE_EXECUTION_KPI = 'rule_get_execution_kpi', SNOOZE = 'rule_snooze', UNSNOOZE = 'rule_unsnooze', RUN_SOON = 'rule_run_soon', @@ -68,6 +70,16 @@ const eventVerbs: Record = { rule_snooze: ['snooze', 'snoozing', 'snoozed'], rule_unsnooze: ['unsnooze', 'unsnoozing', 'unsnoozed'], rule_run_soon: ['run', 'running', 'ran'], + rule_get_execution_kpi: [ + 'access execution KPI for', + 'accessing execution KPI for', + 'accessed execution KPI for', + ], + rule_get_global_execution_kpi: [ + 'access global execution KPI for', + 'accessing global execution KPI for', + 'accessed global execution KPI for', + ], }; const eventTypes: Record = { @@ -92,6 +104,8 @@ const eventTypes: Record = { rule_snooze: 'change', rule_unsnooze: 'change', rule_run_soon: 'access', + rule_get_execution_kpi: 'access', + rule_get_global_execution_kpi: 'access', }; export interface RuleAuditEventParams { diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index ed330dbd22e0..89ce20b59ae9 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -22,7 +22,7 @@ import { } from 'lodash'; import { i18n } from '@kbn/i18n'; import { AlertConsumers } from '@kbn/rule-data-utils'; -import { fromKueryExpression, KueryNode, nodeBuilder } from '@kbn/es-query'; +import { KueryNode, nodeBuilder } from '@kbn/es-query'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Logger, @@ -118,7 +118,9 @@ import { import { AlertingRulesConfig } from '../config'; import { formatExecutionLogResult, + formatExecutionKPIResult, getExecutionLogAggregation, + getExecutionKPIAggregation, } from '../lib/get_execution_log_aggregation'; import { IExecutionLogResult, IExecutionErrorsResult } from '../../common'; import { validateSnoozeStartDate } from '../lib/validate_snooze_date'; @@ -381,6 +383,19 @@ export interface GetExecutionLogByIdParams { sort: estypes.Sort; } +export interface GetRuleExecutionKPIParams { + id: string; + dateStart: string; + dateEnd?: string; + filter?: string; +} + +export interface GetGlobalExecutionKPIParams { + dateStart: string; + dateEnd?: string; + filter?: string; +} + export interface GetGlobalExecutionLogParams { dateStart: string; dateEnd?: string; @@ -1039,6 +1054,125 @@ export class RulesClient { } } + public async getGlobalExecutionKpiWithAuth({ + dateStart, + dateEnd, + filter, + }: GetGlobalExecutionKPIParams) { + this.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`); + + let authorizationTuple; + try { + authorizationTuple = await this.authorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Alert, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'kibana.alert.rule.rule_type_id', + consumer: 'kibana.alert.rule.consumer', + }, + } + ); + } catch (error) { + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI, + }) + ); + + const dateNow = new Date(); + const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); + const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); + + const eventLogClient = await this.getEventLogClient(); + + try { + const aggResult = await eventLogClient.aggregateEventsWithAuthFilter( + 'alert', + authorizationTuple.filter as KueryNode, + { + start: parsedDateStart.toISOString(), + end: parsedDateEnd.toISOString(), + aggs: getExecutionKPIAggregation(filter), + } + ); + + return formatExecutionKPIResult(aggResult); + } catch (err) { + this.logger.debug( + `rulesClient.getGlobalExecutionKpiWithAuth(): error searching global execution KPI: ${err.message}` + ); + throw err; + } + } + + public async getRuleExecutionKPI({ id, dateStart, dateEnd, filter }: GetRuleExecutionKPIParams) { + this.logger.debug(`getRuleExecutionKPI(): getting execution KPI for rule ${id}`); + const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; + + try { + // Make sure user has access to this rule + await this.authorization.ensureAuthorized({ + ruleTypeId: rule.alertTypeId, + consumer: rule.consumer, + operation: ReadOperations.GetRuleExecutionKPI, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_RULE_EXECUTION_KPI, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_RULE_EXECUTION_KPI, + savedObject: { type: 'alert', id }, + }) + ); + + // default duration of instance summary is 60 * rule interval + const dateNow = new Date(); + const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); + const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); + + const eventLogClient = await this.getEventLogClient(); + + try { + const aggResult = await eventLogClient.aggregateEventsBySavedObjectIds( + 'alert', + [id], + { + start: parsedDateStart.toISOString(), + end: parsedDateEnd.toISOString(), + aggs: getExecutionKPIAggregation(filter), + }, + rule.legacyId !== null ? [rule.legacyId] : undefined + ); + + return formatExecutionKPIResult(aggResult); + } catch (err) { + this.logger.debug( + `rulesClient.getRuleExecutionKPI(): error searching execution KPI for rule ${id}: ${err.message}` + ); + throw err; + } + } + public async find({ options: { fields, ...options } = {}, excludeFromPublicApi = false, @@ -1110,7 +1244,7 @@ export class RulesClient { filter: (authorizationFilter && filterKueryNode ? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode]) - : authorizationFilter) ?? options.filter, + : authorizationFilter) ?? filterKueryNode, fields: fields ? this.includeFieldsRequiredForAuthentication(fields) : fields, type: 'alert', }); @@ -1569,14 +1703,7 @@ export class RulesClient { ); } - let qNodeQueryFilter: null | KueryNode; - if (!queryFilter) { - qNodeQueryFilter = null; - } else if (typeof queryFilter === 'string') { - qNodeQueryFilter = fromKueryExpression(queryFilter); - } else { - qNodeQueryFilter = queryFilter; - } + const qNodeQueryFilter = buildKueryNodeFilter(queryFilter); const qNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : qNodeQueryFilter; let authorizationTuple; @@ -1797,7 +1924,7 @@ export class RulesClient { break; } if (operation.operation === 'set') { - const snoozeAttributes = getSnoozeAttributes(attributes, operation.value); + const snoozeAttributes = getBulkSnoozeAttributes(attributes, operation.value); try { verifySnoozeScheduleLimit(snoozeAttributes); } catch (error) { @@ -1819,7 +1946,7 @@ export class RulesClient { } attributes = { ...attributes, - ...getUnsnoozeAttributes(attributes, idsToDelete), + ...getBulkUnsnoozeAttributes(attributes, idsToDelete), }; } break; @@ -2277,7 +2404,7 @@ export class RulesClient { const recoveredAlertInstanceIds = Object.keys(recoveredAlertInstances); for (const instanceId of recoveredAlertInstanceIds) { - const { group: actionGroup, subgroup: actionSubgroup } = + const { group: actionGroup } = recoveredAlertInstances[instanceId].getLastScheduledActions() ?? {}; const instanceState = recoveredAlertInstances[instanceId].getState(); const message = `instance '${instanceId}' has recovered due to the rule was disabled`; @@ -2292,7 +2419,6 @@ export class RulesClient { message, state: instanceState, group: actionGroup, - subgroup: actionSubgroup, namespace: this.namespace, spaceId: this.spaceId, savedObjects: [ @@ -3254,6 +3380,33 @@ function getSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSche }; } +function getBulkSnoozeAttributes(attributes: RawRule, snoozeSchedule: RuleSnoozeSchedule) { + // If duration is -1, instead mute all + const { id: snoozeId, duration } = snoozeSchedule; + + if (duration === -1) { + return { + muteAll: true, + snoozeSchedule: clearUnscheduledSnooze(attributes), + }; + } + + // Bulk adding snooze schedule, don't touch the existing snooze/indefinite snooze + if (snoozeId) { + const existingSnoozeSchedules = attributes.snoozeSchedule || []; + return { + muteAll: attributes.muteAll, + snoozeSchedule: [...existingSnoozeSchedules, snoozeSchedule], + }; + } + + // Bulk snoozing, don't touch the existing snooze schedules + return { + muteAll: false, + snoozeSchedule: [...clearUnscheduledSnooze(attributes), snoozeSchedule], + }; +} + function getUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) { const snoozeSchedule = scheduleIds ? clearScheduledSnoozesById(attributes, scheduleIds) @@ -3265,6 +3418,27 @@ function getUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) { }; } +function getBulkUnsnoozeAttributes(attributes: RawRule, scheduleIds?: string[]) { + // Bulk removing snooze schedules, don't touch the current snooze/indefinite snooze + if (scheduleIds) { + const newSchedules = clearScheduledSnoozesById(attributes, scheduleIds); + // Unscheduled snooze is also known as snooze now + const unscheduledSnooze = + attributes.snoozeSchedule?.filter((s) => typeof s.id === 'undefined') || []; + + return { + snoozeSchedule: [...unscheduledSnooze, ...newSchedules], + muteAll: attributes.muteAll, + }; + } + + // Bulk unsnoozing, don't touch current snooze schedules that are NOT active + return { + snoozeSchedule: clearCurrentActiveSnooze(attributes), + muteAll: false, + }; +} + function clearUnscheduledSnooze(attributes: RawRule) { // Clear any snoozes that have no ID property. These are "simple" snoozes created with the quick UI, e.g. snooze for 3 days starting now return attributes.snoozeSchedule diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts index 1cafcd652349..5e440d2e6b6d 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts @@ -26,9 +26,11 @@ jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation })); jest.mock('../../lib/snooze/is_snooze_active', () => ({ - isSnoozeActive: jest.fn(() => true), + isSnoozeActive: jest.fn(), })); +const { isSnoozeActive } = jest.requireMock('../../lib/snooze/is_snooze_active'); + const taskManager = taskManagerMock.createStart(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); @@ -310,9 +312,13 @@ describe('bulkEdit()', () => { }); describe('snoozeSchedule operations', () => { - const getSnoozeSchedule = () => { + afterEach(() => { + isSnoozeActive.mockImplementation(() => false); + }); + + const getSnoozeSchedule = (useId: boolean = true) => { return { - id: uuid.v4(), + ...(useId && { id: uuid.v4() }), duration: 28800000, rRule: { dtstart: '2010-09-19T11:49:59.329Z', @@ -321,8 +327,10 @@ describe('bulkEdit()', () => { }, }; }; - test('should add snooze', async () => { - unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const getMockAttribute = (override: Record = {}) => { + return { saved_objects: [ { id: '1', @@ -339,15 +347,137 @@ describe('bulkEdit()', () => { notifyWhen: null, actions: [], snoozeSchedule: [], + ...override, }, references: [], version: '123', }, ], + }; + }; + + test('should snooze', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue(getMockAttribute()); + const snoozePayload = getSnoozeSchedule(false); + await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + operation: 'set', + field: 'snoozeSchedule', + value: snoozePayload, + }, + ], + }); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ + id: '1', + type: 'alert', + attributes: expect.objectContaining({ + snoozeSchedule: [snoozePayload], + }), + }), + ], + { overwrite: true } + ); + }); + + test('should add snooze schedule', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue(getMockAttribute()); + + const snoozePayload = getSnoozeSchedule(); + await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + operation: 'set', + field: 'snoozeSchedule', + value: snoozePayload, + }, + ], + }); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ + id: '1', + type: 'alert', + attributes: expect.objectContaining({ + snoozeSchedule: [snoozePayload], + }), + }), + ], + { overwrite: true } + ); + }); + + test('should not unsnooze a snoozed rule when bulk adding snooze schedules', async () => { + const existingSnooze = [getSnoozeSchedule(false), getSnoozeSchedule()]; + + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, + attributes: { + ...existingDecryptedRule.attributes, + snoozeSchedule: existingSnooze, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + }, + ], }); + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue(getMockAttribute()); + const snoozePayload = getSnoozeSchedule(); + await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + operation: 'set', + field: 'snoozeSchedule', + value: snoozePayload, + }, + ], + }); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ + id: '1', + type: 'alert', + attributes: expect.objectContaining({ + snoozeSchedule: [...existingSnooze, snoozePayload], + }), + }), + ], + { overwrite: true } + ); + }); + + test('should not unsnooze an indefinitely snoozed rule when bulk adding snooze schedules', async () => { + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, + attributes: { + ...existingDecryptedRule.attributes, + muteAll: true, + snoozeSchedule: [], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + }, + ], + }); + + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue(getMockAttribute()); + + const snoozePayload = getSnoozeSchedule(); await rulesClient.bulkEdit({ filter: '', operations: [ @@ -366,6 +496,7 @@ describe('bulkEdit()', () => { id: '1', type: 'alert', attributes: expect.objectContaining({ + muteAll: true, snoozeSchedule: [snoozePayload], }), }), @@ -374,39 +505,74 @@ describe('bulkEdit()', () => { ); }); - test('should delete snooze', async () => { - const existingSnooze = [getSnoozeSchedule(), getSnoozeSchedule()]; + test('should unsnooze', async () => { + const existingSnooze = [getSnoozeSchedule(false), getSnoozeSchedule(), getSnoozeSchedule()]; - unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + mockCreatePointInTimeFinderAsInternalUser({ saved_objects: [ { + ...existingDecryptedRule, + attributes: { + ...existingDecryptedRule.attributes, + snoozeSchedule: existingSnooze, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + }, + ], + }); + + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue(getMockAttribute()); + + await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + operation: 'delete', + field: 'snoozeSchedule', + }, + ], + }); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ id: '1', type: 'alert', + attributes: expect.objectContaining({ + snoozeSchedule: [existingSnooze[1], existingSnooze[2]], + }), + }), + ], + { overwrite: true } + ); + }); + + test('should remove snooze schedules', async () => { + const existingSnooze = [getSnoozeSchedule(), getSnoozeSchedule()]; + + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, attributes: { - enabled: true, - tags: ['foo', 'test-1'], - alertTypeId: 'myType', - schedule: { interval: '1m' }, - consumer: 'myApp', - scheduledTaskId: 'task-123', - params: {}, - throttle: null, - notifyWhen: null, - actions: [], + ...existingDecryptedRule.attributes, snoozeSchedule: existingSnooze, - }, - references: [], - version: '123', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, }, ], }); + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue(getMockAttribute()); + await rulesClient.bulkEdit({ filter: '', operations: [ { operation: 'delete', field: 'snoozeSchedule', + value: [], }, ], }); @@ -426,14 +592,8 @@ describe('bulkEdit()', () => { ); }); - test('should error if adding snooze schedule to rule with 5 schedules', async () => { - const existingSnooze = [ - getSnoozeSchedule(), - getSnoozeSchedule(), - getSnoozeSchedule(), - getSnoozeSchedule(), - getSnoozeSchedule(), - ]; + test('should not unsnooze rule when removing snooze schedules', async () => { + const existingSnooze = [getSnoozeSchedule(false), getSnoozeSchedule(), getSnoozeSchedule()]; mockCreatePointInTimeFinderAsInternalUser({ saved_objects: [ @@ -448,30 +608,58 @@ describe('bulkEdit()', () => { ], }); - unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: [ + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue(getMockAttribute()); + + await rulesClient.bulkEdit({ + filter: '', + operations: [ { + operation: 'delete', + field: 'snoozeSchedule', + value: [], + }, + ], + }); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ id: '1', type: 'alert', + attributes: expect.objectContaining({ + snoozeSchedule: [existingSnooze[0]], + }), + }), + ], + { overwrite: true } + ); + }); + + test('should error if adding snooze schedule to rule with 5 schedules', async () => { + const existingSnooze = [ + getSnoozeSchedule(), + getSnoozeSchedule(), + getSnoozeSchedule(), + getSnoozeSchedule(), + getSnoozeSchedule(), + ]; + + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, attributes: { - enabled: true, - tags: ['foo', 'test-1'], - alertTypeId: 'myType', - schedule: { interval: '1m' }, - consumer: 'myApp', - scheduledTaskId: 'task-123', - params: {}, - throttle: null, - notifyWhen: null, - actions: [], + ...existingDecryptedRule.attributes, snoozeSchedule: existingSnooze, - }, - references: [], - version: '123', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, }, ], }); + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue(getMockAttribute()); + const snoozePayload = getSnoozeSchedule(); const response = await rulesClient.bulkEdit({ @@ -501,29 +689,7 @@ describe('bulkEdit()', () => { ], }); - unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ - saved_objects: [ - { - id: '1', - type: 'alert', - attributes: { - enabled: true, - tags: ['foo', 'test-1'], - alertTypeId: 'myType', - schedule: { interval: '1m' }, - consumer: 'myApp', - scheduledTaskId: 'task-123', - params: {}, - throttle: null, - notifyWhen: null, - actions: [], - snoozeSchedule: [], - }, - references: [], - version: '123', - }, - ], - }); + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue(getMockAttribute()); const snoozePayload = getSnoozeSchedule(); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index cca7cd0d7032..558d33ecca87 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -250,7 +250,6 @@ describe('disable()', () => { meta: { lastScheduledActions: { group: 'default', - subgroup: 'newSubgroup', date: new Date().toISOString(), }, }, @@ -319,7 +318,6 @@ describe('disable()', () => { }, alerting: { action_group_id: 'default', - action_subgroup: 'newSubgroup', instance_id: '1', }, saved_objects: [ diff --git a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts index e26004de99a3..4cfa13f69b84 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts @@ -176,7 +176,7 @@ describe('find()', () => { Array [ Object { "fields": undefined, - "filter": undefined, + "filter": null, "sortField": undefined, "type": "alert", }, @@ -277,7 +277,7 @@ describe('find()', () => { Array [ Object { "fields": undefined, - "filter": undefined, + "filter": null, "sortField": undefined, "type": "alert", }, @@ -730,6 +730,8 @@ describe('find()', () => { expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({ fields: ['tags', 'alertTypeId', 'consumer'], + filter: null, + sortField: undefined, type: 'alert', }); expect(ensureRuleTypeIsAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'rule'); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts index 6dcd64565c56..4aa7ae40f878 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts @@ -156,21 +156,18 @@ describe('getAlertSummary()', () => { "alerts": Object { "alert-currently-active": Object { "actionGroupId": "action group A", - "actionSubgroup": undefined, "activeStartDate": "2019-02-12T21:01:22.479Z", "muted": false, "status": "Active", }, "alert-muted-no-activity": Object { "actionGroupId": undefined, - "actionSubgroup": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", }, "alert-previously-active": Object { "actionGroupId": undefined, - "actionSubgroup": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 7146ef4a5225..51ba1404b2a4 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -65,7 +65,6 @@ export function createExecutionHandler< return async ({ actionGroup, - actionSubgroup, context, state, ruleRunMetricsStore, @@ -92,7 +91,6 @@ export function createExecutionHandler< alertInstanceId: alertId, alertActionGroup: actionGroup, alertActionGroupName: ruleTypeActionGroups.get(actionGroup)!, - alertActionSubgroup: actionSubgroup, context, actionParams: action.params, actionId: action.id, @@ -203,7 +201,6 @@ export function createExecutionHandler< typeId: actionTypeId, alertId, alertGroup: actionGroup, - alertSubgroup: actionSubgroup, }); } diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index 39b90efbc786..0a90456dbdad 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -178,7 +178,7 @@ export const mockTaskInstance = () => ({ ownerId: null, }); -export const generateAlertOpts = ({ action, group, subgroup, state, id }: GeneratorParams = {}) => { +export const generateAlertOpts = ({ action, group, state, id }: GeneratorParams = {}) => { id = id ?? '1'; let message: string = ''; switch (action) { @@ -186,9 +186,7 @@ export const generateAlertOpts = ({ action, group, subgroup, state, id }: Genera message = `test:1: 'rule-name' created new alert: '${id}'`; break; case EVENT_LOG_ACTIONS.activeInstance: - message = subgroup - ? `test:1: 'rule-name' active alert: '${id}' in actionGroup(subgroup): 'default(${subgroup})'` - : `test:1: 'rule-name' active alert: '${id}' in actionGroup: 'default'`; + message = `test:1: 'rule-name' active alert: '${id}' in actionGroup: 'default'`; break; case EVENT_LOG_ACTIONS.recoveredInstance: message = `test:1: 'rule-name' alert '${id}' has recovered`; @@ -200,21 +198,14 @@ export const generateAlertOpts = ({ action, group, subgroup, state, id }: Genera message, state, ...(group ? { group } : {}), - ...(subgroup ? { subgroup } : {}), }; }; -export const generateActionOpts = ({ - subgroup, - id, - alertGroup, - alertId, -}: GeneratorParams = {}) => ({ +export const generateActionOpts = ({ id, alertGroup, alertId }: GeneratorParams = {}) => ({ id: id ?? '1', typeId: 'action', alertId: alertId ?? '1', alertGroup: alertGroup ?? 'default', - ...(subgroup ? { alertSubgroup: subgroup } : {}), }); export const generateRunnerResult = ({ @@ -280,7 +271,6 @@ export const generateAlertInstance = ({ id, duration, start }: GeneratorParams = lastScheduledActions: { date: new Date(DATE_1970), group: 'default', - subgroup: undefined, }, }, state: { diff --git a/x-pack/plugins/alerting/server/task_runner/log_alerts.ts b/x-pack/plugins/alerting/server/task_runner/log_alerts.ts index 87da1fb67f33..2abe72ed06cb 100644 --- a/x-pack/plugins/alerting/server/task_runner/log_alerts.ts +++ b/x-pack/plugins/alerting/server/task_runner/log_alerts.ts @@ -92,8 +92,7 @@ export function logAlerts< ruleRunMetricsStore.setNumberOfRecoveredAlerts(recoveredAlertIds.length); for (const id of recoveredAlertIds) { - const { group: actionGroup, subgroup: actionSubgroup } = - recoveredAlerts[id].getLastScheduledActions() ?? {}; + const { group: actionGroup } = recoveredAlerts[id].getLastScheduledActions() ?? {}; const state = recoveredAlerts[id].getState(); const message = `${ruleLogPrefix} alert '${id}' has recovered`; @@ -101,41 +100,32 @@ export function logAlerts< action: EVENT_LOG_ACTIONS.recoveredInstance, id, group: actionGroup, - subgroup: actionSubgroup, message, state, }); } for (const id of newAlertIds) { - const { actionGroup, subgroup: actionSubgroup } = - activeAlerts[id].getScheduledActionOptions() ?? {}; + const { actionGroup } = activeAlerts[id].getScheduledActionOptions() ?? {}; const state = activeAlerts[id].getState(); const message = `${ruleLogPrefix} created new alert: '${id}'`; alertingEventLogger.logAlert({ action: EVENT_LOG_ACTIONS.newInstance, id, group: actionGroup, - subgroup: actionSubgroup, message, state, }); } for (const id of activeAlertIds) { - const { actionGroup, subgroup: actionSubgroup } = - activeAlerts[id].getScheduledActionOptions() ?? {}; + const { actionGroup } = activeAlerts[id].getScheduledActionOptions() ?? {}; const state = activeAlerts[id].getState(); - const message = `${ruleLogPrefix} active alert: '${id}' in ${ - actionSubgroup - ? `actionGroup(subgroup): '${actionGroup}(${actionSubgroup})'` - : `actionGroup: '${actionGroup}'` - }`; + const message = `${ruleLogPrefix} active alert: '${id}' in actionGroup: '${actionGroup}'`; alertingEventLogger.logAlert({ action: EVENT_LOG_ACTIONS.activeInstance, id, group: actionGroup, - subgroup: actionSubgroup, message, state, }); diff --git a/x-pack/plugins/alerting/server/task_runner/schedule_actions_for_alerts.ts b/x-pack/plugins/alerting/server/task_runner/schedule_actions_for_alerts.ts index e0cc6d5b81c3..8c1d62ecf4e3 100644 --- a/x-pack/plugins/alerting/server/task_runner/schedule_actions_for_alerts.ts +++ b/x-pack/plugins/alerting/server/task_runner/schedule_actions_for_alerts.ts @@ -49,16 +49,8 @@ export async function scheduleActionsForAlerts< notifyWhen ); if (executeAction && alert.hasScheduledActions()) { - const { actionGroup, subgroup: actionSubgroup, state } = alert.getScheduledActionOptions()!; - await executeAlert( - alertId, - alert, - executionHandler, - ruleRunMetricsStore, - actionGroup, - state, - actionSubgroup - ); + const { actionGroup, state } = alert.getScheduledActionOptions()!; + await executeAlert(alertId, alert, executionHandler, ruleRunMetricsStore, actionGroup, state); } } @@ -94,14 +86,12 @@ async function executeAlert< executionHandler: ExecutionHandler, ruleRunMetricsStore: RuleRunMetricsStore, actionGroup: ActionGroupIds | RecoveryActionGroupId, - state: InstanceState, - actionSubgroup?: string + state: InstanceState ) { - alert.updateLastScheduledActions(actionGroup, actionSubgroup); + alert.updateLastScheduledActions(actionGroup); alert.unscheduleActions(); return executionHandler({ actionGroup, - actionSubgroup, context: alert.getContext(), state, alertId, @@ -133,10 +123,7 @@ function shouldExecuteAction< muted ? 'muted' : 'throttled' }` ); - } else if ( - notifyWhen === 'onActionGroupChange' && - !alert.scheduledActionGroupOrSubgroupHasChanged() - ) { + } else if (notifyWhen === 'onActionGroupChange' && !alert.scheduledActionGroupHasChanged()) { executeAction = false; logger.debug( `skipping scheduling of actions for '${alertId}' in rule ${ruleLabel}: alert is active but action group has not changed` diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 4ce85f54c3dc..a199fb3b5599 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -312,9 +312,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertFactory - .create('1') - .scheduleActionsWithSubGroup('default', 'subDefault'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -360,7 +358,6 @@ describe('Task Runner', () => { generateAlertOpts({ action: EVENT_LOG_ACTIONS.newInstance, group: 'default', - subgroup: 'subDefault', state: { start: DATE_1970, duration: '0' }, }) ); @@ -369,14 +366,10 @@ describe('Task Runner', () => { generateAlertOpts({ action: EVENT_LOG_ACTIONS.activeInstance, group: 'default', - subgroup: 'subDefault', state: { start: DATE_1970, duration: '0' }, }) ); - expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith( - 1, - generateActionOpts({ subgroup: 'subDefault' }) - ); + expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith(1, generateActionOpts()); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); } @@ -859,90 +852,6 @@ describe('Task Runner', () => { } ); - test.each(ephemeralTestParams)( - 'actionsPlugin.execute is called when notifyWhen=onActionGroupChange and alert state subgroup has changed %s', - async (nameExtension, customTaskRunnerFactoryInitializerParams, enqueueFunction) => { - customTaskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue( - true - ); - - customTaskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue( - true - ); - ruleType.executor.mockImplementation( - async ({ - services: executorServices, - }: RuleExecutorOptions< - RuleTypeParams, - RuleTypeState, - AlertInstanceState, - AlertInstanceContext, - string - >) => { - executorServices.alertFactory - .create('1') - .scheduleActionsWithSubGroup('default', 'subgroup1'); - } - ); - const taskRunner = new TaskRunner( - ruleType, - { - ...mockedTaskInstance, - state: { - ...mockedTaskInstance.state, - alertInstances: { - '1': { - meta: { - lastScheduledActions: { - group: 'default', - subgroup: 'newSubgroup', - date: new Date().toISOString(), - }, - }, - state: { bar: false }, - }, - }, - }, - }, - customTaskRunnerFactoryInitializerParams, - inMemoryMetrics - ); - expect(AlertingEventLogger).toHaveBeenCalled(); - - rulesClient.get.mockResolvedValue({ - ...mockedRuleTypeSavedObject, - notifyWhen: 'onActionGroupChange', - }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); - await taskRunner.run(); - - testAlertingEventLogCalls({ - activeAlerts: 1, - triggeredActions: 1, - generatedActions: 1, - status: 'active', - logAlert: 1, - logAction: 1, - }); - expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( - 1, - generateAlertOpts({ - action: EVENT_LOG_ACTIONS.activeInstance, - state: { bar: false }, - group: 'default', - subgroup: 'subgroup1', - }) - ); - expect(alertingEventLogger.logAction).toHaveBeenNthCalledWith( - 1, - generateActionOpts({ subgroup: 'subgroup1' }) - ); - - expect(enqueueFunction).toHaveBeenCalledTimes(1); - expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); - } - ); - test.each(ephemeralTestParams)( 'includes the apiKey in the request used to initialize the actionsClient %s', async ( diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 457b2872faa6..41029af56752 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -420,7 +420,6 @@ export class TaskRunner< checkHasReachedAlertLimit(); this.alertingEventLogger.setExecutionSucceeded(`rule executed: ${ruleLabel}`); - ruleRunMetricsStore.setSearchMetrics([ wrappedScopedClusterClient.getMetrics(), wrappedSearchSourceClient.getMetrics(), diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts index 6235e630ba0b..ababe16dea37 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts @@ -417,7 +417,6 @@ test('rule variables are passed to templates', () => { alertInstanceId: '2', alertActionGroup: 'action-group', alertActionGroupName: 'Action Group', - alertActionSubgroup: 'subgroup', alertParams: {}, }); expect(result).toMatchInlineSnapshot(` @@ -429,8 +428,7 @@ test('rule variables are passed to templates', () => { test('rule alert variables are passed to templates', () => { const actionParams = { - message: - 'Value "{{alert.id}}", "{{alert.actionGroup}}", "{{alert.actionGroupName}}" and "{{alert.actionSubgroup}}" exist', + message: 'Value "{{alert.id}}", "{{alert.actionGroup}}" and "{{alert.actionGroupName}}" exist', }; const result = transformActionParams({ actionsPlugin, @@ -447,12 +445,11 @@ test('rule alert variables are passed to templates', () => { alertInstanceId: '2', alertActionGroup: 'action-group', alertActionGroupName: 'Action Group', - alertActionSubgroup: 'subgroup', alertParams: {}, }); expect(result).toMatchInlineSnapshot(` Object { - "message": "Value \\"2\\", \\"action-group\\", \\"Action Group\\" and \\"subgroup\\" exist", + "message": "Value \\"2\\", \\"action-group\\" and \\"Action Group\\" exist", } `); }); diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts index 322a16c1c238..61ec54c48886 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts @@ -25,7 +25,6 @@ interface TransformActionParamsOptions { alertInstanceId: string; alertActionGroup: string; alertActionGroupName: string; - alertActionSubgroup?: string; actionParams: RuleActionParams; alertParams: RuleTypeParams; state: AlertInstanceState; @@ -44,7 +43,6 @@ export function transformActionParams({ tags, alertInstanceId, alertActionGroup, - alertActionSubgroup, alertActionGroupName, context, actionParams, @@ -63,7 +61,6 @@ export function transformActionParams({ alertInstanceId, alertActionGroup, alertActionGroupName, - alertActionSubgroup, context, date: new Date().toISOString(), state, @@ -80,7 +77,6 @@ export function transformActionParams({ id: alertInstanceId, actionGroup: alertActionGroup, actionGroupName: alertActionGroupName, - actionSubgroup: alertActionSubgroup, }, }; return actionsPlugin.renderActionParameterTemplates( diff --git a/x-pack/plugins/alerting/server/task_runner/types.ts b/x-pack/plugins/alerting/server/task_runner/types.ts index 87bd65b02848..ce439fa3b3b0 100644 --- a/x-pack/plugins/alerting/server/task_runner/types.ts +++ b/x-pack/plugins/alerting/server/task_runner/types.ts @@ -116,7 +116,6 @@ export interface CreateExecutionHandlerOptions< export interface ExecutionHandlerOptions { actionGroup: ActionGroupIds; - actionSubgroup?: string; alertId: string; context: AlertInstanceContext; state: AlertInstanceState; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/storage_explorer/storage_explorer.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/storage_explorer/storage_explorer.cy.ts new file mode 100644 index 000000000000..e989ea5cf0fa --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/storage_explorer/storage_explorer.cy.ts @@ -0,0 +1,192 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import moment from 'moment'; +import url from 'url'; +import { synthtrace } from '../../../../synthtrace'; +import { opbeans } from '../../../fixtures/synthtrace/opbeans'; +import { checkA11y } from '../../../support/commands'; + +const timeRange = { + rangeFrom: '2021-10-10T00:00:00.000Z', + rangeTo: '2021-10-10T00:15:00.000Z', +}; + +const storageExplorerHref = url.format({ + pathname: '/app/apm/storage-explorer', + query: timeRange, +}); + +const mainApiRequestsToIntercept = [ + { + endpoint: '/internal/apm/storage_chart', + aliasName: 'storageChartRequest', + }, + { + endpoint: '/internal/apm/storage_explorer_summary_stats', + aliasName: 'summaryStatsRequest', + }, + { + endpoint: '/internal/apm/storage_explorer', + aliasName: 'storageExlorerRequest', + }, +]; + +const mainAliasNames = mainApiRequestsToIntercept.map( + ({ aliasName }) => `@${aliasName}` +); + +describe('Storage Explorer', () => { + before(() => { + const { rangeFrom, rangeTo } = timeRange; + synthtrace.index( + opbeans({ + from: new Date(rangeFrom).getTime(), + to: new Date(rangeTo).getTime(), + }) + ); + }); + + after(() => { + synthtrace.clean(); + }); + + describe('When navigating to storage explorer without the required permissions', () => { + beforeEach(() => { + cy.loginAsViewerUser(); + cy.visitKibana(storageExplorerHref); + }); + + it('displays a prompt for permissions', () => { + cy.contains('You need permission'); + }); + }); + + describe('When navigating to storage explorer with the required permissions', () => { + beforeEach(() => { + cy.loginAsMonitorUser(); + cy.visitKibana(storageExplorerHref); + }); + + it('has no detectable a11y violations on load', () => { + cy.contains('h1', 'Storage explorer'); + // set skipFailures to true to not fail the test when there are accessibility failures + checkA11y({ skipFailures: true }); + }); + + it('has a list of summary stats', () => { + cy.contains('Total APM size'); + cy.contains('Daily data generation'); + cy.contains('Traces per minute'); + cy.contains('Number of services'); + }); + + it('renders the storage timeseries chart', () => { + cy.get('[data-test-subj="storageExplorerTimeseriesChart"]'); + }); + + it('has a list of services and environments', () => { + cy.contains('opbeans-node'); + cy.contains('opbeans-java'); + cy.contains('opbeans-rum'); + cy.get('td:contains(production)').should('have.length', 3); + }); + + it('when clicking on a service it loads the service overview for that service', () => { + cy.contains('opbeans-node').click({ force: true }); + cy.url().should('include', '/apm/services/opbeans-node/overview'); + cy.contains('h1', 'opbeans-node'); + }); + }); + + describe('Calls APIs', () => { + beforeEach(() => { + mainApiRequestsToIntercept.forEach(({ endpoint, aliasName }) => { + cy.intercept({ pathname: endpoint }).as(aliasName); + }); + + cy.loginAsMonitorUser(); + cy.visitKibana(storageExplorerHref); + }); + + it('with the correct environment when changing the environment', () => { + cy.wait(mainAliasNames); + + cy.get('[data-test-subj="environmentFilter"]').type('production'); + + cy.contains('button', 'production').click({ force: true }); + + cy.expectAPIsToHaveBeenCalledWith({ + apisIntercepted: mainAliasNames, + value: 'environment=production', + }); + }); + + it('when clicking the refresh button', () => { + cy.wait(mainAliasNames); + cy.contains('Refresh').click(); + cy.wait(mainAliasNames); + }); + + it('when selecting a different time range and clicking the update button', () => { + cy.wait(mainAliasNames); + + cy.selectAbsoluteTimeRange( + moment(timeRange.rangeFrom).subtract(5, 'm').toISOString(), + moment(timeRange.rangeTo).subtract(5, 'm').toISOString() + ); + cy.contains('Update').click(); + cy.wait(mainAliasNames); + + cy.contains('Refresh').click(); + cy.wait(mainAliasNames); + }); + + it('with the correct lifecycle phase when changing the lifecycle phase', () => { + cy.wait(mainAliasNames); + + cy.get('[data-test-subj="storageExplorerLifecyclePhaseSelect"]').click(); + cy.contains('button', 'Warm').click(); + + cy.expectAPIsToHaveBeenCalledWith({ + apisIntercepted: mainAliasNames, + value: 'indexLifecyclePhase=warm', + }); + }); + }); + + describe('Storage details per service', () => { + beforeEach(() => { + const apiRequestsToIntercept = [ + ...mainApiRequestsToIntercept, + { + endpoint: '/internal/apm/services/opbeans-node/storage_details', + aliasName: 'storageDetailsRequest', + }, + ]; + + apiRequestsToIntercept.forEach(({ endpoint, aliasName }) => { + cy.intercept({ pathname: endpoint }).as(aliasName); + }); + + cy.loginAsMonitorUser(); + cy.visitKibana(storageExplorerHref); + }); + + it('shows storage details', () => { + cy.wait(mainAliasNames); + cy.contains('opbeans-node'); + + cy.get('[data-test-subj="storageDetailsButton_opbeans-node"]').click(); + cy.get('[data-test-subj="loadingSpinner"]').should('be.visible'); + cy.wait('@storageDetailsRequest'); + + cy.contains('Service storage details'); + cy.get('[data-test-subj="storageExplorerTimeseriesChart"]'); + cy.get('[data-test-subj="serviceStorageDetailsTable"]'); + }); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index 692926d3049c..7830e791c365 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -9,13 +9,21 @@ import { Interception } from 'cypress/types/net-stubbing'; import 'cypress-axe'; import moment from 'moment'; import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/axe-config'; +import { ApmUsername } from '../../../server/test_helpers/create_apm_users/authentication'; Cypress.Commands.add('loginAsViewerUser', () => { - return cy.loginAs({ username: 'viewer', password: 'changeme' }); + return cy.loginAs({ username: ApmUsername.viewerUser, password: 'changeme' }); }); Cypress.Commands.add('loginAsEditorUser', () => { - return cy.loginAs({ username: 'editor', password: 'changeme' }); + return cy.loginAs({ username: ApmUsername.editorUser, password: 'changeme' }); +}); + +Cypress.Commands.add('loginAsMonitorUser', () => { + return cy.loginAs({ + username: ApmUsername.apmMonitorIndices, + password: 'changeme', + }); }); Cypress.Commands.add( diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts index 27720210b668..2235847e584a 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts @@ -9,6 +9,7 @@ declare namespace Cypress { interface Chainable { loginAsViewerUser(): Cypress.Chainable>; loginAsEditorUser(): Cypress.Chainable>; + loginAsMonitorUser(): Cypress.Chainable>; loginAs(params: { username: string; password: string; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts b/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts index 8af466b87262..eba57b28ec0b 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_test_runner.ts @@ -11,7 +11,7 @@ import { esTestConfig } from '@kbn/test'; import { apm, createLogger, LogLevel } from '@kbn/apm-synthtrace'; import path from 'path'; import { FtrProviderContext } from './ftr_provider_context'; -import { createApmUsers } from '../scripts/create_apm_users/create_apm_users'; +import { createApmUsers } from '../server/test_helpers/create_apm_users/create_apm_users'; export async function cypressTestRunner({ getService }: FtrProviderContext) { const config = getService('config'); @@ -26,12 +26,6 @@ export async function cypressTestRunner({ getService }: FtrProviderContext) { const username = config.get('servers.elasticsearch.username'); const password = config.get('servers.elasticsearch.password'); - // Creates APM users - await createApmUsers({ - elasticsearch: { username, password }, - kibana: { hostname: kibanaUrl }, - }); - const esNode = Url.format({ protocol: config.get('servers.elasticsearch.protocol'), port: config.get('servers.elasticsearch.port'), @@ -39,6 +33,12 @@ export async function cypressTestRunner({ getService }: FtrProviderContext) { auth: `${username}:${password}`, }); + // Creates APM users + await createApmUsers({ + elasticsearch: { node: esNode, username, password }, + kibana: { hostname: kibanaUrl }, + }); + const esRequestTimeout = config.get('timeouts.esRequestTimeout'); const kibanaClient = new apm.ApmSynthtraceKibanaClient( createLogger(LogLevel.info) diff --git a/x-pack/plugins/apm/ftr_e2e/tsconfig.json b/x-pack/plugins/apm/ftr_e2e/tsconfig.json index 84a66afe4588..730971a284ed 100644 --- a/x-pack/plugins/apm/ftr_e2e/tsconfig.json +++ b/x-pack/plugins/apm/ftr_e2e/tsconfig.json @@ -18,5 +18,6 @@ "references": [ { "path": "../../../test/tsconfig.json" }, { "path": "../../../../test/tsconfig.json" }, + { "path": "../tsconfig.json" }, ] } diff --git a/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/select_services.tsx b/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/select_services.tsx index 251fa03735f1..7a97583bbd4a 100644 --- a/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/select_services.tsx +++ b/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/select_services.tsx @@ -36,7 +36,7 @@ const CentralizedContainer = styled.div` `; const MAX_CONTAINER_HEIGHT = 600; -const MODAL_HEADER_HEIGHT = 122; +const MODAL_HEADER_HEIGHT = 180; const MODAL_FOOTER_HEIGHT = 80; const suggestedFieldsWhitelist = [ @@ -118,10 +118,73 @@ export function SelectServices({ 'xpack.apm.serviceGroups.selectServicesForm.subtitle', { defaultMessage: - 'Use a query to select services for this group. Services that match this query within the last 24 hours will be assigned to the group.', + 'Use a query to select services for this group. The preview shows services that match this query within the last 24 hours.', } )} + + + { + setKuery(value); + }} + onChange={(value) => { + setStagedKuery(value); + }} + value={kuery} + suggestionFilter={(querySuggestion) => { + if ('field' in querySuggestion) { + const { + field: { + spec: { name: fieldName }, + }, + } = querySuggestion; + + return ( + fieldName.startsWith('label') || + suggestedFieldsWhitelist.includes(fieldName) + ); + } + return true; + }} + /> + + + { + setKuery(stagedKuery); + }} + iconType={!kuery ? 'search' : 'refresh'} + isDisabled={isServiceListPreviewLoading || !stagedKuery} + > + {!kuery + ? i18n.translate( + 'xpack.apm.serviceGroups.selectServicesForm.preview', + { defaultMessage: 'Preview' } + ) + : i18n.translate( + 'xpack.apm.serviceGroups.selectServicesForm.refresh', + { defaultMessage: 'Refresh' } + )} + + + + {kuery && data?.items && ( + + {i18n.translate( + 'xpack.apm.serviceGroups.selectServicesForm.matchingServiceCount', + { + defaultMessage: + '{servicesCount} {servicesCount, plural, =0 {services} one {service} other {services}} match the query', + values: { servicesCount: data?.items.length }, + } + )} + + )} - - - - { - setKuery(value); - }} - onChange={(value) => { - setStagedKuery(value); - }} - value={kuery} - suggestionFilter={(querySuggestion) => { - if ('field' in querySuggestion) { - const { - field: { - spec: { name: fieldName }, - }, - } = querySuggestion; - - return ( - fieldName.startsWith('label') || - suggestedFieldsWhitelist.includes(fieldName) - ); - } - return true; - }} - /> - - - { - setKuery(stagedKuery); - }} - iconType={!kuery ? 'search' : 'refresh'} - isDisabled={isServiceListPreviewLoading || !stagedKuery} - > - {!kuery - ? i18n.translate( - 'xpack.apm.serviceGroups.selectServicesForm.preview', - { defaultMessage: 'Preview' } - ) - : i18n.translate( - 'xpack.apm.serviceGroups.selectServicesForm.refresh', - { defaultMessage: 'Refresh' } - )} - - - - - {kuery && data?.items && ( - - - {i18n.translate( - 'xpack.apm.serviceGroups.selectServicesForm.matchingServiceCount', - { - defaultMessage: - '{servicesCount} {servicesCount, plural, =0 {services} one {service} other {services}} match the query', - values: { servicesCount: data?.items.length }, - } - )} - - - )} {!kuery && ( diff --git a/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/service_list_preview.tsx b/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/service_list_preview.tsx index eb366b84ceef..66105c106805 100644 --- a/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/service_list_preview.tsx +++ b/x-pack/plugins/apm/public/components/app/service_groups/service_group_save/service_list_preview.tsx @@ -37,7 +37,7 @@ type SORT_FIELD = 'serviceName' | 'environments' | 'agentName'; export function ServiceListPreview({ items, isLoading }: Props) { const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); + const [pageSize, setPageSize] = useState(10); const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); const [sortDirection, setSortDirection] = useState( DEFAULT_SORT_DIRECTION diff --git a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx index d0bf3a9f24b7..734298dabe9e 100644 --- a/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_groups/service_groups_list/index.tsx @@ -156,6 +156,12 @@ export function ServiceGroupsList() { + + {i18n.translate('xpack.apm.serviceGroups.listDescription', { + defaultMessage: + 'Displayed service counts reflect the last 24 hours.', + })} + {items.length ? ( diff --git a/x-pack/plugins/apm/public/components/app/storage_explorer/index_lifecycle_phase_select.tsx b/x-pack/plugins/apm/public/components/app/storage_explorer/index_lifecycle_phase_select.tsx index 124579e0562e..10554b4ff827 100644 --- a/x-pack/plugins/apm/public/components/app/storage_explorer/index_lifecycle_phase_select.tsx +++ b/x-pack/plugins/apm/public/components/app/storage_explorer/index_lifecycle_phase_select.tsx @@ -130,6 +130,7 @@ export function IndexLifecyclePhaseSelect() { }} hasDividers style={{ minWidth: 200 }} + data-test-subj="storageExplorerLifecyclePhaseSelect" /> ); } diff --git a/x-pack/plugins/apm/public/components/app/storage_explorer/services_table/storage_details_per_service.tsx b/x-pack/plugins/apm/public/components/app/storage_explorer/services_table/storage_details_per_service.tsx index 3d9cd14c8e95..0a101d335768 100644 --- a/x-pack/plugins/apm/public/components/app/storage_explorer/services_table/storage_details_per_service.tsx +++ b/x-pack/plugins/apm/public/components/app/storage_explorer/services_table/storage_details_per_service.tsx @@ -186,7 +186,10 @@ export function StorageDetailsPerService({ - + - + {processorEventStats.map( ({ processorEventLabel, docs, size }) => ( <> diff --git a/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users_cli.ts b/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users_cli.ts index 194639128b4c..24cda99fd669 100644 --- a/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users_cli.ts +++ b/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users_cli.ts @@ -8,13 +8,17 @@ /* eslint-disable no-console */ import { argv } from 'yargs'; -import { AbortError, isAxiosError } from './helpers/call_kibana'; -import { createApmUsers } from './create_apm_users'; -import { getKibanaVersion } from './helpers/get_version'; +import { + AbortError, + isAxiosError, +} from '../../server/test_helpers/create_apm_users/helpers/call_kibana'; +import { createApmUsers } from '../../server/test_helpers/create_apm_users/create_apm_users'; +import { getKibanaVersion } from '../../server/test_helpers/create_apm_users/helpers/get_version'; async function init() { const esUserName = (argv.username as string) || 'elastic'; const esPassword = argv.password as string | undefined; + const esUrl = argv.esUrl as string | undefined; const kibanaBaseUrl = argv.kibanaUrl as string | undefined; if (!esPassword) { @@ -24,6 +28,20 @@ async function init() { process.exit(); } + if (!esUrl) { + console.error( + 'Please specify the url for elasticsearch: `--es-url http://localhost:9200` ' + ); + process.exit(); + } + + if (!esUrl.startsWith('https://') && !esUrl.startsWith('http://')) { + console.error( + 'Elasticsearch url must be prefixed with http(s):// `--es-url http://localhost:9200`' + ); + process.exit(); + } + if (!kibanaBaseUrl) { console.error( 'Please specify the url for Kibana: `--kibana-url http://localhost:5601` ' @@ -42,7 +60,11 @@ async function init() { } const kibana = { hostname: kibanaBaseUrl }; - const elasticsearch = { username: esUserName, password: esPassword }; + const elasticsearch = { + node: esUrl, + username: esUserName, + password: esPassword, + }; console.log({ kibana, elasticsearch }); @@ -50,9 +72,7 @@ async function init() { console.log(`Connected to Kibana ${version}`); const users = await createApmUsers({ elasticsearch, kibana }); - const credentials = users - .map((u) => ` - ${u.username} / ${esPassword}`) - .join('\n'); + const credentials = users.map((u) => ` - ${u} / ${esPassword}`).join('\n'); console.log( `\nYou can now login to ${kibana.hostname} with:\n${credentials}` diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_infra_metrics_client/create_infra_metrics_client.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_infra_metrics_client/create_infra_metrics_client.ts new file mode 100644 index 000000000000..4cd9df6bf213 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_infra_metrics_client/create_infra_metrics_client.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import { APMRouteHandlerResources } from '../../../../routes/typings'; +import { getInfraMetricIndices } from '../../get_infra_metric_indices'; + +type InfraMetricsSearchParams = Omit; + +export type InfraMetricsClient = ReturnType; + +export function createInfraMetricsClient(resources: APMRouteHandlerResources) { + return { + async search( + opts: TParams + ): Promise> { + const { + savedObjects: { client: savedObjectsClient }, + elasticsearch: { client: esClient }, + } = await resources.context.core; + + const indexName = await getInfraMetricIndices({ + infraPlugin: resources.plugins.infra, + savedObjectsClient, + }); + + const searchParams = { + index: [indexName], + ...opts, + }; + + return esClient.asCurrentUser.search( + searchParams + ) as Promise; + }, + }; +} diff --git a/x-pack/plugins/apm/server/routes/infrastructure/get_host_names.ts b/x-pack/plugins/apm/server/routes/infrastructure/get_host_names.ts index 4890f8ab909e..8f47c2d1664b 100644 --- a/x-pack/plugins/apm/server/routes/infrastructure/get_host_names.ts +++ b/x-pack/plugins/apm/server/routes/infrastructure/get_host_names.ts @@ -5,106 +5,55 @@ * 2.0. */ -import { ElasticsearchClient } from '@kbn/core/server'; import { rangeQuery } from '@kbn/observability-plugin/server'; -import { InfraPluginStart, InfraPluginSetup } from '@kbn/infra-plugin/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { CONTAINER_ID, HOST_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { ApmPluginRequestHandlerContext } from '../typings'; -import { getInfraMetricIndices } from '../../lib/helpers/get_infra_metric_indices'; +import { InfraMetricsClient } from '../../lib/helpers/create_es_client/create_infra_metrics_client/create_infra_metrics_client'; -interface Aggs extends estypes.AggregationsMultiBucketAggregateBase { - buckets: Array<{ - key: string; - key_as_string?: string; - }>; -} - -interface InfraPlugin { - setup: InfraPluginSetup; - start: () => Promise; -} - -const getHostNames = async ({ - esClient, +export async function getContainerHostNames({ containerIds, - index, + infraMetricsClient, start, end, }: { - esClient: ElasticsearchClient; containerIds: string[]; - index: string; + infraMetricsClient: InfraMetricsClient; start: number; end: number; -}) => { - const response = await esClient.search({ - index: [index], - body: { - size: 0, - query: { - bool: { - filter: [ - { - terms: { - [CONTAINER_ID]: containerIds, - }, +}): Promise { + if (!containerIds.length) { + return []; + } + + const response = await infraMetricsClient.search({ + size: 0, + query: { + bool: { + filter: [ + { + terms: { + [CONTAINER_ID]: containerIds, }, - ...rangeQuery(start, end), - ], - }, - }, - aggs: { - hostNames: { - terms: { - field: HOST_NAME, - size: 500, }, + ...rangeQuery(start, end), + ], + }, + }, + aggs: { + hostNames: { + terms: { + field: HOST_NAME, + size: 500, }, }, }, }); - return { - hostNames: - response.aggregations?.hostNames?.buckets.map( - (bucket) => bucket.key as string - ) ?? [], - }; -}; + const hostNames = response.aggregations?.hostNames?.buckets.map( + (bucket) => bucket.key as string + ); -export const getContainerHostNames = async ({ - containerIds, - context, - infra, - start, - end, -}: { - containerIds: string[]; - context: ApmPluginRequestHandlerContext; - infra: InfraPlugin; - start: number; - end: number; -}): Promise => { - if (containerIds.length) { - const esClient = (await context.core).elasticsearch.client.asCurrentUser; - const savedObjectsClient = (await context.core).savedObjects.client; - const metricIndices = await getInfraMetricIndices({ - infraPlugin: infra, - savedObjectsClient, - }); - - const containerHostNames = await getHostNames({ - esClient, - containerIds, - index: metricIndices, - start, - end, - }); - return containerHostNames.hostNames; - } - return []; -}; + return hostNames ?? []; +} diff --git a/x-pack/plugins/apm/server/routes/infrastructure/route.ts b/x-pack/plugins/apm/server/routes/infrastructure/route.ts index 756b29d5d457..678f380bc4fd 100644 --- a/x-pack/plugins/apm/server/routes/infrastructure/route.ts +++ b/x-pack/plugins/apm/server/routes/infrastructure/route.ts @@ -10,6 +10,7 @@ import { setupRequest } from '../../lib/helpers/setup_request'; import { environmentRt, kueryRt, rangeRt } from '../default_api_types'; import { getInfrastructureData } from './get_infrastructure_data'; import { getContainerHostNames } from './get_host_names'; +import { createInfraMetricsClient } from '../../lib/helpers/create_es_client/create_infra_metrics_client/create_infra_metrics_client'; const infrastructureRoute = createApmServerRoute({ endpoint: @@ -29,12 +30,9 @@ const infrastructureRoute = createApmServerRoute({ podNames: string[]; }> => { const setup = await setupRequest(resources); + const infraMetricsClient = createInfraMetricsClient(resources); - const { - context, - params, - plugins: { infra }, - } = resources; + const { params } = resources; const { path: { serviceName }, @@ -54,8 +52,7 @@ const infrastructureRoute = createApmServerRoute({ // due some limitations on the data we get from apm-metrics indices, if we have a service running in a container we want to query, to get the host.name, filtering by container.id const containerHostNames = await getContainerHostNames({ containerIds, - context, - infra, + infraMetricsClient, start, end, }); diff --git a/x-pack/plugins/apm/server/routes/service_groups/get_services_counts.ts b/x-pack/plugins/apm/server/routes/service_groups/get_services_counts.ts index 880e3d248889..c820cdd8445f 100644 --- a/x-pack/plugins/apm/server/routes/service_groups/get_services_counts.ts +++ b/x-pack/plugins/apm/server/routes/service_groups/get_services_counts.ts @@ -7,50 +7,72 @@ import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { rangeQuery, kqlQuery } from '@kbn/observability-plugin/server'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { Setup } from '../../lib/helpers/setup_request'; import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; +import { SavedServiceGroup } from '../../../common/service_groups'; export async function getServicesCounts({ setup, - kuery, - maxNumberOfServices, start, end, + serviceGroups, }: { setup: Setup; - kuery: string; - maxNumberOfServices: number; start: number; end: number; + serviceGroups: SavedServiceGroup[]; }) { const { apmEventClient } = setup; - const response = await apmEventClient.search('get_services_count', { + const serviceGroupsKueryMap: Record = + serviceGroups.reduce((acc, sg) => { + return { + ...acc, + [sg.id]: kqlQuery(sg.kuery)[0], + }; + }, {}); + + const params = { apm: { - events: [ - ProcessorEvent.metric, - ProcessorEvent.transaction, - ProcessorEvent.span, - ProcessorEvent.error, - ], + // We're limiting the service count to only metrics documents. If a user + // actively disables system/app metrics and a service only ingests error + // events, that service will not be included in the service groups count. + // This is an edge case that only effects the count preview label. + events: [ProcessorEvent.metric], }, body: { track_total_hits: 0, size: 0, query: { bool: { - filter: [...rangeQuery(start, end), ...kqlQuery(kuery)], + filter: rangeQuery(start, end), }, }, aggs: { - services_count: { - cardinality: { - field: SERVICE_NAME, + service_groups: { + filters: { + filters: serviceGroupsKueryMap, + }, + aggs: { + services_count: { + cardinality: { + field: SERVICE_NAME, + }, + }, }, }, }, }, - }); + }; + const response = await apmEventClient.search('get_services_count', params); - return response?.aggregations?.services_count.value ?? 0; + const buckets: Record = + response?.aggregations?.service_groups.buckets ?? {}; + return Object.keys(buckets).reduce((acc, key) => { + return { + ...acc, + [key]: buckets[key].services_count.value, + }; + }, {}); } diff --git a/x-pack/plugins/apm/server/routes/service_groups/route.ts b/x-pack/plugins/apm/server/routes/service_groups/route.ts index ff37dc1f1c16..4430aaae760e 100644 --- a/x-pack/plugins/apm/server/routes/service_groups/route.ts +++ b/x-pack/plugins/apm/server/routes/service_groups/route.ts @@ -7,7 +7,6 @@ import * as t from 'io-ts'; import { apmServiceGroupMaxNumberOfServices } from '@kbn/observability-plugin/common'; -import { keyBy, mapValues } from 'lodash'; import { setupRequest } from '../../lib/helpers/setup_request'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { kueryRt, rangeRt } from '../default_api_types'; @@ -52,50 +51,26 @@ const serviceGroupsWithServiceCountRoute = createApmServerRoute({ const { context, params } = resources; const { savedObjects: { client: savedObjectsClient }, - uiSettings: { client: uiSettingsClient }, } = await context.core; const { query: { start, end }, } = params; - const [setup, maxNumberOfServices] = await Promise.all([ - setupRequest(resources), - uiSettingsClient.get(apmServiceGroupMaxNumberOfServices), - ]); + const setup = await setupRequest(resources); const serviceGroups = await getServiceGroups({ savedObjectsClient, }); - const serviceGroupsWithServiceCount = await Promise.all( - serviceGroups.map( - async ({ - id, - kuery, - }): Promise<{ id: string; servicesCount: number }> => { - const servicesCount = await getServicesCounts({ - setup, - kuery, - maxNumberOfServices, - start, - end, - }); - - return { - id, - servicesCount, - }; - } - ) - ); - - const servicesCounts = mapValues( - keyBy(serviceGroupsWithServiceCount, 'id'), - 'servicesCount' - ); - - return { servicesCounts }; + return { + servicesCounts: await getServicesCounts({ + setup, + serviceGroups, + start, + end, + }), + }; }, }); diff --git a/x-pack/plugins/apm/server/routes/services/get_service_instance_container_metadata.ts b/x-pack/plugins/apm/server/routes/services/get_service_instance_container_metadata.ts index 729c2f1f1b1e..ebce4f933871 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_instance_container_metadata.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_instance_container_metadata.ts @@ -5,10 +5,8 @@ * 2.0. */ -import { ElasticsearchClient } from '@kbn/core/server'; import { rangeQuery } from '@kbn/observability-plugin/server'; import { - CONTAINER, CONTAINER_ID, CONTAINER_IMAGE, KUBERNETES, @@ -21,6 +19,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { Kubernetes } from '../../../typings/es_schemas/raw/fields/kubernetes'; import { maybe } from '../../../common/utils/maybe'; +import { InfraMetricsClient } from '../../lib/helpers/create_es_client/create_infra_metrics_client/create_infra_metrics_client'; type ServiceInstanceContainerMetadataDetails = | { @@ -29,22 +28,16 @@ type ServiceInstanceContainerMetadataDetails = | undefined; export const getServiceInstanceContainerMetadata = async ({ - esClient, - indexName, + infraMetricsClient, containerId, start, end, }: { - esClient: ElasticsearchClient; - indexName?: string; + infraMetricsClient: InfraMetricsClient; containerId: string; start: number; end: number; }): Promise => { - if (!indexName) { - return undefined; - } - const should = [ { exists: { field: KUBERNETES } }, { exists: { field: CONTAINER_IMAGE } }, @@ -56,9 +49,7 @@ export const getServiceInstanceContainerMetadata = async ({ { exists: { field: KUBERNETES_DEPLOYMENT_NAME } }, ]; - const response = await esClient.search({ - index: [indexName], - _source: [KUBERNETES, CONTAINER], + const response = await infraMetricsClient.search({ size: 1, query: { bool: { diff --git a/x-pack/plugins/apm/server/routes/services/get_service_overview_container_metadata.ts b/x-pack/plugins/apm/server/routes/services/get_service_overview_container_metadata.ts index 5b267e4faa3f..c1dcfe97e208 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_overview_container_metadata.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_overview_container_metadata.ts @@ -5,10 +5,8 @@ * 2.0. */ -import { ElasticsearchClient } from '@kbn/core/server'; import { rangeQuery } from '@kbn/observability-plugin/server'; import { - CONTAINER, CONTAINER_ID, CONTAINER_IMAGE, KUBERNETES, @@ -19,43 +17,19 @@ import { KUBERNETES_REPLICASET_NAME, KUBERNETES_DEPLOYMENT_NAME, } from '../../../common/elasticsearch_fieldnames'; - -type ServiceOverviewContainerMetadataDetails = - | { - kubernetes: { - deployments?: string[]; - replicasets?: string[]; - namespaces?: string[]; - containerImages?: string[]; - }; - } - | undefined; - -interface ResponseAggregations { - [key: string]: { - buckets: Array<{ - key: string; - }>; - }; -} +import { InfraMetricsClient } from '../../lib/helpers/create_es_client/create_infra_metrics_client/create_infra_metrics_client'; export const getServiceOverviewContainerMetadata = async ({ - esClient, - indexName, + infraMetricsClient, containerIds, start, end, }: { - esClient: ElasticsearchClient; - indexName?: string; + infraMetricsClient: InfraMetricsClient; containerIds: string[]; start: number; end: number; -}): Promise => { - if (!indexName) { - return undefined; - } - +}) => { const should = [ { exists: { field: KUBERNETES } }, { exists: { field: CONTAINER_IMAGE } }, @@ -67,9 +41,7 @@ export const getServiceOverviewContainerMetadata = async ({ { exists: { field: KUBERNETES_DEPLOYMENT_NAME } }, ]; - const response = await esClient.search({ - index: [indexName], - _source: [KUBERNETES, CONTAINER], + const response = await infraMetricsClient.search({ size: 0, query: { bool: { diff --git a/x-pack/plugins/apm/server/routes/services/route.ts b/x-pack/plugins/apm/server/routes/services/route.ts index 6b73d72367d0..79ff09101aec 100644 --- a/x-pack/plugins/apm/server/routes/services/route.ts +++ b/x-pack/plugins/apm/server/routes/services/route.ts @@ -55,7 +55,8 @@ import { ServiceHealthStatus } from '../../../common/service_health_status'; import { getServiceGroup } from '../service_groups/get_service_group'; import { offsetRt } from '../../../common/comparison_rt'; import { getRandomSampler } from '../../lib/helpers/get_random_sampler'; -import { getInfraMetricIndices } from '../../lib/helpers/get_infra_metric_indices'; +import { createInfraMetricsClient } from '../../lib/helpers/create_es_client/create_infra_metrics_client/create_infra_metrics_client'; + const servicesRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/services', params: t.type({ @@ -274,7 +275,8 @@ const serviceMetadataDetailsRoute = createApmServerRoute({ import('./get_service_metadata_details').ServiceMetadataDetails > => { const setup = await setupRequest(resources); - const { params, context, plugins } = resources; + const infraMetricsClient = createInfraMetricsClient(resources); + const { params } = resources; const { serviceName } = params.path; const { start, end } = params.query; @@ -295,19 +297,8 @@ const serviceMetadataDetailsRoute = createApmServerRoute({ }); if (serviceMetadataDetails?.container?.ids) { - const { - savedObjects: { client: savedObjectsClient }, - elasticsearch: { client: esClient }, - } = await context.core; - - const indexName = await getInfraMetricIndices({ - infraPlugin: plugins.infra, - savedObjectsClient, - }); - const containerMetadata = await getServiceOverviewContainerMetadata({ - esClient: esClient.asCurrentUser, - indexName, + infraMetricsClient, containerIds: serviceMetadataDetails.container.ids, start, end, @@ -911,7 +902,8 @@ export const serviceInstancesMetadataDetails = createApmServerRoute({ | undefined; }> => { const setup = await setupRequest(resources); - const { params, context, plugins } = resources; + const infraMetricsClient = createInfraMetricsClient(resources); + const { params } = resources; const { serviceName, serviceNodeName } = params.path; const { start, end } = params.query; @@ -925,19 +917,8 @@ export const serviceInstancesMetadataDetails = createApmServerRoute({ }); if (serviceInstanceMetadataDetails?.container?.id) { - const { - savedObjects: { client: savedObjectsClient }, - elasticsearch: { client: esClient }, - } = await context.core; - - const indexName = await getInfraMetricIndices({ - infraPlugin: plugins.infra, - savedObjectsClient, - }); - const containerMetadata = await getServiceInstanceContainerMetadata({ - esClient: esClient.asCurrentUser, - indexName, + infraMetricsClient, containerId: serviceInstanceMetadataDetails.container.id, start, end, diff --git a/x-pack/test/apm_api_integration/common/authentication.ts b/x-pack/plugins/apm/server/test_helpers/create_apm_users/authentication.ts similarity index 61% rename from x-pack/test/apm_api_integration/common/authentication.ts rename to x-pack/plugins/apm/server/test_helpers/create_apm_users/authentication.ts index 93bae236d5dc..f3d15ee6db21 100644 --- a/x-pack/test/apm_api_integration/common/authentication.ts +++ b/x-pack/plugins/apm/server/test_helpers/create_apm_users/authentication.ts @@ -5,15 +5,7 @@ * 2.0. */ -import { Client } from '@elastic/elasticsearch'; -import { PrivilegeType } from '@kbn/apm-plugin/common/privilege_type'; -import { ToolingLog } from '@kbn/tooling-log'; -import { omit } from 'lodash'; -import { KbnClientRequesterError } from '@kbn/test'; -import { AxiosError } from 'axios'; -import { SecurityServiceProvider } from '../../../../test/common/services/security'; - -type SecurityService = Awaited>; +import { PrivilegeType } from '../../../common/privilege_type'; export enum ApmUsername { noAccessUser = 'no_access_user', @@ -34,7 +26,7 @@ export enum ApmCustomRolename { apmMonitorIndices = 'apm_monitor_indices', } -const customRoles = { +export const customRoles = { [ApmCustomRolename.apmReadUserWithoutMlAccess]: { elasticsearch: { cluster: [], @@ -97,7 +89,7 @@ const customRoles = { }, }; -const users: Record< +export const users: Record< ApmUsername, { builtInRoleNames?: string[]; customRoleNames?: ApmCustomRolename[] } > = { @@ -132,70 +124,4 @@ const users: Record< }, }; -function logErrorResponse(logger: ToolingLog, e: Error) { - if (e instanceof KbnClientRequesterError) { - logger.error(`KbnClientRequesterError: ${JSON.stringify(e.axiosError?.response?.data)}`); - } else if (e instanceof AxiosError) { - logger.error(`AxiosError: ${JSON.stringify(e.response?.data)}`); - } else { - logger.error(`Unknown error: ${e.constructor.name}`); - } -} - -export async function createApmUser({ - username, - security, - es, - logger, -}: { - username: ApmUsername; - security: SecurityService; - es: Client; - logger: ToolingLog; -}) { - const user = users[username]; - - if (!user) { - throw new Error(`No configuration found for ${username}`); - } - - const { builtInRoleNames = [], customRoleNames = [] } = user; - - try { - // create custom roles - await Promise.all( - customRoleNames.map(async (roleName) => createCustomRole({ roleName, security, es })) - ); - - // create user - await security.user.create(username, { - full_name: username, - password: APM_TEST_PASSWORD, - roles: [...builtInRoleNames, ...customRoleNames], - }); - } catch (e) { - logErrorResponse(logger, e); - throw e; - } -} - -async function createCustomRole({ - roleName, - security, - es, -}: { - roleName: ApmCustomRolename; - security: SecurityService; - es: Client; -}) { - const role = customRoles[roleName]; - - // Add application privileges with es client as they are not supported by - // security.user.create. They are preserved when updating the role below - if ('applications' in role) { - await es.security.putRole({ name: roleName, body: role }); - } - await security.role.create(roleName, omit(role, 'applications')); -} - export const APM_TEST_PASSWORD = 'changeme'; diff --git a/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users.ts b/x-pack/plugins/apm/server/test_helpers/create_apm_users/create_apm_users.ts similarity index 69% rename from x-pack/plugins/apm/scripts/create_apm_users/create_apm_users.ts rename to x-pack/plugins/apm/server/test_helpers/create_apm_users/create_apm_users.ts index 7532392c9a8b..8e9e39373752 100644 --- a/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users.ts +++ b/x-pack/plugins/apm/server/test_helpers/create_apm_users/create_apm_users.ts @@ -5,10 +5,13 @@ * 2.0. */ +import { asyncForEach } from '@kbn/std'; import { AbortError, callKibana } from './helpers/call_kibana'; import { createOrUpdateUser } from './helpers/create_or_update_user'; - +import { ApmUsername, users } from './authentication'; +import { createCustomRole } from './helpers/create_custom_role'; export interface Elasticsearch { + node: string; username: string; password: string; } @@ -28,6 +31,7 @@ export async function createApmUsers({ elasticsearch, kibana, }); + if (!isCredentialsValid) { throw new AbortError('Invalid username/password'); } @@ -36,24 +40,33 @@ export async function createApmUsers({ elasticsearch, kibana, }); + if (!isSecurityEnabled) { throw new AbortError('Security must be enabled!'); } - // user definitions - const users = [ - { username: 'viewer', roles: ['viewer'] }, - { username: 'editor', roles: ['editor'] }, - ]; + const apmUsers = Object.values(ApmUsername); + await asyncForEach(apmUsers, async (username) => { + const user = users[username]; + const { builtInRoleNames = [], customRoleNames = [] } = user; + + // create custom roles + await Promise.all( + customRoleNames.map(async (roleName) => + createCustomRole({ elasticsearch, kibana, roleName }) + ) + ); - // create users - await Promise.all( - users.map(async (user) => - createOrUpdateUser({ elasticsearch, kibana, user }) - ) - ); + // create user + const roles = builtInRoleNames.concat(customRoleNames); + await createOrUpdateUser({ + elasticsearch, + kibana, + user: { username, roles }, + }); + }); - return users; + return apmUsers; } async function getIsSecurityEnabled({ diff --git a/x-pack/plugins/apm/scripts/create_apm_users/helpers/call_kibana.ts b/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/call_kibana.ts similarity index 97% rename from x-pack/plugins/apm/scripts/create_apm_users/helpers/call_kibana.ts rename to x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/call_kibana.ts index 1e6bd2e02c41..72312645a644 100644 --- a/x-pack/plugins/apm/scripts/create_apm_users/helpers/call_kibana.ts +++ b/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/call_kibana.ts @@ -13,7 +13,7 @@ export async function callKibana({ kibana, options, }: { - elasticsearch: Elasticsearch; + elasticsearch: Omit; kibana: Kibana; options: AxiosRequestConfig; }): Promise { diff --git a/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/create_custom_role.ts b/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/create_custom_role.ts new file mode 100644 index 000000000000..ac906fcbfb5e --- /dev/null +++ b/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/create_custom_role.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Client } from '@elastic/elasticsearch'; +import { omit } from 'lodash'; +import { Elasticsearch, Kibana } from '../create_apm_users'; +import { callKibana } from './call_kibana'; +import { customRoles, ApmCustomRolename } from '../authentication'; + +export async function createCustomRole({ + elasticsearch, + kibana, + roleName, +}: { + elasticsearch: Elasticsearch; + kibana: Kibana; + roleName: ApmCustomRolename; +}) { + const role = customRoles[roleName]; + + // Add application privileges with es client as they are not supported by + // the security API. They are preserved when updating the role below + if ('applications' in role) { + const esClient = getEsClient(elasticsearch); + await esClient.security.putRole({ name: roleName, body: role }); + } + + await callKibana({ + elasticsearch, + kibana, + options: { + method: 'PUT', + url: `/api/security/role/${roleName}`, + data: { + ...omit(role, 'applications'), + }, + }, + }); +} + +export function getEsClient(elasticsearch: Elasticsearch) { + const { node, username, password } = elasticsearch; + const client = new Client({ + node, + tls: { + rejectUnauthorized: false, + }, + requestTimeout: 120000, + auth: { + username, + password, + }, + }); + + return client; +} diff --git a/x-pack/plugins/apm/scripts/create_apm_users/helpers/create_or_update_user.ts b/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/create_or_update_user.ts similarity index 100% rename from x-pack/plugins/apm/scripts/create_apm_users/helpers/create_or_update_user.ts rename to x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/create_or_update_user.ts diff --git a/x-pack/plugins/apm/scripts/create_apm_users/helpers/get_version.ts b/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/get_version.ts similarity index 96% rename from x-pack/plugins/apm/scripts/create_apm_users/helpers/get_version.ts rename to x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/get_version.ts index a809efe7402e..a00747ae7858 100644 --- a/x-pack/plugins/apm/scripts/create_apm_users/helpers/get_version.ts +++ b/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/get_version.ts @@ -13,7 +13,7 @@ export async function getKibanaVersion({ elasticsearch, kibana, }: { - elasticsearch: Elasticsearch; + elasticsearch: Omit; kibana: Kibana; }) { try { diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 9af253fc9559..65a7fec902f7 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -82,6 +82,7 @@ export type Case = Omit, 'comments'> & { comments export type Cases = Omit, 'cases'> & { cases: Case[] }; export type CasesStatus = SnakeToCamelCase; export type CasesMetrics = SnakeToCamelCase; +export type CaseUpdateRequest = SnakeToCamelCase; export interface ResolvedCase { case: Case; @@ -133,12 +134,6 @@ export interface ApiProps { signal: AbortSignal; } -export interface BulkUpdateStatus { - status: string; - id: string; - version: string; -} - export interface ActionLicense { id: string; name: string; @@ -147,11 +142,6 @@ export interface ActionLicense { enabledInLicense: boolean; } -export interface DeleteCase { - id: string; - title: string; -} - export interface FieldMappings { id: string; title?: string; diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 1dbc271e9be4..974833987822 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -18,7 +18,7 @@ export const CANCEL = i18n.translate('xpack.cases.caseView.cancel', { export const DELETE_CASE = (quantity: number = 1) => i18n.translate('xpack.cases.confirmDeleteCase.deleteCase', { values: { quantity }, - defaultMessage: `Delete {quantity, plural, =1 {case} other {cases}}`, + defaultMessage: `Delete {quantity, plural, =1 {case} other {{quantity} cases}}`, }); export const NAME = i18n.translate('xpack.cases.caseView.name', { @@ -296,3 +296,9 @@ export const READ_ACTIONS_PERMISSIONS_ERROR_MSG = i18n.translate( 'You do not have permission to view connectors. If you would like to view connectors, contact your Kibana administrator.', } ); + +export const DELETED_CASES = (totalCases: number) => + i18n.translate('xpack.cases.containers.deletedCases', { + values: { totalCases }, + defaultMessage: 'Deleted {totalCases, plural, =1 {case} other {{totalCases} cases}}', + }); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx index e8661387e1e6..cfa2d3a6e005 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -23,6 +23,7 @@ const useKibanaMock = useKibana as jest.Mocked; describe('Use cases toast hook', () => { const successMock = jest.fn(); + const errorMock = jest.fn(); const getUrlForApp = jest.fn().mockReturnValue(`/app/cases/${mockCase.id}`); const navigateToUrl = jest.fn(); @@ -51,6 +52,7 @@ describe('Use cases toast hook', () => { useToastsMock.mockImplementation(() => { return { addSuccess: successMock, + addError: errorMock, }; }); @@ -63,159 +65,260 @@ describe('Use cases toast hook', () => { }; }); - describe('Toast hook', () => { - it('should create a success toast when invoked with a case', () => { - const { result } = renderHook( - () => { - return useCasesToast(); - }, - { wrapper: TestProviders } - ); - result.current.showSuccessAttach({ - theCase: mockCase, + describe('showSuccessAttach', () => { + describe('Toast hook', () => { + it('should create a success toast when invoked with a case', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + }); + expect(successMock).toHaveBeenCalled(); }); - expect(successMock).toHaveBeenCalled(); }); - }); - describe('toast title', () => { - it('should create a success toast when invoked with a case and a custom title', () => { - const { result } = renderHook( - () => { - return useCasesToast(); - }, - { wrapper: TestProviders } - ); - result.current.showSuccessAttach({ theCase: mockCase, title: 'Custom title' }); - validateTitle('Custom title'); - }); + describe('toast title', () => { + it('should create a success toast when invoked with a case and a custom title', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ theCase: mockCase, title: 'Custom title' }); + validateTitle('Custom title'); + }); - it('should display the alert sync title when called with an alert attachment (1 alert)', () => { - const { result } = renderHook( - () => { - return useCasesToast(); - }, - { wrapper: TestProviders } - ); - result.current.showSuccessAttach({ - theCase: mockCase, - attachments: [alertComment as SupportedCaseAttachment], + it('should display the alert sync title when called with an alert attachment (1 alert)', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateTitle('An alert was added to "Another horrible breach!!'); + }); + + it('should display the alert sync title when called with an alert attachment (multiple alerts)', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + const alert = { + ...alertComment, + alertId: ['1234', '54321'], + } as SupportedCaseAttachment; + + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alert], + }); + validateTitle('Alerts were added to "Another horrible breach!!'); + }); + + it('should display a generic title when called with a non-alert attachament', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [basicComment as SupportedCaseAttachment], + }); + validateTitle('Another horrible breach!! has been updated'); }); - validateTitle('An alert was added to "Another horrible breach!!'); }); - it('should display the alert sync title when called with an alert attachment (multiple alerts)', () => { - const { result } = renderHook( - () => { - return useCasesToast(); - }, - { wrapper: TestProviders } - ); - const alert = { - ...alertComment, - alertId: ['1234', '54321'], - } as SupportedCaseAttachment; - - result.current.showSuccessAttach({ - theCase: mockCase, - attachments: [alert], + describe('Toast content', () => { + let appMockRender: AppMockRenderer; + const onViewCaseClick = jest.fn(); + beforeEach(() => { + appMockRender = createAppMockRenderer(); + onViewCaseClick.mockReset(); + }); + + it('should create a success toast when invoked with a case and a custom content', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ theCase: mockCase, content: 'Custom content' }); + validateContent('Custom content'); + }); + + it('renders an alert-specific content when called with an alert attachment and sync on', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateContent('The alert statuses are synched with the case status.'); + }); + + it('renders empty content when called with an alert attachment and sync off', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: { ...mockCase, settings: { ...mockCase.settings, syncAlerts: false } }, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateContent('View case'); + }); + + it('renders a correct successful message content', () => { + const result = appMockRender.render( + + ); + expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent('my content'); + expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View case'); + expect(onViewCaseClick).not.toHaveBeenCalled(); + }); + + it('renders a correct successful message without content', () => { + const result = appMockRender.render( + + ); + expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy(); + expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View case'); + expect(onViewCaseClick).not.toHaveBeenCalled(); + }); + + it('Calls the onViewCaseClick when clicked', () => { + const result = appMockRender.render( + + ); + userEvent.click(result.getByTestId('toaster-content-case-view-link')); + expect(onViewCaseClick).toHaveBeenCalled(); }); - validateTitle('Alerts were added to "Another horrible breach!!'); }); - it('should display a generic title when called with a non-alert attachament', () => { - const { result } = renderHook( - () => { - return useCasesToast(); - }, - { wrapper: TestProviders } - ); - result.current.showSuccessAttach({ - theCase: mockCase, - attachments: [basicComment as SupportedCaseAttachment], + describe('Toast navigation', () => { + const tests = Object.entries(OWNER_INFO).map(([owner, ownerInfo]) => [ + owner, + ownerInfo.appId, + ]); + + it.each(tests)('should navigate correctly with owner %s and appId %s', (owner, appId) => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + + result.current.showSuccessAttach({ + theCase: { ...mockCase, owner }, + title: 'Custom title', + }); + + navigateToCase(); + + expect(getUrlForApp).toHaveBeenCalledWith(appId, { + deepLinkId: 'cases', + path: '/mock-id', + }); + + expect(navigateToUrl).toHaveBeenCalledWith('/app/cases/mock-id'); + }); + + it('navigates to the current app if the owner is invalid', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + + result.current.showSuccessAttach({ + theCase: { ...mockCase, owner: 'in-valid' }, + title: 'Custom title', + }); + + navigateToCase(); + + expect(getUrlForApp).toHaveBeenCalledWith('testAppId', { + deepLinkId: 'cases', + path: '/mock-id', + }); }); - validateTitle('Another horrible breach!! has been updated'); }); }); - describe('Toast content', () => { - let appMockRender: AppMockRenderer; - const onViewCaseClick = jest.fn(); - beforeEach(() => { - appMockRender = createAppMockRenderer(); - onViewCaseClick.mockReset(); - }); + describe('showErrorToast', () => { + it('should show an error toast', () => { + const error = new Error('showErrorToast: an error occurred'); - it('should create a success toast when invoked with a case and a custom content', () => { const { result } = renderHook( () => { return useCasesToast(); }, { wrapper: TestProviders } ); - result.current.showSuccessAttach({ theCase: mockCase, content: 'Custom content' }); - validateContent('Custom content'); + + result.current.showErrorToast(error); + + expect(errorMock).toHaveBeenCalledWith(error, { title: error.message }); }); - it('renders an alert-specific content when called with an alert attachment and sync on', () => { + it('should override the title', () => { + const error = new Error('showErrorToast: an error occurred'); + const { result } = renderHook( () => { return useCasesToast(); }, { wrapper: TestProviders } ); - result.current.showSuccessAttach({ - theCase: mockCase, - attachments: [alertComment as SupportedCaseAttachment], - }); - validateContent('The alert statuses are synched with the case status.'); + + result.current.showErrorToast(error, { title: 'my title' }); + + expect(errorMock).toHaveBeenCalledWith(error, { title: 'my title' }); }); - it('renders empty content when called with an alert attachment and sync off', () => { + it('should not show an error toast if the error is AbortError', () => { + const error = new Error('showErrorToast: an error occurred'); + error.name = 'AbortError'; + const { result } = renderHook( () => { return useCasesToast(); }, { wrapper: TestProviders } ); - result.current.showSuccessAttach({ - theCase: { ...mockCase, settings: { ...mockCase.settings, syncAlerts: false } }, - attachments: [alertComment as SupportedCaseAttachment], - }); - validateContent('View case'); - }); - it('renders a correct successful message content', () => { - const result = appMockRender.render( - - ); - expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent('my content'); - expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View case'); - expect(onViewCaseClick).not.toHaveBeenCalled(); - }); - - it('renders a correct successful message without content', () => { - const result = appMockRender.render( - - ); - expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy(); - expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View case'); - expect(onViewCaseClick).not.toHaveBeenCalled(); - }); + result.current.showErrorToast(error); - it('Calls the onViewCaseClick when clicked', () => { - const result = appMockRender.render( - - ); - userEvent.click(result.getByTestId('toaster-content-case-view-link')); - expect(onViewCaseClick).toHaveBeenCalled(); + expect(errorMock).not.toHaveBeenCalled(); }); - }); - describe('Toast navigation', () => { - const tests = Object.entries(OWNER_INFO).map(([owner, ownerInfo]) => [owner, ownerInfo.appId]); + it('should show the body message if it is a ServerError', () => { + const error = new Error('showErrorToast: an error occurred'); + // @ts-expect-error: need to create a ServerError + error.body = { message: 'message error' }; - it.each(tests)('should navigate correctly with owner %s and appId %s', (owner, appId) => { const { result } = renderHook( () => { return useCasesToast(); @@ -223,22 +326,16 @@ describe('Use cases toast hook', () => { { wrapper: TestProviders } ); - result.current.showSuccessAttach({ - theCase: { ...mockCase, owner }, - title: 'Custom title', - }); + result.current.showErrorToast(error); - navigateToCase(); - - expect(getUrlForApp).toHaveBeenCalledWith(appId, { - deepLinkId: 'cases', - path: '/mock-id', + expect(errorMock).toHaveBeenCalledWith(new Error('message error'), { + title: 'message error', }); - - expect(navigateToUrl).toHaveBeenCalledWith('/app/cases/mock-id'); }); + }); - it('navigates to the current app if the owner is invalid', () => { + describe('showSuccessToast', () => { + it('should show a success toast', () => { const { result } = renderHook( () => { return useCasesToast(); @@ -246,17 +343,9 @@ describe('Use cases toast hook', () => { { wrapper: TestProviders } ); - result.current.showSuccessAttach({ - theCase: { ...mockCase, owner: 'in-valid' }, - title: 'Custom title', - }); - - navigateToCase(); + result.current.showSuccessToast('my title'); - expect(getUrlForApp).toHaveBeenCalledWith('testAppId', { - deepLinkId: 'cases', - path: '/mock-id', - }); + expect(successMock).toHaveBeenCalledWith('my title'); }); }); }); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx index 7d445a4edffa..42a358897bc6 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import type { ErrorToastOptions } from '@kbn/core/public'; import { EuiButtonEmpty, EuiText } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; @@ -12,7 +13,7 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { Case, CommentType } from '../../common'; import { useKibana, useToasts } from './lib/kibana'; import { generateCaseViewPath } from './navigation'; -import { CaseAttachmentsWithoutOwner } from '../types'; +import { CaseAttachmentsWithoutOwner, ServerError } from '../types'; import { CASE_ALERT_SUCCESS_SYNC_TEXT, CASE_ALERT_SUCCESS_TOAST, @@ -98,6 +99,25 @@ function getToastContent({ const isValidOwner = (owner: string): owner is keyof typeof OWNER_INFO => Object.keys(OWNER_INFO).includes(owner); +const isServerError = (error: Error | ServerError): error is ServerError => + Object.hasOwn(error, 'body'); + +const getError = (error: Error | ServerError): Error => { + if (isServerError(error)) { + return new Error(error.body?.message); + } + + return error; +}; + +const getErrorMessage = (error: Error | ServerError): string => { + if (isServerError(error)) { + return error.body?.message ?? ''; + } + + return error.message; +}; + export const useCasesToast = () => { const { appId } = useCasesContext(); const { getUrlForApp, navigateToUrl } = useKibana().services.application; @@ -141,6 +161,14 @@ export const useCasesToast = () => { ), }); }, + showErrorToast: (error: Error | ServerError, opts?: ErrorToastOptions) => { + if (error.name !== 'AbortError') { + toasts.addError(getError(error), { title: getErrorMessage(error), ...opts }); + } + }, + showSuccessToast: (title: string) => { + toasts.addSuccess(title); + }, }; }; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index a504bf251ff2..1cc3a5f0263e 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; import moment from 'moment-timezone'; -import { act, render, waitFor, screen } from '@testing-library/react'; +import { render, waitFor, screen, act } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import userEvent from '@testing-library/user-event'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; @@ -20,15 +20,12 @@ import { noDeleteCasesPermissions, TestProviders, } from '../../common/mock'; -import { casesStatus, useGetCasesMockState, mockCase, connectorsMock } from '../../containers/mock'; +import { useGetCasesMockState, connectorsMock } from '../../containers/mock'; import { StatusAll } from '../../../common/ui/types'; import { CaseSeverity, CaseStatuses } from '../../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { getEmptyTagValue } from '../empty_value'; -import { useDeleteCases } from '../../containers/use_delete_cases'; -import { useGetCasesStatus } from '../../containers/use_get_cases_status'; -import { useUpdateCases } from '../../containers/use_bulk_update_case'; import { useKibana } from '../../common/lib/kibana'; import { AllCasesList } from './all_cases_list'; import { CasesColumns, GetCasesColumn, useCasesColumns } from './columns'; @@ -37,7 +34,6 @@ import { registerConnectorsToMockActionRegistry } from '../../common/mock/regist import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; import { waitForComponentToUpdate } from '../../common/test_utils'; import { useCreateAttachments } from '../../containers/use_create_attachments'; -import { useGetCasesMetrics } from '../../containers/use_get_cases_metrics'; import { useGetConnectors } from '../../containers/configure/use_connectors'; import { useGetTags } from '../../containers/use_get_tags'; import { useUpdateCase } from '../../containers/use_update_case'; @@ -46,13 +42,10 @@ import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock'; import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles'; import { useLicense } from '../../common/use_license'; +import * as api from '../../containers/api'; jest.mock('../../containers/use_create_attachments'); -jest.mock('../../containers/use_bulk_update_case'); -jest.mock('../../containers/use_delete_cases'); jest.mock('../../containers/use_get_cases'); -jest.mock('../../containers/use_get_cases_status'); -jest.mock('../../containers/use_get_cases_metrics'); jest.mock('../../containers/use_get_action_license'); jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/user_profiles/use_get_current_user_profile'); @@ -66,11 +59,7 @@ jest.mock('../app/use_available_owners', () => ({ jest.mock('../../containers/use_update_case'); jest.mock('../../common/use_license'); -const useDeleteCasesMock = useDeleteCases as jest.Mock; const useGetCasesMock = useGetCases as jest.Mock; -const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; -const useGetCasesMetricsMock = useGetCasesMetrics as jest.Mock; -const useUpdateCasesMock = useUpdateCases as jest.Mock; const useGetTagsMock = useGetTags as jest.Mock; const useGetCurrentUserProfileMock = useGetCurrentUserProfile as jest.Mock; const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock; @@ -92,13 +81,7 @@ const mockKibana = () => { }; describe('AllCasesListGeneric', () => { - const dispatchResetIsDeleted = jest.fn(); - const dispatchResetIsUpdated = jest.fn(); - const handleOnDeleteConfirm = jest.fn(); - const handleToggleModal = jest.fn(); const refetchCases = jest.fn(); - const updateBulkStatus = jest.fn(); - const fetchCasesStatus = jest.fn(); const onRowClick = jest.fn(); const updateCaseProperty = jest.fn(); @@ -113,36 +96,6 @@ describe('AllCasesListGeneric', () => { refetch: refetchCases, }; - const defaultDeleteCases = { - dispatchResetIsDeleted, - handleOnDeleteConfirm, - handleToggleModal, - isDeleted: false, - isDisplayConfirmDeleteModal: false, - isLoading: false, - }; - - const defaultCasesStatus = { - ...casesStatus, - fetchCasesStatus, - isError: false, - isLoading: false, - }; - - const defaultCasesMetrics = { - mttr: 5, - isLoading: false, - fetchCasesMetrics: jest.fn(), - }; - - const defaultUpdateCases = { - isUpdated: false, - isLoading: false, - isError: false, - dispatchResetIsUpdated, - updateBulkStatus, - }; - const defaultColumnArgs = { caseDetailsNavigation: { href: jest.fn(), @@ -167,11 +120,7 @@ describe('AllCasesListGeneric', () => { beforeEach(() => { jest.clearAllMocks(); appMockRenderer = createAppMockRenderer(); - useUpdateCasesMock.mockReturnValue(defaultUpdateCases); useGetCasesMock.mockReturnValue(defaultGetCases); - useDeleteCasesMock.mockReturnValue(defaultDeleteCases); - useGetCasesStatusMock.mockReturnValue(defaultCasesStatus); - useGetCasesMetricsMock.mockReturnValue(defaultCasesMetrics); useGetTagsMock.mockReturnValue({ data: ['coke', 'pepsi'], refetch: jest.fn() }); useGetCurrentUserProfileMock.mockReturnValue({ data: userProfiles[0], isLoading: false }); useBulkGetUserProfilesMock.mockReturnValue({ data: userProfilesMap }); @@ -368,43 +317,48 @@ describe('AllCasesListGeneric', () => { expect(wrapper.find('[data-test-subj="cases-count-stats"]')).toBeTruthy(); }); - it.skip('Bulk delete', async () => { - useDeleteCasesMock - .mockReturnValueOnce({ - ...defaultDeleteCases, - isDisplayConfirmDeleteModal: false, - }) - .mockReturnValue({ - ...defaultDeleteCases, - isDisplayConfirmDeleteModal: true, - }); + it('Bulk delete', async () => { + const deleteCasesSpy = jest.spyOn(api, 'deleteCases'); + const result = appMockRenderer.render(); - const wrapper = mount( - - - - ); + act(() => { + userEvent.click(result.getByTestId('checkboxSelectAll')); + }); - wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); - wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().simulate('click'); + act(() => { + userEvent.click(result.getByText('Bulk actions')); + }); - wrapper - .find( - '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' - ) - .last() - .simulate('click'); + await waitForEuiPopoverOpen(); + + act(() => { + userEvent.click(result.getByTestId('cases-bulk-delete-button'), undefined, { + skipPointerEventsCheck: true, + }); + }); await waitFor(() => { - expect(handleToggleModal).toBeCalled(); + expect(result.getByTestId('confirm-delete-case-modal')).toBeInTheDocument(); + }); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual([ - ...useGetCasesMockState.data.cases.map(({ id, title }) => ({ id, title })), - { - id: mockCase.id, - title: mockCase.title, - }, - ]); + act(() => { + userEvent.click(result.getByTestId('confirmModalConfirmButton')); + }); + + await waitFor(() => { + expect(deleteCasesSpy).toHaveBeenCalledWith( + [ + 'basic-case-id', + '1', + '2', + '3', + '4', + 'case-with-alerts-id', + 'case-with-alerts-syncoff-id', + 'case-with-registered-attachment', + ], + expect.anything() + ); }); }); @@ -431,6 +385,8 @@ describe('AllCasesListGeneric', () => { }); it('Bulk close status update', async () => { + const updateCasesSpy = jest.spyOn(api, 'updateCases'); + const result = appMockRenderer.render(); const theCase = useGetCasesMockState.data.cases[0]; userEvent.click(result.getByTestId('case-status-filter')); @@ -441,10 +397,16 @@ describe('AllCasesListGeneric', () => { await waitForEuiPopoverOpen(); userEvent.click(result.getByTestId('cases-bulk-close-button')); await waitFor(() => {}); - expect(updateBulkStatus).toBeCalledWith([theCase], CaseStatuses.closed); + + expect(updateCasesSpy).toBeCalledWith( + [{ id: theCase.id, version: theCase.version, status: CaseStatuses.closed }], + expect.anything() + ); }); it('Bulk open status update', async () => { + const updateCasesSpy = jest.spyOn(api, 'updateCases'); + const result = appMockRenderer.render(); const theCase = useGetCasesMockState.data.cases[0]; userEvent.click(result.getByTestId('case-status-filter')); @@ -455,10 +417,16 @@ describe('AllCasesListGeneric', () => { await waitForEuiPopoverOpen(); userEvent.click(result.getByTestId('cases-bulk-open-button')); await waitFor(() => {}); - expect(updateBulkStatus).toBeCalledWith([theCase], CaseStatuses.open); + + expect(updateCasesSpy).toBeCalledWith( + [{ id: theCase.id, version: theCase.version, status: CaseStatuses.open }], + expect.anything() + ); }); it('Bulk in-progress status update', async () => { + const updateCasesSpy = jest.spyOn(api, 'updateCases'); + const result = appMockRenderer.render(); const theCase = useGetCasesMockState.data.cases[0]; userEvent.click(result.getByTestId('case-status-filter')); @@ -469,43 +437,11 @@ describe('AllCasesListGeneric', () => { await waitForEuiPopoverOpen(); userEvent.click(result.getByTestId('cases-bulk-in-progress-button')); await waitFor(() => {}); - expect(updateBulkStatus).toBeCalledWith([theCase], CaseStatuses['in-progress']); - }); - it('isDeleted is true, refetch', async () => { - useDeleteCasesMock.mockReturnValue({ - ...defaultDeleteCases, - isDeleted: true, - }); - - mount( - - - + expect(updateCasesSpy).toBeCalledWith( + [{ id: theCase.id, version: theCase.version, status: CaseStatuses['in-progress'] }], + expect.anything() ); - await waitFor(() => { - expect(refetchCases).toBeCalled(); - // expect(fetchCasesStatus).toBeCalled(); - expect(dispatchResetIsDeleted).toBeCalled(); - }); - }); - - it('isUpdated is true, refetch', async () => { - useUpdateCasesMock.mockReturnValue({ - ...defaultUpdateCases, - isUpdated: true, - }); - - mount( - - - - ); - await waitFor(() => { - expect(refetchCases).toBeCalled(); - // expect(fetchCasesStatus).toBeCalled(); - expect(dispatchResetIsUpdated).toBeCalled(); - }); }); it('should not render table utility bar when isSelectorView=true', async () => { @@ -769,28 +705,10 @@ describe('AllCasesListGeneric', () => { expect(wrapper.find('[data-test-subj="status-badge-in-progress"]').exists()).toBeTruthy(); }); - it('should call doRefresh if provided', async () => { - const doRefresh = jest.fn(); - - const wrapper = mount( - - - - ); - - await act(async () => { - wrapper.find('[data-test-subj="all-cases-refresh"] button').first().simulate('click'); - }); - - expect(doRefresh).toHaveBeenCalled(); - }); - it('shows Solution column if there are no set owners', async () => { - const doRefresh = jest.fn(); - const wrapper = mount( - + ); @@ -801,11 +719,9 @@ describe('AllCasesListGeneric', () => { }); it('hides Solution column if there is a set owner', async () => { - const doRefresh = jest.fn(); - const wrapper = mount( - + ); diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index 0bc5e9b14b29..c7b2d4895f94 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -10,7 +10,6 @@ import { EuiProgress, EuiBasicTable, EuiTableSelectionType } from '@elastic/eui' import { difference, head, isEmpty } from 'lodash/fp'; import styled, { css } from 'styled-components'; -import { useQueryClient } from '@tanstack/react-query'; import { Case, CaseStatusWithAllStatus, @@ -38,11 +37,8 @@ import { } from '../../containers/use_get_cases'; import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles'; import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile'; -import { - USER_PROFILES_BULK_GET_CACHE_KEY, - USER_PROFILES_CACHE_KEY, -} from '../../containers/constants'; import { getAllPermissionsExceptFrom } from '../../utils/permissions'; +import { useIsLoadingCases } from './use_is_loading_cases'; const ProgressLoader = styled(EuiProgress)` ${({ $isShow }: { $isShow: boolean }) => @@ -64,14 +60,13 @@ export interface AllCasesListProps { hiddenStatuses?: CaseStatusWithAllStatus[]; isSelectorView?: boolean; onRowClick?: (theCase?: Case) => void; - doRefresh?: () => void; } export const AllCasesList = React.memo( - ({ hiddenStatuses = [], isSelectorView = false, onRowClick, doRefresh }) => { + ({ hiddenStatuses = [], isSelectorView = false, onRowClick }) => { const { owner, permissions } = useCasesContext(); const availableSolutions = useAvailableCasesOwners(getAllPermissionsExceptFrom('delete')); - const [refresh, setRefresh] = useState(0); + const isLoading = useIsLoadingCases(); const hasOwner = !!owner.length; @@ -80,19 +75,15 @@ export const AllCasesList = React.memo( ...(!isEmpty(hiddenStatuses) && firstAvailableStatus && { status: firstAvailableStatus }), owner: hasOwner ? owner : availableSolutions, }; + const [filterOptions, setFilterOptions] = useState({ ...DEFAULT_FILTER_OPTIONS, ...initialFilterOptions, }); const [queryParams, setQueryParams] = useState(DEFAULT_QUERY_PARAMS); const [selectedCases, setSelectedCases] = useState([]); - const queryClient = useQueryClient(); - const { - data = initialData, - isFetching: isLoadingCases, - refetch: refetchCases, - } = useGetCases({ + const { data = initialData, isFetching: isLoadingCases } = useGetCases({ filterOptions, queryParams, }); @@ -126,41 +117,13 @@ export const AllCasesList = React.memo( [queryParams.sortField, queryParams.sortOrder] ); - const filterRefetch = useRef<() => void>(); const tableRef = useRef(null); - const [isLoading, handleIsLoading] = useState(false); - - const setFilterRefetch = useCallback( - (refetchFilter: () => void) => { - filterRefetch.current = refetchFilter; - }, - [filterRefetch] - ); const deselectCases = useCallback(() => { setSelectedCases([]); tableRef.current?.setSelection([]); }, [setSelectedCases]); - const refreshCases = useCallback( - (dataRefresh = true) => { - deselectCases(); - if (dataRefresh) { - refetchCases(); - queryClient.refetchQueries([USER_PROFILES_CACHE_KEY, USER_PROFILES_BULK_GET_CACHE_KEY]); - - setRefresh((currRefresh: number) => currRefresh + 1); - } - if (doRefresh) { - doRefresh(); - } - if (filterRefetch.current != null) { - filterRefetch.current(); - } - }, - [deselectCases, doRefresh, queryClient, refetchCases] - ); - const tableOnChangeCallback = useCallback( ({ page, sort }: EuiBasicTableOnChange) => { let newQueryParams = queryParams; @@ -179,9 +142,9 @@ export const AllCasesList = React.memo( }; } setQueryParams(newQueryParams); - refreshCases(false); + deselectCases(); }, - [queryParams, refreshCases, setQueryParams] + [queryParams, deselectCases, setQueryParams] ); const onFilterChangedCallback = useCallback( @@ -229,9 +192,8 @@ export const AllCasesList = React.memo( } : {}), })); - refreshCases(false); }, - [deselectCases, refreshCases, hasOwner, availableSolutions, owner] + [deselectCases, hasOwner, availableSolutions, owner] ); /** @@ -244,8 +206,6 @@ export const AllCasesList = React.memo( filterStatus: filterOptions.status ?? StatusAll, userProfiles: userProfiles ?? new Map(), currentUserProfile, - handleIsLoading, - refreshCases, isSelectorView, connectors, onRowClick, @@ -286,7 +246,7 @@ export const AllCasesList = React.memo( className="essentialAnimation" $isShow={isLoading || isLoadingCases} /> - {!isSelectorView ? : null} + {!isSelectorView ? : null} ( owner: filterOptions.owner, severity: filterOptions.severity, }} - setFilterRefetch={setFilterRefetch} hiddenStatuses={hiddenStatuses} displayCreateCaseButton={isSelectorView} onCreateCasePressed={onRowClick} @@ -315,20 +274,19 @@ export const AllCasesList = React.memo( data={data} filterOptions={filterOptions} goToCreateCase={onRowClick} - handleIsLoading={handleIsLoading} isCasesLoading={isLoadingCases} isCommentUpdating={isLoadingCases} isDataEmpty={isDataEmpty} isSelectorView={isSelectorView} onChange={tableOnChangeCallback} pagination={pagination} - refreshCases={refreshCases} selectedCases={selectedCases} selection={euiBasicTableSelectionProps} showActions={showActions} sorting={sorting} tableRef={tableRef} tableRowProps={tableRowProps} + deselectCases={deselectCases} /> ); diff --git a/x-pack/plugins/cases/public/components/all_cases/cases_metrics.test.tsx b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.test.tsx index 6141527d59f4..323f9deead3e 100644 --- a/x-pack/plugins/cases/public/components/all_cases/cases_metrics.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.test.tsx @@ -5,46 +5,29 @@ * 2.0. */ -import { within } from '@testing-library/dom'; +import { waitFor, within } from '@testing-library/react'; import React from 'react'; import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; -import { useGetCasesMetrics } from '../../containers/use_get_cases_metrics'; -import { useGetCasesStatus } from '../../containers/use_get_cases_status'; import { CasesMetrics } from './cases_metrics'; -jest.mock('../../containers/use_get_cases_metrics'); -jest.mock('../../containers/use_get_cases_status'); - -const useGetCasesMetricsMock = useGetCasesMetrics as jest.Mock; -const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; +jest.mock('../../api'); describe('Cases metrics', () => { - useGetCasesStatusMock.mockReturnValue({ - countOpenCases: 2, - countInProgressCases: 3, - countClosedCases: 4, - isLoading: false, - fetchCasesStatus: jest.fn(), - }); - useGetCasesMetricsMock.mockReturnValue({ - // 600 seconds = 10m - mttr: 600, - isLoading: false, - fetchCasesMetrics: jest.fn(), - }); - let appMockRenderer: AppMockRenderer; beforeEach(() => { appMockRenderer = createAppMockRenderer(); }); - it('renders the correct stats', () => { - const result = appMockRenderer.render(); - expect(result.getByTestId('cases-metrics-stats')).toBeTruthy(); - expect(within(result.getByTestId('openStatsHeader')).getByText(2)).toBeTruthy(); - expect(within(result.getByTestId('inProgressStatsHeader')).getByText(3)).toBeTruthy(); - expect(within(result.getByTestId('closedStatsHeader')).getByText(4)).toBeTruthy(); - expect(within(result.getByTestId('mttrStatsHeader')).getByText('10m')).toBeTruthy(); + it('renders the correct stats', async () => { + const result = appMockRenderer.render(); + + await waitFor(() => { + expect(result.getByTestId('cases-metrics-stats')).toBeTruthy(); + expect(within(result.getByTestId('openStatsHeader')).getByText(20)).toBeTruthy(); + expect(within(result.getByTestId('inProgressStatsHeader')).getByText(40)).toBeTruthy(); + expect(within(result.getByTestId('closedStatsHeader')).getByText(130)).toBeTruthy(); + expect(within(result.getByTestId('mttrStatsHeader')).getByText('12s')).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx index e75f0fd8a670..ca6434aa11c2 100644 --- a/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/cases_metrics.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FunctionComponent, useEffect, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { EuiDescriptionList, EuiFlexGroup, @@ -22,9 +22,6 @@ import { StatusStats } from '../status/status_stats'; import { useGetCasesMetrics } from '../../containers/use_get_cases_metrics'; import { ATTC_DESCRIPTION, ATTC_STAT } from './translations'; -interface CountProps { - refresh?: number; -} const MetricsFlexGroup = styled.div` ${({ theme }) => css` .euiFlexGroup { @@ -40,29 +37,23 @@ const MetricsFlexGroup = styled.div` `} `; -export const CasesMetrics: FunctionComponent = ({ refresh }) => { +export const CasesMetrics: React.FC = () => { const { - countOpenCases, - countInProgressCases, - countClosedCases, + data: { countOpenCases, countInProgressCases, countClosedCases } = { + countOpenCases: 0, + countInProgressCases: 0, + countClosedCases: 0, + }, isLoading: isCasesStatusLoading, - fetchCasesStatus, } = useGetCasesStatus(); - const { mttr, isLoading: isCasesMetricsLoading, fetchCasesMetrics } = useGetCasesMetrics(); + const { data: { mttr } = { mttr: 0 }, isLoading: isCasesMetricsLoading } = useGetCasesMetrics(); const mttrValue = useMemo( () => (mttr != null ? prettyMilliseconds(mttr * 1000, { compact: true, verbose: false }) : '-'), [mttr] ); - useEffect(() => { - if (refresh != null) { - fetchCasesStatus(); - fetchCasesMetrics(); - } - }, [fetchCasesMetrics, fetchCasesStatus, refresh]); - return ( diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index 00e9fb077589..948abdbbcd2f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { EuiBadgeGroup, EuiBadge, @@ -24,7 +24,7 @@ import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import styled from 'styled-components'; import { UserProfileWithAvatar } from '@kbn/user-profile-components'; -import { Case, DeleteCase, UpdateByKey } from '../../../common/ui/types'; +import { Case, UpdateByKey } from '../../../common/ui/types'; import { CaseStatuses, ActionConnector, CaseSeverity } from '../../../common/api'; import { OWNER_INFO } from '../../../common/constants'; import { getEmptyTagValue } from '../empty_value'; @@ -49,6 +49,7 @@ import { getUsernameDataTestSubj } from '../user_profiles/data_test_subject'; import { CurrentUserProfile } from '../types'; import { SmallUserAvatar } from '../user_profiles/small_user_avatar'; import { useCasesFeatures } from '../../common/use_cases_features'; +import { useRefreshCases } from './use_on_refresh_cases'; export type CasesColumns = | EuiTableActionsColumnType @@ -103,8 +104,6 @@ export interface GetCasesColumn { filterStatus: string; userProfiles: Map; currentUserProfile: CurrentUserProfile; - handleIsLoading: (a: boolean) => void; - refreshCases?: (a?: boolean) => void; isSelectorView: boolean; connectors?: ActionConnector[]; onRowClick?: (theCase: Case) => void; @@ -115,41 +114,40 @@ export const useCasesColumns = ({ filterStatus, userProfiles, currentUserProfile, - handleIsLoading, - refreshCases, isSelectorView, connectors = [], onRowClick, showSolutionColumn, }: GetCasesColumn): CasesColumns[] => { - // Delete case - const { - dispatchResetIsDeleted, - handleOnDeleteConfirm, - handleToggleModal, - isDeleted, - isDisplayConfirmDeleteModal, - isLoading: isDeleting, - } = useDeleteCases(); - + const [isModalVisible, setIsModalVisible] = useState(false); + const { mutate: deleteCases } = useDeleteCases(); + const refreshCases = useRefreshCases(); const { isAlertsEnabled, caseAssignmentAuthorized } = useCasesFeatures(); const { permissions } = useCasesContext(); - - const [deleteThisCase, setDeleteThisCase] = useState({ - id: '', - title: '', - }); - + const [caseToBeDeleted, setCaseToBeDeleted] = useState(); const { updateCaseProperty, isLoading: isLoadingUpdateCase } = useUpdateCase(); - const toggleDeleteModal = useCallback( - (deleteCase: Case) => { - handleToggleModal(); - setDeleteThisCase({ id: deleteCase.id, title: deleteCase.title }); + const closeModal = useCallback(() => setIsModalVisible(false), []); + const openModal = useCallback(() => setIsModalVisible(true), []); + + const onDeleteAction = useCallback( + (theCase: Case) => { + openModal(); + setCaseToBeDeleted(theCase.id); }, - [handleToggleModal] + [openModal] ); + const onConfirmDeletion = useCallback(() => { + closeModal(); + if (caseToBeDeleted) { + deleteCases({ + caseIds: [caseToBeDeleted], + successToasterTitle: i18n.DELETED_CASES(1), + }); + } + }, [caseToBeDeleted, closeModal, deleteCases]); + const handleDispatchUpdate = useCallback( ({ updateKey, updateValue, caseData }: UpdateByKey) => { updateCaseProperty({ @@ -157,7 +155,7 @@ export const useCasesColumns = ({ updateValue, caseData, onSuccess: () => { - if (refreshCases != null) refreshCases(); + refreshCases(); }, }); }, @@ -167,9 +165,9 @@ export const useCasesColumns = ({ const actions = useMemo( () => getActions({ - deleteCaseOnClick: toggleDeleteModal, + deleteCaseOnClick: onDeleteAction, }), - [toggleDeleteModal] + [onDeleteAction] ); const assignCaseAction = useCallback( @@ -181,17 +179,6 @@ export const useCasesColumns = ({ [onRowClick] ); - useEffect(() => { - handleIsLoading(isDeleting || isLoadingUpdateCase); - }, [handleIsLoading, isDeleting, isLoadingUpdateCase]); - - useEffect(() => { - if (isDeleted) { - if (refreshCases != null) refreshCases(); - dispatchResetIsDeleted(); - } - }, [isDeleted, dispatchResetIsDeleted, refreshCases]); - return [ { name: i18n.NAME, @@ -421,12 +408,13 @@ export const useCasesColumns = ({ name: ( <> {i18n.ACTIONS} - + {isModalVisible ? ( + + ) : null} ), actions, diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index 6fd1556f7d4e..55777dd5bd34 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -15,8 +15,7 @@ import { noCreateCasesPermissions, } from '../../common/mock'; import { useGetActionLicense } from '../../containers/use_get_action_license'; -import { casesStatus, connectorsMock, useGetCasesMockState } from '../../containers/mock'; -import { useGetCasesStatus } from '../../containers/use_get_cases_status'; +import { connectorsMock, useGetCasesMockState } from '../../containers/mock'; import { useGetConnectors } from '../../containers/configure/use_connectors'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetCases } from '../../containers/use_get_cases'; @@ -33,13 +32,12 @@ jest.mock('../../containers/use_get_action_license', () => { jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/api'); jest.mock('../../containers/use_get_cases'); -jest.mock('../../containers/use_get_cases_status'); jest.mock('../../containers/user_profiles/use_get_current_user_profile'); jest.mock('../../containers/user_profiles/use_bulk_get_user_profiles'); +jest.mock('../../api'); const useGetConnectorsMock = useGetConnectors as jest.Mock; const useGetCasesMock = useGetCases as jest.Mock; -const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; const useGetActionLicenseMock = useGetActionLicense as jest.Mock; const useGetCurrentUserProfileMock = useGetCurrentUserProfile as jest.Mock; const useBulkGetUserProfilesMock = useBulkGetUserProfiles as jest.Mock; @@ -49,7 +47,6 @@ describe('AllCases', () => { const setFilters = jest.fn(); const setQueryParams = jest.fn(); const setSelectedCases = jest.fn(); - const fetchCasesStatus = jest.fn(); const defaultGetCases = { ...useGetCasesMockState, @@ -59,13 +56,6 @@ describe('AllCases', () => { setSelectedCases, }; - const defaultCasesStatus = { - ...casesStatus, - fetchCasesStatus, - isError: false, - isLoading: false, - }; - const defaultActionLicense = { data: null, isLoading: false, @@ -75,7 +65,6 @@ describe('AllCases', () => { beforeAll(() => { (useGetTags as jest.Mock).mockReturnValue({ data: ['coke', 'pepsi'], refetch: jest.fn() }); useGetConnectorsMock.mockImplementation(() => ({ data: connectorsMock, isLoading: false })); - useGetCasesStatusMock.mockReturnValue(defaultCasesStatus); useGetActionLicenseMock.mockReturnValue(defaultActionLicense); useGetCasesMock.mockReturnValue(defaultGetCases); @@ -142,8 +131,6 @@ describe('AllCases', () => { }); it('should render the loading spinner when loading stats', async () => { - useGetCasesStatusMock.mockReturnValue({ ...defaultCasesStatus, isLoading: true }); - const result = appMockRender.render(); await waitFor(() => { diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index afe653a50e4d..208c26e47648 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -29,20 +29,19 @@ interface CasesTableProps { data: Cases; filterOptions: FilterOptions; goToCreateCase?: () => void; - handleIsLoading: (a: boolean) => void; isCasesLoading: boolean; isCommentUpdating: boolean; isDataEmpty: boolean; isSelectorView?: boolean; onChange: EuiBasicTableProps['onChange']; pagination: Pagination; - refreshCases: (a?: boolean) => void; selectedCases: Case[]; selection: EuiTableSelectionType; showActions: boolean; sorting: EuiBasicTableProps['sorting']; tableRef: MutableRefObject; tableRowProps: EuiBasicTableProps['rowProps']; + deselectCases: () => void; } const Div = styled.div` @@ -54,20 +53,19 @@ export const CasesTable: FunctionComponent = ({ data, filterOptions, goToCreateCase, - handleIsLoading, isCasesLoading, isCommentUpdating, isDataEmpty, isSelectorView, onChange, pagination, - refreshCases, selectedCases, selection, showActions, sorting, tableRef, tableRowProps, + deselectCases, }) => { const { permissions } = useCasesContext(); const { getCreateCaseUrl, navigateToCreateCase } = useCreateCaseNavigation(); @@ -93,9 +91,8 @@ export const CasesTable: FunctionComponent = ({ data={data} enableBulkActions={showActions} filterOptions={filterOptions} - handleIsLoading={handleIsLoading} selectedCases={selectedCases} - refreshCases={refreshCases} + deselectCases={deselectCases} /> { expect(onFilterChanged).toBeCalledWith({ status: CaseStatuses.closed }); }); - it('should call on load setFilterRefetch', () => { - mount( - - - - ); - expect(setFilterRefetch).toHaveBeenCalled(); - }); - it('should remove tag from selected tags when tag no longer exists', () => { const ourProps = { ...props, diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx index 5ab587ad3175..75fc22a9fee2 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx @@ -19,7 +19,6 @@ import { StatusFilter } from './status_filter'; import * as i18n from './translations'; import { SeverityFilter } from './severity_filter'; import { useGetTags } from '../../containers/use_get_tags'; -import { CASE_LIST_CACHE_KEY } from '../../containers/constants'; import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases'; import { AssigneesFilterPopover } from './assignees_filter'; import { CurrentUserProfile } from '../types'; @@ -31,7 +30,6 @@ interface CasesTableFiltersProps { countOpenCases: number | null; onFilterChanged: (filterOptions: Partial) => void; initial: FilterOptions; - setFilterRefetch: (val: () => void) => void; hiddenStatuses?: CaseStatusWithAllStatus[]; availableSolutions: string[]; displayCreateCaseButton?: boolean; @@ -59,7 +57,6 @@ const CasesTableFiltersComponent = ({ countInProgressCases, onFilterChanged, initial = DEFAULT_FILTER_OPTIONS, - setFilterRefetch, hiddenStatuses, availableSolutions, displayCreateCaseButton, @@ -71,19 +68,9 @@ const CasesTableFiltersComponent = ({ const [selectedTags, setSelectedTags] = useState(initial.tags); const [selectedOwner, setSelectedOwner] = useState([]); const [selectedAssignees, setSelectedAssignees] = useState([]); - const { data: tags = [], refetch: fetchTags } = useGetTags(CASE_LIST_CACHE_KEY); + const { data: tags = [] } = useGetTags(); const { caseAssignmentAuthorized } = useCasesFeatures(); - const refetch = useCallback(() => { - fetchTags(); - }, [fetchTags]); - - useEffect(() => { - if (setFilterRefetch != null) { - setFilterRefetch(refetch); - } - }, [refetch, setFilterRefetch]); - const handleSelectedAssignees = useCallback( (newAssignees: UserProfileWithAvatar[]) => { if (!isEqual(newAssignees, selectedAssignees)) { diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts index 0c4fe02993ea..96a683aee507 100644 --- a/x-pack/plugins/cases/public/components/all_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts @@ -71,10 +71,6 @@ export const CLOSED = i18n.translate('xpack.cases.caseTable.closed', { defaultMessage: 'Closed', }); -export const DELETE = i18n.translate('xpack.cases.caseTable.delete', { - defaultMessage: 'Delete', -}); - export const SELECT = i18n.translate('xpack.cases.caseTable.select', { defaultMessage: 'Select', }); @@ -134,3 +130,40 @@ export const TOTAL_ASSIGNEES_FILTERED = (total: number) => defaultMessage: '{total, plural, one {# assignee} other {# assignees}} filtered', values: { total }, }); + +export const CLOSED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.cases.containers.closedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Closed {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const REOPENED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.cases.containers.reopenedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Opened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const MARK_IN_PROGRESS_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.cases.containers.markInProgressCases', { + values: { caseTitle, totalCases }, + defaultMessage: + 'Marked {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}} as in progress', + }); diff --git a/x-pack/plugins/cases/public/components/all_cases/use_is_loading_cases.tsx b/x-pack/plugins/cases/public/components/all_cases/use_is_loading_cases.tsx new file mode 100644 index 000000000000..70f46464ed2f --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/use_is_loading_cases.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useIsMutating } from '@tanstack/react-query'; +import { casesMutationsKeys } from '../../containers/constants'; + +/** + * Returns true or false if any of the queries and mutations + * are fetching in the all cases page + */ + +export const useIsLoadingCases = (): boolean => { + const isDeletingCases = useIsMutating(casesMutationsKeys.deleteCases); + const isUpdatingCases = useIsMutating(casesMutationsKeys.updateCases); + return Boolean(isDeletingCases) || Boolean(isUpdatingCases); +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/use_on_refresh_cases.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_on_refresh_cases.test.tsx new file mode 100644 index 000000000000..cd077004b5cf --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/use_on_refresh_cases.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; +import { casesQueriesKeys } from '../../containers/constants'; +import { useRefreshCases } from './use_on_refresh_cases'; + +describe('useRefreshCases', () => { + let appMockRender: AppMockRenderer; + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + jest.clearAllMocks(); + }); + + it('should refresh data on refresh', async () => { + const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); + + const { result } = renderHook(() => useRefreshCases(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current(); + }); + + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.casesList()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.userProfiles()); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/use_on_refresh_cases.tsx b/x-pack/plugins/cases/public/components/all_cases/use_on_refresh_cases.tsx new file mode 100644 index 000000000000..b50bed282cce --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/use_on_refresh_cases.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { casesQueriesKeys } from '../../containers/constants'; + +/** + * Using react-query queryClient to invalidate all the + * cases table page cache namespace. + * + * This effectively clears the cache for all the cases table pages and + * forces the page to fetch all the data again. Including + * all cases, user profiles, statuses, metrics, tags, etc. + */ + +export const useRefreshCases = () => { + const queryClient = useQueryClient(); + return useCallback(() => { + queryClient.invalidateQueries(casesQueriesKeys.casesList()); + queryClient.invalidateQueries(casesQueriesKeys.tags()); + queryClient.invalidateQueries(casesQueriesKeys.userProfiles()); + }, [queryClient]); +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx index a6247cedd3fa..fdfcdc17d472 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; +import React, { FunctionComponent, useCallback, useState } from 'react'; import { EuiContextMenuPanel } from '@elastic/eui'; +import { CaseStatuses } from '../../../common'; import { UtilityBar, UtilityBarAction, @@ -15,88 +16,70 @@ import { UtilityBarText, } from '../utility_bar'; import * as i18n from './translations'; -import { Cases, Case, DeleteCase, FilterOptions } from '../../../common/ui/types'; +import { Cases, Case, FilterOptions } from '../../../common/ui/types'; import { getBulkItems } from '../bulk_actions'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; import { useUpdateCases } from '../../containers/use_bulk_update_case'; +import { useRefreshCases } from './use_on_refresh_cases'; -interface OwnProps { +interface Props { data: Cases; enableBulkActions: boolean; filterOptions: FilterOptions; - handleIsLoading: (a: boolean) => void; - refreshCases?: (a?: boolean) => void; selectedCases: Case[]; + deselectCases: () => void; } -type Props = OwnProps; +export const getStatusToasterMessage = (status: CaseStatuses, cases: Case[]): string => { + const totalCases = cases.length; + const caseTitle = totalCases === 1 ? cases[0].title : ''; + + if (status === CaseStatuses.open) { + return i18n.REOPENED_CASES({ totalCases, caseTitle }); + } else if (status === CaseStatuses['in-progress']) { + return i18n.MARK_IN_PROGRESS_CASES({ totalCases, caseTitle }); + } else if (status === CaseStatuses.closed) { + return i18n.CLOSED_CASES({ totalCases, caseTitle }); + } + + return ''; +}; export const CasesTableUtilityBar: FunctionComponent = ({ data, enableBulkActions = false, filterOptions, - handleIsLoading, - refreshCases, selectedCases, + deselectCases, }) => { - const [deleteCases, setDeleteCases] = useState([]); - - // Delete case - const { - dispatchResetIsDeleted, - handleOnDeleteConfirm, - handleToggleModal, - isLoading: isDeleting, - isDeleted, - isDisplayConfirmDeleteModal, - } = useDeleteCases(); + const [isModalVisible, setIsModalVisible] = useState(false); + const onCloseModal = useCallback(() => setIsModalVisible(false), []); + const refreshCases = useRefreshCases(); - // Update case - const { - dispatchResetIsUpdated, - isLoading: isUpdating, - isUpdated, - updateBulkStatus, - } = useUpdateCases(); + const { mutate: deleteCases } = useDeleteCases(); + const { mutate: updateCases } = useUpdateCases(); - useEffect(() => { - handleIsLoading(isDeleting); - }, [handleIsLoading, isDeleting]); + const toggleBulkDeleteModal = useCallback((cases: Case[]) => { + setIsModalVisible(true); + }, []); - useEffect(() => { - handleIsLoading(isUpdating); - }, [handleIsLoading, isUpdating]); - useEffect(() => { - if (isDeleted) { - if (refreshCases != null) refreshCases(); - dispatchResetIsDeleted(); - } - if (isUpdated) { - if (refreshCases != null) refreshCases(); - dispatchResetIsUpdated(); - } - }, [isDeleted, isUpdated, refreshCases, dispatchResetIsDeleted, dispatchResetIsUpdated]); - - const toggleBulkDeleteModal = useCallback( - (cases: Case[]) => { - handleToggleModal(); - - const convertToDeleteCases: DeleteCase[] = cases.map(({ id, title }) => ({ - id, - title, + const handleUpdateCaseStatus = useCallback( + (status: CaseStatuses) => { + const casesToUpdate = selectedCases.map((theCase) => ({ + status, + id: theCase.id, + version: theCase.version, })); - setDeleteCases(convertToDeleteCases); - }, - [setDeleteCases, handleToggleModal] - ); - const handleUpdateCaseStatus = useCallback( - (status: string) => { - updateBulkStatus(selectedCases, status); + updateCases({ + cases: casesToUpdate, + successToasterTitle: getStatusToasterMessage(status, selectedCases), + }); }, - [selectedCases, updateBulkStatus] + [selectedCases, updateCases] ); + const getBulkItemsPopoverContent = useCallback( (closePopover: () => void) => ( = ({ [selectedCases, filterOptions.status, toggleBulkDeleteModal, handleUpdateCaseStatus] ); + const onConfirmDeletion = useCallback(() => { + setIsModalVisible(false); + deleteCases({ + caseIds: selectedCases.map(({ id }) => id), + successToasterTitle: i18n.DELETED_CASES(selectedCases.length), + }); + }, [deleteCases, selectedCases]); + + const onRefresh = useCallback(() => { + deselectCases(); + refreshCases(); + }, [deselectCases, refreshCases]); + return ( @@ -141,20 +137,20 @@ export const CasesTableUtilityBar: FunctionComponent = ({ {i18n.REFRESH} - + {isModalVisible ? ( + + ) : null} ); }; diff --git a/x-pack/plugins/cases/public/components/bulk_actions/index.tsx b/x-pack/plugins/cases/public/components/bulk_actions/index.tsx index 8cf3a14ffd3c..fcf2002f8882 100644 --- a/x-pack/plugins/cases/public/components/bulk_actions/index.tsx +++ b/x-pack/plugins/cases/public/components/bulk_actions/index.tsx @@ -19,7 +19,7 @@ interface GetBulkItems { closePopover: () => void; deleteCasesAction: (cases: Case[]) => void; selectedCases: Case[]; - updateCaseStatus: (status: string) => void; + updateCaseStatus: (status: CaseStatuses) => void; } export const getBulkItems = ({ diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx index 8735b94d71b2..cf15d546415b 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx @@ -8,13 +8,14 @@ import React from 'react'; import { mount } from 'enzyme'; -import { useDeleteCases } from '../../containers/use_delete_cases'; import { noDeleteCasesPermissions, TestProviders } from '../../common/mock'; import { basicCase, basicPush } from '../../containers/mock'; import { Actions } from './actions'; import * as i18n from '../case_view/translations'; -jest.mock('../../containers/use_delete_cases'); -const useDeleteCasesMock = useDeleteCases as jest.Mock; +import * as api from '../../containers/api'; +import { waitFor } from '@testing-library/dom'; + +jest.mock('../../containers/api'); jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -26,6 +27,7 @@ jest.mock('react-router-dom', () => { }), }; }); + const defaultProps = { allCasesNavigation: { href: 'all-cases-href', @@ -34,23 +36,10 @@ const defaultProps = { caseData: basicCase, currentExternalIncident: null, }; -describe('CaseView actions', () => { - const handleOnDeleteConfirm = jest.fn(); - const handleToggleModal = jest.fn(); - const dispatchResetIsDeleted = jest.fn(); - const defaultDeleteState = { - dispatchResetIsDeleted, - handleToggleModal, - handleOnDeleteConfirm, - isLoading: false, - isError: false, - isDeleted: false, - isDisplayConfirmDeleteModal: false, - }; +describe('CaseView actions', () => { beforeEach(() => { jest.resetAllMocks(); - useDeleteCasesMock.mockImplementation(() => defaultDeleteState); }); it('clicking trash toggles modal', () => { @@ -61,10 +50,9 @@ describe('CaseView actions', () => { ); expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy(); - wrapper.find('button[data-test-subj="property-actions-ellipses"]').first().simulate('click'); wrapper.find('button[data-test-subj="property-actions-trash"]').simulate('click'); - expect(handleToggleModal).toHaveBeenCalled(); + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); }); it('does not show trash icon when user does not have deletion privileges', () => { @@ -78,22 +66,26 @@ describe('CaseView actions', () => { expect(wrapper.find('button[data-test-subj="property-actions-ellipses"]').exists()).toBeFalsy(); }); - it('toggle delete modal and confirm', () => { - useDeleteCasesMock.mockImplementation(() => ({ - ...defaultDeleteState, - isDisplayConfirmDeleteModal: true, - })); + it('toggle delete modal and confirm', async () => { + const deleteCasesSpy = jest + .spyOn(api, 'deleteCases') + .mockRejectedValue(new Error('useDeleteCases: Test error')); + const wrapper = mount( ); + wrapper.find('button[data-test-subj="property-actions-ellipses"]').first().simulate('click'); + wrapper.find('button[data-test-subj="property-actions-trash"]').simulate('click'); + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([ - { id: basicCase.id, title: basicCase.title }, - ]); + + await waitFor(() => { + expect(deleteCasesSpy).toHaveBeenCalledWith(['basic-case-id'], expect.anything()); + }); }); it('displays active incident link', () => { diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx index 975cf89fc103..2481822f76b8 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx @@ -6,7 +6,7 @@ */ import { isEmpty } from 'lodash/fp'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { EuiFlexItem } from '@elastic/eui'; import * as i18n from '../case_view/translations'; import { useDeleteCases } from '../../containers/use_delete_cases'; @@ -23,11 +23,18 @@ interface CaseViewActions { } const ActionsComponent: React.FC = ({ caseData, currentExternalIncident }) => { - // Delete case - const { handleToggleModal, handleOnDeleteConfirm, isDeleted, isDisplayConfirmDeleteModal } = - useDeleteCases(); + const { mutate: deleteCases } = useDeleteCases(); const { navigateToAllCases } = useAllCasesNavigation(); const { permissions } = useCasesContext(); + const [isModalVisible, setIsModalVisible] = useState(false); + + const openModal = useCallback(() => { + setIsModalVisible(true); + }, []); + + const closeModal = useCallback(() => { + setIsModalVisible(false); + }, []); const propertyActions = useMemo( () => [ @@ -36,7 +43,7 @@ const ActionsComponent: React.FC = ({ caseData, currentExternal { iconType: 'trash', label: i18n.DELETE_CASE(), - onClick: handleToggleModal, + onClick: openModal, }, ] : []), @@ -50,13 +57,16 @@ const ActionsComponent: React.FC = ({ caseData, currentExternal ] : []), ], - [handleToggleModal, currentExternalIncident, permissions.delete] + [permissions.delete, openModal, currentExternalIncident] ); - if (isDeleted) { - navigateToAllCases(); - return null; - } + const onConfirmDeletion = useCallback(() => { + setIsModalVisible(false); + deleteCases( + { caseIds: [caseData.id], successToasterTitle: i18n.DELETED_CASES(1) }, + { onSuccess: navigateToAllCases } + ); + }, [caseData.id, deleteCases, navigateToAllCases]); if (propertyActions.length === 0) { return null; @@ -65,12 +75,13 @@ const ActionsComponent: React.FC = ({ caseData, currentExternal return ( - + {isModalVisible ? ( + + ) : null} ); }; diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index 20cf2c8d0d9b..6a5fd8fad811 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -30,7 +30,7 @@ import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; import CaseView from '.'; import { waitFor } from '@testing-library/dom'; import { useGetTags } from '../../containers/use_get_tags'; -import { CASE_VIEW_CACHE_KEY } from '../../containers/constants'; +import { casesQueriesKeys } from '../../containers/constants'; import { alertsHit, caseViewProps, @@ -173,7 +173,8 @@ describe('CaseView', () => { const queryClientSpy = jest.spyOn(appMockRenderer.queryClient, 'invalidateQueries'); const result = appMockRenderer.render(); userEvent.click(result.getByTestId('case-refresh')); - expect(queryClientSpy).toHaveBeenCalledWith(['case']); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.caseView()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); }); describe('when a `refreshRef` prop is provided', () => { @@ -205,7 +206,8 @@ describe('CaseView', () => { it('should refresh actions and comments', async () => { refreshRef!.current!.refreshCase(); await waitFor(() => { - expect(queryClientSpy).toHaveBeenCalledWith([CASE_VIEW_CACHE_KEY]); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.caseView()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); }); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/use_on_refresh_case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/use_on_refresh_case_view_page.tsx index 396f180a074a..d12ddc258e76 100644 --- a/x-pack/plugins/cases/public/components/case_view/use_on_refresh_case_view_page.tsx +++ b/x-pack/plugins/cases/public/components/case_view/use_on_refresh_case_view_page.tsx @@ -7,7 +7,7 @@ import { useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; -import { CASE_TAGS_CACHE_KEY, CASE_VIEW_CACHE_KEY } from '../../containers/constants'; +import { casesQueriesKeys } from '../../containers/constants'; /** * Using react-query queryClient to invalidate all the @@ -17,10 +17,11 @@ import { CASE_TAGS_CACHE_KEY, CASE_VIEW_CACHE_KEY } from '../../containers/const * forces the page to fetch all the data again. Including * metrics, actions, comments, etc. */ + export const useRefreshCaseViewPage = () => { const queryClient = useQueryClient(); return useCallback(() => { - queryClient.invalidateQueries([CASE_VIEW_CACHE_KEY]); - queryClient.invalidateQueries([CASE_TAGS_CACHE_KEY]); + queryClient.invalidateQueries(casesQueriesKeys.caseView()); + queryClient.invalidateQueries(casesQueriesKeys.tags()); }, [queryClient]); }; diff --git a/x-pack/plugins/cases/public/components/confirm_delete_case/index.tsx b/x-pack/plugins/cases/public/components/confirm_delete_case/index.tsx index ce8287310fb1..e1c4e0c1fa02 100644 --- a/x-pack/plugins/cases/public/components/confirm_delete_case/index.tsx +++ b/x-pack/plugins/cases/public/components/confirm_delete_case/index.tsx @@ -10,35 +10,28 @@ import { EuiConfirmModal } from '@elastic/eui'; import * as i18n from './translations'; interface ConfirmDeleteCaseModalProps { - caseTitle: string; - isModalVisible: boolean; - caseQuantity?: number; + totalCasesToBeDeleted: number; onCancel: () => void; onConfirm: () => void; } const ConfirmDeleteCaseModalComp: React.FC = ({ - caseTitle, - isModalVisible, - caseQuantity = 1, + totalCasesToBeDeleted, onCancel, onConfirm, }) => { - if (!isModalVisible) { - return null; - } return ( - {i18n.CONFIRM_QUESTION(caseQuantity)} + {i18n.CONFIRM_QUESTION(totalCasesToBeDeleted)} ); }; diff --git a/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts b/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts index f8e4ab2a83a7..e6f5072f08f8 100644 --- a/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts +++ b/x-pack/plugins/cases/public/components/confirm_delete_case/translations.ts @@ -14,12 +14,6 @@ export const DELETE_TITLE = (caseTitle: string) => defaultMessage: 'Delete "{caseTitle}"', }); -export const DELETE_SELECTED_CASES = (quantity: number, title: string) => - i18n.translate('xpack.cases.confirmDeleteCase.selectedCases', { - values: { quantity, title }, - defaultMessage: 'Delete "{quantity, plural, =1 {{title}} other {Selected {quantity} cases}}"', - }); - export const CONFIRM_QUESTION = (quantity: number) => i18n.translate('xpack.cases.confirmDeleteCase.confirmQuestion', { values: { quantity }, diff --git a/x-pack/plugins/cases/public/components/user_profiles/no_matches.test.tsx b/x-pack/plugins/cases/public/components/user_profiles/no_matches.test.tsx index 3471aad3fec3..e2d23dfd988e 100644 --- a/x-pack/plugins/cases/public/components/user_profiles/no_matches.test.tsx +++ b/x-pack/plugins/cases/public/components/user_profiles/no_matches.test.tsx @@ -13,6 +13,6 @@ describe('NoMatches', () => { it('renders the no matches messages', () => { render(); - expect(screen.getByText('No matching users with required access.')); + expect(screen.getByText("User doesn't exist or is unavailable")); }); }); diff --git a/x-pack/plugins/cases/public/components/user_profiles/no_matches.tsx b/x-pack/plugins/cases/public/components/user_profiles/no_matches.tsx index 638d705fade8..ccd27b123f23 100644 --- a/x-pack/plugins/cases/public/components/user_profiles/no_matches.tsx +++ b/x-pack/plugins/cases/public/components/user_profiles/no_matches.tsx @@ -5,7 +5,15 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiText, EuiTextAlign } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiSpacer, + EuiText, + EuiTextAlign, +} from '@elastic/eui'; import React from 'react'; import * as i18n from './translations'; @@ -25,11 +33,18 @@ const NoMatchesComponent: React.FC = () => { - {i18n.NO_MATCHING_USERS} + {i18n.USER_DOES_NOT_EXIST}
- {i18n.TRY_MODIFYING_SEARCH} + {i18n.MODIFY_SEARCH} +
+ + {i18n.LEARN_PRIVILEGES_GRANT_ACCESS} +
diff --git a/x-pack/plugins/cases/public/components/user_profiles/translations.ts b/x-pack/plugins/cases/public/components/user_profiles/translations.ts index db76408759be..2624ec834cd2 100644 --- a/x-pack/plugins/cases/public/components/user_profiles/translations.ts +++ b/x-pack/plugins/cases/public/components/user_profiles/translations.ts @@ -44,12 +44,19 @@ export const ASSIGNEES = i18n.translate('xpack.cases.userProfile.assigneesTitle' defaultMessage: 'Assignees', }); -export const NO_MATCHING_USERS = i18n.translate('xpack.cases.userProfiles.noMatchingUsers', { - defaultMessage: 'No matching users with required access.', +export const USER_DOES_NOT_EXIST = i18n.translate('xpack.cases.userProfiles.userDoesNotExist', { + defaultMessage: "User doesn't exist or is unavailable", }); -export const TRY_MODIFYING_SEARCH = i18n.translate('xpack.cases.userProfiles.tryModifyingSearch', { - defaultMessage: 'Try modifying your search.', +export const LEARN_PRIVILEGES_GRANT_ACCESS = i18n.translate( + 'xpack.cases.userProfiles.learnPrivileges', + { + defaultMessage: 'Learn what privileges grant access to cases.', + } +); + +export const MODIFY_SEARCH = i18n.translate('xpack.cases.userProfiles.modifySearch', { + defaultMessage: "Modify your search or check the user's privileges.", }); export const INVALID_ASSIGNEES = i18n.translate('xpack.cases.create.invalidAssignees', { diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts index 2b4313512308..d01f927d3c32 100644 --- a/x-pack/plugins/cases/public/containers/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -8,7 +8,6 @@ import { ActionLicense, Cases, - BulkUpdateStatus, Case, CasesStatus, CaseUserActions, @@ -28,7 +27,7 @@ import { pushedCase, tags, } from '../mock'; -import { ResolvedCase, SeverityAll } from '../../../common/ui/types'; +import { CaseUpdateRequest, ResolvedCase, SeverityAll } from '../../../common/ui/types'; import { CasePatchRequest, CasePostRequest, @@ -99,8 +98,8 @@ export const patchCase = async ( signal: AbortSignal ): Promise => Promise.resolve([basicCase]); -export const patchCasesStatus = async ( - cases: BulkUpdateStatus[], +export const updateCases = async ( + cases: CaseUpdateRequest[], signal: AbortSignal ): Promise => Promise.resolve(allCases.cases); diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index e51224dc593d..d5618ff8a7e9 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -25,7 +25,7 @@ import { getCaseUserActions, getTags, patchCase, - patchCasesStatus, + updateCases, patchComment, postCase, createAttachments, @@ -449,7 +449,7 @@ describe('Cases API', () => { }); }); - describe('patchCasesStatus', () => { + describe('updateCases', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(casesSnake); @@ -464,7 +464,7 @@ describe('Cases API', () => { ]; test('should be called with correct check url, method, signal', async () => { - await patchCasesStatus(data, abortCtrl.signal); + await updateCases(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'PATCH', body: JSON.stringify({ cases: data }), @@ -473,7 +473,7 @@ describe('Cases API', () => { }); test('should return correct response should not covert to camel case registered attachments', async () => { - const resp = await patchCasesStatus(data, abortCtrl.signal); + const resp = await updateCases(data, abortCtrl.signal); expect(resp).toEqual(cases); }); }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 697635030eb4..ff8d05ef653d 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -10,6 +10,7 @@ import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common/const import { isEmpty } from 'lodash'; import { Cases, + CaseUpdateRequest, FetchCasesProps, ResolvedCase, SeverityAll, @@ -58,7 +59,6 @@ import { import { ActionLicense, - BulkUpdateStatus, Case, SingleCaseMetrics, SingleCaseMetricsFeature, @@ -238,8 +238,8 @@ export const patchCase = async ( return convertCasesToCamelCase(decodeCasesResponse(response)); }; -export const patchCasesStatus = async ( - cases: BulkUpdateStatus[], +export const updateCases = async ( + cases: CaseUpdateRequest[], signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch(CASES_URL, { diff --git a/x-pack/plugins/cases/public/containers/configure/use_action_types.tsx b/x-pack/plugins/cases/public/containers/configure/use_action_types.tsx index 76a48e5d09ca..8503ccd6eddf 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_action_types.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_action_types.tsx @@ -9,13 +9,13 @@ import { useQuery } from '@tanstack/react-query'; import * as i18n from '../translations'; import { fetchActionTypes } from './api'; import { useToasts } from '../../common/lib/kibana'; -import { CASE_CONFIGURATION_CACHE_KEY } from '../constants'; +import { casesQueriesKeys } from '../constants'; import { ServerError } from '../../types'; export const useGetActionTypes = () => { const toasts = useToasts(); return useQuery( - [CASE_CONFIGURATION_CACHE_KEY, 'actionTypes'], + casesQueriesKeys.connectorTypes(), () => { const abortController = new AbortController(); return fetchActionTypes({ signal: abortController.signal }); diff --git a/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx b/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx index 95124af988fb..5e96dd86ae98 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx @@ -9,14 +9,14 @@ import { useQuery } from '@tanstack/react-query'; import { fetchConnectors } from './api'; import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana'; import * as i18n from './translations'; -import { CASE_CONNECTORS_CACHE_KEY } from '../constants'; +import { casesQueriesKeys } from '../constants'; import { ServerError } from '../../types'; export function useGetConnectors() { const toasts = useToasts(); const { actions } = useApplicationCapabilities(); return useQuery( - [CASE_CONNECTORS_CACHE_KEY], + casesQueriesKeys.connectorsList(), async () => { if (!actions.read) { return []; diff --git a/x-pack/plugins/cases/public/containers/constants.ts b/x-pack/plugins/cases/public/containers/constants.ts index a87d77330344..a6acbbd68d41 100644 --- a/x-pack/plugins/cases/public/containers/constants.ts +++ b/x-pack/plugins/cases/public/containers/constants.ts @@ -5,23 +5,36 @@ * 2.0. */ +import { SingleCaseMetricsFeature } from './types'; + export const DEFAULT_TABLE_ACTIVE_PAGE = 1; export const DEFAULT_TABLE_LIMIT = 5; -export const CASE_VIEW_CACHE_KEY = 'case'; -export const CASE_VIEW_ACTIONS_CACHE_KEY = 'user-actions'; -export const CASE_VIEW_METRICS_CACHE_KEY = 'metrics'; -export const CASE_CONFIGURATION_CACHE_KEY = 'case-configuration'; -export const CASE_LIST_CACHE_KEY = 'case-list'; -export const CASE_CONNECTORS_CACHE_KEY = 'case-connectors'; -export const CASE_LICENSE_CACHE_KEY = 'case-license-action'; -export const CASE_TAGS_CACHE_KEY = 'case-tags'; - -/** - * User profiles - */ +export const casesQueriesKeys = { + all: ['cases'] as const, + users: ['users'] as const, + connectors: ['connectors'] as const, + connectorsList: () => [...casesQueriesKeys.connectors, 'list'] as const, + casesList: () => [...casesQueriesKeys.all, 'list'] as const, + casesMetrics: () => [...casesQueriesKeys.casesList(), 'metrics'] as const, + casesStatuses: () => [...casesQueriesKeys.casesList(), 'statuses'] as const, + cases: (params: unknown) => [...casesQueriesKeys.casesList(), 'all-cases', params] as const, + caseView: () => [...casesQueriesKeys.all, 'case'] as const, + case: (id: string) => [...casesQueriesKeys.caseView(), id] as const, + caseMetrics: (id: string, features: SingleCaseMetricsFeature[]) => + [...casesQueriesKeys.case(id), 'metrics', features] as const, + userActions: (id: string, connectorId: string) => + [...casesQueriesKeys.case(id), 'user-actions', connectorId] as const, + userProfiles: () => [...casesQueriesKeys.users, 'user-profiles'] as const, + userProfilesList: (ids: string[]) => [...casesQueriesKeys.userProfiles(), ids] as const, + currentUser: () => [...casesQueriesKeys.users, 'current-user'] as const, + suggestUsers: (params: unknown) => [...casesQueriesKeys.users, 'suggest', params] as const, + connectorTypes: () => [...casesQueriesKeys.connectors, 'types'] as const, + license: () => [...casesQueriesKeys.connectors, 'license'] as const, + tags: () => [...casesQueriesKeys.all, 'tags'] as const, +}; -export const USER_PROFILES_CACHE_KEY = 'user-profiles'; -export const USER_PROFILES_SUGGEST_CACHE_KEY = 'suggest'; -export const USER_PROFILES_BULK_GET_CACHE_KEY = 'bulk-get'; -export const USER_PROFILES_GET_CURRENT_CACHE_KEY = 'get-current'; +export const casesMutationsKeys = { + deleteCases: ['delete-cases'] as const, + updateCases: ['update-cases'] as const, +}; diff --git a/x-pack/plugins/cases/public/containers/translations.ts b/x-pack/plugins/cases/public/containers/translations.ts index 72aeb66772c5..5bf4acf385fc 100644 --- a/x-pack/plugins/cases/public/containers/translations.ts +++ b/x-pack/plugins/cases/public/containers/translations.ts @@ -23,48 +23,9 @@ export const UPDATED_CASE = (caseTitle: string) => defaultMessage: 'Updated "{caseTitle}"', }); -export const DELETED_CASES = (totalCases: number, caseTitle?: string) => - i18n.translate('xpack.cases.containers.deletedCases', { - values: { caseTitle, totalCases }, - defaultMessage: 'Deleted {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', - }); - -export const CLOSED_CASES = ({ - totalCases, - caseTitle, -}: { - totalCases: number; - caseTitle?: string; -}) => - i18n.translate('xpack.cases.containers.closedCases', { - values: { caseTitle, totalCases }, - defaultMessage: 'Closed {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', - }); - -export const REOPENED_CASES = ({ - totalCases, - caseTitle, -}: { - totalCases: number; - caseTitle?: string; -}) => - i18n.translate('xpack.cases.containers.reopenedCases', { - values: { caseTitle, totalCases }, - defaultMessage: 'Opened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', - }); - -export const MARK_IN_PROGRESS_CASES = ({ - totalCases, - caseTitle, -}: { - totalCases: number; - caseTitle?: string; -}) => - i18n.translate('xpack.cases.containers.markInProgressCases', { - values: { caseTitle, totalCases }, - defaultMessage: - 'Marked {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}} as in progress', - }); +export const UPDATED_CASES = i18n.translate('xpack.cases.containers.updatedCases', { + defaultMessage: 'Updated cases', +}); export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = (serviceName: string) => i18n.translate('xpack.cases.containers.pushToExternalService', { diff --git a/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx b/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx index d00b361828a6..d46f79569622 100644 --- a/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx @@ -6,126 +6,89 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { CaseStatuses } from '../../common/api'; -import { useUpdateCases, UseUpdateCases } from './use_bulk_update_case'; -import { basicCase } from './mock'; +import { useUpdateCases } from './use_bulk_update_case'; +import { allCases } from './mock'; +import { useToasts } from '../common/lib/kibana'; import * as api from './api'; +import { createAppMockRenderer, AppMockRenderer } from '../common/mock'; +import { casesQueriesKeys } from './constants'; jest.mock('./api'); jest.mock('../common/lib/kibana'); describe('useUpdateCases', () => { const abortCtrl = new AbortController(); + const addSuccess = jest.fn(); + const addError = jest.fn(); + + (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); + + let appMockRender: AppMockRenderer; + beforeEach(() => { + appMockRender = createAppMockRenderer(); jest.clearAllMocks(); - jest.restoreAllMocks(); }); - it('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateCases() - ); - await waitForNextUpdate(); - expect(result.current).toEqual({ - isLoading: false, - isError: false, - isUpdated: false, - updateBulkStatus: result.current.updateBulkStatus, - dispatchResetIsUpdated: result.current.dispatchResetIsUpdated, - }); + it('calls the api when invoked with the correct parameters', async () => { + const spy = jest.spyOn(api, 'updateCases'); + const { waitForNextUpdate, result } = renderHook(() => useUpdateCases(), { + wrapper: appMockRender.AppWrapper, }); - }); - it('calls patchCase with correct arguments', async () => { - const spyOnPatchCases = jest.spyOn(api, 'patchCasesStatus'); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateCases() - ); - await waitForNextUpdate(); - - result.current.updateBulkStatus([basicCase], CaseStatuses.closed); - await waitForNextUpdate(); - expect(spyOnPatchCases).toBeCalledWith( - [ - { - status: CaseStatuses.closed, - id: basicCase.id, - version: basicCase.version, - }, - ], - abortCtrl.signal - ); + act(() => { + result.current.mutate({ cases: allCases.cases, successToasterTitle: 'Success title' }); }); - }); - it('patch cases', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateCases() - ); - await waitForNextUpdate(); - result.current.updateBulkStatus([basicCase], CaseStatuses.closed); - await waitForNextUpdate(); - expect(result.current).toEqual({ - isUpdated: true, - isLoading: false, - isError: false, - updateBulkStatus: result.current.updateBulkStatus, - dispatchResetIsUpdated: result.current.dispatchResetIsUpdated, - }); - }); + await waitForNextUpdate(); + + expect(spy).toHaveBeenCalledWith(allCases.cases, abortCtrl.signal); }); - it('set isLoading to true when posting case', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateCases() - ); - await waitForNextUpdate(); - result.current.updateBulkStatus([basicCase], CaseStatuses.closed); + it('invalidates the queries correctly', async () => { + const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); + const { waitForNextUpdate, result } = renderHook(() => useUpdateCases(), { + wrapper: appMockRender.AppWrapper, + }); - expect(result.current.isLoading).toBe(true); + act(() => { + result.current.mutate({ cases: allCases.cases, successToasterTitle: 'Success title' }); }); + + await waitForNextUpdate(); + + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.casesList()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.userProfiles()); }); - it('dispatchResetIsUpdated resets is updated', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateCases() - ); - - await waitForNextUpdate(); - result.current.updateBulkStatus([basicCase], CaseStatuses.closed); - await waitForNextUpdate(); - expect(result.current.isUpdated).toBeTruthy(); - result.current.dispatchResetIsUpdated(); - expect(result.current.isUpdated).toBeFalsy(); + it('shows a success toaster', async () => { + const { waitForNextUpdate, result } = renderHook(() => useUpdateCases(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.mutate({ cases: allCases.cases, successToasterTitle: 'Success title' }); }); + + await waitForNextUpdate(); + + expect(addSuccess).toHaveBeenCalledWith('Success title'); }); - it('unhappy path', async () => { - const spyOnPatchCases = jest.spyOn(api, 'patchCasesStatus'); - spyOnPatchCases.mockImplementation(() => { - throw new Error('Something went wrong'); + it('shows a toast error when the api return an error', async () => { + jest.spyOn(api, 'updateCases').mockRejectedValue(new Error('useUpdateCases: Test error')); + + const { waitForNextUpdate, result } = renderHook(() => useUpdateCases(), { + wrapper: appMockRender.AppWrapper, }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useUpdateCases() - ); - await waitForNextUpdate(); - result.current.updateBulkStatus([basicCase], CaseStatuses.closed); - - expect(result.current).toEqual({ - isUpdated: false, - isLoading: false, - isError: true, - updateBulkStatus: result.current.updateBulkStatus, - dispatchResetIsUpdated: result.current.dispatchResetIsUpdated, - }); + act(() => { + result.current.mutate({ cases: allCases.cases, successToasterTitle: 'Success title' }); }); + + await waitForNextUpdate(); + + expect(addError).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx b/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx index 715b0c611c3b..e0866bf0166a 100644 --- a/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx +++ b/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx @@ -5,149 +5,42 @@ * 2.0. */ -import { useCallback, useReducer, useRef, useEffect } from 'react'; -import { CaseStatuses } from '../../common/api'; +import { useQueryClient, useMutation } from '@tanstack/react-query'; import * as i18n from './translations'; -import { patchCasesStatus } from './api'; -import { BulkUpdateStatus, Case } from './types'; -import { useToasts } from '../common/lib/kibana'; - -interface UpdateState { - isUpdated: boolean; - isLoading: boolean; - isError: boolean; -} -type Action = - | { type: 'FETCH_INIT' } - | { type: 'FETCH_SUCCESS'; payload: boolean } - | { type: 'FETCH_FAILURE' } - | { type: 'RESET_IS_UPDATED' }; - -const dataFetchReducer = (state: UpdateState, action: Action): UpdateState => { - switch (action.type) { - case 'FETCH_INIT': - return { - ...state, - isLoading: true, - isError: false, - }; - case 'FETCH_SUCCESS': - return { - ...state, - isLoading: false, - isError: false, - isUpdated: action.payload, - }; - case 'FETCH_FAILURE': - return { - ...state, - isLoading: false, - isError: true, - }; - case 'RESET_IS_UPDATED': - return { - ...state, - isUpdated: false, - }; - default: - return state; - } -}; -export interface UseUpdateCases extends UpdateState { - updateBulkStatus: (cases: Case[], status: string) => void; - dispatchResetIsUpdated: () => void; +import { updateCases } from './api'; +import { CaseUpdateRequest } from './types'; +import { useCasesToast } from '../common/use_cases_toast'; +import { ServerError } from '../types'; +import { casesQueriesKeys, casesMutationsKeys } from './constants'; + +interface MutationArgs { + cases: CaseUpdateRequest[]; + successToasterTitle: string; } -const getStatusToasterMessage = ( - status: CaseStatuses, - messageArgs: { - totalCases: number; - caseTitle?: string; - } -): string => { - if (status === CaseStatuses.open) { - return i18n.REOPENED_CASES(messageArgs); - } else if (status === CaseStatuses['in-progress']) { - return i18n.MARK_IN_PROGRESS_CASES(messageArgs); - } else if (status === CaseStatuses.closed) { - return i18n.CLOSED_CASES(messageArgs); - } - - return ''; -}; - -export const useUpdateCases = (): UseUpdateCases => { - const [state, dispatch] = useReducer(dataFetchReducer, { - isLoading: false, - isError: false, - isUpdated: false, - }); - const toasts = useToasts(); - const isCancelledRef = useRef(false); - const abortCtrlRef = useRef(new AbortController()); - - const dispatchUpdateCases = useCallback(async (cases: BulkUpdateStatus[], action: string) => { - try { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - - dispatch({ type: 'FETCH_INIT' }); - const patchResponse = await patchCasesStatus(cases, abortCtrlRef.current.signal); - - if (!isCancelledRef.current) { - const resultCount = Object.keys(patchResponse).length; - const firstTitle = patchResponse[0].title; - - dispatch({ type: 'FETCH_SUCCESS', payload: true }); - const messageArgs = { - totalCases: resultCount, - caseTitle: resultCount === 1 ? firstTitle : '', - }; +export const useUpdateCases = () => { + const queryClient = useQueryClient(); + const { showErrorToast, showSuccessToast } = useCasesToast(); - const message = - action === 'status' ? getStatusToasterMessage(patchResponse[0].status, messageArgs) : ''; - - toasts.addSuccess(message); - } - } catch (error) { - if (!isCancelledRef.current) { - if (error.name !== 'AbortError') { - toasts.addError( - error.body && error.body.message ? new Error(error.body.message) : error, - { title: i18n.ERROR_TITLE } - ); - } - dispatch({ type: 'FETCH_FAILURE' }); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const dispatchResetIsUpdated = useCallback(() => { - dispatch({ type: 'RESET_IS_UPDATED' }); - }, []); - - const updateBulkStatus = useCallback( - (cases: Case[], status: string) => { - const updateCasesStatus: BulkUpdateStatus[] = cases.map((theCase) => ({ - status, - id: theCase.id, - version: theCase.version, - })); - dispatchUpdateCases(updateCasesStatus, 'status'); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - useEffect( - () => () => { - isCancelledRef.current = true; - abortCtrlRef.current.abort(); + return useMutation( + ({ cases }: MutationArgs) => { + const abortCtrlRef = new AbortController(); + return updateCases(cases, abortCtrlRef.signal); }, - [] + { + mutationKey: casesMutationsKeys.updateCases, + onSuccess: (_, { successToasterTitle }) => { + queryClient.invalidateQueries(casesQueriesKeys.casesList()); + queryClient.invalidateQueries(casesQueriesKeys.tags()); + queryClient.invalidateQueries(casesQueriesKeys.userProfiles()); + + showSuccessToast(successToasterTitle); + }, + onError: (error: ServerError) => { + showErrorToast(error, { title: i18n.ERROR_DELETING }); + }, + } ); - - return { ...state, updateBulkStatus, dispatchResetIsUpdated }; }; + +export type UseUpdateCases = ReturnType; diff --git a/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx index 88f6db42144f..623a01746e3c 100644 --- a/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx @@ -7,125 +7,88 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { useDeleteCases, UseDeleteCase } from './use_delete_cases'; +import { useDeleteCases } from './use_delete_cases'; import * as api from './api'; +import { useToasts } from '../common/lib/kibana'; +import { AppMockRenderer, createAppMockRenderer } from '../common/mock'; +import { casesQueriesKeys } from './constants'; jest.mock('./api'); jest.mock('../common/lib/kibana'); describe('useDeleteCases', () => { const abortCtrl = new AbortController(); - const deleteObj = [ - { id: '1', title: 'case 1' }, - { id: '2', title: 'case 2' }, - { id: '3', title: 'case 3' }, - ]; - const deleteArr = ['1', '2', '3']; - it('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useDeleteCases() - ); - await waitForNextUpdate(); - expect(result.current).toEqual({ - isDisplayConfirmDeleteModal: false, - isLoading: false, - isError: false, - isDeleted: false, - dispatchResetIsDeleted: result.current.dispatchResetIsDeleted, - handleOnDeleteConfirm: result.current.handleOnDeleteConfirm, - handleToggleModal: result.current.handleToggleModal, - }); - }); - }); + const addSuccess = jest.fn(); + const addError = jest.fn(); - it('calls deleteCases with correct arguments', async () => { - const spyOnDeleteCases = jest.spyOn(api, 'deleteCases'); + (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useDeleteCases() - ); - await waitForNextUpdate(); + let appMockRender: AppMockRenderer; - result.current.handleOnDeleteConfirm(deleteObj); - await waitForNextUpdate(); - expect(spyOnDeleteCases).toBeCalledWith(deleteArr, abortCtrl.signal); - }); + beforeEach(() => { + appMockRender = createAppMockRenderer(); + jest.clearAllMocks(); }); - it('deletes cases', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useDeleteCases() - ); - await waitForNextUpdate(); - result.current.handleToggleModal(); - result.current.handleOnDeleteConfirm(deleteObj); - await waitForNextUpdate(); - expect(result.current).toEqual({ - isDisplayConfirmDeleteModal: false, - isLoading: false, - isError: false, - isDeleted: true, - dispatchResetIsDeleted: result.current.dispatchResetIsDeleted, - handleOnDeleteConfirm: result.current.handleOnDeleteConfirm, - handleToggleModal: result.current.handleToggleModal, - }); + it('calls the api when invoked with the correct parameters', async () => { + const spy = jest.spyOn(api, 'deleteCases'); + const { waitForNextUpdate, result } = renderHook(() => useDeleteCases(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.mutate({ caseIds: ['1', '2'], successToasterTitle: 'Success title' }); }); + + await waitForNextUpdate(); + + expect(spy).toHaveBeenCalledWith(['1', '2'], abortCtrl.signal); }); - it('resets is deleting', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useDeleteCases() - ); - await waitForNextUpdate(); - result.current.handleToggleModal(); - result.current.handleOnDeleteConfirm(deleteObj); - await waitForNextUpdate(); - expect(result.current.isDeleted).toBeTruthy(); - result.current.handleToggleModal(); - result.current.dispatchResetIsDeleted(); - expect(result.current.isDeleted).toBeFalsy(); + it('invalidates the queries correctly', async () => { + const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); + const { waitForNextUpdate, result } = renderHook(() => useDeleteCases(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.mutate({ caseIds: ['1', '2'], successToasterTitle: 'Success title' }); }); + + await waitForNextUpdate(); + + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.casesList()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.userProfiles()); }); - it('set isLoading to true when deleting cases', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useDeleteCases() - ); - await waitForNextUpdate(); - result.current.handleToggleModal(); - result.current.handleOnDeleteConfirm(deleteObj); - expect(result.current.isLoading).toBe(true); + it('shows a success toaster', async () => { + const { waitForNextUpdate, result } = renderHook(() => useDeleteCases(), { + wrapper: appMockRender.AppWrapper, }); + + act(() => { + result.current.mutate({ caseIds: ['1', '2'], successToasterTitle: 'Success title' }); + }); + + await waitForNextUpdate(); + + expect(addSuccess).toHaveBeenCalledWith('Success title'); }); - it('unhappy path', async () => { - const spyOnDeleteCases = jest.spyOn(api, 'deleteCases'); - spyOnDeleteCases.mockImplementation(() => { - throw new Error('Something went wrong'); + it('shows a toast error when the api return an error', async () => { + jest.spyOn(api, 'deleteCases').mockRejectedValue(new Error('useDeleteCases: Test error')); + + const { waitForNextUpdate, result } = renderHook(() => useDeleteCases(), { + wrapper: appMockRender.AppWrapper, }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useDeleteCases() - ); - await waitForNextUpdate(); - result.current.handleToggleModal(); - result.current.handleOnDeleteConfirm(deleteObj); - - expect(result.current).toEqual({ - isDisplayConfirmDeleteModal: false, - isLoading: false, - isError: true, - isDeleted: false, - dispatchResetIsDeleted: result.current.dispatchResetIsDeleted, - handleOnDeleteConfirm: result.current.handleOnDeleteConfirm, - handleToggleModal: result.current.handleToggleModal, - }); + act(() => { + result.current.mutate({ caseIds: ['1', '2'], successToasterTitle: 'Success title' }); }); + + await waitForNextUpdate(); + + expect(addError).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_delete_cases.tsx b/x-pack/plugins/cases/public/containers/use_delete_cases.tsx index 7ccec4436ec0..da2258f8f5d8 100644 --- a/x-pack/plugins/cases/public/containers/use_delete_cases.tsx +++ b/x-pack/plugins/cases/public/containers/use_delete_cases.tsx @@ -5,139 +5,41 @@ * 2.0. */ -import { useCallback, useReducer, useRef, useEffect } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as i18n from './translations'; import { deleteCases } from './api'; -import { DeleteCase } from './types'; -import { useToasts } from '../common/lib/kibana'; +import { ServerError } from '../types'; +import { casesQueriesKeys, casesMutationsKeys } from './constants'; +import { useCasesToast } from '../common/use_cases_toast'; -interface DeleteState { - isDisplayConfirmDeleteModal: boolean; - isDeleted: boolean; - isLoading: boolean; - isError: boolean; +interface MutationArgs { + caseIds: string[]; + successToasterTitle: string; } -type Action = - | { type: 'DISPLAY_MODAL'; payload: boolean } - | { type: 'FETCH_INIT' } - | { type: 'FETCH_SUCCESS'; payload: boolean } - | { type: 'FETCH_FAILURE' } - | { type: 'RESET_IS_DELETED' }; -const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => { - switch (action.type) { - case 'DISPLAY_MODAL': - return { - ...state, - isDisplayConfirmDeleteModal: action.payload, - }; - case 'FETCH_INIT': - return { - ...state, - isLoading: true, - isError: false, - }; - case 'FETCH_SUCCESS': - return { - ...state, - isLoading: false, - isError: false, - isDeleted: action.payload, - }; - case 'FETCH_FAILURE': - return { - ...state, - isLoading: false, - isError: true, - }; - case 'RESET_IS_DELETED': - return { - ...state, - isDeleted: false, - }; - default: - return state; - } -}; - -export interface UseDeleteCase extends DeleteState { - dispatchResetIsDeleted: () => void; - handleOnDeleteConfirm: (cases: DeleteCase[]) => void; - handleToggleModal: () => void; -} - -export const useDeleteCases = (): UseDeleteCase => { - const [state, dispatch] = useReducer(dataFetchReducer, { - isDisplayConfirmDeleteModal: false, - isLoading: false, - isError: false, - isDeleted: false, - }); - const toasts = useToasts(); - const isCancelledRef = useRef(false); - const abortCtrlRef = useRef(new AbortController()); - - const dispatchDeleteCases = useCallback(async (cases: DeleteCase[]) => { - try { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - dispatch({ type: 'FETCH_INIT' }); - - const caseIds = cases.map((theCase) => theCase.id); - if (cases.length > 0) { - await deleteCases(caseIds, abortCtrlRef.current.signal); - } - - if (!isCancelledRef.current) { - dispatch({ type: 'FETCH_SUCCESS', payload: true }); - toasts.addSuccess( - i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : '') - ); - } - } catch (error) { - if (!isCancelledRef.current) { - if (error.name !== 'AbortError') { - toasts.addError( - error.body && error.body.message ? new Error(error.body.message) : error, - { title: i18n.ERROR_DELETING } - ); - } - dispatch({ type: 'FETCH_FAILURE' }); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); +export const useDeleteCases = () => { + const queryClient = useQueryClient(); + const { showErrorToast, showSuccessToast } = useCasesToast(); - const dispatchToggleDeleteModal = useCallback(() => { - dispatch({ type: 'DISPLAY_MODAL', payload: !state.isDisplayConfirmDeleteModal }); - }, [state.isDisplayConfirmDeleteModal]); - - const dispatchResetIsDeleted = useCallback(() => { - dispatch({ type: 'RESET_IS_DELETED' }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.isDisplayConfirmDeleteModal]); - - const handleOnDeleteConfirm = useCallback( - (cases: DeleteCase[]) => { - dispatchDeleteCases(cases); - dispatchToggleDeleteModal(); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [state.isDisplayConfirmDeleteModal] - ); - const handleToggleModal = useCallback(() => { - dispatchToggleDeleteModal(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.isDisplayConfirmDeleteModal]); - - useEffect( - () => () => { - isCancelledRef.current = true; - abortCtrlRef.current.abort(); + return useMutation( + ({ caseIds }: MutationArgs) => { + const abortCtrlRef = new AbortController(); + return deleteCases(caseIds, abortCtrlRef.signal); }, - [] + { + mutationKey: casesMutationsKeys.deleteCases, + onSuccess: (_, { successToasterTitle }) => { + queryClient.invalidateQueries(casesQueriesKeys.casesList()); + queryClient.invalidateQueries(casesQueriesKeys.tags()); + queryClient.invalidateQueries(casesQueriesKeys.userProfiles()); + + showSuccessToast(successToasterTitle); + }, + onError: (error: ServerError) => { + showErrorToast(error, { title: i18n.ERROR_DELETING }); + }, + } ); - - return { ...state, dispatchResetIsDeleted, handleOnDeleteConfirm, handleToggleModal }; }; + +export type UseDeleteCases = ReturnType; diff --git a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx index 8e9aa28de440..7f05012cbbe6 100644 --- a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx @@ -10,7 +10,7 @@ import { useToasts } from '../common/lib/kibana'; import { getActionLicense } from './api'; import * as i18n from './translations'; import { ConnectorTypes } from '../../common/api'; -import { CASE_LICENSE_CACHE_KEY } from './constants'; +import { casesQueriesKeys } from './constants'; import { ServerError } from '../types'; const MINIMUM_LICENSE_REQUIRED_CONNECTOR = ConnectorTypes.jira; @@ -18,7 +18,7 @@ const MINIMUM_LICENSE_REQUIRED_CONNECTOR = ConnectorTypes.jira; export const useGetActionLicense = () => { const toasts = useToasts(); return useQuery( - [CASE_LICENSE_CACHE_KEY], + casesQueriesKeys.license(), async () => { const abortCtrl = new AbortController(); const response = await getActionLicense(abortCtrl.signal); diff --git a/x-pack/plugins/cases/public/containers/use_get_case.tsx b/x-pack/plugins/cases/public/containers/use_get_case.tsx index ded91240239a..bf588cc1e71d 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.tsx @@ -11,12 +11,12 @@ import * as i18n from './translations'; import { useToasts } from '../common/lib/kibana'; import { resolveCase } from './api'; import { ServerError } from '../types'; -import { CASE_VIEW_CACHE_KEY } from './constants'; +import { casesQueriesKeys } from './constants'; export const useGetCase = (caseId: string) => { const toasts = useToasts(); return useQuery( - [CASE_VIEW_CACHE_KEY, caseId], + casesQueriesKeys.case(caseId), () => { const abortCtrlRef = new AbortController(); return resolveCase(caseId, true, abortCtrlRef.signal); diff --git a/x-pack/plugins/cases/public/containers/use_get_case_metrics.tsx b/x-pack/plugins/cases/public/containers/use_get_case_metrics.tsx index 1e294c4a5ba6..32d63fcc3b42 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_metrics.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_metrics.tsx @@ -11,13 +11,13 @@ import { useToasts } from '../common/lib/kibana'; import { getSingleCaseMetrics } from './api'; import { ServerError } from '../types'; import { ERROR_TITLE } from './translations'; -import { CASE_VIEW_CACHE_KEY, CASE_VIEW_METRICS_CACHE_KEY } from './constants'; +import { casesQueriesKeys } from './constants'; export const useGetCaseMetrics = (caseId: string, features: SingleCaseMetricsFeature[]) => { const toasts = useToasts(); const abortCtrlRef = new AbortController(); return useQuery( - [CASE_VIEW_CACHE_KEY, CASE_VIEW_METRICS_CACHE_KEY, caseId, features], + casesQueriesKeys.caseMetrics(caseId, features), async () => { const response: SingleCaseMetrics = await getSingleCaseMetrics( caseId, diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx index fde45207b673..c92d56b41ea7 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx @@ -20,7 +20,7 @@ import { import { ServerError } from '../types'; import { useToasts } from '../common/lib/kibana'; import { ERROR_TITLE } from './translations'; -import { CASE_VIEW_ACTIONS_CACHE_KEY, CASE_VIEW_CACHE_KEY } from './constants'; +import { casesQueriesKeys } from './constants'; export interface CaseService extends CaseExternalService { firstPushIndex: number; @@ -238,7 +238,7 @@ export const useGetCaseUserActions = (caseId: string, caseConnectorId: string) = const toasts = useToasts(); const abortCtrlRef = new AbortController(); return useQuery( - [CASE_VIEW_CACHE_KEY, CASE_VIEW_ACTIONS_CACHE_KEY, caseId, caseConnectorId], + casesQueriesKeys.userActions(caseId, caseConnectorId), async () => { const response = await getCaseUserActions(caseId, abortCtrlRef.signal); const participants = !isEmpty(response) diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.tsx index 7b046cac3f13..d630534957e5 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.tsx @@ -6,7 +6,7 @@ */ import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { CASE_LIST_CACHE_KEY, DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; +import { casesQueriesKeys, DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; import { Cases, FilterOptions, QueryParams, SortFieldCase, StatusAll, SeverityAll } from './types'; import { useToasts } from '../common/lib/kibana'; import * as i18n from './translations'; @@ -51,7 +51,7 @@ export const useGetCases = ( ): UseQueryResult => { const toasts = useToasts(); return useQuery( - [CASE_LIST_CACHE_KEY, 'cases', params], + casesQueriesKeys.cases(params), () => { const abortCtrl = new AbortController(); return getCases({ diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx index a8747a2bd43a..0b0cdc59a487 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx @@ -5,124 +5,56 @@ * 2.0. */ -import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; import * as api from '../api'; -import { TestProviders } from '../common/mock'; -import { useGetCasesMetrics, UseGetCasesMetrics } from './use_get_cases_metrics'; +import { AppMockRenderer, createAppMockRenderer } from '../common/mock'; +import { useGetCasesMetrics } from './use_get_cases_metrics'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; +import { useToasts } from '../common/lib/kibana'; jest.mock('../api'); jest.mock('../common/lib/kibana'); describe('useGetCasesMetrics', () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); + const abortCtrl = new AbortController(); + const addSuccess = jest.fn(); + const addError = jest.fn(); - it('init', async () => { - const { result } = renderHook(() => useGetCasesMetrics(), { - wrapper: ({ children }) => {children}, - }); + (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); - await act(async () => { - expect(result.current).toEqual({ - mttr: 0, - isLoading: true, - isError: false, - fetchCasesMetrics: result.current.fetchCasesMetrics, - }); - }); + let appMockRender: AppMockRenderer; + + beforeEach(() => { + appMockRender = createAppMockRenderer(); + jest.clearAllMocks(); }); - it('calls getCasesMetrics api', async () => { + it('calls the api when invoked with the correct parameters', async () => { const spy = jest.spyOn(api, 'getCasesMetrics'); - await act(async () => { - const { waitForNextUpdate } = renderHook( - () => useGetCasesMetrics(), - { - wrapper: ({ children }) => {children}, - } - ); - - await waitForNextUpdate(); - expect(spy).toBeCalledWith({ - http: expect.anything(), - signal: expect.anything(), - query: { - features: ['mttr'], - owner: [SECURITY_SOLUTION_OWNER], - }, - }); + const { waitForNextUpdate } = renderHook(() => useGetCasesMetrics(), { + wrapper: appMockRender.AppWrapper, }); - }); - it('fetch cases metrics', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => useGetCasesMetrics(), - { - wrapper: ({ children }) => {children}, - } - ); + await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual({ - mttr: 12, - isLoading: false, - isError: false, - fetchCasesMetrics: result.current.fetchCasesMetrics, - }); + expect(spy).toHaveBeenCalledWith({ + http: expect.anything(), + signal: abortCtrl.signal, + query: { owner: [SECURITY_SOLUTION_OWNER], features: ['mttr'] }, }); }); - it('fetches metrics when fetchCasesMetrics is invoked', async () => { - const spy = jest.spyOn(api, 'getCasesMetrics'); - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => useGetCasesMetrics(), - { - wrapper: ({ children }) => {children}, - } - ); - - await waitForNextUpdate(); - expect(spy).toBeCalledWith({ - http: expect.anything(), - signal: expect.anything(), - query: { - features: ['mttr'], - owner: [SECURITY_SOLUTION_OWNER], - }, - }); - result.current.fetchCasesMetrics(); - await waitForNextUpdate(); - expect(spy).toHaveBeenCalledTimes(2); - }); - }); + it('shows a toast error when the api return an error', async () => { + jest + .spyOn(api, 'getCasesMetrics') + .mockRejectedValue(new Error('useGetCasesMetrics: Test error')); - it('unhappy path', async () => { - const spy = jest.spyOn(api, 'getCasesMetrics'); - spy.mockImplementation(() => { - throw new Error('Oh on. this is impossible'); + const { waitForNextUpdate } = renderHook(() => useGetCasesMetrics(), { + wrapper: appMockRender.AppWrapper, }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => useGetCasesMetrics(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); + await waitForNextUpdate(); - expect(result.current).toEqual({ - mttr: 0, - isLoading: false, - isError: true, - fetchCasesMetrics: result.current.fetchCasesMetrics, - }); - }); + expect(addError).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx index a5cb116acc55..b43266e55340 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx @@ -5,88 +5,37 @@ * 2.0. */ -import { useCallback, useEffect, useState, useRef } from 'react'; - +import { useQuery } from '@tanstack/react-query'; import { useCasesContext } from '../components/cases_context/use_cases_context'; import * as i18n from './translations'; -import { useHttp, useToasts } from '../common/lib/kibana'; +import { useHttp } from '../common/lib/kibana'; import { getCasesMetrics } from '../api'; import { CasesMetrics } from './types'; +import { useCasesToast } from '../common/use_cases_toast'; +import { ServerError } from '../types'; +import { casesQueriesKeys } from './constants'; -interface CasesMetricsState extends CasesMetrics { - isLoading: boolean; - isError: boolean; -} - -const initialData: CasesMetricsState = { - mttr: 0, - isLoading: true, - isError: false, -}; - -export interface UseGetCasesMetrics extends CasesMetricsState { - fetchCasesMetrics: () => void; -} - -export const useGetCasesMetrics = (): UseGetCasesMetrics => { +export const useGetCasesMetrics = () => { const http = useHttp(); const { owner } = useCasesContext(); - const [casesMetricsState, setCasesMetricsState] = useState(initialData); - const toasts = useToasts(); - const isCancelledRef = useRef(false); - const abortCtrlRef = useRef(new AbortController()); - - const fetchCasesMetrics = useCallback(async () => { - try { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - setCasesMetricsState({ - ...initialData, - isLoading: true, - }); + const { showErrorToast } = useCasesToast(); - const response = await getCasesMetrics({ + return useQuery( + casesQueriesKeys.casesMetrics(), + () => { + const abortCtrlRef = new AbortController(); + return getCasesMetrics({ http, - signal: abortCtrlRef.current.signal, + signal: abortCtrlRef.signal, query: { owner, features: ['mttr'] }, }); - - if (!isCancelledRef.current) { - setCasesMetricsState({ - ...response, - isLoading: false, - isError: false, - }); - } - } catch (error) { - if (!isCancelledRef.current) { - if (error.name !== 'AbortError') { - toasts.addError( - error.body && error.body.message ? new Error(error.body.message) : error, - { title: i18n.ERROR_TITLE } - ); - } - setCasesMetricsState({ - mttr: 0, - isLoading: false, - isError: true, - }); - } + }, + { + onError: (error: ServerError) => { + showErrorToast(error, { title: i18n.ERROR_TITLE }); + }, } - }, [http, owner, toasts]); - - useEffect(() => { - fetchCasesMetrics(); - - return () => { - isCancelledRef.current = true; - abortCtrlRef.current.abort(); - }; - }, [fetchCasesMetrics]); - - return { - ...casesMetricsState, - fetchCasesMetrics, - }; + ); }; + +export type UseGetCasesMetrics = ReturnType; diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx index 3978e944db94..4f2572093a28 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx @@ -5,104 +5,56 @@ * 2.0. */ -import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { useGetCasesStatus, UseGetCasesStatus } from './use_get_cases_status'; -import { casesStatus } from './mock'; +import { renderHook } from '@testing-library/react-hooks'; +import { useGetCasesStatus } from './use_get_cases_status'; import * as api from '../api'; -import { TestProviders } from '../common/mock'; +import { AppMockRenderer, createAppMockRenderer } from '../common/mock'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; +import { useToasts } from '../common/lib/kibana'; jest.mock('../api'); jest.mock('../common/lib/kibana'); -describe('useGetCasesStatus', () => { +describe('useGetCasesMetrics', () => { const abortCtrl = new AbortController(); + const addSuccess = jest.fn(); + const addError = jest.fn(); + + (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); + + let appMockRender: AppMockRenderer; + beforeEach(() => { + appMockRender = createAppMockRenderer(); jest.clearAllMocks(); - jest.restoreAllMocks(); }); - it('init', async () => { - const { result } = renderHook(() => useGetCasesStatus(), { - wrapper: ({ children }) => {children}, - }); - - await act(async () => { - expect(result.current).toEqual({ - countClosedCases: 0, - countOpenCases: 0, - countInProgressCases: 0, - isLoading: true, - isError: false, - fetchCasesStatus: result.current.fetchCasesStatus, - }); + it('calls the api when invoked with the correct parameters', async () => { + const spy = jest.spyOn(api, 'getCasesStatus'); + const { waitForNextUpdate } = renderHook(() => useGetCasesStatus(), { + wrapper: appMockRender.AppWrapper, }); - }); - it('calls getCasesStatus api', async () => { - const spyOnGetCasesStatus = jest.spyOn(api, 'getCasesStatus'); - await act(async () => { - const { waitForNextUpdate } = renderHook( - () => useGetCasesStatus(), - { - wrapper: ({ children }) => {children}, - } - ); + await waitForNextUpdate(); - await waitForNextUpdate(); - expect(spyOnGetCasesStatus).toBeCalledWith({ - http: expect.anything(), - signal: abortCtrl.signal, - query: { owner: [SECURITY_SOLUTION_OWNER] }, - }); + expect(spy).toHaveBeenCalledWith({ + http: expect.anything(), + signal: abortCtrl.signal, + query: { owner: [SECURITY_SOLUTION_OWNER] }, }); }); - it('fetch statuses', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => useGetCasesStatus(), - { - wrapper: ({ children }) => {children}, - } - ); + it('shows a toast error when the api return an error', async () => { + jest + .spyOn(api, 'getCasesStatus') + .mockRejectedValue(new Error('useGetCasesMetrics: Test error')); - await waitForNextUpdate(); - expect(result.current).toEqual({ - countClosedCases: casesStatus.countClosedCases, - countOpenCases: casesStatus.countOpenCases, - countInProgressCases: casesStatus.countInProgressCases, - isLoading: false, - isError: false, - fetchCasesStatus: result.current.fetchCasesStatus, - }); + const { waitForNextUpdate } = renderHook(() => useGetCasesStatus(), { + wrapper: appMockRender.AppWrapper, }); - }); - it('unhappy path', async () => { - const spyOnGetCasesStatus = jest.spyOn(api, 'getCasesStatus'); - spyOnGetCasesStatus.mockImplementation(() => { - throw new Error('Something went wrong'); - }); + await waitForNextUpdate(); - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => useGetCasesStatus(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - countClosedCases: 0, - countOpenCases: 0, - countInProgressCases: 0, - isLoading: false, - isError: true, - fetchCasesStatus: result.current.fetchCasesStatus, - }); - }); + expect(addError).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx index 6530236a2fee..c2ba6659edcb 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx @@ -5,94 +5,37 @@ * 2.0. */ -import { useCallback, useEffect, useState, useRef } from 'react'; - +import { useQuery } from '@tanstack/react-query'; import { useCasesContext } from '../components/cases_context/use_cases_context'; import * as i18n from './translations'; import { CasesStatus } from './types'; -import { useHttp, useToasts } from '../common/lib/kibana'; +import { useHttp } from '../common/lib/kibana'; import { getCasesStatus } from '../api'; +import { useCasesToast } from '../common/use_cases_toast'; +import { ServerError } from '../types'; +import { casesQueriesKeys } from './constants'; -interface CasesStatusState extends CasesStatus { - isLoading: boolean; - isError: boolean; -} - -const initialData: CasesStatusState = { - countClosedCases: 0, - countInProgressCases: 0, - countOpenCases: 0, - isLoading: true, - isError: false, -}; - -export interface UseGetCasesStatus extends CasesStatusState { - fetchCasesStatus: () => void; -} - -export const useGetCasesStatus = (): UseGetCasesStatus => { +export const useGetCasesStatus = () => { const http = useHttp(); const { owner } = useCasesContext(); - const [casesStatusState, setCasesStatusState] = useState(initialData); - const toasts = useToasts(); - const isCancelledRef = useRef(false); - const abortCtrlRef = useRef(new AbortController()); - - const fetchCasesStatus = useCallback(async () => { - try { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - setCasesStatusState({ - ...initialData, - isLoading: true, - }); + const { showErrorToast } = useCasesToast(); - const response = await getCasesStatus({ + return useQuery( + casesQueriesKeys.casesStatuses(), + () => { + const abortCtrlRef = new AbortController(); + return getCasesStatus({ http, - signal: abortCtrlRef.current.signal, + signal: abortCtrlRef.signal, query: { owner }, }); - - if (!isCancelledRef.current) { - setCasesStatusState({ - ...response, - isLoading: false, - isError: false, - }); - } - } catch (error) { - if (!isCancelledRef.current) { - if (error.name !== 'AbortError') { - toasts.addError( - error.body && error.body.message ? new Error(error.body.message) : error, - { title: i18n.ERROR_TITLE } - ); - } - setCasesStatusState({ - countClosedCases: 0, - countInProgressCases: 0, - countOpenCases: 0, - isLoading: false, - isError: true, - }); - } + }, + { + onError: (error: ServerError) => { + showErrorToast(error, { title: i18n.ERROR_TITLE }); + }, } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - fetchCasesStatus(); - - return () => { - isCancelledRef.current = true; - abortCtrlRef.current.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return { - ...casesStatusState, - fetchCasesStatus, - }; + ); }; + +export type UseGetCasesStatus = ReturnType; diff --git a/x-pack/plugins/cases/public/containers/use_get_tags.tsx b/x-pack/plugins/cases/public/containers/use_get_tags.tsx index 1696a9d1413a..da56521536cb 100644 --- a/x-pack/plugins/cases/public/containers/use_get_tags.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_tags.tsx @@ -10,15 +10,14 @@ import { useToasts } from '../common/lib/kibana'; import { useCasesContext } from '../components/cases_context/use_cases_context'; import { ServerError } from '../types'; import { getTags } from './api'; -import { CASE_TAGS_CACHE_KEY } from './constants'; +import { casesQueriesKeys } from './constants'; import * as i18n from './translations'; -export const useGetTags = (cacheKey?: string) => { +export const useGetTags = () => { const toasts = useToasts(); const { owner } = useCasesContext(); - const key = [...(cacheKey ? [cacheKey] : []), CASE_TAGS_CACHE_KEY]; return useQuery( - key, + casesQueriesKeys.tags(), () => { const abortCtrl = new AbortController(); return getTags(abortCtrl.signal, owner); diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts index de180b5970f3..b2928295dbb3 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_bulk_get_user_profiles.ts @@ -10,7 +10,7 @@ import { UserProfileWithAvatar } from '@kbn/user-profile-components'; import * as i18n from '../translations'; import { useKibana, useToasts } from '../../common/lib/kibana'; import { ServerError } from '../../types'; -import { USER_PROFILES_CACHE_KEY, USER_PROFILES_BULK_GET_CACHE_KEY } from '../constants'; +import { casesQueriesKeys } from '../constants'; import { bulkGetUserProfiles } from './api'; const profilesToMap = (profiles: UserProfileWithAvatar[]): Map => @@ -25,7 +25,7 @@ export const useBulkGetUserProfiles = ({ uids }: { uids: string[] }) => { const toasts = useToasts(); return useQuery>( - [USER_PROFILES_CACHE_KEY, USER_PROFILES_BULK_GET_CACHE_KEY, uids], + casesQueriesKeys.userProfilesList(uids), () => { return bulkGetUserProfiles({ security, uids }); }, diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.ts index 37c29fa0b2d0..d6e348367255 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_get_current_user_profile.ts @@ -10,7 +10,7 @@ import { UserProfile } from '@kbn/security-plugin/common'; import * as i18n from '../translations'; import { useKibana, useToasts } from '../../common/lib/kibana'; import { ServerError } from '../../types'; -import { USER_PROFILES_CACHE_KEY, USER_PROFILES_GET_CURRENT_CACHE_KEY } from '../constants'; +import { casesQueriesKeys } from '../constants'; import { getCurrentUserProfile } from './api'; export const useGetCurrentUserProfile = () => { @@ -19,7 +19,7 @@ export const useGetCurrentUserProfile = () => { const toasts = useToasts(); return useQuery( - [USER_PROFILES_CACHE_KEY, USER_PROFILES_GET_CURRENT_CACHE_KEY], + casesQueriesKeys.currentUser(), () => { return getCurrentUserProfile({ security }); }, diff --git a/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.ts b/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.ts index 26e03d0163c8..74c492850acd 100644 --- a/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.ts +++ b/x-pack/plugins/cases/public/containers/user_profiles/use_suggest_user_profiles.ts @@ -14,7 +14,7 @@ import { DEFAULT_USER_SIZE, SEARCH_DEBOUNCE_MS } from '../../../common/constants import * as i18n from '../translations'; import { useKibana, useToasts } from '../../common/lib/kibana'; import { ServerError } from '../../types'; -import { USER_PROFILES_CACHE_KEY, USER_PROFILES_SUGGEST_CACHE_KEY } from '../constants'; +import { casesQueriesKeys } from '../constants'; import { suggestUserProfiles, SuggestUserProfilesArgs } from './api'; type Props = Omit & { onDebounce?: () => void }; @@ -49,11 +49,7 @@ export const useSuggestUserProfiles = ({ const toasts = useToasts(); return useQuery( - [ - USER_PROFILES_CACHE_KEY, - USER_PROFILES_SUGGEST_CACHE_KEY, - { name: debouncedName, owners, size }, - ], + casesQueriesKeys.suggestUsers({ name: debouncedName, owners, size }), () => { const abortCtrlRef = new AbortController(); return suggestUserProfiles({ diff --git a/x-pack/plugins/enterprise_search/common/types/pipelines.ts b/x-pack/plugins/enterprise_search/common/types/pipelines.ts index 60fd87ca523b..269f7149cc7b 100644 --- a/x-pack/plugins/enterprise_search/common/types/pipelines.ts +++ b/x-pack/plugins/enterprise_search/common/types/pipelines.ts @@ -6,7 +6,16 @@ */ export interface InferencePipeline { - isDeployed: boolean; + modelState: TrainedModelState; + modelStateReason?: string; pipelineName: string; types: string[]; } + +export enum TrainedModelState { + NotDeployed = '', + Starting = 'starting', + Stopping = 'stopping', + Started = 'started', + Failed = 'failed', +} diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate.tsx index 47f0f161263a..36b527097a30 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate.tsx @@ -49,7 +49,7 @@ export const AnalyticsCollectionIntegrate: React.FC diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx index 1c79cff0244e..27dc055564bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.test.tsx @@ -11,14 +11,16 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiBadge, EuiHealth, EuiPanel, EuiTitle } from '@elastic/eui'; +import { EuiBadge, EuiPanel, EuiTitle } from '@elastic/eui'; + +import { InferencePipeline, TrainedModelState } from '../../../../../../common/types/pipelines'; import { InferencePipelineCard } from './inference_pipeline_card'; +import { TrainedModelHealth } from './ml_model_health'; -export const DEFAULT_VALUES = { - isDeployed: true, +export const DEFAULT_VALUES: InferencePipeline = { + modelState: TrainedModelState.Started, pipelineName: 'Sample Processor', - trainedModelName: 'example_trained_model', types: ['pytorch'], }; @@ -34,8 +36,6 @@ describe('InferencePipelineCard', () => { expect(wrapper.find(EuiPanel)).toHaveLength(1); expect(wrapper.find(EuiTitle)).toHaveLength(1); expect(wrapper.find(EuiBadge)).toHaveLength(1); - - const health = wrapper.find(EuiHealth); - expect(health.prop('children')).toEqual('Deployed'); + expect(wrapper.find(TrainedModelHealth)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.tsx index b73121f947d7..e8017ff15a19 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/inference_pipeline_card.tsx @@ -12,31 +12,30 @@ import { useActions, useValues } from 'kea'; import { EuiBadge, EuiButtonEmpty, + EuiButtonIcon, EuiConfirmModal, EuiFlexGroup, EuiFlexItem, - EuiHealth, EuiPanel, EuiPopover, EuiPopoverTitle, EuiText, EuiTitle, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { InferencePipeline } from '../../../../../../common/types/pipelines'; +import { InferencePipeline, TrainedModelState } from '../../../../../../common/types/pipelines'; import { CANCEL_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../../../../shared/constants'; import { HttpLogic } from '../../../../shared/http'; +import { ML_MANAGE_TRAINED_MODELS_PATH } from '../../../routes'; import { IndexNameLogic } from '../index_name_logic'; +import { TrainedModelHealth } from './ml_model_health'; import { PipelinesLogic } from './pipelines_logic'; -export const InferencePipelineCard: React.FC = ({ - pipelineName, - isDeployed, - types, -}) => { +export const InferencePipelineCard: React.FC = (pipeline) => { const { http } = useValues(HttpLogic); const { indexName } = useValues(IndexNameLogic); const [isPopOverOpen, setIsPopOverOpen] = useState(false); @@ -46,10 +45,7 @@ export const InferencePipelineCard: React.FC = ({ setShowConfirmDelete(true); setIsPopOverOpen(false); }; - - const deployedText = i18n.translate('xpack.enterpriseSearch.inferencePipelineCard.isDeployed', { - defaultMessage: 'Deployed', - }); + const { pipelineName, types } = pipeline; const actionButton = ( = ({ - - {isDeployed && ( - - {deployedText} + + + + + {pipeline.modelState === TrainedModelState.NotDeployed && ( + + + + )} {types.map((type) => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx index ca6140b7b962..7bf1ef06e1e7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines_card.tsx @@ -26,6 +26,7 @@ import { KibanaLogic } from '../../../../shared/kibana'; import { LicensingLogic } from '../../../../shared/licensing'; import { CreateCustomPipelineApiLogic } from '../../../api/index/create_custom_pipeline_api_logic'; import { FetchCustomPipelineApiLogic } from '../../../api/index/fetch_custom_pipeline_api_logic'; +import { isApiIndex } from '../../../utils/indices'; import { CurlRequest } from '../components/curl_request/curl_request'; import { IndexViewLogic } from '../index_view_logic'; @@ -36,7 +37,7 @@ import { PipelinesLogic } from './pipelines_logic'; export const IngestPipelinesCard: React.FC = () => { const { indexName } = useValues(IndexViewLogic); - const { canSetPipeline, pipelineState, showModal } = useValues(PipelinesLogic); + const { canSetPipeline, index, pipelineState, showModal } = useValues(PipelinesLogic); const { closeModal, openModal, setPipelineState, savePipeline } = useActions(PipelinesLogic); const { makeRequest: fetchCustomPipeline } = useActions(FetchCustomPipelineApiLogic); const { makeRequest: createCustomPipeline } = useActions(CreateCustomPipelineApiLogic); @@ -97,22 +98,24 @@ export const IngestPipelinesCard: React.FC = () => { - - - - - - + + {isApiIndex(index) && ( + + + + + + )} {i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_model_health.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_model_health.test.tsx new file mode 100644 index 000000000000..0eb88abb317e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_model_health.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiHealth } from '@elastic/eui'; + +import { InferencePipeline, TrainedModelState } from '../../../../../../common/types/pipelines'; + +import { TrainedModelHealth } from './ml_model_health'; + +describe('TrainedModelHealth', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockValues({}); + }); + + const commonModelData: InferencePipeline = { + modelState: TrainedModelState.NotDeployed, + pipelineName: 'Sample Processor', + types: ['pytorch'], + }; + it('renders model started', () => { + const pipeline: InferencePipeline = { + ...commonModelData, + modelState: TrainedModelState.Started, + }; + const wrapper = shallow(); + const health = wrapper.find(EuiHealth); + expect(health.prop('children')).toEqual('Started'); + expect(health.prop('color')).toEqual('success'); + }); + it('renders model not deployed', () => { + const pipeline: InferencePipeline = { + ...commonModelData, + }; + const wrapper = shallow(); + const health = wrapper.find(EuiHealth); + expect(health.prop('children')).toEqual('Not deployed'); + expect(health.prop('color')).toEqual('danger'); + }); + it('renders model stopping', () => { + const pipeline: InferencePipeline = { + ...commonModelData, + modelState: TrainedModelState.Stopping, + }; + const wrapper = shallow(); + const health = wrapper.find(EuiHealth); + expect(health.prop('children')).toEqual('Stopping'); + expect(health.prop('color')).toEqual('warning'); + }); + it('renders model starting', () => { + const pipeline: InferencePipeline = { + ...commonModelData, + modelState: TrainedModelState.Starting, + }; + const wrapper = shallow(); + const health = wrapper.find(EuiHealth); + expect(health.prop('children')).toEqual('Starting'); + expect(health.prop('color')).toEqual('warning'); + }); + it('renders model failed', () => { + const pipeline: InferencePipeline = { + ...commonModelData, + modelState: TrainedModelState.Failed, + modelStateReason: 'Model start boom.', + }; + const wrapper = shallow(); + const health = wrapper.find(EuiHealth); + expect(health.prop('children')).toEqual('Deployment failed'); + expect(health.prop('color')).toEqual('danger'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_model_health.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_model_health.tsx new file mode 100644 index 000000000000..0d47c7018d4f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_model_health.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiHealth, EuiToolTip } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { InferencePipeline, TrainedModelState } from '../../../../../../common/types/pipelines'; + +const modelStartedText = i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.modelState.started', + { + defaultMessage: 'Started', + } +); +const modelStartedTooltip = i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.modelState.started.tooltip', + { + defaultMessage: 'This trained model is running and fully available', + } +); +const modelStartingText = i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.modelState.starting', + { + defaultMessage: 'Starting', + } +); +const modelStartingTooltip = i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.modelState.starting.tooltip', + { + defaultMessage: + 'This trained model is in the process of starting up and will be available shortly', + } +); +const modelStoppingText = i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.modelState.stopping', + { + defaultMessage: 'Stopping', + } +); +const modelStoppingTooltip = i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.modelState.stopping.tooltip', + { + defaultMessage: + 'This trained model is in the process of shutting down and is currently unavailable', + } +); +const modelDeploymentFailedText = i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.modelState.deploymentFailed', + { + defaultMessage: 'Deployment failed', + } +); +const modelNotDeployedText = i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.modelState.notDeployed', + { + defaultMessage: 'Not deployed', + } +); +const modelNotDeployedTooltip = i18n.translate( + 'xpack.enterpriseSearch.inferencePipelineCard.modelState.notDeployed.tooltip', + { + defaultMessage: + 'This trained model is not currently deployed. Visit the trained models page to make changes', + } +); + +export const TrainedModelHealth: React.FC = ({ + modelState, + modelStateReason, +}) => { + let modelHealth: { + healthColor: string; + healthText: React.ReactNode; + tooltipText: React.ReactNode; + }; + switch (modelState) { + case TrainedModelState.Started: + modelHealth = { + healthColor: 'success', + healthText: modelStartedText, + tooltipText: modelStartedTooltip, + }; + break; + case TrainedModelState.Stopping: + modelHealth = { + healthColor: 'warning', + healthText: modelStoppingText, + tooltipText: modelStoppingTooltip, + }; + break; + case TrainedModelState.Starting: + modelHealth = { + healthColor: 'warning', + healthText: modelStartingText, + tooltipText: modelStartingTooltip, + }; + break; + case TrainedModelState.Failed: + modelHealth = { + healthColor: 'danger', + healthText: modelDeploymentFailedText, + tooltipText: ( + + ), + }; + break; + case TrainedModelState.NotDeployed: + modelHealth = { + healthColor: 'danger', + healthText: modelNotDeployedText, + tooltipText: modelNotDeployedTooltip, + }; + break; + } + return ( + + {modelHealth.healthText} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts index 60260dcaa537..a98040211906 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts @@ -21,3 +21,5 @@ export const SEARCH_INDEX_PATH = `${SEARCH_INDICES_PATH}/:indexName`; export const SEARCH_INDEX_TAB_PATH = `${SEARCH_INDEX_PATH}/:tabId`; export const SEARCH_INDEX_CRAWLER_DOMAIN_DETAIL_PATH = `${SEARCH_INDEX_PATH}/crawler/domains/:domainId`; export const SEARCH_INDEX_SELECT_CONNECTOR_PATH = `${SEARCH_INDEX_PATH}/select_connector`; + +export const ML_MANAGE_TRAINED_MODELS_PATH = '/app/ml/trained_models'; diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.test.ts b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.test.ts index 9a34bb42ec87..79d4600bb31e 100644 --- a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.test.ts @@ -9,7 +9,7 @@ import { MlTrainedModelConfig } from '@elastic/elasticsearch/lib/api/typesWithBo import { ElasticsearchClient } from '@kbn/core/server'; import { BUILT_IN_MODEL_TAG } from '@kbn/ml-plugin/common/constants/data_frame_analytics'; -import { InferencePipeline } from '../../../common/types/pipelines'; +import { InferencePipeline, TrainedModelState } from '../../../common/types/pipelines'; import { fetchAndAddTrainedModelData, @@ -169,35 +169,74 @@ const mockGetTrainedModelsData = { model_type: 'pytorch', tags: [], }, + { + inference_config: { text_classification: {} }, + model_id: 'trained-model-id-3', + model_type: 'pytorch', + tags: [], + }, + { + inference_config: { fill_mask: {} }, + model_id: 'trained-model-id-4', + model_type: 'pytorch', + tags: [], + }, ], }; const mockGetTrainedModelStats = { - count: 1, + count: 4, trained_model_stats: [ { model_id: 'trained-model-id-1', }, { deployment_stats: { + allocation_status: { + allocation_count: 1, + }, state: 'started', }, model_id: 'trained-model-id-2', }, + { + deployment_stats: { + allocation_status: { + allocation_count: 1, + }, + state: 'failed', + reason: 'something is wrong, boom', + }, + model_id: 'trained-model-id-3', + }, + { + deployment_stats: { + allocation_status: { + allocation_count: 1, + }, + state: 'starting', + }, + model_id: 'trained-model-id-4', + }, ], }; -const trainedModelDataObject = { +const trainedModelDataObject: Record = { 'trained-model-id-1': { - isDeployed: false, + modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-1', types: ['lang_ident', 'ner'], }, 'trained-model-id-2': { - isDeployed: true, + modelState: TrainedModelState.Started, pipelineName: 'ml-inference-pipeline-2', types: ['pytorch', 'ner'], }, + 'ml-inference-pipeline-3': { + modelState: TrainedModelState.NotDeployed, + pipelineName: 'ml-inference-pipeline-3', + types: ['lang_ident', 'ner'], + }, }; describe('fetchMlInferencePipelineProcessorNames lib function', () => { @@ -254,15 +293,15 @@ describe('fetchPipelineProcessorInferenceData lib function', () => { it('should return the inference processor data for the pipelines', async () => { mockClient.ingest.getPipeline.mockImplementation(() => Promise.resolve(mockGetPipeline2)); - const expected = [ + const expected: InferencePipelineData[] = [ { - isDeployed: false, + modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-1', trainedModelName: 'trained-model-id-1', types: [], }, { - isDeployed: false, + modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-2', trainedModelName: 'trained-model-id-2', types: [], @@ -338,20 +377,20 @@ describe('getMlModelConfigsForModelIds lib function', () => { Promise.resolve(mockGetTrainedModelStats) ); - const input = { + const input: Record = { 'trained-model-id-1': { - isDeployed: true, + modelState: TrainedModelState.Started, pipelineName: '', trainedModelName: 'trained-model-id-1', types: ['pytorch', 'ner'], }, 'trained-model-id-2': { - isDeployed: true, + modelState: TrainedModelState.Started, pipelineName: '', trainedModelName: 'trained-model-id-2', types: ['pytorch', 'ner'], }, - } as Record; + }; const expected = { 'trained-model-id-2': input['trained-model-id-2'], @@ -392,32 +431,57 @@ describe('fetchAndAddTrainedModelData lib function', () => { const pipelines: InferencePipelineData[] = [ { - isDeployed: false, + modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-1', trainedModelName: 'trained-model-id-1', types: [], }, { - isDeployed: false, + modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-2', trainedModelName: 'trained-model-id-2', types: [], }, + { + modelState: TrainedModelState.NotDeployed, + pipelineName: 'ml-inference-pipeline-3', + trainedModelName: 'trained-model-id-3', + types: [], + }, + { + modelState: TrainedModelState.NotDeployed, + pipelineName: 'ml-inference-pipeline-4', + trainedModelName: 'trained-model-id-4', + types: [], + }, ]; const expected: InferencePipelineData[] = [ { - isDeployed: false, + modelState: TrainedModelState.NotDeployed, pipelineName: 'ml-inference-pipeline-1', trainedModelName: 'trained-model-id-1', types: ['lang_ident', 'ner'], }, { - isDeployed: true, + modelState: TrainedModelState.Started, pipelineName: 'ml-inference-pipeline-2', trainedModelName: 'trained-model-id-2', types: ['pytorch', 'ner'], }, + { + modelState: TrainedModelState.Failed, + modelStateReason: 'something is wrong, boom', + pipelineName: 'ml-inference-pipeline-3', + trainedModelName: 'trained-model-id-3', + types: ['pytorch', 'text_classification'], + }, + { + modelState: TrainedModelState.Starting, + pipelineName: 'ml-inference-pipeline-4', + trainedModelName: 'trained-model-id-4', + types: ['pytorch', 'fill_mask'], + }, ]; const response = await fetchAndAddTrainedModelData( @@ -426,10 +490,10 @@ describe('fetchAndAddTrainedModelData lib function', () => { ); expect(mockClient.ml.getTrainedModels).toHaveBeenCalledWith({ - model_id: 'trained-model-id-1,trained-model-id-2', + model_id: 'trained-model-id-1,trained-model-id-2,trained-model-id-3,trained-model-id-4', }); expect(mockClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({ - model_id: 'trained-model-id-1,trained-model-id-2', + model_id: 'trained-model-id-1,trained-model-id-2,trained-model-id-3,trained-model-id-4', }); expect(response).toEqual(expected); }); @@ -551,11 +615,7 @@ describe('fetchMlInferencePipelineProcessors lib function', () => { const expected: InferencePipeline[] = [ trainedModelDataObject['trained-model-id-1'], - { - isDeployed: false, - pipelineName: 'ml-inference-pipeline-3', - types: ['lang_ident', 'ner'], - }, + trainedModelDataObject['ml-inference-pipeline-3'], ]; const response = await fetchMlInferencePipelineProcessors( diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.ts b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.ts index 3b695d53ba9a..95839a9b6ac2 100644 --- a/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.ts +++ b/x-pack/plugins/enterprise_search/server/lib/indices/fetch_ml_inference_pipeline_processors.ts @@ -9,7 +9,7 @@ import { MlTrainedModelConfig } from '@elastic/elasticsearch/lib/api/typesWithBo import { ElasticsearchClient } from '@kbn/core/server'; import { BUILT_IN_MODEL_TAG } from '@kbn/ml-plugin/common/constants/data_frame_analytics'; -import { InferencePipeline } from '../../../common/types/pipelines'; +import { InferencePipeline, TrainedModelState } from '../../../common/types/pipelines'; import { getInferencePipelineNameFromIndexName } from '../../utils/ml_inference_pipeline_utils'; export type InferencePipelineData = InferencePipeline & { @@ -58,7 +58,7 @@ export const fetchPipelineProcessorInferenceData = async ( const trainedModelName = inferenceProcessor?.inference?.model_id; if (trainedModelName) pipelineProcessorData.push({ - isDeployed: false, + modelState: TrainedModelState.NotDeployed, pipelineName: pipelineProcessorName, trainedModelName, types: [], @@ -98,7 +98,7 @@ export const getMlModelConfigsForModelIds = async ( if (trainedModelNames.includes(trainedModelName)) { modelConfigs[trainedModelName] = { - isDeployed: false, + modelState: TrainedModelState.NotDeployed, pipelineName: '', trainedModelName, types: getMlModelTypesForModelConfig(trainedModelData), @@ -109,8 +109,27 @@ export const getMlModelConfigsForModelIds = async ( trainedModelsStats.trained_model_stats.forEach((trainedModelStats) => { const trainedModelName = trainedModelStats.model_id; if (modelConfigs.hasOwnProperty(trainedModelName)) { - const isDeployed = trainedModelStats.deployment_stats?.state === 'started'; - modelConfigs[trainedModelName].isDeployed = isDeployed; + let modelState: TrainedModelState; + switch (trainedModelStats.deployment_stats?.state) { + case 'started': + modelState = TrainedModelState.Started; + break; + case 'starting': + modelState = TrainedModelState.Starting; + break; + case 'stopping': + modelState = TrainedModelState.Stopping; + break; + // @ts-ignore: type is wrong, "failed" is a possible state + case 'failed': + modelState = TrainedModelState.Failed; + break; + default: + modelState = TrainedModelState.NotDeployed; + break; + } + modelConfigs[trainedModelName].modelState = modelState; + modelConfigs[trainedModelName].modelStateReason = trainedModelStats.deployment_stats?.reason; } }); @@ -131,11 +150,12 @@ export const fetchAndAddTrainedModelData = async ( if (!model) { return data; } - const { types, isDeployed } = model; + const { types, modelState, modelStateReason } = model; return { ...data, types, - isDeployed, + modelState, + modelStateReason, }; }); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts index c0dcc2dbdb0a..0aaf30ef126d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts @@ -114,7 +114,7 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) { connectorId: schema.string(), }), body: schema.object({ - nextSyncConfig: schema.string(), + nextSyncConfig: schema.maybe(schema.string()), }), }, }, diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index c1d5869e7ed4..05b05946b652 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -159,6 +159,7 @@ Below is a document in the expected structure, with descriptions of the fields: es_search_duration_ms: "total time spent performing ES searches as measured by Elasticsearch", total_search_duration_ms: "total time spent performing ES searches as measured by Kibana; includes network latency and time spent serializing/deserializing request/response", total_indexing_duration_ms: "total time spent indexing documents during current rule execution cycle", + total_enrichment_duration_ms: "total time spent enriching documents during current rule execution cycle", execution_gap_duration_s: "duration in seconds of execution gap" } } diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index db6719fed996..4756b4f2e553 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -347,6 +347,9 @@ }, "total_run_duration_ms": { "type": "long" + }, + "total_enrichment_duration_ms": { + "type": "long" } } } diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index b5557f64e9ac..523ad683eabf 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -150,6 +150,7 @@ export const EventSchema = schema.maybe( claim_to_start_duration_ms: ecsStringOrNumber(), prepare_rule_duration_ms: ecsStringOrNumber(), total_run_duration_ms: ecsStringOrNumber(), + total_enrichment_duration_ms: ecsStringOrNumber(), }) ), }) diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index 98050b655721..65c9220fb535 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -130,6 +130,9 @@ exports.EcsCustomPropertyMappings = { total_run_duration_ms: { type: 'long', }, + total_enrichment_duration_ms: { + type: 'long', + }, }, }, }, diff --git a/x-pack/plugins/fleet/.storybook/context/index.tsx b/x-pack/plugins/fleet/.storybook/context/index.tsx index 7ef04979969e..1d5416cf0483 100644 --- a/x-pack/plugins/fleet/.storybook/context/index.tsx +++ b/x-pack/plugins/fleet/.storybook/context/index.tsx @@ -72,7 +72,7 @@ export const StorybookContext: React.FC<{ storyContext?: Parameters }, customIntegrations: { ContextProvider: getStorybookContextProvider(), - languageClientsUiComponents: new Map(), + languageClientsUiComponents: {}, }, docLinks: getDocLinks(), http: getHttp(), diff --git a/x-pack/plugins/fleet/common/authz.test.ts b/x-pack/plugins/fleet/common/authz.test.ts index ced783c6d4ad..cadb90651b01 100644 --- a/x-pack/plugins/fleet/common/authz.test.ts +++ b/x-pack/plugins/fleet/common/authz.test.ts @@ -15,7 +15,10 @@ import { ENDPOINT_PRIVILEGES } from './constants'; const SECURITY_SOLUTION_ID = DEFAULT_APP_CATEGORIES.security.id; -function generateActions(privileges: string[] = [], overrides: Record = {}) { +function generateActions( + privileges: typeof ENDPOINT_PRIVILEGES, + overrides: Record = {} +) { return privileges.reduce((acc, privilege) => { const executePackageAction = overrides[privilege] || false; diff --git a/x-pack/plugins/fleet/common/constants/authz.ts b/x-pack/plugins/fleet/common/constants/authz.ts index 3bf1aeb46adc..d7a0ca4ade2e 100644 --- a/x-pack/plugins/fleet/common/constants/authz.ts +++ b/x-pack/plugins/fleet/common/constants/authz.ts @@ -23,4 +23,4 @@ export const ENDPOINT_PRIVILEGES = [ 'writeHostIsolation', 'writeProcessOperations', 'writeFileOperations', -]; +] as const; diff --git a/x-pack/plugins/fleet/common/mocks.ts b/x-pack/plugins/fleet/common/mocks.ts index bc8880ed385a..d5aca8398abd 100644 --- a/x-pack/plugins/fleet/common/mocks.ts +++ b/x-pack/plugins/fleet/common/mocks.ts @@ -7,6 +7,7 @@ import type { DeletePackagePoliciesResponse, NewPackagePolicy, PackagePolicy } from './types'; import type { FleetAuthz } from './authz'; +import { ENDPOINT_PRIVILEGES } from './constants'; export const createNewPackagePolicyMock = (): NewPackagePolicy => { return { @@ -61,6 +62,15 @@ export const deletePackagePolicyMock = (): DeletePackagePoliciesResponse => { * Creates mock `authz` object */ export const createFleetAuthzMock = (): FleetAuthz => { + const endpointActions = ENDPOINT_PRIVILEGES.reduce((acc, privilege) => { + return { + ...acc, + [privilege]: { + executePackageAction: true, + }, + }; + }, {}); + return { fleet: { all: true, @@ -80,5 +90,10 @@ export const createFleetAuthzMock = (): FleetAuthz => { readIntegrationPolicies: true, writeIntegrationPolicies: true, }, + packagePrivileges: { + endpoint: { + actions: endpointActions, + }, + }, }; }; diff --git a/x-pack/plugins/fleet/common/types/models/package_spec.ts b/x-pack/plugins/fleet/common/types/models/package_spec.ts index 4463bb81097e..52f993499ea4 100644 --- a/x-pack/plugins/fleet/common/types/models/package_spec.ts +++ b/x-pack/plugins/fleet/common/types/models/package_spec.ts @@ -42,6 +42,7 @@ export type PackageSpecCategory = | 'datastore' | 'elastic_stack' | 'google_cloud' + | 'infrastructure' | 'kubernetes' | 'languages' | 'message_queue' diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx index af0e3c882ea5..011134151cbd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx @@ -75,7 +75,8 @@ const HitPreview: React.FC<{ hit: SearchHit }> = ({ hit }) => { ); const listItems = Object.entries(hitForDisplay).map(([key, value]) => ({ title: `${key}:`, - description: value, + // Ensures arrays and collections of nested objects are displayed correctly + description: JSON.stringify(value), })); return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx index bb0ec90f2b88..356753a0d004 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx @@ -6,7 +6,6 @@ */ import React, { useMemo, useState } from 'react'; -import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, @@ -32,9 +31,6 @@ import { getCommonTags } from '../utils'; import type { SelectionMode } from './types'; import { TagsAddRemove } from './tags_add_remove'; -const FlexItem = styled(EuiFlexItem)` - height: ${(props) => props.theme.eui.euiSizeL}; -`; export interface Props { totalAgents: number; totalInactiveAgents: number; @@ -228,42 +224,36 @@ export const AgentBulkActions: React.FunctionComponent = ({ }} onClosePopover={() => { setIsTagAddVisible(false); + closeMenu(); }} /> )} - {(selectionMode === 'manual' && selectedAgents.length) || - (selectionMode === 'query' && totalAgents > 0) ? ( - <> - - - - - } - isOpen={isMenuOpen} - closePopover={closeMenu} - panelPaddingSize="none" - anchorPosition="downLeft" + + - - - - - ) : ( - - )} + + + } + isOpen={isMenuOpen} + closePopover={closeMenu} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.test.tsx similarity index 67% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.test.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.test.tsx index 71e673fd30e1..33fd16419a1b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.test.tsx @@ -20,8 +20,7 @@ import { FleetStatusProvider, ConfigContext, KibanaVersionContext } from '../../ import { getMockTheme } from '../../../../../../mocks'; -import { AgentBulkActions } from './bulk_actions'; -import type { Props } from './bulk_actions'; +import { SearchAndFilterBar } from './search_and_filter_bar'; const mockTheme = getMockTheme({ eui: { @@ -29,13 +28,19 @@ const mockTheme = getMockTheme({ }, }); -const TestComponent = (props: Props) => ( +jest.mock('../../../../components', () => { + return { + SearchBar: () =>
, + }; +}); + +const TestComponent = (props: any) => ( - + @@ -43,10 +48,10 @@ const TestComponent = (props: Props) => ( ); -describe('AgentBulkActions', () => { +describe('SearchAndFilterBar', () => { it('should show no Actions button when no agent is selected', async () => { const selectedAgents: Agent[] = []; - const props: Props = { + const props: any = { totalAgents: 10, totalInactiveAgents: 2, selectionMode: 'manual', @@ -54,8 +59,12 @@ describe('AgentBulkActions', () => { selectedAgents, refreshAgents: () => undefined, visibleAgents: [], - allTags: [], + tags: [], agentPolicies: [], + selectedStatus: [], + selectedTags: [], + selectedAgentPolicies: [], + showAgentActivityTour: {}, }; const testBed = registerTestBed(TestComponent)(props); const { exists } = testBed; @@ -76,7 +85,7 @@ describe('AgentBulkActions', () => { local_metadata: {}, }, ]; - const props: Props = { + const props: any = { totalAgents: 10, totalInactiveAgents: 2, selectionMode: 'manual', @@ -84,8 +93,34 @@ describe('AgentBulkActions', () => { selectedAgents, refreshAgents: () => undefined, visibleAgents: [], - allTags: [], + tags: [], + agentPolicies: [], + selectedStatus: [], + selectedTags: [], + selectedAgentPolicies: [], + showAgentActivityTour: {}, + }; + const testBed = registerTestBed(TestComponent)(props); + const { exists } = testBed; + + expect(exists('agentBulkActionsButton')).not.toBeNull(); + }); + + it('should show an Actions button when agents selected in query mode', async () => { + const props: any = { + totalAgents: 10, + totalInactiveAgents: 2, + selectionMode: 'query', + currentQuery: '', + selectedAgents: [], + refreshAgents: () => undefined, + visibleAgents: [], + tags: [], agentPolicies: [], + selectedStatus: [], + selectedTags: [], + selectedAgentPolicies: [], + showAgentActivityTour: {}, }; const testBed = registerTestBed(TestComponent)(props); const { exists } = testBed; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx index e90e1ae71322..9b16136df2d9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx @@ -69,6 +69,10 @@ const ClearAllTagsFilterItem = styled(EuiFilterSelectItem)` padding: ${(props) => props.theme.eui.euiSizeS}; `; +const FlexEndEuiFlexItem = styled(EuiFlexItem)` + align-self: flex-end; +`; + export const SearchAndFilterBar: React.FunctionComponent<{ agentPolicies: AgentPolicy[]; draftKuery: string; @@ -151,7 +155,51 @@ export const SearchAndFilterBar: React.FunctionComponent<{ return ( <> {/* Search and filter bar */} - + + + + + + + + + } + > + + + + + + + + } + > + + + + + + + @@ -328,63 +376,22 @@ export const SearchAndFilterBar: React.FunctionComponent<{ - {selectedAgents.length === 0 && ( - - - } - > - - - - + {(selectionMode === 'manual' && selectedAgents.length) || + (selectionMode === 'query' && totalAgents > 0) ? ( + + - )} - - - - {selectedAgents.length === 0 && ( - - - } - > - - - - - - )} - - - + ) : null} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx index 8c4f9f3003c8..465db5236338 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx @@ -270,6 +270,59 @@ describe('TagsAddRemove', () => { ); }); + it('should add new tag twice quickly when not found in search and button clicked - bulk selection', () => { + mockBulkUpdateTags.mockImplementation((agents, tagsToAdd, tagsToRemove, onSuccess) => + onSuccess(false) + ); + + const result = renderComponent(undefined, 'query'); + const searchInput = result.getByRole('combobox'); + + fireEvent.input(searchInput, { + target: { value: 'newTag' }, + }); + + fireEvent.click(result.getAllByText('Create a new tag "newTag"')[0].closest('button')!); + + fireEvent.input(searchInput, { + target: { value: 'newTag2' }, + }); + + fireEvent.click(result.getAllByText('Create a new tag "newTag2"')[0].closest('button')!); + + expect(mockBulkUpdateTags).toHaveBeenCalledWith( + 'query', + ['newTag2', 'newTag'], + [], + expect.anything(), + 'Tag created', + 'Tag creation failed' + ); + }); + + it('should remove tags twice quickly on bulk selection', () => { + selectedTags = ['tag1', 'tag2']; + mockBulkUpdateTags.mockImplementation((agents, tagsToAdd, tagsToRemove, onSuccess) => + onSuccess(false) + ); + + const result = renderComponent(undefined, ''); + const getTag = (name: string) => result.getByText(name); + + fireEvent.click(getTag('tag1')); + + fireEvent.click(getTag('tag2')); + + expect(mockBulkUpdateTags).toHaveBeenCalledWith( + '', + [], + ['tag2', 'tag1'], + expect.anything(), + undefined, + undefined + ); + }); + it('should make tag options button visible on mouse enter', async () => { const result = renderComponent('agent1'); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx index a03ec3808e9a..70b4da44dad6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useEffect, useState, useMemo, useCallback } from 'react'; -import { difference } from 'lodash'; +import { difference, uniq } from 'lodash'; import styled from 'styled-components'; import type { EuiSelectableOption } from '@elastic/eui'; import { @@ -95,8 +95,9 @@ export const TagsAddRemove: React.FC = ({ if (hasCompleted) { return onTagsUpdated(); } - const newSelectedTags = difference(selectedTags, tagsToRemove).concat(tagsToAdd); - const allTagsWithNew = allTags.includes(tagsToAdd[0]) ? allTags : allTags.concat(tagsToAdd); + const selected = labels.filter((tag) => tag.checked === 'on').map((tag) => tag.label); + const newSelectedTags = difference(selected, tagsToRemove).concat(tagsToAdd); + const allTagsWithNew = uniq(allTags.concat(newSelectedTags)); const allTagsWithRemove = isRenameOrDelete ? difference(allTagsWithNew, tagsToRemove) : allTagsWithNew; @@ -109,8 +110,8 @@ export const TagsAddRemove: React.FC = ({ successMessage?: string, errorMessage?: string ) => { - const newSelectedTags = difference(selectedTags, tagsToRemove).concat(tagsToAdd); if (agentId) { + const newSelectedTags = difference(selectedTags, tagsToRemove).concat(tagsToAdd); updateTagsHook.updateTags( agentId, newSelectedTags, @@ -119,10 +120,22 @@ export const TagsAddRemove: React.FC = ({ errorMessage ); } else { + // sending updated tags to add/remove, in case multiple actions are done quickly and the first one is not yet propagated + const updatedTagsToAdd = tagsToAdd.concat( + labels + .filter((tag) => tag.checked === 'on' && !selectedTags.includes(tag.label)) + .map((tag) => tag.label) + ); + const updatedTagsToRemove = tagsToRemove.concat( + labels + .filter((tag) => tag.checked !== 'on' && selectedTags.includes(tag.label)) + .map((tag) => tag.label) + ); + updateTagsHook.bulkUpdateTags( agents!, - tagsToAdd, - tagsToRemove, + updatedTagsToAdd, + updatedTagsToRemove, (hasCompleted) => handleTagsUpdated(tagsToAdd, tagsToRemove, hasCompleted), successMessage, errorMessage diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/custom_languages_overview.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/custom_languages_overview.tsx index 93e7c6e0fac2..c8c1914599f0 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/custom_languages_overview.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/custom_languages_overview.tsx @@ -6,9 +6,10 @@ */ import React from 'react'; import { useParams, Redirect } from 'react-router-dom'; +import { capitalize } from 'lodash'; import { getCustomIntegrationsStart } from '../../../../../../services/custom_integrations'; -import { useLink } from '../../../../../../hooks'; +import { useLink, useBreadcrumbs } from '../../../../hooks'; export interface CustomLanguageClientsParams { pkgkey: string; } @@ -20,10 +21,9 @@ export interface CustomLanguageClientsParams { export const CustomLanguagesOverview = () => { const { pkgkey } = useParams(); const { getPath } = useLink(); + useBreadcrumbs('integration_details_overview', { pkgTitle: capitalize(pkgkey) }); - const Component = getCustomIntegrationsStart().languageClientsUiComponents.get( - `language_client.${pkgkey}` - ); + const Component = getCustomIntegrationsStart().languageClientsUiComponents[pkgkey]; return Component ? : ; }; diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index 4a6c772b69b9..17c745bfd285 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -134,6 +134,7 @@ export async function bulkCreateAgentActionResults( await esClient.bulk({ index: AGENT_ACTIONS_RESULTS_INDEX, body: bulkBody, + refresh: 'wait_for', }); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 305bf2b6bacb..55a244664238 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -405,6 +405,7 @@ export async function bulkUpdateAgents( { update: { _id: agentId, + retry_on_conflict: 3, }, }, { diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts index a54c2cb56c94..3fb9d2ee1f33 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts @@ -96,4 +96,29 @@ describe('reassignAgents (plural)', () => { }); expect(calledWithActionResults.body?.[1] as any).toEqual(expectedObject); }); + + it('should report errors from ES agent update call', async () => { + const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO2 } = createClientMock(); + esClient.bulk.mockResponse({ + items: [ + { + update: { + _id: agentInRegularDoc._id, + error: new Error('version conflict'), + }, + }, + ], + } as any); + const idsToReassign = [agentInRegularDoc._id]; + await reassignAgents(soClient, esClient, { agentIds: idsToReassign }, regularAgentPolicySO2.id); + + const calledWithActionResults = esClient.bulk.mock.calls[1][0] as estypes.BulkRequest; + const expectedObject = expect.objectContaining({ + '@timestamp': expect.anything(), + action_id: expect.anything(), + agent_id: agentInRegularDoc._id, + error: 'version conflict', + }); + expect(calledWithActionResults.body?.[1] as any).toEqual(expectedObject); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts index 55c0e00728d1..96405e464b35 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts @@ -72,7 +72,7 @@ export async function reassignBatch( throw new AgentReassignmentError('No agents to reassign, already assigned or hosted agents'); } - await bulkUpdateAgents( + const res = await bulkUpdateAgents( esClient, agentsToUpdate.map((agent) => ({ agentId: agent.id, @@ -83,6 +83,12 @@ export async function reassignBatch( })) ); + res.items + .filter((item) => !item.success) + .forEach((item) => { + errors[item.id] = item.error!; + }); + const actionId = options.actionId ?? uuid(); const errorCount = Object.keys(errors).length; const total = options.total ?? agentsToUpdate.length + errorCount; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts index 6504b6548f07..bf5fe89574b8 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts @@ -35,6 +35,7 @@ jest.mock('../..', () => ({ getKibanaBranch: () => 'main', getKibanaVersion: () => '99.0.0', getConfig: () => ({}), + getIsProductionMode: () => false, }, })); diff --git a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts index 407a61ad7a91..31bb776ee1d6 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/registry_url.ts @@ -21,7 +21,8 @@ const SNAPSHOT_REGISTRY_URL_CDN = 'https://epr-snapshot.elastic.co'; const getDefaultRegistryUrl = (): string => { const branch = appContextService.getKibanaBranch(); - if (branch === 'main') { + const isProduction = appContextService.getIsProductionMode(); + if (!isProduction || branch === 'main') { return SNAPSHOT_REGISTRY_URL_CDN; } else if (appContextService.getKibanaVersion().includes('-SNAPSHOT')) { return STAGING_REGISTRY_URL_CDN; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts index a8eaa39d427d..e6fa2e008e4b 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts @@ -112,6 +112,33 @@ describe('output preconfiguration', () => { expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled(); }); + it('should create a preconfigured output with ca_trusted_fingerprint that does not exists', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await createOrUpdatePreconfiguredOutputs(soClient, esClient, [ + { + id: 'non-existing-output-1', + name: 'Output 1', + type: 'elasticsearch', + is_default: false, + is_default_monitoring: false, + hosts: ['http://test.fr'], + ca_trusted_fingerprint: 'testfingerprint', + }, + ]); + + expect(mockedOutputService.create).toBeCalled(); + expect(mockedOutputService.create).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + ca_trusted_fingerprint: 'testfingerprint', + }), + expect.anything() + ); + expect(mockedOutputService.update).not.toBeCalled(); + expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled(); + }); + it('should create preconfigured logstash output that does not exist', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts index a61d8316dcc5..d7f43ce181a4 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts @@ -78,7 +78,7 @@ export async function createOrUpdatePreconfiguredOutputs( config_yaml: configYaml ?? null, // Set value to null to update these fields on update ca_sha256: outputData.ca_sha256 ?? null, - ca_trusted_fingerprint: outputData.ca_sha256 ?? null, + ca_trusted_fingerprint: outputData.ca_trusted_fingerprint ?? null, ssl: outputData.ssl ?? null, }; diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json index 641e595893c0..6db4ca194d7d 100644 --- a/x-pack/plugins/graph/kibana.json +++ b/x-pack/plugins/graph/kibana.json @@ -9,7 +9,8 @@ "data", "navigation", "savedObjects", - "unifiedSearch" + "unifiedSearch", + "inspector" ], "optionalPlugins": [ "home", diff --git a/x-pack/plugins/graph/public/application.tsx b/x-pack/plugins/graph/public/application.tsx index 1976b12621f4..bbb14a96ac5e 100644 --- a/x-pack/plugins/graph/public/application.tsx +++ b/x-pack/plugins/graph/public/application.tsx @@ -27,6 +27,7 @@ import { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import { NavigationPublicPluginStart as NavigationStart } from '@kbn/navigation-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { FormattedRelative } from '@kbn/i18n-react'; +import { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public'; import { TableListViewKibanaProvider } from '@kbn/content-management-table-list'; import './index.scss'; @@ -70,6 +71,7 @@ export interface GraphDependencies { uiSettings: IUiSettingsClient; history: ScopedHistory; spaces?: SpacesApi; + inspect: InspectorPublicPluginStart; } export type GraphServices = Omit; diff --git a/x-pack/plugins/graph/public/apps/workspace_route.tsx b/x-pack/plugins/graph/public/apps/workspace_route.tsx index 051c0fa66ab8..9b1fde2e7cb2 100644 --- a/x-pack/plugins/graph/public/apps/workspace_route.tsx +++ b/x-pack/plugins/graph/public/apps/workspace_route.tsx @@ -43,6 +43,7 @@ export const WorkspaceRoute = ({ setHeaderActionMenu, spaces, indexPatterns: getIndexPatternProvider, + inspect, }, }: WorkspaceRouteProps) => { /** @@ -64,11 +65,6 @@ export const WorkspaceRoute = ({ [getIndexPatternProvider.get] ); - const { loading, callNodeProxy, callSearchNodeProxy, handleSearchQueryError } = useGraphLoader({ - toastNotifications, - coreStart, - }); - const services = useMemo( () => ({ appName: 'graph', @@ -80,6 +76,12 @@ export const WorkspaceRoute = ({ [coreStart, data, storage, unifiedSearch] ); + const { loading, requestAdapter, callNodeProxy, callSearchNodeProxy, handleSearchQueryError } = + useGraphLoader({ + toastNotifications, + coreStart, + }); + const [store] = useState(() => createGraphStore({ basePath: getBasePath(), @@ -150,6 +152,8 @@ export const WorkspaceRoute = ({ overlays={overlays} savedWorkspace={savedWorkspace} indexPatternProvider={indexPatternProvider} + inspect={inspect} + requestAdapter={requestAdapter} /> diff --git a/x-pack/plugins/graph/public/components/inspect_panel.tsx b/x-pack/plugins/graph/public/components/inspect_panel.tsx deleted file mode 100644 index e6197bac16ba..000000000000 --- a/x-pack/plugins/graph/public/components/inspect_panel.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo, useState } from 'react'; -import { EuiTab, EuiTabs, EuiText } from '@elastic/eui'; -import { monaco, XJsonLang } from '@kbn/monaco'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { CodeEditor } from '@kbn/kibana-react-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/public'; - -interface InspectPanelProps { - showInspect: boolean; - indexPattern?: DataView; - lastRequest?: string; - lastResponse?: string; -} - -const CODE_EDITOR_OPTIONS: monaco.editor.IStandaloneEditorConstructionOptions = { - automaticLayout: true, - fontSize: 12, - lineNumbers: 'on', - minimap: { - enabled: false, - }, - overviewRulerBorder: false, - readOnly: true, - scrollbar: { - alwaysConsumeMouseWheel: false, - }, - scrollBeyondLastLine: false, - wordWrap: 'on', - wrappingIndent: 'indent', -}; - -const dummyCallback = () => {}; - -export const InspectPanel = ({ - showInspect, - lastRequest, - lastResponse, - indexPattern, -}: InspectPanelProps) => { - const [selectedTabId, setSelectedTabId] = useState('request'); - - const onRequestClick = () => setSelectedTabId('request'); - const onResponseClick = () => setSelectedTabId('response'); - - const editorContent = useMemo( - () => (selectedTabId === 'request' ? lastRequest : lastResponse), - [lastRequest, lastResponse, selectedTabId] - ); - - if (showInspect) { - return ( -
-
-
- -
- -
- - http://host:port/{indexPattern?.id}/_graph/explore - - - - - - - - - - -
-
-
- ); - } - - return null; -}; diff --git a/x-pack/plugins/graph/public/components/settings/url_template_form.tsx b/x-pack/plugins/graph/public/components/settings/url_template_form.tsx index 57e69d0912a7..4509a003e839 100644 --- a/x-pack/plugins/graph/public/components/settings/url_template_form.tsx +++ b/x-pack/plugins/graph/public/components/settings/url_template_form.tsx @@ -261,7 +261,15 @@ export function UrlTemplateForm(props: UrlTemplateFormProps) { defaultMessage: 'Toolbar icon', })} > -
+
{urlTemplateIconChoices.map((icon) => ( { aliasTargetId: '', } as SharingSavedObjectProps, spaces: spacesPluginMock.createStartContract(), + inspect: { open: jest.fn() } as unknown as InspectorStart, + requestAdapter: { + start: () => ({ + stats: jest.fn(), + json: jest.fn(), + }), + reset: jest.fn(), + } as unknown as RequestAdapter, workspace: {} as unknown as Workspace, }; it('should display conflict notification if outcome is conflict', () => { diff --git a/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx b/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx index 3eede479bd80..5de95636a61d 100644 --- a/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx +++ b/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx @@ -11,6 +11,7 @@ import { EuiSpacer } from '@elastic/eui'; import { connect } from 'react-redux'; import { useLocation } from 'react-router-dom'; import type { DataView } from '@kbn/data-views-plugin/public'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { SearchBar } from '../search_bar'; import { GraphState, @@ -20,7 +21,6 @@ import { import { FieldManager } from '../field_manager'; import { ControlType, IndexPatternProvider, TermIntersect, WorkspaceNode } from '../../types'; import { WorkspaceTopNavMenu } from './workspace_top_nav_menu'; -import { InspectPanel } from '../inspect_panel'; import { GuidancePanel } from '../guidance_panel'; import { GraphTitle } from '../graph_title'; import { GraphWorkspaceSavedObject, Workspace } from '../../types'; @@ -49,6 +49,7 @@ type WorkspaceLayoutProps = Pick< | 'canEditDrillDownUrls' | 'overlays' | 'spaces' + | 'inspect' > & { renderCounter: number; workspace?: Workspace; @@ -56,6 +57,7 @@ type WorkspaceLayoutProps = Pick< savedWorkspace: GraphWorkspaceSavedObject; indexPatternProvider: IndexPatternProvider; sharingSavedObjectProps?: SharingSavedObjectProps; + requestAdapter: RequestAdapter; }; interface WorkspaceLayoutStateProps { @@ -80,9 +82,10 @@ export const WorkspaceLayoutComponent = ({ setHeaderActionMenu, sharingSavedObjectProps, spaces, + inspect, + requestAdapter, }: WorkspaceLayoutProps & WorkspaceLayoutStateProps) => { const [currentIndexPattern, setCurrentIndexPattern] = useState(); - const [showInspect, setShowInspect] = useState(false); const [pickerOpen, setPickerOpen] = useState(false); const [mergeCandidates, setMergeCandidates] = useState([]); const [control, setControl] = useState('none'); @@ -188,20 +191,15 @@ export const WorkspaceLayoutComponent = ({ graphSavePolicy={graphSavePolicy} navigation={navigation} capabilities={capabilities} + inspect={inspect} + requestAdapter={requestAdapter} coreStart={coreStart} canEditDrillDownUrls={canEditDrillDownUrls} - setShowInspect={setShowInspect} confirmWipeWorkspace={confirmWipeWorkspace} setHeaderActionMenu={setHeaderActionMenu} isInitialized={isInitialized} /> - {isInitialized && }
>; confirmWipeWorkspace: ( onConfirm: () => void, text?: string, @@ -30,9 +31,11 @@ interface WorkspaceTopNavMenuProps { graphSavePolicy: GraphSavePolicy; navigation: NavigationStart; capabilities: Capabilities; + inspect: InspectorPublicPluginStart; coreStart: CoreStart; canEditDrillDownUrls: boolean; isInitialized: boolean; + requestAdapter: RequestAdapter; } export const WorkspaceTopNavMenu = (props: WorkspaceTopNavMenuProps) => { @@ -40,6 +43,12 @@ export const WorkspaceTopNavMenu = (props: WorkspaceTopNavMenuProps) => { const location = useLocation(); const history = useHistory(); const allSavingDisabled = props.graphSavePolicy === 'none'; + const isInspectDisabled = !props.workspace?.lastRequest || !props.workspace.lastRequest; + + const { onOpenInspector } = useInspector({ + inspect: props.inspect, + requestAdapter: props.requestAdapter, + }); // ===== Menubar configuration ========= const { TopNavMenu } = props.navigation.ui; @@ -107,7 +116,7 @@ export const WorkspaceTopNavMenu = (props: WorkspaceTopNavMenuProps) => { topNavMenu.push({ key: 'inspect', disableButton() { - return props.workspace === null; + return isInspectDisabled; }, label: i18n.translate('xpack.graph.topNavMenu.inspectLabel', { defaultMessage: 'Inspect', @@ -116,7 +125,14 @@ export const WorkspaceTopNavMenu = (props: WorkspaceTopNavMenuProps) => { defaultMessage: 'Inspect', }), run: () => { - props.setShowInspect((prevShowInspect) => !prevShowInspect); + onOpenInspector(); + }, + tooltip: () => { + if (isInspectDisabled) { + return i18n.translate('xpack.graph.topNavMenu.inspectButton.disabledTooltip', { + defaultMessage: 'Perform a search or expand a node to enable Inspect', + }); + } }, testId: 'graphInspectButton', }); @@ -162,6 +178,9 @@ export const WorkspaceTopNavMenu = (props: WorkspaceTopNavMenuProps) => { ownFocus: true, className: 'gphSettingsFlyout', maxWidth: 520, + 'aria-label': i18n.translate('xpack.graph.settings.ariaLabel', { + defaultMessage: 'Settings', + }), } ); }, diff --git a/x-pack/plugins/graph/public/helpers/use_graph_loader.ts b/x-pack/plugins/graph/public/helpers/use_graph_loader.ts index 5b4f3bbbf1e4..0d50039ab979 100644 --- a/x-pack/plugins/graph/public/helpers/use_graph_loader.ts +++ b/x-pack/plugins/graph/public/helpers/use_graph_loader.ts @@ -5,10 +5,12 @@ * 2.0. */ -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import type { CoreStart, ToastsStart } from '@kbn/core/public'; import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser'; import { i18n } from '@kbn/i18n'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { RequestAdapter } from '@kbn/inspector-plugin/public'; import type { ExploreRequest, GraphExploreCallback, @@ -24,6 +26,7 @@ interface UseGraphLoaderProps { export const useGraphLoader = ({ toastNotifications, coreStart }: UseGraphLoaderProps) => { const [loading, setLoading] = useState(false); + const requestAdapter = useMemo(() => new RequestAdapter(), []); const handleHttpError = useCallback( (error: IHttpFetchError) => { @@ -52,21 +55,56 @@ export const useGraphLoader = ({ toastNotifications, coreStart }: UseGraphLoader [toastNotifications] ); + const getRequestInspector = useCallback( + (indexName: string) => { + const inspectRequest = requestAdapter.start( + i18n.translate('xpack.graph.inspectAdapter.graphExploreRequest.name', { + defaultMessage: 'Data', + }), + { + description: i18n.translate( + 'xpack.graph.inspectAdapter.graphExploreRequest.description', + { + defaultMessage: 'This request queries Elasticsearch to fetch the data for the Graph.', + } + ), + } + ); + inspectRequest.stats({ + indexPattern: { + label: i18n.translate( + 'xpack.graph.inspectAdapter.graphExploreRequest.dataView.description.label', + { defaultMessage: 'Data view' } + ), + value: indexName, + description: i18n.translate( + 'xpack.graph.inspectAdapter.graphExploreRequest.dataView.description', + { defaultMessage: 'The data view that connected to the Elasticsearch indices.' } + ), + }, + }); + return inspectRequest; + }, + [requestAdapter] + ); + // Replacement function for graphClientWorkspace's comms so // that it works with Kibana. const callNodeProxy = useCallback( (indexName: string, query: ExploreRequest, responseHandler: GraphExploreCallback) => { - const request = { - body: JSON.stringify({ - index: indexName, - query, - }), - }; + const dsl = { index: indexName, query }; + const request = { body: JSON.stringify(dsl) }; setLoading(true); + + requestAdapter.reset(); + const inspectRequest = getRequestInspector(indexName); + inspectRequest.json(dsl); + return coreStart.http - .post<{ resp: { timed_out: unknown } }>('../api/graph/graphExplore', request) + .post<{ resp: estypes.GraphExploreResponse }>('../api/graph/graphExplore', request) .then(function (data) { const response = data.resp; + if (response?.timed_out) { toastNotifications.addWarning( i18n.translate('xpack.graph.exploreGraph.timedOutWarningText', { @@ -74,38 +112,48 @@ export const useGraphLoader = ({ toastNotifications, coreStart }: UseGraphLoader }) ); } + inspectRequest.stats({}).ok({ json: response }); responseHandler(response); }) - .catch(handleHttpError) + .catch((e) => { + inspectRequest.error({ json: e }); + handleHttpError(e); + }) .finally(() => setLoading(false)); }, - [coreStart.http, handleHttpError, toastNotifications] + [coreStart.http, getRequestInspector, handleHttpError, requestAdapter, toastNotifications] ); // Helper function for the graphClientWorkspace to perform a query const callSearchNodeProxy = useCallback( (indexName: string, query: SearchRequest, responseHandler: GraphSearchCallback) => { - const request = { - body: JSON.stringify({ - index: indexName, - body: query, - }), - }; + const dsl = { index: indexName, body: query }; + const request = { body: JSON.stringify(dsl) }; setLoading(true); + + requestAdapter.reset(); + const inspectRequest = getRequestInspector(indexName); + inspectRequest.json(dsl); + coreStart.http - .post<{ resp: unknown }>('../api/graph/searchProxy', request) + .post<{ resp: estypes.GraphExploreResponse }>('../api/graph/searchProxy', request) .then(function (data) { const response = data.resp; + inspectRequest.stats({}).ok({ json: response }); responseHandler(response); }) - .catch(handleHttpError) + .catch((e) => { + inspectRequest.error({ json: e }); + handleHttpError(e); + }) .finally(() => setLoading(false)); }, - [coreStart.http, handleHttpError] + [coreStart.http, getRequestInspector, handleHttpError, requestAdapter] ); return { loading, + requestAdapter, callNodeProxy, callSearchNodeProxy, handleSearchQueryError, diff --git a/x-pack/plugins/graph/public/helpers/use_inspector.ts b/x-pack/plugins/graph/public/helpers/use_inspector.ts new file mode 100644 index 000000000000..d6f237e625c3 --- /dev/null +++ b/x-pack/plugins/graph/public/helpers/use_inspector.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useState } from 'react'; +import { + Start as InspectorPublicPluginStart, + InspectorSession, + RequestAdapter, +} from '@kbn/inspector-plugin/public'; + +export const useInspector = ({ + inspect, + requestAdapter, +}: { + inspect: InspectorPublicPluginStart; + requestAdapter: RequestAdapter; +}) => { + const [inspectorSession, setInspectorSession] = useState(); + + useEffect(() => { + return () => { + if (inspectorSession) { + inspectorSession.close(); + } + }; + }, [inspectorSession]); + + const onOpenInspector = useCallback(() => { + const session = inspect.open({ requests: requestAdapter }, {}); + setInspectorSession(session); + }, [inspect, requestAdapter]); + + return { onOpenInspector }; +}; diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index d4f8471426cc..96dac017eeab 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -19,6 +19,7 @@ import { DEFAULT_APP_CATEGORIES, } from '@kbn/core/public'; +import { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { NavigationPublicPluginStart as NavigationStart } from '@kbn/navigation-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; @@ -40,6 +41,7 @@ export interface GraphPluginStartDependencies { data: DataPublicPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; savedObjects: SavedObjectsStart; + inspector: InspectorPublicPluginStart; home?: HomePublicPluginStart; spaces?: SpacesApi; } @@ -110,6 +112,7 @@ export class GraphPlugin savedObjects: pluginsStart.savedObjects, uiSettings: core.uiSettings, spaces: pluginsStart.spaces, + inspect: pluginsStart.inspector, }); }, }); diff --git a/x-pack/plugins/index_lifecycle_management/README.md b/x-pack/plugins/index_lifecycle_management/README.md index 35c2aa063ec2..912b19295790 100644 --- a/x-pack/plugins/index_lifecycle_management/README.md +++ b/x-pack/plugins/index_lifecycle_management/README.md @@ -115,4 +115,8 @@ this by running: ```bash yarn es snapshot --license=trial -``` \ No newline at end of file +``` + +## Integration tests + +See `./integration_tests/README.md` \ No newline at end of file diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx index ecefe8132c49..246de6e8ed25 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -7,7 +7,7 @@ import moment from 'moment-timezone'; -import { init } from './client_integration/helpers/http_requests'; +import { init } from '../integration_tests/helpers/http_requests'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks'; import { Index } from '../common/types'; diff --git a/x-pack/plugins/index_lifecycle_management/integration_tests/README.md b/x-pack/plugins/index_lifecycle_management/integration_tests/README.md new file mode 100644 index 000000000000..5e4bf4360cb7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/README.md @@ -0,0 +1,14 @@ +Most plugins of the deployment management team follow +the similar testing infrastructure where integration tests are located in `__jest__` and run as unit tests. + +The `index_lifecycle_management` tests [were refactored](https://github.com/elastic/kibana/pull/141750) to be run as integration tests because they [became flaky hitting the 5 seconds timeout](https://github.com/elastic/kibana/issues/115307#issuecomment-1238417474) for a jest unit test. + +Jest integration tests are just sit in a different directory and have two main differences: +- They never use parallelism, this allows them to access file system resources, launch services, etc. without needing to worry about conflicts with other tests +- They are allowed to take their sweet time, the default timeout is currently 10 minutes. + +To run these tests use: + +``` +node scripts/jest_integration x-pack/plugins/index_lifecycle_management/ +``` \ No newline at end of file diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/app/app.helpers.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/app/app.helpers.ts index df64fbce3fa1..66810ebb1e54 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/app/app.helpers.ts @@ -8,7 +8,7 @@ import { act } from 'react-dom/test-utils'; import { HttpSetup } from '@kbn/core/public'; import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test-jest-helpers'; -import { App } from '../../../public/application/app'; +import { App } from '../../public/application/app'; import { WithAppDependencies } from '../helpers'; const getTestBedConfig = (initialEntries: string[]): TestBedConfig => ({ diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/app/app.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/app/app.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/constants.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/constants.ts index 620cb9d6f8dd..a41bea1cb841 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/constants.ts @@ -7,9 +7,9 @@ import moment from 'moment-timezone'; -import { PolicyFromES } from '../../../common/types'; +import { PolicyFromES } from '../../common/types'; -import { defaultRolloverAction } from '../../../public/application/constants'; +import { defaultRolloverAction } from '../../public/application/constants'; export const POLICY_NAME = 'my_policy'; export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/delete_phase.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/delete_phase.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/delete_phase.test.ts similarity index 98% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/delete_phase.test.ts index d877f05d06ae..df6dd516c8a5 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/delete_phase.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { API_BASE_PATH } from '../../../../common/constants'; +import { API_BASE_PATH } from '../../../common/constants'; import { setupEnvironment } from '../../helpers'; import { DELETE_PHASE_POLICY, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/downsample.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/downsample.helpers.ts similarity index 95% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/downsample.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/downsample.helpers.ts index be96aaabe4a7..1e6ed336a5f0 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/downsample.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/downsample.helpers.ts @@ -14,7 +14,7 @@ import { createTogglePhaseAction, } from '../../helpers'; import { initTestBed } from '../init_test_bed'; -import { AppServicesContext } from '../../../../public/types'; +import { AppServicesContext } from '../../../public/types'; type SetupReturn = ReturnType; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/downsample.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/downsample.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/downsample.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/downsample.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/edit_warning.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/edit_warning.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/edit_warning.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/edit_warning.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/frozen_phase.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/frozen_phase.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts similarity index 94% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts index 7092d52289de..b41075c6b6b6 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts @@ -8,7 +8,7 @@ import { HttpSetup } from '@kbn/core/public'; import { TestBedConfig } from '@kbn/test-jest-helpers'; -import { AppServicesContext } from '../../../../../public/types'; +import { AppServicesContext } from '../../../../public/types'; import { createTogglePhaseAction, createNodeAllocationActions } from '../../../helpers'; import { initTestBed } from '../../init_test_bed'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cold_phase.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cold_phase.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cold_phase.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cold_phase.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/general_behavior.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/general_behavior.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/general_behavior.test.ts similarity index 98% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/general_behavior.test.ts index 1eecd5207664..4830cee8ee23 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/general_behavior.test.ts @@ -17,7 +17,7 @@ import { POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION, POLICY_WITH_NODE_ROLE_ALLOCATION, } from '../../constants'; -import { API_BASE_PATH } from '../../../../../common/constants'; +import { API_BASE_PATH } from '../../../../common/constants'; describe(' node allocation general behavior', () => { let testBed: GeneralNodeAllocationTestBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/warm_phase.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/warm_phase.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/warm_phase.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/warm_phase.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/request_flyout.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/request_flyout.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/request_flyout.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/request_flyout.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/rollover.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/rollover.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/rollover.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/rollover.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.helpers.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.helpers.ts index e70f721a4807..d3b68ffc6a13 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.helpers.ts @@ -18,7 +18,7 @@ import { createTogglePhaseAction, } from '../../helpers'; import { initTestBed } from '../init_test_bed'; -import { AppServicesContext } from '../../../../public/types'; +import { AppServicesContext } from '../../../public/types'; type SetupReturn = ReturnType; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts similarity index 99% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts index 03b7670c1eac..68e74e23a781 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts @@ -10,7 +10,7 @@ import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; import { HttpFetchOptionsWithPath } from '@kbn/core/public'; import { setupEnvironment } from '../../helpers'; import { getDefaultHotPhasePolicy } from '../constants'; -import { API_BASE_PATH } from '../../../../common/constants'; +import { API_BASE_PATH } from '../../../common/constants'; import { SearchableSnapshotsTestBed, setupSearchableSnapshotsTestBed, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timeline.helpers.ts similarity index 94% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timeline.helpers.ts index 496b27330c93..202388a84446 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timeline.helpers.ts @@ -8,7 +8,7 @@ import { HttpSetup } from '@kbn/core/public'; import { createTogglePhaseAction } from '../../helpers'; import { initTestBed } from '../init_test_bed'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; type SetupReturn = ReturnType; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timeline.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timeline.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timing.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timing.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timing.test.ts similarity index 95% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timing.test.ts index 0aee8eb5f0be..72c6a2a4e789 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timing.test.ts @@ -8,7 +8,7 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment } from '../../helpers'; import { setupTimingTestBed, TimingTestBed } from './timing.helpers'; -import { PhaseWithTiming } from '../../../../common/types'; +import { PhaseWithTiming } from '../../../common/types'; describe(' timing', () => { let testBed: TimingTestBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/cold_phase_validation.test.ts similarity index 91% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/cold_phase_validation.test.ts index ef2fc67002c1..e75d2cb72ab2 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/cold_phase_validation.test.ts @@ -6,12 +6,11 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141645 -describe.skip(' cold phase validation', () => { +describe(' cold phase validation', () => { let testBed: ValidationTestBed; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/downsample_interval.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/downsample_interval.test.ts similarity index 93% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/downsample_interval.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/downsample_interval.test.ts index 79f5fdc6e284..d2f9943eba68 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/downsample_interval.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/downsample_interval.test.ts @@ -6,14 +6,13 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; -import { PhaseWithDownsample } from '../../../../common/types'; +import { PhaseWithDownsample } from '../../../common/types'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141160 -describe.skip(' downsample interval validation', () => { +describe(' downsample interval validation', () => { let testBed: ValidationTestBed; let actions: ValidationTestBed['actions']; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/error_indicators.test.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/error_indicators.test.ts index 349f98766620..bd4a2caec0be 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/error_indicators.test.ts @@ -9,8 +9,7 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141645 -describe.skip(' error indicators', () => { +describe(' error indicators', () => { let testBed: ValidationTestBed; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/hot_phase_validation.test.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/hot_phase_validation.test.ts index 82b3568d39ce..71f83a59360d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/hot_phase_validation.test.ts @@ -6,12 +6,11 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141645 -describe.skip(' hot phase validation', () => { +describe(' hot phase validation', () => { let testBed: ValidationTestBed; let actions: ValidationTestBed['actions']; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/policy_name_validation.test.ts similarity index 93% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/policy_name_validation.test.ts index 928380e3d1ea..c530f73a66c1 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/policy_name_validation.test.ts @@ -6,13 +6,12 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; import { getGeneratedPolicies } from '../constants'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141645 -describe.skip(' policy name validation', () => { +describe(' policy name validation', () => { let testBed: ValidationTestBed; let actions: ValidationTestBed['actions']; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/timing.test.ts similarity index 93% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/timing.test.ts index 7db483d6d0ef..5838f04ba70e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/timing.test.ts @@ -6,14 +6,13 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; -import { PhaseWithTiming } from '../../../../common/types'; +import { PhaseWithTiming } from '../../../common/types'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/115307 -describe.skip(' timing validation', () => { +describe(' timing validation', () => { let testBed: ValidationTestBed; let actions: ValidationTestBed['actions']; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/validation.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/validation.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/validation.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/validation.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/warm_phase_validation.test.ts similarity index 95% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/warm_phase_validation.test.ts index df0607a0a0e6..47917b1f8e3d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/warm_phase_validation.test.ts @@ -6,12 +6,11 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141645 -describe.skip(' warm phase validation', () => { +describe(' warm phase validation', () => { let testBed: ValidationTestBed; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/init_test_bed.ts similarity index 89% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/init_test_bed.ts index 875bf9db3626..56bed7a6e50a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/init_test_bed.ts @@ -5,12 +5,12 @@ * 2.0. */ +import { HttpSetup } from '@kbn/core/public'; import { registerTestBed, TestBedConfig } from '@kbn/test-jest-helpers'; -import { HttpSetup } from '@kbn/core/public'; import { WithAppDependencies } from '../helpers'; -import { EditPolicy } from '../../../public/application/sections/edit_policy'; -import { AppServicesContext } from '../../../public/types'; +import { EditPolicy } from '../../public/application/sections/edit_policy'; +import { AppServicesContext } from '../../public/types'; import { POLICY_NAME } from './constants'; const getTestBedConfig = (testBedConfig?: Partial): TestBedConfig => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.helpers.ts similarity index 95% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.helpers.ts index 417f53c63a20..e439fca0de51 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.helpers.ts @@ -6,7 +6,7 @@ */ import { HttpSetup } from '@kbn/core/public'; -import { AppServicesContext } from '../../../../public/types'; +import { AppServicesContext } from '../../../public/types'; import { createColdPhaseActions, createDeletePhaseActions, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.test.ts similarity index 99% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.test.ts index 983cfbadfa01..05aa66fcc5f2 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.test.ts @@ -9,7 +9,7 @@ import { act } from 'react-dom/test-utils'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; import { HttpFetchOptionsWithPath } from '@kbn/core/public'; import { setupEnvironment } from '../../helpers'; -import { API_BASE_PATH } from '../../../../common/constants'; +import { API_BASE_PATH } from '../../../common/constants'; import { getDefaultHotPhasePolicy, POLICY_WITH_INCLUDE_EXCLUDE, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/downsample_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/downsample_actions.ts similarity index 96% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/downsample_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/downsample_actions.ts index 315ed3d58520..a389a9deebe3 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/downsample_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/downsample_actions.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { TestBed } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import { Phase } from '../../../../common/types'; +import { TestBed } from '@kbn/test-jest-helpers'; +import { Phase } from '../../../common/types'; import { createFormToggleAction } from '..'; const createSetDownsampleIntervalAction = diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/errors_actions.ts similarity index 96% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/errors_actions.ts index 4b863071e191..18f07734fade 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/errors_actions.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; const createWaitForValidationAction = (testBed: TestBed) => () => { const { component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/forcemerge_actions.ts similarity index 96% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/forcemerge_actions.ts index b6ed40de36ca..619d6bd8be85 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/forcemerge_actions.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormSetValueAction } from './form_set_value_action'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_set_value_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_set_value_action.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_set_value_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_set_value_action.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_toggle_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_toggle_action.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_toggle_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_toggle_action.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_toggle_and_set_value_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_toggle_and_set_value_action.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_toggle_and_set_value_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_toggle_and_set_value_action.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/index.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/index.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/index_priority_actions.ts similarity index 94% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/index_priority_actions.ts index 79fb77e53cc5..4c6e4604be7d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/index_priority_actions.ts @@ -6,7 +6,7 @@ */ import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormToggleAndSetValueAction } from './form_toggle_and_set_value_action'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/min_age_actions.ts similarity index 94% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/min_age_actions.ts index ef00fdc8d775..8b4fa2e5f617 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/min_age_actions.ts @@ -6,7 +6,7 @@ */ import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormSetValueAction } from './form_set_value_action'; export const createMinAgeActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/node_allocation_actions.ts similarity index 95% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/node_allocation_actions.ts index 2a680590654a..a3cb607b60f9 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/node_allocation_actions.ts @@ -8,8 +8,8 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test-jest-helpers'; -import { DataTierAllocationType } from '../../../../public/application/sections/edit_policy/types'; -import { Phase } from '../../../../common/types'; +import { DataTierAllocationType } from '../../../public/application/sections/edit_policy/types'; +import { Phase } from '../../../common/types'; import { createFormSetValueAction } from './form_set_value_action'; export const createNodeAllocationActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/phases.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/phases.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/readonly_actions.ts similarity index 92% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/readonly_actions.ts index 1a7ec56815b0..fe4c71d76265 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/readonly_actions.ts @@ -6,7 +6,7 @@ */ import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; export const createReadonlyActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/replicas_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/replicas_action.ts similarity index 92% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/replicas_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/replicas_action.ts index 43eca26a4e1c..35c3afc60751 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/replicas_action.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/replicas_action.ts @@ -6,7 +6,7 @@ */ import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormToggleAndSetValueAction } from './form_toggle_and_set_value_action'; export const createReplicasAction = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/request_flyout_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/request_flyout_actions.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/request_flyout_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/request_flyout_actions.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/rollover_actions.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/rollover_actions.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/save_policy_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/save_policy_action.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/save_policy_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/save_policy_action.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/searchable_snapshot_actions.ts similarity index 96% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/searchable_snapshot_actions.ts index 3efffcddbece..c9a019c2bc84 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/searchable_snapshot_actions.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; export const createSearchableSnapshotActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/shrink_actions.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/shrink_actions.ts index def20f73b82f..4bf39185b8c0 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/shrink_actions.ts @@ -7,7 +7,7 @@ import { TestBed } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormSetValueAction } from './form_set_value_action'; export const createShrinkActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/snapshot_policy_actions.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/snapshot_policy_actions.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/toggle_phase_action.ts similarity index 96% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/toggle_phase_action.ts index c22efae87d5a..fc89332e47a6 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/toggle_phase_action.ts @@ -8,7 +8,7 @@ import { TestBed } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; const toggleDeletePhase = async (testBed: TestBed) => { const { find, component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/global_mocks.tsx b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/global_mocks.tsx similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/global_mocks.tsx rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/global_mocks.tsx diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/http_requests.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/http_requests.ts index 7ddcd5358404..70e85c8bc5df 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/http_requests.ts @@ -6,12 +6,12 @@ */ import { httpServiceMock } from '@kbn/core/public/mocks'; -import { API_BASE_PATH } from '../../../common/constants'; +import { API_BASE_PATH } from '../../common/constants'; import { ListNodesRouteResponse, ListSnapshotReposResponse, NodesDetailsResponse, -} from '../../../common/types'; +} from '../../common/types'; import { getDefaultHotPhasePolicy } from '../edit_policy/constants'; type HttpMethod = 'GET' | 'PUT' | 'DELETE' | 'POST'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/index.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/index.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/setup_environment.tsx similarity index 80% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/setup_environment.tsx rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/setup_environment.tsx index 055097c883e9..91aebb485ea7 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/setup_environment.tsx @@ -19,12 +19,12 @@ import { executionContextServiceMock, } from '@kbn/core/public/mocks'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; -import { init as initHttp } from '../../../public/application/services/http'; +import { init as initHttp } from '../../public/application/services/http'; import { init as initHttpRequests } from './http_requests'; -import { init as initUiMetric } from '../../../public/application/services/ui_metric'; -import { init as initNotification } from '../../../public/application/services/notification'; -import { KibanaContextProvider } from '../../../public/shared_imports'; -import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; +import { init as initUiMetric } from '../../public/application/services/ui_metric'; +import { init as initNotification } from '../../public/application/services/notification'; +import { KibanaContextProvider } from '../../public/shared_imports'; +import { createBreadcrumbsMock } from '../../public/application/services/breadcrumbs.mock'; const breadcrumbService = createBreadcrumbsMock(); const appContextMock = { diff --git a/x-pack/plugins/index_lifecycle_management/jest.integration.config.js b/x-pack/plugins/index_lifecycle_management/jest.integration.config.js new file mode 100644 index 000000000000..6d1ac5ec2fd8 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/jest.integration.config.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../../..', + roots: ['/x-pack/plugins/index_lifecycle_management'], + testMatch: ['/**/integration_tests/**/*.test.{js,mjs,ts,tsx}'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/index_lifecycle_management', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/index_lifecycle_management/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/index_lifecycle_management/tsconfig.json b/x-pack/plugins/index_lifecycle_management/tsconfig.json index d3a342e11021..4b5d7657ed9f 100644 --- a/x-pack/plugins/index_lifecycle_management/tsconfig.json +++ b/x-pack/plugins/index_lifecycle_management/tsconfig.json @@ -8,6 +8,7 @@ }, "include": [ "__jest__/**/*", + "integration_tests/**/*", "common/**/*", "public/**/*", "server/**/*", diff --git a/x-pack/plugins/infra/public/apps/metrics_app.tsx b/x-pack/plugins/infra/public/apps/metrics_app.tsx index fdeb7173d5ce..ce8123b5f222 100644 --- a/x-pack/plugins/infra/public/apps/metrics_app.tsx +++ b/x-pack/plugins/infra/public/apps/metrics_app.tsx @@ -45,6 +45,7 @@ export const renderApp = ( ); return () => { + core.chrome.docTitle.reset(); ReactDOM.unmountComponentAtNode(element); plugins.data.search.session.clear(); }; diff --git a/x-pack/plugins/infra/public/components/document_title.tsx b/x-pack/plugins/infra/public/components/document_title.tsx deleted file mode 100644 index 20e482d9df5b..000000000000 --- a/x-pack/plugins/infra/public/components/document_title.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -type TitleProp = string | ((previousTitle: string) => string); - -interface DocumentTitleProps { - title: TitleProp; -} - -interface DocumentTitleState { - index: number; -} - -const wrapWithSharedState = () => { - const titles: string[] = []; - const TITLE_SUFFIX = ' - Kibana'; - - return class extends React.Component { - public componentDidMount() { - this.setState( - () => { - return { index: titles.push('') - 1 }; - }, - () => { - this.pushTitle(this.getTitle(this.props.title)); - this.updateDocumentTitle(); - } - ); - } - - public componentDidUpdate() { - this.pushTitle(this.getTitle(this.props.title)); - this.updateDocumentTitle(); - } - - public componentWillUnmount() { - this.removeTitle(); - this.updateDocumentTitle(); - } - - public render() { - return null; - } - - public getTitle(title: TitleProp) { - return typeof title === 'function' ? title(titles[this.state.index - 1]) : title; - } - - public pushTitle(title: string) { - titles[this.state.index] = title; - } - - public removeTitle() { - titles.pop(); - } - - public updateDocumentTitle() { - const title = (titles[titles.length - 1] || '') + TITLE_SUFFIX; - if (title !== document.title) { - document.title = title; - } - } - }; -}; - -export const DocumentTitle = wrapWithSharedState(); diff --git a/x-pack/plugins/infra/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/infra/public/hooks/use_breadcrumbs.ts index 37801c16bcf1..97f737380022 100644 --- a/x-pack/plugins/infra/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/infra/public/hooks/use_breadcrumbs.ts @@ -22,7 +22,7 @@ export const useBreadcrumbs = (app: AppId, appTitle: string, extraCrumbs: Chrome const appLinkProps = useLinkProps({ app }); useEffect(() => { - chrome?.setBreadcrumbs?.([ + const breadcrumbs = [ { ...observabilityLinkProps, text: observabilityTitle, @@ -32,6 +32,11 @@ export const useBreadcrumbs = (app: AppId, appTitle: string, extraCrumbs: Chrome text: appTitle, }, ...extraCrumbs, - ]); + ]; + + const docTitle = [...breadcrumbs].reverse().map((breadcrumb) => breadcrumb.text as string); + + chrome.docTitle.change(docTitle); + chrome.setBreadcrumbs(breadcrumbs); }, [appLinkProps, appTitle, chrome, extraCrumbs, observabilityLinkProps]); }; diff --git a/x-pack/plugins/infra/public/hooks/use_document_title.tsx b/x-pack/plugins/infra/public/hooks/use_document_title.tsx new file mode 100644 index 000000000000..82fb5a669eb9 --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_document_title.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChromeBreadcrumb } from '@kbn/core/public'; +import { useEffect } from 'react'; +import { observabilityTitle } from '../translations'; +import { useKibanaContextForPlugin } from './use_kibana'; + +export const useDocumentTitle = (extraTitles: ChromeBreadcrumb[]) => { + const { + services: { chrome }, + } = useKibanaContextForPlugin(); + + useEffect(() => { + const docTitle = [{ text: observabilityTitle }, ...extraTitles] + .reverse() + .map((breadcrumb) => breadcrumb.text as string); + + chrome.docTitle.change(docTitle); + }, [chrome, extraTitles]); +}; diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index e42cfc2d7c54..8acd4004603e 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -12,7 +12,6 @@ import { Route, Switch } from 'react-router-dom'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { HeaderMenuPortal, useLinkProps } from '@kbn/observability-plugin/public'; import { AlertDropdown } from '../../alerting/log_threshold'; -import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; import { useReadOnlyBadge } from '../../hooks/use_readonly_badge'; import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; @@ -62,8 +61,6 @@ export const LogsPageContent: React.FunctionComponent = () => { return ( <> - - {setHeaderActionMenu && theme$ && ( diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx index bb8e4307fe3b..19f098a6721b 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { useTrackPageview } from '@kbn/observability-plugin/public'; import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; import { StreamPageContent } from './page_content'; -import { StreamPageHeader } from './page_header'; import { LogsPageProviders } from './page_providers'; import { streamTitle } from '../../../translations'; @@ -26,7 +25,6 @@ export const StreamPage = () => { return ( - diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_header.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_header.tsx deleted file mode 100644 index f6c4ad8c8c13..000000000000 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_header.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import React from 'react'; - -import { DocumentTitle } from '../../../components/document_title'; - -export const StreamPageHeader = () => { - return ( - <> - - i18n.translate('xpack.infra.logs.streamPage.documentTitle', { - defaultMessage: '{previousTitle} | Stream', - values: { - previousTitle, - }, - }) - } - /> - - ); -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx index 5c2fed9753b8..a5dfd7f2ddd0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx @@ -6,13 +6,10 @@ */ import { EuiErrorBoundary } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import React from 'react'; import { useTrackPageview } from '@kbn/observability-plugin/public'; import { APP_WRAPPER_CLASS } from '@kbn/core/public'; -import { DocumentTitle } from '../../../components/document_title'; - import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useSourceContext } from '../../../containers/metrics_source'; @@ -42,16 +39,6 @@ export const HostsPage = () => { ]); return ( - - i18n.translate('xpack.infra.infrastructureHostsPage.documentTitle', { - defaultMessage: '{previousTitle} | Hosts', - values: { - previousTitle, - }, - }) - } - /> {isLoading && !source ? ( ) : metricIndicesExist && source ? ( diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 9c02424aac94..691069a978e8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -15,7 +15,6 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import { HeaderMenuPortal } from '@kbn/observability-plugin/public'; import { useLinkProps } from '@kbn/observability-plugin/public'; import { MetricsSourceConfigurationProperties } from '../../../common/metrics_sources'; -import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; import { useReadOnlyBadge } from '../../hooks/use_readonly_badge'; import { @@ -73,12 +72,6 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { - - Legend Options - + - + ); }; + +const StyledEuiForm = euiStyled(EuiForm)` + min-width: 400px; + @media (max-width: 480px) { + min-width: 100%; + max-width: 100%; + width: 100vw; + } +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx index e0d03284d63f..a4558d6a7e9b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx @@ -105,6 +105,7 @@ const WaffleMapOuterContainer = euiStyled.div<{ bottomMargin: number; staticHeig overflow-x: hidden; overflow-y: auto; margin-bottom: ${(props) => props.bottomMargin}px; + max-width: calc(100vw - 90px); ${(props) => props.staticHeight && 'min-height: 300px;'} `; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/palette_preview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/palette_preview.tsx index 132982eae69b..9dc5f3acd6e5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/palette_preview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/palette_preview.tsx @@ -28,9 +28,9 @@ export const PalettePreview = ({ steps, palette, reverse }: Props) => { }; const Swatch = euiStyled.div` - width: 15px; + max-width: 15px; height: 12px; - flex: 0 0 auto; + flex: 1 1 auto; &:first-child { border-radius: ${(props) => props.theme.eui.euiBorderRadius} 0 0 ${(props) => props.theme.eui.euiBorderRadius}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index 8031af55828c..77b8e9a35a64 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -6,14 +6,10 @@ */ import { EuiErrorBoundary } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import React from 'react'; import { useTrackPageview } from '@kbn/observability-plugin/public'; import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import { FilterBar } from './components/filter_bar'; - -import { DocumentTitle } from '../../../components/document_title'; - import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useSourceContext } from '../../../containers/metrics_source'; @@ -49,16 +45,6 @@ export const SnapshotPage = () => { return ( - - i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', { - defaultMessage: '{previousTitle} | Inventory', - values: { - previousTitle, - }, - }) - } - /> {isLoading && !source ? ( ) : metricIndicesExist ? ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.test.tsx new file mode 100644 index 000000000000..25ae3b3717bd --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.test.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { PageError } from './page_error'; +import { errorTitle } from '../../../../translations'; +import { InfraHttpError } from '../../../../types'; +import { useDocumentTitle } from '../../../../hooks/use_document_title'; +import { I18nProvider } from '@kbn/i18n-react'; + +jest.mock('../../../../hooks/use_document_title', () => ({ + useDocumentTitle: jest.fn(), +})); + +const renderErrorPage = () => + render( + + + + ); + +describe('PageError component', () => { + it('renders correctly and set title', () => { + const { getByText } = renderErrorPage(); + expect(useDocumentTitle).toHaveBeenCalledWith([{ text: `${errorTitle}` }]); + + expect(getByText('Error Message')).toBeInTheDocument(); + expect(getByText('Please click the back button and try again.')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx index a6665e0d244f..b4cdb47399e9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx @@ -6,11 +6,11 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; +import { useDocumentTitle } from '../../../../hooks/use_document_title'; import { InvalidNodeError } from './invalid_node'; -import { DocumentTitle } from '../../../../components/document_title'; import { ErrorPageBody } from '../../../error'; import { InfraHttpError } from '../../../../types'; +import { errorTitle } from '../../../../translations'; interface Props { name: string; @@ -18,18 +18,10 @@ interface Props { } export const PageError = ({ error, name }: Props) => { + useDocumentTitle([{ text: errorTitle }]); + return ( <> - - i18n.translate('xpack.infra.metricDetailPage.documentTitleError', { - defaultMessage: '{previousTitle} | Uh oh', - values: { - previousTitle, - }, - }) - } - /> {error.body?.statusCode === 404 ? ( ) : ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx index 823b9d703f50..9b92901976bf 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { EuiTheme, withTheme } from '@kbn/kibana-react-plugin/common'; import { useLinkProps } from '@kbn/observability-plugin/public'; -import { DocumentTitle } from '../../../components/document_title'; import { withMetricPageProviders } from './page_providers'; import { useMetadata } from './hooks/use_metadata'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; @@ -100,14 +99,6 @@ export const MetricDetail = withMetricPageProviders( return ( <> - {metadata ? ( - - i18n.translate('xpack.infra.infrastructureMetricsExplorerPage.documentTitle', { - defaultMessage: '{previousTitle} | Metrics Explorer', - values: { - previousTitle, - }, - }) - } - /> { filterOperations: () => true, supportsMoreColumns: true, dataTestSubj: 'lnsGroup', - required: true, + requiredMinDimensionCount: 1, }, ], }); @@ -303,37 +303,83 @@ describe('LayerPanel', () => { expect(groups.findWhere((e) => e.prop('error') === '')).toHaveLength(1); }); - it('should render the required warning when only one group is configured (with requiredMinDimensionCount)', async () => { - mockVisualization.getConfiguration.mockReturnValue({ - groups: [ - { - groupLabel: 'A', - groupId: 'a', - accessors: [{ columnId: 'x' }], - filterOperations: () => true, - supportsMoreColumns: false, - dataTestSubj: 'lnsGroup', - }, - { - groupLabel: 'B', - groupId: 'b', - accessors: [{ columnId: 'y' }], - filterOperations: () => true, - supportsMoreColumns: true, - dataTestSubj: 'lnsGroup', - requiredMinDimensionCount: 2, - }, - ], - }); - - const { instance } = await mountWithProvider(); + it.each` + minDimensions | accessors | errors + ${1} | ${0} | ${1} + ${2} | ${0} | ${2} + ${2} | ${1} | ${2} + `( + 'should render the required warning for $errors fields when only one group is configured with requiredMinDimensionCount: $minDimensions and $accessors accessors', + async ({ minDimensions, accessors, errors }) => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'x' }], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: [{ columnId: 'y' }].slice(0, accessors), + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + requiredMinDimensionCount: minDimensions, + }, + ], + }); + const { instance } = await mountWithProvider(); + + const errorMessage = errors === 1 ? 'Requires field' : 'Requires 2 fields'; + + const group = instance.find(EuiFormRow).findWhere((e) => e.prop('error') === errorMessage); + + expect(group).toHaveLength(1); + } + ); + + it.each` + minDimensions | accessors + ${0} | ${0} + ${0} | ${1} + ${1} | ${1} + ${1} | ${2} + ${2} | ${2} + `( + 'should not render the required warning when only one group is configured with requiredMinDimensionCount: $minDimensions and $accessors accessors', + async ({ minDimensions, accessors }) => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'x' }], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: [{ columnId: 'y' }, { columnId: 'z' }].slice(0, accessors), + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + requiredMinDimensionCount: minDimensions, + }, + ], + }); + const { instance } = await mountWithProvider(); - const group = instance - .find(EuiFormRow) - .findWhere((e) => e.prop('error') === 'Requires 2 fields'); + const group = instance.find(EuiFormRow).findWhere((e) => e.prop('error')); - expect(group).toHaveLength(1); - }); + expect(group).toHaveLength(0); + } + ); it('should render the datasource and visualization panels inside the dimension container', async () => { mockVisualization.getConfiguration.mockReturnValueOnce({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index ed6d6b8c0553..cc748df7c3ec 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -381,20 +381,25 @@ export function LayerPanel( let errorText: string = ''; if (!isEmptyLayer) { - if (group.requiredMinDimensionCount) { - errorText = i18n.translate( - 'xpack.lens.editorFrame.requiresTwoOrMoreFieldsWarningLabel', - { - defaultMessage: 'Requires {requiredMinDimensionCount} fields', - values: { - requiredMinDimensionCount: group.requiredMinDimensionCount, - }, - } - ); - } else if (group.required && group.accessors.length === 0) { - errorText = i18n.translate('xpack.lens.editorFrame.requiresFieldWarningLabel', { - defaultMessage: 'Requires field', - }); + if ( + group.requiredMinDimensionCount && + group.requiredMinDimensionCount > group.accessors.length + ) { + if (group.requiredMinDimensionCount > 1) { + errorText = i18n.translate( + 'xpack.lens.editorFrame.requiresTwoOrMoreFieldsWarningLabel', + { + defaultMessage: 'Requires {requiredMinDimensionCount} fields', + values: { + requiredMinDimensionCount: group.requiredMinDimensionCount, + }, + } + ); + } else { + errorText = i18n.translate('xpack.lens.editorFrame.requiresFieldWarningLabel', { + defaultMessage: 'Requires field', + }); + } } else if (group.dimensionsTooMany && group.dimensionsTooMany > 0) { errorText = i18n.translate( 'xpack.lens.editorFrame.tooManyDimensionsSingularWarningLabel', @@ -408,7 +413,7 @@ export function LayerPanel( ); } } - const isOptional = !group.required && !group.suggestedValue; + const isOptional = !group.requiredMinDimensionCount && !group.suggestedValue; return ( s.changeType === 'unchanged') || - suggestions.find((s) => s.changeType === 'reduced') || + suggestions.find((s) => s.changeType === 'unchanged' || s.changeType === 'reduced') || suggestions[0] ); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index fbd9a5650013..5a975482e625 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -20,6 +20,11 @@ import { useEuiTheme, EuiFlexGroup, EuiFlexItem, + EuiPopover, + EuiPopoverTitle, + EuiPanel, + EuiBasicTable, + EuiButtonIcon, } from '@elastic/eui'; import ReactDOM from 'react-dom'; import type { IndexPatternDimensionEditorProps } from './dimension_panel'; @@ -116,6 +121,10 @@ export function DimensionEditor(props: DimensionEditorProps) { selectedColumn && operationDefinitionMap[selectedColumn.operationType]; const [temporaryState, setTemporaryState] = useState('none'); + const [isHelpOpen, setIsHelpOpen] = useState(false); + + const onHelpClick = () => setIsHelpOpen((prevIsHelpOpen) => !prevIsHelpOpen); + const closeHelp = () => setIsHelpOpen(false); const temporaryQuickFunction = Boolean(temporaryState === quickFunctionsName); const temporaryStaticValue = Boolean(temporaryState === staticValueOperationName); @@ -596,12 +605,88 @@ export function DimensionEditor(props: DimensionEditorProps) { ...services, }; + const helpButton = ; + + const columnsSidebar = [ + { + field: 'function', + name: i18n.translate('xpack.lens.indexPattern.functionTable.functionHeader', { + defaultMessage: 'Function', + }), + width: '150px', + }, + { + field: 'description', + name: i18n.translate('xpack.lens.indexPattern.functionTable.descriptionHeader', { + defaultMessage: 'Description', + }), + }, + ]; + + const items = sideNavItems + .filter((item) => operationDefinitionMap[item.id!].quickFunctionDocumentation) + .map((item) => { + const operationDefinition = operationDefinitionMap[item.id!]!; + return { + id: item.id!, + function: operationDefinition.displayName, + description: operationDefinition.quickFunctionDocumentation!, + }; + }); + const quickFunctions = ( <> + + + + {i18n.translate('xpack.lens.indexPattern.quickFunctions.popoverTitle', { + defaultMessage: 'Quick functions', + })} + + + + + + + + {i18n.translate('xpack.lens.indexPattern.functionsLabel', { + defaultMessage: 'Functions', + })} + + + } fullWidth > { state: expect.objectContaining({ layers: { test: expect.objectContaining({ - columnOrder: ['column-id-3', 'column-id-2', 'column-id-1'], + columnOrder: ['column-id-2', 'column-id-3', 'column-id-1'], columns: { 'column-id-1': expect.objectContaining({ operationType: 'count', @@ -1804,10 +1804,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'column-id-3', + columnId: 'column-id-2', }), expect.objectContaining({ - columnId: 'column-id-2', + columnId: 'column-id-3', }), expect.objectContaining({ columnId: 'column-id-1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index fed70d2970b1..8503c369f4ec 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -341,6 +341,7 @@ function createNewLayerWithMetricAggregationFromVizEditor( newLayer = insertNewColumn({ ...column, layer: newLayer, + respectOrder: true, }); } }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx index 674eac8194e4..f2c421946baf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx @@ -151,5 +151,13 @@ Example: Visualize the rate of bytes received over time by a memcached server: `, }), }, + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.counterRate.documentation.quick', + { + defaultMessage: ` + The rate of change over time for an ever growing time series metric. + `, + } + ), shiftable: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx index 11e1da98b0ca..67260672fa66 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx @@ -147,5 +147,13 @@ Example: Visualize the received bytes accumulated over time: `, }), }, + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.cumulativeSum.documentation.quick', + { + defaultMessage: ` + The sum of all values as they grow over time. + `, + } + ), shiftable: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx index 6873377bd437..1d76667654cb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx @@ -135,5 +135,13 @@ Example: Visualize the change in bytes received over time: `, }), }, + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.differences.documentation.quick', + { + defaultMessage: ` + The change between the values in subsequent intervals. + `, + } + ), shiftable: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index 3876623b5361..0fbdd96153a0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -166,6 +166,14 @@ Example: Smooth a line of measurements: }, }), }, + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.movingAverage.documentation.quick', + { + defaultMessage: ` + The average of a moving window of values over time. + `, + } + ), shiftable: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index b08fbf8f6c89..04b11885c016 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -218,4 +218,12 @@ Example: Calculate the number of different products from the "clothes" group: `, }), }, + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.cardinality.documentation.quick', + { + defaultMessage: ` +The number of unique values for a specified number, string, date, or boolean field. + `, + } + ), }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index 4c325e604f64..b911ac5394a2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -248,5 +248,10 @@ To calculate the number of documents that match a specific filter, use \`count(k `, }), }, + quickFunctionDocumentation: i18n.translate('xpack.lens.indexPattern.count.documentation.quick', { + defaultMessage: ` +The total number of documents. When you provide a field, the total number of field values is counted. Use the count function for fields that have multiple values in a single document. + `, + }), shiftable: true, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index 52d7dff8d6ec..8d6b201b2cc1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -515,6 +515,14 @@ export const dateHistogramOperation: OperationDefinition< ); }, + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.dateHistogram.documentation.quick', + { + defaultMessage: ` +The date or date range values distributed into intervals. + `, + } + ), }; function parseInterval(currentInterval: string) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index da06ea8ae1bf..b972129109e1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -158,6 +158,14 @@ export const filtersOperation: OperationDefinition< }, getMaxPossibleNumValues: (column) => column.params.filters.length, + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.filters.documentation.quick', + { + defaultMessage: ` + Divides values into predefined subsets. + `, + } + ), }; export const FilterList = ({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 5ce13adbf5ca..6c79d314d10f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -364,6 +364,7 @@ interface BaseOperationDefinitionProps< description: string; section: 'elasticsearch' | 'calculation'; }; + quickFunctionDocumentation?: string; /** * React component for operation field specific behaviour */ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx index 2709d22b2a32..4206cd810976 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -428,4 +428,12 @@ Example: Get the current status of server A: `, }), }, + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.lastValue.documentation.quick', + { + defaultMessage: ` +The value of a field from the last document, ordered by the default time field of the data view. + `, + } + ), }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 09dc8576a042..e8c8c54c1cef 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -60,6 +60,7 @@ function buildMetricOperation>({ hideZeroOption, aggConfigParams, documentationDescription, + quickFunctionDocumentation, }: { type: T['operationType']; displayName: string; @@ -71,6 +72,7 @@ function buildMetricOperation>({ hideZeroOption?: boolean; aggConfigParams?: Record; documentationDescription?: string; + quickFunctionDocumentation?: string; }) { const labelLookup = (name: string, column?: BaseIndexPatternColumn) => { const label = ofName(name); @@ -240,6 +242,7 @@ Example: Get the {metric} of price for orders from the UK: }, }), }, + quickFunctionDocumentation, shiftable: true, } as OperationDefinition; } @@ -265,6 +268,12 @@ export const minOperation = buildMetricOperation({ defaultMessage: 'A single-value metrics aggregation that returns the minimum value among the numeric values extracted from the aggregated documents.', }), + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.min.quickFunctionDescription', + { + defaultMessage: 'The minimum value of a number field.', + } + ), supportsDate: true, }); @@ -282,6 +291,12 @@ export const maxOperation = buildMetricOperation({ defaultMessage: 'A single-value metrics aggregation that returns the maximum value among the numeric values extracted from the aggregated documents.', }), + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.max.quickFunctionDescription', + { + defaultMessage: 'The maximum value of a number field.', + } + ), supportsDate: true, }); @@ -300,6 +315,12 @@ export const averageOperation = buildMetricOperation({ defaultMessage: 'A single-value metric aggregation that computes the average of numeric values that are extracted from the aggregated documents', }), + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.avg.quickFunctionDescription', + { + defaultMessage: 'The average value of a number field.', + } + ), }); export const standardDeviationOperation = buildMetricOperation( @@ -334,6 +355,13 @@ To get the variance of price for orders from the UK, use \`square(standard_devia `, } ), + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.standardDeviation.quickFunctionDescription', + { + defaultMessage: + 'The standard deviation of the values of a number field which is the amount of variation of the fields values.', + } + ), } ); @@ -354,6 +382,12 @@ export const sumOperation = buildMetricOperation({ 'A single-value metrics aggregation that sums up numeric values that are extracted from the aggregated documents.', }), hideZeroOption: true, + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.sum.quickFunctionDescription', + { + defaultMessage: 'The total amount of the values of a number field.', + } + ), }); export const medianOperation = buildMetricOperation({ @@ -371,4 +405,10 @@ export const medianOperation = buildMetricOperation({ defaultMessage: 'A single-value metrics aggregation that computes the median value that are extracted from the aggregated documents.', }), + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.median.quickFunctionDescription', + { + defaultMessage: 'The median value of a number field.', + } + ), }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index 3bc91b91ed3d..ec4a3d569e7d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -417,4 +417,12 @@ Example: Get the number of bytes larger than 95 % of values: `, }), }, + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.percentile.documentation.quick', + { + defaultMessage: ` + The largest value that is smaller than n percent of the values that occur in all documents. + `, + } + ), }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.tsx index 7cb8d7ea64ae..45bd05f37372 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.tsx @@ -266,4 +266,12 @@ Example: Get the percentage of values which are below of 100: `, }), }, + quickFunctionDocumentation: i18n.translate( + 'xpack.lens.indexPattern.percentileRanks.documentation.quick', + { + defaultMessage: ` +The percentage of values that are below a specific value. For example, when a value is greater than or equal to 95% of the calculated values, the value is the 95th percentile rank. + `, + } + ), }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index aa84c727ae50..52c9471aefe0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -267,4 +267,9 @@ export const rangeOperation: OperationDefinition< /> ); }, + quickFunctionDocumentation: i18n.translate('xpack.lens.indexPattern.ranges.documentation.quick', { + defaultMessage: ` + Buckets values along defined numeric ranges. + `, + }), }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 9017e91bff08..1f4b4846906d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -557,6 +557,11 @@ export const termsOperation: OperationDefinition< ); }, + quickFunctionDocumentation: i18n.translate('xpack.lens.indexPattern.terms.documentation.quick', { + defaultMessage: ` +The top values of a specified field ranked by the chosen metric. + `, + }), paramEditor: function ParamEditor({ layer, paramEditorUpdater, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 2d8b41ce866b..b3f1b3bc7d5f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -68,6 +68,7 @@ interface ColumnChange { columnParams?: Record; initialParams?: { params: Record }; // TODO: bind this to the op parameter references?: Array>; + respectOrder?: boolean; } interface ColumnCopy { @@ -362,6 +363,7 @@ export function insertNewColumn({ columnParams, initialParams, references, + respectOrder, }: ColumnChange): IndexPatternLayer { const operationDefinition = operationDefinitionMap[op]; @@ -394,7 +396,14 @@ export function insertNewColumn({ : operationDefinition.buildColumn({ ...baseOptions, layer }); return updateDefaultLabels( - addOperationFn(layer, buildColumnFn, columnId, visualizationGroups, targetGroup), + addOperationFn( + layer, + buildColumnFn, + columnId, + visualizationGroups, + targetGroup, + respectOrder + ), indexPattern ); } @@ -445,7 +454,14 @@ export function insertNewColumn({ ) : operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, referenceIds }); return updateDefaultLabels( - addOperationFn(tempLayer, buildColumnFn, columnId, visualizationGroups, targetGroup), + addOperationFn( + tempLayer, + buildColumnFn, + columnId, + visualizationGroups, + targetGroup, + respectOrder + ), indexPattern ); } @@ -468,7 +484,8 @@ export function insertNewColumn({ operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), columnId, visualizationGroups, - targetGroup + targetGroup, + respectOrder ), indexPattern ); @@ -508,7 +525,7 @@ export function insertNewColumn({ const isBucketed = Boolean(possibleOperation.isBucketed); const addOperationFn = isBucketed ? addBucket : addMetric; return updateDefaultLabels( - addOperationFn(layer, newColumn, columnId, visualizationGroups, targetGroup), + addOperationFn(layer, newColumn, columnId, visualizationGroups, targetGroup, respectOrder), indexPattern ); } @@ -1154,7 +1171,8 @@ function addBucket( column: BaseIndexPatternColumn, addedColumnId: string, visualizationGroups: VisualizationDimensionGroupConfig[], - targetGroup?: string + targetGroup?: string, + respectOrder?: boolean ): IndexPatternLayer { const [buckets, metrics] = partition( layer.columnOrder, @@ -1166,7 +1184,7 @@ function addBucket( ); let updatedColumnOrder: string[] = []; - if (oldDateHistogramIndex > -1 && column.operationType === 'terms') { + if (oldDateHistogramIndex > -1 && column.operationType === 'terms' && !respectOrder) { // Insert the new terms bucket above the first date histogram updatedColumnOrder = [ ...buckets.slice(0, oldDateHistogramIndex), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx index 2cb1ba164d57..77a359729a5e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx @@ -194,7 +194,7 @@ export function getTSDBRollupWarningMessages( ).map((label) => i18n.translate('xpack.lens.indexPattern.tsdbRollupWarning', { defaultMessage: - '"{label}" does not work for all indices in the selected data view because it\'s using a function which is not supported on rolled up data. Please edit the visualization to use another function or change the time range.', + '{label} uses a function that is unsupported by rolled up data. Select a different function or change the time range.', values: { label, }, diff --git a/x-pack/plugins/lens/public/trigger_actions/visualize_agg_based_vis_actions.ts b/x-pack/plugins/lens/public/trigger_actions/visualize_agg_based_vis_actions.ts index 2d2b329a7ea6..a5300d550f2c 100644 --- a/x-pack/plugins/lens/public/trigger_actions/visualize_agg_based_vis_actions.ts +++ b/x-pack/plugins/lens/public/trigger_actions/visualize_agg_based_vis_actions.ts @@ -35,7 +35,7 @@ export const visualizeAggBasedVisAction = (application: ApplicationStart) => type: ACTION_CONVERT_TO_LENS, payload, originatingApp: i18n.translate('xpack.lens.AggBasedLabel', { - defaultMessage: 'Aggregation based visualization', + defaultMessage: 'aggregation based visualization', }), }, }); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index af2dcc601c99..32fc21cc4a61 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -17,7 +17,7 @@ import type { IInterpreterRenderHandlers, Datatable, } from '@kbn/expressions-plugin/public'; -import type { NavigateToLensContext } from '@kbn/visualizations-plugin/common'; +import type { Configuration, NavigateToLensContext } from '@kbn/visualizations-plugin/common'; import { Adapters } from '@kbn/inspector-plugin/public'; import type { Query } from '@kbn/es-query'; import type { @@ -217,13 +217,13 @@ export interface InitializationOptions { isFullEditor?: boolean; } -export type VisualizeEditorContext = { +export type VisualizeEditorContext = { savedObjectId?: string; embeddableId?: string; vizEditorOriginatingAppUrl?: string; originatingApp?: string; isVisualizeAction: boolean; -} & NavigateToLensContext; +} & NavigateToLensContext; export interface GetDropPropsArgs { state: T; @@ -704,7 +704,6 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { supportsMoreColumns: boolean; dimensionsTooMany?: number; /** If required, a warning will appear if accessors are empty */ - required?: boolean; requiredMinDimensionCount?: number; dataTestSubj?: string; prioritizedOperation?: string; @@ -1129,7 +1128,7 @@ export interface Visualization { getSuggestionFromConvertToLensContext?: ( props: VisualizationStateFromContextChangeProps - ) => Suggestion; + ) => Suggestion | undefined; } // Use same technique as TriggerContext diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx index 72bf5812b715..ca6f6775f893 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx @@ -161,11 +161,19 @@ export const getDatatableVisualization = ({ }, }); + const changeType = table.changeType; + const changeFactor = + changeType === 'reduced' || changeType === 'layers' + ? 0.3 + : changeType === 'unchanged' + ? 0.5 + : 1; + return [ { title, // table with >= 10 columns will have a score of 0.4, fewer columns reduce score - score: (Math.min(table.columns.length, 10) / 10) * 0.4, + score: (Math.min(table.columns.length, 10) / 10) * 0.4 * changeFactor, state: { ...(state || {}), layerId: table.layerId, @@ -292,7 +300,7 @@ export const getDatatableVisualization = ({ }), supportsMoreColumns: true, filterOperations: (op) => !op.isBucketed, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsDatatable_metrics', enableDimensionEditor: true, }, diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index 94c9ce28987b..e999cac9dd0e 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -105,7 +105,7 @@ describe('gauge', () => { accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], filterOperations: isNumericDynamicMetric, supportsMoreColumns: false, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, enableFormatSelector: true, @@ -155,7 +155,7 @@ describe('gauge', () => { accessors: [{ columnId: 'goal-accessor' }], filterOperations: isNumericMetric, supportsMoreColumns: false, - required: false, + requiredMinDimensionCount: 0, dataTestSubj: 'lnsGauge_goalDimensionPanel', enableFormatSelector: false, supportStaticValue: true, @@ -187,7 +187,7 @@ describe('gauge', () => { accessors: [], filterOperations: isNumericDynamicMetric, supportsMoreColumns: true, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, enableFormatSelector: true, @@ -237,7 +237,7 @@ describe('gauge', () => { accessors: [], filterOperations: isNumericMetric, supportsMoreColumns: true, - required: false, + requiredMinDimensionCount: 0, dataTestSubj: 'lnsGauge_goalDimensionPanel', enableFormatSelector: false, supportStaticValue: true, @@ -275,7 +275,7 @@ describe('gauge', () => { accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], filterOperations: isNumericDynamicMetric, supportsMoreColumns: false, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, enableFormatSelector: true, @@ -325,7 +325,7 @@ describe('gauge', () => { accessors: [{ columnId: 'goal-accessor' }], filterOperations: isNumericMetric, supportsMoreColumns: false, - required: false, + requiredMinDimensionCount: 0, dataTestSubj: 'lnsGauge_goalDimensionPanel', enableFormatSelector: false, supportStaticValue: true, @@ -368,7 +368,7 @@ describe('gauge', () => { accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], filterOperations: isNumericDynamicMetric, supportsMoreColumns: false, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, enableFormatSelector: true, @@ -422,7 +422,7 @@ describe('gauge', () => { accessors: [{ columnId: 'goal-accessor' }], filterOperations: isNumericMetric, supportsMoreColumns: false, - required: false, + requiredMinDimensionCount: 0, dataTestSubj: 'lnsGauge_goalDimensionPanel', enableFormatSelector: false, supportStaticValue: true, diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 37d10918d412..19fd46459b2b 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -275,7 +275,7 @@ export const getGaugeVisualization = ({ : [], filterOperations: isNumericDynamicMetric, supportsMoreColumns: !metricAccessor, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, }, @@ -352,7 +352,7 @@ export const getGaugeVisualization = ({ accessors: state.goalAccessor ? [{ columnId: state.goalAccessor }] : [], filterOperations: isNumericMetric, supportsMoreColumns: !state.goalAccessor, - required: false, + requiredMinDimensionCount: 0, dataTestSubj: 'lnsGauge_goalDimensionPanel', }, ], diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts index ee6a7030a0c9..38cfa9bf10b5 100644 --- a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts @@ -136,7 +136,7 @@ describe('heatmap', () => { accessors: [{ columnId: 'x-accessor' }], filterOperations: filterOperationsAxis, supportsMoreColumns: false, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsHeatmap_xDimensionPanel', }, { @@ -146,7 +146,7 @@ describe('heatmap', () => { accessors: [{ columnId: 'y-accessor' }], filterOperations: filterOperationsAxis, supportsMoreColumns: false, - required: false, + requiredMinDimensionCount: 0, dataTestSubj: 'lnsHeatmap_yDimensionPanel', }, { @@ -165,7 +165,7 @@ describe('heatmap', () => { ], filterOperations: isCellValueSupported, supportsMoreColumns: false, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsHeatmap_cellPanel', enableDimensionEditor: true, }, @@ -194,7 +194,7 @@ describe('heatmap', () => { accessors: [{ columnId: 'x-accessor' }], filterOperations: filterOperationsAxis, supportsMoreColumns: false, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsHeatmap_xDimensionPanel', }, { @@ -204,7 +204,7 @@ describe('heatmap', () => { accessors: [], filterOperations: filterOperationsAxis, supportsMoreColumns: true, - required: false, + requiredMinDimensionCount: 0, dataTestSubj: 'lnsHeatmap_yDimensionPanel', }, { @@ -217,7 +217,7 @@ describe('heatmap', () => { accessors: [], filterOperations: isCellValueSupported, supportsMoreColumns: true, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsHeatmap_cellPanel', enableDimensionEditor: true, }, @@ -250,7 +250,7 @@ describe('heatmap', () => { accessors: [{ columnId: 'x-accessor' }], filterOperations: filterOperationsAxis, supportsMoreColumns: false, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsHeatmap_xDimensionPanel', }, { @@ -260,7 +260,7 @@ describe('heatmap', () => { accessors: [{ columnId: 'y-accessor' }], filterOperations: filterOperationsAxis, supportsMoreColumns: false, - required: false, + requiredMinDimensionCount: 0, dataTestSubj: 'lnsHeatmap_yDimensionPanel', }, { @@ -278,7 +278,7 @@ describe('heatmap', () => { ], filterOperations: isCellValueSupported, supportsMoreColumns: false, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsHeatmap_cellPanel', enableDimensionEditor: true, }, diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx index 09548df0a67e..1fc48b4d71c3 100644 --- a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx @@ -181,7 +181,7 @@ export const getHeatmapVisualization = ({ accessors: state.xAccessor ? [{ columnId: state.xAccessor }] : [], filterOperations: filterOperationsAxis, supportsMoreColumns: !state.xAccessor, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsHeatmap_xDimensionPanel', }, { @@ -191,7 +191,7 @@ export const getHeatmapVisualization = ({ accessors: state.yAccessor ? [{ columnId: state.yAccessor }] : [], filterOperations: filterOperationsAxis, supportsMoreColumns: !state.yAccessor, - required: false, + requiredMinDimensionCount: 0, dataTestSubj: 'lnsHeatmap_yDimensionPanel', }, { @@ -224,7 +224,7 @@ export const getHeatmapVisualization = ({ filterOperations: isCellValueSupported, supportsMoreColumns: !state.valueAccessor, enableDimensionEditor: true, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsHeatmap_cellPanel', }, ], diff --git a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx index f1b1ffd1f00b..02a4cd23ad4f 100644 --- a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx @@ -258,7 +258,7 @@ export const getLegacyMetricVisualization = ({ filterOperations: (op: OperationMetadata) => !op.isBucketed && legacyMetricSupportedTypes.has(op.dataType), enableDimensionEditor: true, - required: true, + requiredMinDimensionCount: 1, }, ], }; diff --git a/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap b/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap index 8c565cbed53b..7d1df6815826 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap +++ b/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap @@ -24,7 +24,7 @@ Object { "paramEditorCustomProps": Object { "headingLabel": "Value", }, - "required": true, + "requiredMinDimensionCount": 1, "supportsMoreColumns": false, }, Object { @@ -46,7 +46,7 @@ Object { "paramEditorCustomProps": Object { "headingLabel": "Value", }, - "required": false, + "requiredMinDimensionCount": 0, "supportsMoreColumns": false, }, Object { @@ -70,7 +70,7 @@ Object { "headingLabel": "Value", }, "prioritizedOperation": "max", - "required": false, + "requiredMinDimensionCount": 0, "supportStaticValue": true, "supportsMoreColumns": false, }, @@ -91,7 +91,7 @@ Object { "groupId": "breakdownBy", "groupLabel": "Break down by", "layerId": "first", - "required": false, + "requiredMinDimensionCount": 0, "supportsMoreColumns": false, }, ], diff --git a/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts index 47229650e05f..45f332776a4d 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts @@ -79,6 +79,10 @@ describe('metric suggestions', () => { ...metricColumn, columnId: 'metric-column2', }, + { + ...metricColumn, + columnId: 'metric-column3', + }, ], changeType: 'unchanged', }, @@ -99,6 +103,10 @@ describe('metric suggestions', () => { ...metricColumn, columnId: 'metric-column2', }, + { + ...metricColumn, + columnId: 'metric-column3', + }, ], changeType: 'unchanged', }, diff --git a/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts b/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts index ae40bf83574f..c0354d4db65e 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts @@ -11,7 +11,7 @@ import { layerTypes } from '../../../common'; import { metricLabel, MetricVisualizationState, supportedDataTypes } from './visualization'; const MAX_BUCKETED_COLUMNS = 1; -const MAX_METRIC_COLUMNS = 1; +const MAX_METRIC_COLUMNS = 2; // primary and secondary metric const hasLayerMismatch = (keptLayerIds: string[], table: TableSuggestion) => keptLayerIds.length > 1 || (keptLayerIds.length && table.layerId !== keptLayerIds[0]); diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx index ed1efa900cf2..81f7f08fe322 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx @@ -19,13 +19,20 @@ import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { IconChartMetric } from '@kbn/chart-icons'; import { LayerType } from '../../../common'; import { getSuggestions } from './suggestions'; -import { Visualization, OperationMetadata, DatasourceLayers, AccessorConfig } from '../../types'; +import { + Visualization, + OperationMetadata, + DatasourceLayers, + AccessorConfig, + Suggestion, +} from '../../types'; import { layerTypes } from '../../../common'; import { GROUP_ID, LENS_METRIC_ID } from './constants'; import { DimensionEditor } from './dimension_editor'; import { Toolbar } from './toolbar'; import { generateId } from '../../id_generator'; import { FormatSelectorOptions } from '../../indexpattern_datasource/dimension_panel/format_selector'; +import { IndexPatternLayer } from '../../indexpattern_datasource/types'; export const DEFAULT_MAX_COLUMNS = 3; @@ -50,6 +57,16 @@ export interface MetricVisualizationState { maxCols?: number; } +interface MetricDatasourceState { + [prop: string]: unknown; + layers: IndexPatternLayer[]; +} + +export interface MetricSuggestion extends Suggestion { + datasourceState: MetricDatasourceState; + visualizationState: MetricVisualizationState; +} + export const supportedDataTypes = new Set(['number']); // TODO - deduplicate with gauges? @@ -298,7 +315,7 @@ export const getMetricVisualization = ({ enableDimensionEditor: true, enableFormatSelector: true, formatSelectorOptions: formatterOptions, - required: true, + requiredMinDimensionCount: 1, }, { groupId: GROUP_ID.SECONDARY_METRIC, @@ -324,7 +341,7 @@ export const getMetricVisualization = ({ enableDimensionEditor: true, enableFormatSelector: true, formatSelectorOptions: formatterOptions, - required: false, + requiredMinDimensionCount: 0, }, { groupId: GROUP_ID.MAX, @@ -350,7 +367,7 @@ export const getMetricVisualization = ({ formatSelectorOptions: formatterOptions, supportStaticValue: true, prioritizedOperation: 'max', - required: false, + requiredMinDimensionCount: 0, groupTooltip: i18n.translate('xpack.lens.metric.maxTooltip', { defaultMessage: 'If the maximum value is specified, the minimum value is fixed at zero.', @@ -376,7 +393,7 @@ export const getMetricVisualization = ({ enableDimensionEditor: true, enableFormatSelector: true, formatSelectorOptions: formatterOptions, - required: false, + requiredMinDimensionCount: 0, }, ], }; @@ -484,4 +501,25 @@ export const getMetricVisualization = ({ noPadding: true, }; }, + + getSuggestionFromConvertToLensContext({ suggestions, context }) { + const allSuggestions = suggestions as MetricSuggestion[]; + return { + ...allSuggestions[0], + datasourceState: { + ...allSuggestions[0].datasourceState, + layers: allSuggestions.reduce( + (acc, s) => ({ + ...acc, + ...s.datasourceState.layers, + }), + {} + ), + }, + visualizationState: { + ...allSuggestions[0].visualizationState, + ...context.configuration, + }, + }; + }, }); diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts index eea673dc531d..dc0ce54b5bc0 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts @@ -93,7 +93,7 @@ describe('suggestions', () => { ).toHaveLength(0); }); - it('should reject date operations', () => { + it('should hide date operations', () => { expect( suggestions({ table: { @@ -118,11 +118,17 @@ describe('suggestions', () => { }, state: undefined, keptLayerIds: ['first'], - }) - ).toHaveLength(0); + }).map((s) => [s.hide, s.score]) + ).toEqual([ + [true, 0], + [true, 0], + [true, 0], + [true, 0], + [true, 0], + ]); }); - it('should reject histogram operations', () => { + it('should hide histogram operations', () => { expect( suggestions({ table: { @@ -147,8 +153,14 @@ describe('suggestions', () => { }, state: undefined, keptLayerIds: ['first'], - }) - ).toHaveLength(0); + }).map((s) => [s.hide, s.score]) + ).toEqual([ + [true, 0], + [true, 0], + [true, 0], + [true, 0], + [true, 0], + ]); }); it('should not reject histogram operations in case of switching between partition charts', () => { diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts index 36e367ca3ad6..9f2d0920983a 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts @@ -29,15 +29,10 @@ function hasIntervalScale(columns: TableSuggestionColumn[]) { } function shouldReject({ table, keptLayerIds, state }: SuggestionRequest) { - // Histograms are not good for pi. But we should not reject them on switching between partition charts. - const shouldRejectIntervals = - state?.shape && isPartitionShape(state.shape) ? false : hasIntervalScale(table.columns); - return ( keptLayerIds.length > 1 || (keptLayerIds.length && table.layerId !== keptLayerIds[0]) || table.changeType === 'reorder' || - shouldRejectIntervals || table.columns.some((col) => col.operation.isStaticValue) ); } @@ -111,6 +106,10 @@ export function suggestions({ const results: Array> = []; + // Histograms are not good for pi. But we should not hide suggestion on switching between partition charts. + const shouldHideSuggestion = + state?.shape && isPartitionShape(state.shape) ? false : hasIntervalScale(table.columns); + if ( groups.length <= PartitionChartsMeta.pie.maxBuckets && !hasCustomSuggestionsExists(subVisualizationId) @@ -309,11 +308,11 @@ export function suggestions({ return [...results] .map((suggestion) => ({ ...suggestion, - score: suggestion.score + 0.05 * groups.length, + score: shouldHideSuggestion ? 0 : suggestion.score + 0.05 * groups.length, })) .sort((a, b) => b.score - a.score) .map((suggestion) => ({ ...suggestion, - hide: incompleteConfiguration || suggestion.hide, + hide: shouldHideSuggestion || incompleteConfiguration || suggestion.hide, })); } diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx index 6a58d46b34ca..2a8bdc2dd4ab 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx @@ -14,11 +14,14 @@ import { ThemeServiceStart } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; import { EuiSpacer } from '@elastic/eui'; +import { PartitionVisConfiguration } from '@kbn/visualizations-plugin/common/convert_to_lens'; import type { Visualization, OperationMetadata, AccessorConfig, VisualizationDimensionGroupConfig, + Suggestion, + VisualizeEditorContext, } from '../../types'; import { getSortedGroups, toExpression, toPreviewExpression } from './to_expression'; import { CategoryDisplay, layerTypes, LegendDisplay, NumberDisplay } from '../../../common'; @@ -27,6 +30,17 @@ import { PartitionChartsMeta } from './partition_charts_meta'; import { DimensionEditor, PieToolbar } from './toolbar'; import { checkTableForContainsSmallValues } from './render_helpers'; import { PieChartTypes, PieLayerState, PieVisualizationState } from '../../../common'; +import { IndexPatternLayer } from '../..'; + +interface DatatableDatasourceState { + [prop: string]: unknown; + layers: IndexPatternLayer[]; +} + +export interface PartitionSuggestion extends Suggestion { + datasourceState: DatatableDatasourceState; + visualizationState: PieVisualizationState; +} function newLayerState(layerId: string): PieLayerState { return { @@ -42,6 +56,12 @@ function newLayerState(layerId: string): PieLayerState { }; } +function isPartitionVisConfiguration( + context: VisualizeEditorContext +): context is VisualizeEditorContext { + return context.type === 'lnsPie'; +} + const bucketedOperations = (op: OperationMetadata) => op.isBucketed; const numberMetricOperations = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number' && !op.isStaticValue; @@ -148,7 +168,7 @@ export const getPieVisualization = ({ } const primaryGroupConfigBaseProps = { - required: true, + requiredMinDimensionCount: 1, groupId: 'primaryGroups', accessors, enableDimensionEditor: true, @@ -264,7 +284,7 @@ export const getPieVisualization = ({ accessors: layer.metric ? [{ columnId: layer.metric }] : [], supportsMoreColumns: !layer.metric, filterOperations: numberMetricOperations, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsPie_sizeByDimensionPanel', }); @@ -422,6 +442,29 @@ export const getPieVisualization = ({ return warningMessages; }, + getSuggestionFromConvertToLensContext(props) { + const context = props.context; + if (!isPartitionVisConfiguration(context)) { + return; + } + if (!props.suggestions.length) { + return; + } + const suggestionByShape = (props.suggestions as PartitionSuggestion[]).find( + (suggestion) => suggestion.visualizationState.shape === context.configuration.shape + ); + if (!suggestionByShape) { + return; + } + return { + ...suggestionByShape, + visualizationState: { + ...suggestionByShape.visualizationState, + ...context.configuration, + }, + }; + }, + getErrorMessages(state) { const hasTooManyBucketDimensions = state.layers .map( diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx index 34aff2658277..baaed78ec023 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx @@ -414,7 +414,7 @@ export const getAnnotationsConfiguration = ({ invalidMessage: i18n.translate('xpack.lens.xyChart.addAnnotationsLayerLabelDisabledHelp', { defaultMessage: 'Annotations require a time based chart to work. Add a date histogram.', }), - required: false, + requiredMinDimensionCount: 0, supportsMoreColumns: true, supportFieldFormat: false, enableDimensionEditor: true, diff --git a/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.tsx index 362b63c46eb2..84f0a35d3b95 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.tsx @@ -461,7 +461,7 @@ export const getReferenceConfiguration = ({ accessors: config.map(({ forAccessor, color }) => getSingleColorConfig(forAccessor, color)), filterOperations: isNumericMetric, supportsMoreColumns: true, - required: false, + requiredMinDimensionCount: 0, enableDimensionEditor: true, supportStaticValue: true, paramEditorCustomProps: { diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index 818df64fba94..556a89c9a855 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -1047,7 +1047,7 @@ describe('xy_visualization', () => { frame, layerId: 'first', }).groups; - expect(splitGroup.required).toBe(true); + expect(splitGroup.requiredMinDimensionCount).toBe(1); }); test.each([ @@ -1087,7 +1087,7 @@ describe('xy_visualization', () => { frame, layerId: 'first', }).groups; - expect(splitGroup.required).toBe(false); + expect(splitGroup.requiredMinDimensionCount).toBe(0); } ); @@ -1236,7 +1236,7 @@ describe('xy_visualization', () => { frame, layerId: 'first', }).groups; - expect(splitGroup.required).toBe(true); + expect(splitGroup.requiredMinDimensionCount).toBe(1); } ); }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 61bdd4151219..34ab6c88ffa1 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -276,10 +276,6 @@ export const getXyVisualization = ({ accessors: sortedAccessors, }); - if (isReferenceLayer(layer)) { - return getReferenceConfiguration({ state, frame, layer, sortedAccessors }); - } - const dataLayer: XYDataLayerConfig = layer; const dataLayers = getDataLayers(state.layers); @@ -332,7 +328,7 @@ export const getXyVisualization = ({ accessors: mappedAccessors, filterOperations: isNumericDynamicMetric, supportsMoreColumns: true, - required: true, + requiredMinDimensionCount: 1, dataTestSubj: 'lnsXY_yDimensionPanel', enableDimensionEditor: true, }, @@ -357,7 +353,8 @@ export const getXyVisualization = ({ filterOperations: isBucketed, supportsMoreColumns: !dataLayer.splitAccessor, dataTestSubj: 'lnsXY_splitDimensionPanel', - required: dataLayer.seriesType.includes('percentage') && hasOnlyOneAccessor, + requiredMinDimensionCount: + dataLayer.seriesType.includes('percentage') && hasOnlyOneAccessor ? 1 : 0, enableDimensionEditor: true, }, ], diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts index 1184712ae033..b63bfeb5fbd9 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts @@ -615,7 +615,12 @@ function getScore( changeType: TableChangeType ) { // Unchanged table suggestions half the score because the underlying data doesn't change - const changeFactor = changeType === 'unchanged' ? 0.5 : 1; + const changeFactor = + changeType === 'reduced' || changeType === 'layers' + ? 0.3 + : changeType === 'unchanged' + ? 0.5 + : 1; // chart with multiple y values and split series will have a score of 1, single y value and no split series reduce score return (((yValues.length > 1 ? 2 : 1) + (splitBy ? 1 : 0)) / 3) * changeFactor; } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/decimal_degrees_form.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/decimal_degrees_form.tsx new file mode 100644 index 000000000000..03b23e409b77 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/decimal_degrees_form.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ChangeEvent, Component } from 'react'; +import { + EuiForm, + EuiFormRow, + EuiButton, + EuiFieldNumber, + EuiTextAlign, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { MapCenter, MapSettings } from '../../../../common/descriptor_types'; +import { withinRange } from './utils'; + +interface Props { + settings: MapSettings; + zoom: number; + center: MapCenter; + onSubmit: (lat: number, lon: number, zoom: number) => void; +} + +interface State { + lat: number | string; + lon: number | string; + zoom: number | string; +} + +export class DecimalDegreesForm extends Component { + state: State = { + lat: this.props.center.lat, + lon: this.props.center.lon, + zoom: this.props.zoom, + }; + + _onLatChange = (evt: ChangeEvent) => { + const sanitizedValue = parseFloat(evt.target.value); + this.setState({ + lat: isNaN(sanitizedValue) ? '' : sanitizedValue, + }); + }; + + _onLonChange = (evt: ChangeEvent) => { + const sanitizedValue = parseFloat(evt.target.value); + this.setState({ + lon: isNaN(sanitizedValue) ? '' : sanitizedValue, + }); + }; + + _onZoomChange = (evt: ChangeEvent) => { + const sanitizedValue = parseFloat(evt.target.value); + this.setState({ + zoom: isNaN(sanitizedValue) ? '' : sanitizedValue, + }); + }; + + _onSubmit = () => { + const { lat, lon, zoom } = this.state; + this.props.onSubmit(lat as number, lon as number, zoom as number); + }; + + render() { + const { isInvalid: isLatInvalid, error: latError } = withinRange(this.state.lat, -90, 90); + const { isInvalid: isLonInvalid, error: lonError } = withinRange(this.state.lon, -180, 180); + const { isInvalid: isZoomInvalid, error: zoomError } = withinRange( + this.state.zoom, + this.props.settings.minZoom, + this.props.settings.maxZoom + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/mgrs_form.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/mgrs_form.tsx new file mode 100644 index 000000000000..48455f89b746 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/mgrs_form.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import _ from 'lodash'; +import React, { ChangeEvent, Component } from 'react'; +import { + EuiForm, + EuiFormRow, + EuiButton, + EuiFieldNumber, + EuiFieldText, + EuiTextAlign, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { MapCenter, MapSettings } from '../../../../common/descriptor_types'; +import { ddToMGRS, mgrsToDD, withinRange } from './utils'; + +interface Props { + settings: MapSettings; + zoom: number; + center: MapCenter; + onSubmit: (lat: number, lon: number, zoom: number) => void; +} + +interface State { + mgrs: string; + zoom: number | string; +} + +export class MgrsForm extends Component { + state: State = { + mgrs: ddToMGRS(this.props.center.lat, this.props.center.lon), + zoom: this.props.zoom, + }; + + _toPoint() { + return this.state.mgrs === '' ? undefined : mgrsToDD(this.state.mgrs); + } + + _isMgrsInvalid() { + const point = this._toPoint(); + return ( + point === undefined || + !point.north || + _.isNaN(point.north) || + !point.south || + _.isNaN(point.south) || + !point.east || + _.isNaN(point.east) || + !point.west || + _.isNaN(point.west) + ); + } + + _onMGRSChange = (evt: ChangeEvent) => { + this.setState({ + mgrs: _.isNull(evt.target.value) ? '' : evt.target.value, + }); + }; + + _onZoomChange = (evt: ChangeEvent) => { + const sanitizedValue = parseFloat(evt.target.value); + this.setState({ + zoom: isNaN(sanitizedValue) ? '' : sanitizedValue, + }); + }; + + _onSubmit = () => { + const point = this._toPoint(); + if (point) { + this.props.onSubmit(point.north, point.east, this.state.zoom as number); + } + }; + + render() { + const isMgrsInvalid = this._isMgrsInvalid(); + const mgrsError = isMgrsInvalid + ? i18n.translate('xpack.maps.setViewControl.mgrsInvalid', { + defaultMessage: 'MGRS is invalid', + }) + : null; + const { isInvalid: isZoomInvalid, error: zoomError } = withinRange( + this.state.zoom, + this.props.settings.minZoom, + this.props.settings.maxZoom + ); + + return ( + + + + + + + + + + + + + + + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/number_form_row.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/number_form_row.tsx new file mode 100644 index 000000000000..1fec1c76430e --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/number_form_row.tsx @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.tsx index eb0b1cfc1dda..906cbb8a0bb0 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.tsx @@ -5,50 +5,11 @@ * 2.0. */ -import React, { ChangeEvent, Component, Fragment } from 'react'; -import { - EuiForm, - EuiFormRow, - EuiButton, - EuiFieldNumber, - EuiFieldText, - EuiButtonIcon, - EuiPopover, - EuiTextAlign, - EuiSpacer, - EuiPanel, -} from '@elastic/eui'; -import { EuiButtonEmpty } from '@elastic/eui'; -import { EuiRadioGroup } from '@elastic/eui'; +import React, { Component } from 'react'; +import { EuiButtonIcon, EuiPopover, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import * as usng from 'usng.js'; -import { isNaN, isNull } from 'lodash'; import { MapCenter, MapSettings } from '../../../../common/descriptor_types'; - -export const COORDINATE_SYSTEM_DEGREES_DECIMAL = 'dd'; -export const COORDINATE_SYSTEM_MGRS = 'mgrs'; -export const COORDINATE_SYSTEM_UTM = 'utm'; - -export const DEFAULT_SET_VIEW_COORDINATE_SYSTEM = COORDINATE_SYSTEM_DEGREES_DECIMAL; - -// @ts-ignore -const converter = new usng.Converter(); - -const COORDINATE_SYSTEMS = [ - { - id: COORDINATE_SYSTEM_DEGREES_DECIMAL, - label: 'Degrees Decimal', - }, - { - id: COORDINATE_SYSTEM_UTM, - label: 'UTM', - }, - { - id: COORDINATE_SYSTEM_MGRS, - label: 'MGRS', - }, -]; +import { SetViewForm } from './set_view_form'; export interface Props { settings: MapSettings; @@ -59,73 +20,17 @@ export interface Props { interface State { isPopoverOpen: boolean; - lat: number | string; - lon: number | string; - zoom: number | string; - coord: string; - mgrs: string; - utm: { - northing: string; - easting: string; - zoneNumber: string; - zoneLetter: string | undefined; - zone: string; - }; - isCoordPopoverOpen: boolean; - prevView: string | undefined; } export class SetViewControl extends Component { state: State = { isPopoverOpen: false, - lat: 0, - lon: 0, - zoom: 0, - coord: DEFAULT_SET_VIEW_COORDINATE_SYSTEM, - mgrs: '', - utm: { - northing: '', - easting: '', - zoneNumber: '', - zoneLetter: '', - zone: '', - }, - isCoordPopoverOpen: false, - prevView: '', }; - static getDerivedStateFromProps(nextProps: Props, prevState: State) { - const nextView = getViewString(nextProps.center.lat, nextProps.center.lon, nextProps.zoom); - - const utm = convertLatLonToUTM(nextProps.center.lat, nextProps.center.lon); - const mgrs = convertLatLonToMGRS(nextProps.center.lat, nextProps.center.lon); - - if (nextView !== prevState.prevView) { - return { - lat: nextProps.center.lat, - lon: nextProps.center.lon, - zoom: nextProps.zoom, - utm, - mgrs, - prevView: nextView, - }; - } - - return null; - } - _togglePopover = () => { - if (this.state.isPopoverOpen) { - this._closePopover(); - return; - } - - this.setState({ - lat: this.props.center.lat, - lon: this.props.center.lon, - zoom: this.props.zoom, - isPopoverOpen: true, - }); + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); }; _closePopover = () => { @@ -134,567 +39,11 @@ export class SetViewControl extends Component { }); }; - _onCoordinateSystemChange = (coordId: string) => { - this.setState({ - coord: coordId, - }); - }; - - _onLatChange = (evt: ChangeEvent) => { - this._onChange('lat', evt); - }; - - _onLonChange = (evt: ChangeEvent) => { - this._onChange('lon', evt); - }; - - _onZoomChange = (evt: ChangeEvent) => { - const sanitizedValue = parseFloat(evt.target.value); - this.setState({ - ['zoom']: isNaN(sanitizedValue) ? '' : sanitizedValue, - }); - }; - - _onUTMZoneChange = (evt: ChangeEvent) => { - this._onUTMChange('zone', evt); - }; - - _onUTMEastingChange = (evt: ChangeEvent) => { - this._onUTMChange('easting', evt); - }; - - _onUTMNorthingChange = (evt: ChangeEvent) => { - this._onUTMChange('northing', evt); - }; - - _onMGRSChange = (evt: ChangeEvent) => { - this.setState( - { - ['mgrs']: isNull(evt.target.value) ? '' : evt.target.value, - }, - this._syncToMGRS - ); - }; - - _onUTMChange = (name: 'easting' | 'northing' | 'zone', evt: ChangeEvent) => { - const value = evt.target.value; - const updateObj = { ...this.state.utm }; - updateObj[name] = isNull(value) ? '' : value; - if (name === 'zone' && value.length > 0) { - const zoneLetter = value.substring(value.length - 1); - const zoneNumber = value.substring(0, value.length - 1); - updateObj.zoneLetter = isNaN(zoneLetter) ? zoneLetter : ''; - updateObj.zoneNumber = isNaN(zoneNumber) ? '' : zoneNumber; - } - this.setState( - { - // @ts-ignore - ['utm']: updateObj, - }, - this._syncToUTM - ); - }; - - _onChange = (name: 'lat' | 'lon', evt: ChangeEvent) => { - const sanitizedValue = parseFloat(evt.target.value); - - this.setState( - // @ts-ignore - { - [name]: isNaN(sanitizedValue) ? '' : sanitizedValue, - }, - this._syncToLatLon - ); - }; - - /** - * Sync all coordinates to the lat/lon that is set - */ - _syncToLatLon = () => { - if (this.state.lat !== '' && this.state.lon !== '') { - const utm = convertLatLonToUTM(this.state.lat, this.state.lon); - const mgrs = convertLatLonToMGRS(this.state.lat, this.state.lon); - - this.setState({ mgrs, utm }); - } else { - this.setState({ - mgrs: '', - utm: { northing: '', easting: '', zoneNumber: '', zoneLetter: '', zone: '' }, - }); - } - }; - - /** - * Sync the current lat/lon to MGRS that is set - */ - _syncToMGRS = () => { - if (this.state.mgrs !== '') { - let lon; - let lat; - - try { - const { north, east } = convertMGRStoLL(this.state.mgrs); - lat = north; - lon = east; - } catch (err) { - return; - } - - const utm = convertLatLonToUTM(lat, lon); - - this.setState({ - lat: isNaN(lat) ? '' : lat, - lon: isNaN(lon) ? '' : lon, - utm, - }); - } else { - this.setState({ - lat: '', - lon: '', - utm: { northing: '', easting: '', zoneNumber: '', zoneLetter: '', zone: '' }, - }); - } - }; - - /** - * Sync the current lat/lon to UTM that is set - */ - _syncToUTM = () => { - if (this.state.utm) { - let lat; - let lon; - try { - ({ lat, lon } = converter.UTMtoLL( - this.state.utm.northing, - this.state.utm.easting, - this.state.utm.zoneNumber - )); - } catch (err) { - return; - } - - const mgrs = convertLatLonToMGRS(lat, lon); - - this.setState({ - lat: isNaN(lat) ? '' : lat, - lon: isNaN(lon) ? '' : lon, - mgrs, - }); - } else { - this.setState({ - lat: '', - lon: '', - mgrs: '', - }); - } - }; - - _renderNumberFormRow = ({ - value, - min, - max, - onChange, - label, - dataTestSubj, - }: { - value: string | number; - min: number; - max: number; - onChange: (evt: ChangeEvent) => void; - label: string; - dataTestSubj: string; - }) => { - const isInvalid = value === '' || value > max || value < min; - const error = isInvalid ? `Must be between ${min} and ${max}` : null; - return { - isInvalid, - component: ( - - - - ), - }; - }; - - _renderMGRSFormRow = ({ - value, - onChange, - label, - dataTestSubj, - }: { - value: string; - onChange: (evt: ChangeEvent) => void; - label: string; - dataTestSubj: string; - }) => { - let point; - try { - point = convertMGRStoLL(value); - } catch (err) { - point = undefined; - } - - const isInvalid = - value === '' || - point === undefined || - !point.north || - isNaN(point.north) || - !point.south || - isNaN(point.south) || - !point.east || - isNaN(point.east) || - !point.west || - isNaN(point.west); - const error = isInvalid - ? i18n.translate('xpack.maps.setViewControl.mgrsInvalid', { - defaultMessage: 'MGRS is invalid', - }) - : null; - return { - isInvalid, - component: ( - - - - ), - }; - }; - - _renderUTMZoneRow = ({ - value, - onChange, - label, - dataTestSubj, - }: { - value: string | number; - onChange: (evt: ChangeEvent) => void; - label: string; - dataTestSubj: string; - }) => { - let point; - try { - point = converter.UTMtoLL( - this.state.utm.northing, - this.state.utm.easting, - this.state.utm.zoneNumber - ); - } catch { - point = undefined; - } - - const isInvalid = value === '' || point === undefined; - const error = isInvalid - ? i18n.translate('xpack.maps.setViewControl.utmInvalidZone', { - defaultMessage: 'UTM Zone is invalid', - }) - : null; - return { - isInvalid, - component: ( - - - - ), - }; - }; - - _renderUTMEastingRow = ({ - value, - onChange, - label, - dataTestSubj, - }: { - value: string | number; - onChange: (evt: ChangeEvent) => void; - label: string; - dataTestSubj: string; - }) => { - let point; - try { - point = converter.UTMtoLL(this.state.utm.northing, value, this.state.utm.zoneNumber); - } catch { - point = undefined; - } - const isInvalid = value === '' || point === undefined; - const error = isInvalid - ? i18n.translate('xpack.maps.setViewControl.utmInvalidEasting', { - defaultMessage: 'UTM Easting is invalid', - }) - : null; - return { - isInvalid, - component: ( - - - - ), - }; - }; - - _renderUTMNorthingRow = ({ - value, - onChange, - label, - dataTestSubj, - }: { - value: string | number; - onChange: (evt: ChangeEvent) => void; - label: string; - dataTestSubj: string; - }) => { - let point; - try { - point = converter.UTMtoLL(value, this.state.utm.easting, this.state.utm.zoneNumber); - } catch { - point = undefined; - } - const isInvalid = value === '' || point === undefined; - const error = isInvalid - ? i18n.translate('xpack.maps.setViewControl.utmInvalidNorthing', { - defaultMessage: 'UTM Northing is invalid', - }) - : null; - return { - isInvalid, - component: ( - - - - ), - }; - }; - - _onSubmit = () => { - const { lat, lon, zoom } = this.state; + _onSubmit = (lat: number, lon: number, zoom: number) => { this._closePopover(); - this.props.onSubmit({ lat: lat as number, lon: lon as number, zoom: zoom as number }); + this.props.onSubmit({ lat, lon, zoom }); }; - _renderSetViewForm() { - let isLatInvalid; - let latFormRow; - let isLonInvalid; - let lonFormRow; - let isMGRSInvalid; - let mgrsFormRow; - let isUtmZoneInvalid; - let utmZoneRow; - let isUtmEastingInvalid; - let utmEastingRow; - let isUtmNorthingInvalid; - let utmNorthingRow; - - if (this.state.coord === COORDINATE_SYSTEM_DEGREES_DECIMAL) { - const latRenderObject = this._renderNumberFormRow({ - value: this.state.lat, - min: -90, - max: 90, - onChange: this._onLatChange, - label: i18n.translate('xpack.maps.setViewControl.latitudeLabel', { - defaultMessage: 'Latitude', - }), - dataTestSubj: 'latitudeInput', - }); - - isLatInvalid = latRenderObject.isInvalid; - latFormRow = latRenderObject.component; - - const lonRenderObject = this._renderNumberFormRow({ - value: this.state.lon, - min: -180, - max: 180, - onChange: this._onLonChange, - label: i18n.translate('xpack.maps.setViewControl.longitudeLabel', { - defaultMessage: 'Longitude', - }), - dataTestSubj: 'longitudeInput', - }); - - isLonInvalid = lonRenderObject.isInvalid; - lonFormRow = lonRenderObject.component; - } else if (this.state.coord === COORDINATE_SYSTEM_MGRS) { - const mgrsRenderObject = this._renderMGRSFormRow({ - value: this.state.mgrs, - onChange: this._onMGRSChange, - label: i18n.translate('xpack.maps.setViewControl.mgrsLabel', { - defaultMessage: 'MGRS', - }), - dataTestSubj: 'mgrsInput', - }); - - isMGRSInvalid = mgrsRenderObject.isInvalid; - mgrsFormRow = mgrsRenderObject.component; - } else if (this.state.coord === COORDINATE_SYSTEM_UTM) { - const utmZoneRenderObject = this._renderUTMZoneRow({ - value: this.state.utm !== undefined ? this.state.utm.zone : '', - onChange: this._onUTMZoneChange, - label: i18n.translate('xpack.maps.setViewControl.utmZoneLabel', { - defaultMessage: 'UTM Zone', - }), - dataTestSubj: 'utmZoneInput', - }); - - isUtmZoneInvalid = utmZoneRenderObject.isInvalid; - utmZoneRow = utmZoneRenderObject.component; - - const utmEastingRenderObject = this._renderUTMEastingRow({ - value: this.state.utm !== undefined ? this.state.utm.easting : '', - onChange: this._onUTMEastingChange, - label: i18n.translate('xpack.maps.setViewControl.utmEastingLabel', { - defaultMessage: 'UTM Easting', - }), - dataTestSubj: 'utmEastingInput', - }); - - isUtmEastingInvalid = utmEastingRenderObject.isInvalid; - utmEastingRow = utmEastingRenderObject.component; - - const utmNorthingRenderObject = this._renderUTMNorthingRow({ - value: this.state.utm !== undefined ? this.state.utm.northing : '', - onChange: this._onUTMNorthingChange, - label: i18n.translate('xpack.maps.setViewControl.utmNorthingLabel', { - defaultMessage: 'UTM Northing', - }), - dataTestSubj: 'utmNorthingInput', - }); - - isUtmNorthingInvalid = utmNorthingRenderObject.isInvalid; - utmNorthingRow = utmNorthingRenderObject.component; - } - - const { isInvalid: isZoomInvalid, component: zoomFormRow } = this._renderNumberFormRow({ - value: this.state.zoom, - min: this.props.settings.minZoom, - max: this.props.settings.maxZoom, - onChange: this._onZoomChange, - label: i18n.translate('xpack.maps.setViewControl.zoomLabel', { - defaultMessage: 'Zoom', - }), - dataTestSubj: 'zoomInput', - }); - - let coordinateInputs; - if (this.state.coord === 'dd') { - coordinateInputs = ( - - {latFormRow} - {lonFormRow} - {zoomFormRow} - - ); - } else if (this.state.coord === 'dms') { - coordinateInputs = ( - - {latFormRow} - {lonFormRow} - {zoomFormRow} - - ); - } else if (this.state.coord === 'utm') { - coordinateInputs = ( - - {utmZoneRow} - {utmEastingRow} - {utmNorthingRow} - {zoomFormRow} - - ); - } else if (this.state.coord === 'mgrs') { - coordinateInputs = ( - - {mgrsFormRow} - {zoomFormRow} - - ); - } - - return ( - - { - this.setState({ isCoordPopoverOpen: false }); - }} - button={ - { - this.setState({ isCoordPopoverOpen: !this.state.isCoordPopoverOpen }); - }} - > - Coordinate System - - } - > - - - - {coordinateInputs} - - - - - - - - - - ); - } - render() { return ( { isOpen={this.state.isPopoverOpen} closePopover={this._closePopover} > - {this._renderSetViewForm()} + ); } } - -function convertLatLonToUTM(lat: string | number, lon: string | number) { - const utmCoord = converter.LLtoUTM(lat, lon); - - let eastwest = 'E'; - if (utmCoord.easting < 0) { - eastwest = 'W'; - } - let norwest = 'N'; - if (utmCoord.northing < 0) { - norwest = 'S'; - } - - if (utmCoord !== 'undefined') { - utmCoord.zoneLetter = isNaN(lat) ? '' : converter.UTMLetterDesignator(lat); - utmCoord.zone = `${utmCoord.zoneNumber}${utmCoord.zoneLetter}`; - utmCoord.easting = Math.round(utmCoord.easting); - utmCoord.northing = Math.round(utmCoord.northing); - utmCoord.str = `${utmCoord.zoneNumber}${utmCoord.zoneLetter} ${utmCoord.easting}${eastwest} ${utmCoord.northing}${norwest}`; - } - - return utmCoord; -} - -function convertLatLonToMGRS(lat: string | number, lon: string | number) { - const mgrsCoord = converter.LLtoMGRS(lat, lon, 5); - return mgrsCoord; -} - -function getViewString(lat: number, lon: number, zoom: number) { - return `${lat},${lon},${zoom}`; -} - -function convertMGRStoUSNG(mgrs: string) { - let squareIdEastSpace = 0; - for (let i = mgrs.length - 1; i > -1; i--) { - // check if we have hit letters yet - if (isNaN(mgrs.substr(i, 1))) { - squareIdEastSpace = i + 1; - break; - } - } - const gridZoneSquareIdSpace = squareIdEastSpace ? squareIdEastSpace - 2 : -1; - const numPartLength = mgrs.substr(squareIdEastSpace).length / 2; - // add the number split space - const eastNorthSpace = squareIdEastSpace ? squareIdEastSpace + numPartLength : -1; - const stringArray = mgrs.split(''); - - stringArray.splice(eastNorthSpace, 0, ' '); - stringArray.splice(squareIdEastSpace, 0, ' '); - stringArray.splice(gridZoneSquareIdSpace, 0, ' '); - - const rejoinedArray = stringArray.join(''); - return rejoinedArray; -} - -function convertMGRStoLL(mgrs: string) { - return mgrs ? converter.USNGtoLL(convertMGRStoUSNG(mgrs)) : ''; -} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_form.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_form.tsx new file mode 100644 index 000000000000..28fe6073d764 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_form.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component } from 'react'; +import { EuiButtonEmpty, EuiPopover, EuiRadioGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { MapCenter, MapSettings } from '../../../../common/descriptor_types'; +import { DecimalDegreesForm } from './decimal_degrees_form'; +import { MgrsForm } from './mgrs_form'; +import { UtmForm } from './utm_form'; + +const DEGREES_DECIMAL = 'dd'; +const MGRS = 'mgrs'; +const UTM = 'utm'; + +const COORDINATE_SYSTEM_OPTIONS = [ + { + id: DEGREES_DECIMAL, + label: i18n.translate('xpack.maps.setViewControl.decimalDegreesLabel', { + defaultMessage: 'Decimal degrees', + }), + }, + { + id: UTM, + label: 'UTM', + }, + { + id: MGRS, + label: 'MGRS', + }, +]; + +interface Props { + settings: MapSettings; + zoom: number; + center: MapCenter; + onSubmit: (lat: number, lon: number, zoom: number) => void; +} + +interface State { + isPopoverOpen: boolean; + coordinateSystem: string; +} + +export class SetViewForm extends Component { + state: State = { + coordinateSystem: DEGREES_DECIMAL, + isPopoverOpen: false, + }; + + _togglePopover = () => { + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + _closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + _onCoordinateSystemChange = (optionId: string) => { + this._closePopover(); + this.setState({ + coordinateSystem: optionId, + }); + }; + + _renderForm() { + if (this.state.coordinateSystem === MGRS) { + return ( + + ); + } + + if (this.state.coordinateSystem === UTM) { + return ( + + ); + } + + return ( + + ); + } + + render() { + return ( +
+ + + + } + > + + + {this._renderForm()} +
+ ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/utils.test.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/utils.test.ts new file mode 100644 index 000000000000..e6a6819687d1 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/utils.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ddToMGRS, mgrsToDD, ddToUTM, utmToDD } from './utils'; + +describe('MGRS', () => { + test('ddToMGRS should convert lat lon to MGRS', () => { + expect(ddToMGRS(29.29926, 32.05495)).toEqual('36RVT08214151'); + }); + + test('ddToMGRS should return empty string for lat lon that does not translate to MGRS grid', () => { + expect(ddToMGRS(90, 32.05495)).toEqual(''); + }); + + test('mgrsToDD should convert MGRS to lat lon', () => { + expect(mgrsToDD('36RVT08214151')).toEqual({ + east: 32.05498649594143, + north: 29.299330195900975, + south: 29.299239224067065, + west: 32.054884373627345, + }); + }); +}); + +describe('UTM', () => { + test('ddToUTM should convert lat lon to UTM', () => { + expect(ddToUTM(29.29926, 32.05495)).toEqual({ + easting: '408216', + northing: '3241512', + zone: '36R', + }); + }); + + test('ddToUTM should return empty strings for lat lon that does not translate to UTM grid', () => { + expect(ddToUTM(90, 32.05495)).toEqual({ + northing: '', + easting: '', + zone: '', + }); + }); + + test('utmToDD should convert UTM to lat lon', () => { + expect(utmToDD('3241512', '408216', '36R')).toEqual({ + lat: 29.29925770984472, + lon: 32.05494597943409, + }); + }); +}); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/utils.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/utils.ts new file mode 100644 index 000000000000..7edf1428d931 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/utils.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import * as usng from 'usng.js'; + +// @ts-ignore +const converter = new usng.Converter(); + +export function withinRange(value: string | number, min: number, max: number) { + const isInvalid = value === '' || value > max || value < min; + const error = isInvalid + ? i18n.translate('xpack.maps.setViewControl.outOfRangeErrorMsg', { + defaultMessage: `Must be between {min} and {max}`, + values: { min, max }, + }) + : null; + return { isInvalid, error }; +} + +export function ddToUTM(lat: number, lon: number) { + try { + const utm = converter.LLtoUTM(lat, lon); + return { + northing: utm === converter.UNDEFINED_STR ? '' : String(Math.round(utm.northing)), + easting: utm === converter.UNDEFINED_STR ? '' : String(Math.round(utm.easting)), + zone: + utm === converter.UNDEFINED_STR + ? '' + : `${utm.zoneNumber}${converter.UTMLetterDesignator(lat)}`, + }; + } catch (e) { + return { + northing: '', + easting: '', + zone: '', + }; + } +} + +export function utmToDD(northing: string, easting: string, zoneNumber: string) { + try { + return converter.UTMtoLL(northing, easting, zoneNumber); + } catch (e) { + return undefined; + } +} + +export function ddToMGRS(lat: number, lon: number) { + try { + const mgrsCoord = converter.LLtoMGRS(lat, lon, 5); + return mgrsCoord; + } catch (e) { + return ''; + } +} + +function mgrstoUSNG(mgrs: string) { + let squareIdEastSpace = 0; + for (let i = mgrs.length - 1; i > -1; i--) { + // check if we have hit letters yet + if (isNaN(parseInt(mgrs.substr(i, 1), 10))) { + squareIdEastSpace = i + 1; + break; + } + } + const gridZoneSquareIdSpace = squareIdEastSpace ? squareIdEastSpace - 2 : -1; + const numPartLength = mgrs.substr(squareIdEastSpace).length / 2; + // add the number split space + const eastNorthSpace = squareIdEastSpace ? squareIdEastSpace + numPartLength : -1; + const stringArray = mgrs.split(''); + + stringArray.splice(eastNorthSpace, 0, ' '); + stringArray.splice(squareIdEastSpace, 0, ' '); + stringArray.splice(gridZoneSquareIdSpace, 0, ' '); + + const rejoinedArray = stringArray.join(''); + return rejoinedArray; +} + +export function mgrsToDD(mgrs: string) { + try { + return converter.USNGtoLL(mgrstoUSNG(mgrs)); + } catch (e) { + return undefined; + } +} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/utm_form.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/utm_form.tsx new file mode 100644 index 000000000000..76a362821705 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/utm_form.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import _ from 'lodash'; +import React, { ChangeEvent, Component } from 'react'; +import { + EuiForm, + EuiFormRow, + EuiButton, + EuiFieldNumber, + EuiFieldText, + EuiTextAlign, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { MapCenter, MapSettings } from '../../../../common/descriptor_types'; +import { ddToUTM, utmToDD, withinRange } from './utils'; + +interface Props { + settings: MapSettings; + zoom: number; + center: MapCenter; + onSubmit: (lat: number, lon: number, zoom: number) => void; +} + +interface State { + northing: string; + easting: string; + zone: string; + zoom: number | string; +} + +export class UtmForm extends Component { + constructor(props: Props) { + super(props); + const utm = ddToUTM(this.props.center.lat, this.props.center.lon); + this.state = { + northing: utm.northing, + easting: utm.easting, + zone: utm.zone, + zoom: this.props.zoom, + }; + } + + _toPoint() { + const { northing, easting, zone } = this.state; + return northing === '' || easting === '' || zone.length < 2 + ? undefined + : utmToDD(northing, easting, zone.substring(0, zone.length - 1)); + } + + _isUtmInvalid() { + const point = this._toPoint(); + return point === undefined; + } + + _onZoneChange = (evt: ChangeEvent) => { + this.setState({ + zone: _.isNull(evt.target.value) ? '' : evt.target.value, + }); + }; + + _onEastingChange = (evt: ChangeEvent) => { + this.setState({ + easting: _.isNull(evt.target.value) ? '' : evt.target.value, + }); + }; + + _onNorthingChange = (evt: ChangeEvent) => { + this.setState({ + northing: _.isNull(evt.target.value) ? '' : evt.target.value, + }); + }; + + _onZoomChange = (evt: ChangeEvent) => { + const sanitizedValue = parseFloat(evt.target.value); + this.setState({ + zoom: isNaN(sanitizedValue) ? '' : sanitizedValue, + }); + }; + + _onSubmit = () => { + const point = this._toPoint(); + if (point) { + this.props.onSubmit(point.lat, point.lon, this.state.zoom as number); + } + }; + + render() { + const isUtmInvalid = this._isUtmInvalid(); + const northingError = + isUtmInvalid || this.state.northing === '' + ? i18n.translate('xpack.maps.setViewControl.utmInvalidNorthing', { + defaultMessage: 'UTM Northing is invalid', + }) + : null; + const eastingError = + isUtmInvalid || this.state.northing === '' + ? i18n.translate('xpack.maps.setViewControl.utmInvalidEasting', { + defaultMessage: 'UTM Easting is invalid', + }) + : null; + const zoneError = + isUtmInvalid || this.state.northing === '' + ? i18n.translate('xpack.maps.setViewControl.utmInvalidZone', { + defaultMessage: 'UTM Zone is invalid', + }) + : null; + const { isInvalid: isZoomInvalid, error: zoomError } = withinRange( + this.state.zoom, + this.props.settings.minZoom, + this.props.settings.maxZoom + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +} diff --git a/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx b/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx index e1d816d61357..899006b5918d 100644 --- a/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx +++ b/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx @@ -34,7 +34,7 @@ export const LogCategorizationPage: FC = () => { diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx index 0d20267f33ec..7566d3664af6 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx @@ -20,7 +20,7 @@ jest.mock('./full_time_range_selector_service', () => ({ mockSetFullTimeRange(indexPattern, query), })); -jest.mock('../../contexts/ml/use_storage', () => { +jest.mock('../../contexts/storage', () => { return { useStorage: jest.fn(() => 'exclude-frozen'), }; diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx index 06bb873971ba..9b9154eb3366 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx @@ -23,7 +23,7 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/type import { i18n } from '@kbn/i18n'; import type { DataView } from '@kbn/data-views-plugin/public'; import { setFullTimeRange } from './full_time_range_selector_service'; -import { useStorage } from '../../contexts/ml/use_storage'; +import { useStorage } from '../../contexts/storage'; import { ML_FROZEN_TIER_PREFERENCE } from '../../../../common/types/storage'; import { GetTimeFieldRangeResponse } from '../../services/ml_api_service'; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index 79d33ef9cd2a..16fbc81b23f1 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -28,7 +28,7 @@ import { JobSelectorFlyoutProps, } from './job_selector_flyout'; import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; -import { useStorage } from '../../contexts/ml/use_storage'; +import { useStorage } from '../../contexts/storage'; import { ML_APPLY_TIME_RANGE_CONFIG } from '../../../../common/types/storage'; interface GroupObj { diff --git a/x-pack/plugins/ml/public/application/components/ml_page/notifications_indicator.tsx b/x-pack/plugins/ml/public/application/components/ml_page/notifications_indicator.tsx index 20d771f5654d..d0e3516af3db 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/notifications_indicator.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/notifications_indicator.tsx @@ -16,7 +16,10 @@ import { EuiToolTip, } from '@elastic/eui'; import { combineLatest, of, timer } from 'rxjs'; -import { catchError, filter, switchMap } from 'rxjs/operators'; +import { catchError, switchMap } from 'rxjs/operators'; +import moment from 'moment'; +import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; +import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; import { useAsObservable } from '../../hooks'; import { NotificationsCountResponse } from '../../../../common/types/notifications'; import { useMlKibana } from '../../contexts/kibana'; @@ -31,21 +34,26 @@ export const NotificationsIndicator: FC = () => { mlServices: { mlApiServices }, }, } = useMlKibana(); - const [lastCheckedAt] = useStorage(ML_NOTIFICATIONS_LAST_CHECKED_AT); + const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE); + const [lastCheckedAt] = useStorage(ML_NOTIFICATIONS_LAST_CHECKED_AT); const lastCheckedAt$ = useAsObservable(lastCheckedAt); + /** Holds the value used for the actual request */ + const [lastCheckRequested, setLastCheckRequested] = useState(); const [notificationsCounts, setNotificationsCounts] = useState(); useEffect(function startPollingNotifications() { - const subscription = combineLatest([ - lastCheckedAt$.pipe(filter((v): v is number => !!v)), - timer(0, NOTIFICATIONS_CHECK_INTERVAL), - ]) + const subscription = combineLatest([lastCheckedAt$, timer(0, NOTIFICATIONS_CHECK_INTERVAL)]) .pipe( - switchMap(([lastChecked]) => - mlApiServices.notifications.countMessages$({ lastCheckedAt: lastChecked }) - ), + switchMap(([lastChecked]) => { + const lastCheckedAtQuery = lastChecked ?? moment().subtract(7, 'd').valueOf(); + setLastCheckRequested(lastCheckedAtQuery); + // Use the latest check time or 7 days ago by default. + return mlApiServices.notifications.countMessages$({ + lastCheckedAt: lastCheckedAtQuery, + }); + }), catchError((error) => { // Fail silently for now return of({} as NotificationsCountResponse); @@ -80,12 +88,22 @@ export const NotificationsIndicator: FC = () => { content={ } > - {errorsAndWarningCount} + + {errorsAndWarningCount} + ) : null} @@ -96,7 +114,8 @@ export const NotificationsIndicator: FC = () => { content={ } > @@ -106,6 +125,7 @@ export const NotificationsIndicator: FC = () => { aria-label={i18n.translate('xpack.ml.notificationsIndicator.unreadIcon', { defaultMessage: 'Unread notifications indicator.', })} + data-test-subj={'mlNotificationsIndicator'} /> diff --git a/x-pack/plugins/ml/public/application/contexts/ml/use_storage.ts b/x-pack/plugins/ml/public/application/contexts/ml/use_storage.ts deleted file mode 100644 index 761434a7bb3b..000000000000 --- a/x-pack/plugins/ml/public/application/contexts/ml/use_storage.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback, useState } from 'react'; -import { isDefined } from '../../../../common/types/guards'; -import { useMlKibana } from '../kibana'; -import type { MlStorageKey } from '../../../../common/types/storage'; -import { TMlStorageMapped } from '../../../../common/types/storage'; - -/** - * Hook for accessing and changing a value in the storage. - * @param key - Storage key - * @param initValue - */ -export function useStorage>( - key: K, - initValue?: T -): [ - typeof initValue extends undefined - ? TMlStorageMapped - : Exclude, undefined>, - (value: TMlStorageMapped) => void -] { - const { - services: { storage }, - } = useMlKibana(); - - const [val, setVal] = useState(storage.get(key) ?? initValue); - - const setStorage = useCallback((value: TMlStorageMapped): void => { - try { - if (isDefined(value)) { - storage.set(key, value); - setVal(value); - } else { - storage.remove(key); - setVal(initValue); - } - } catch (e) { - throw new Error('Unable to update storage with provided value'); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return [val, setStorage]; -} diff --git a/x-pack/plugins/ml/public/application/contexts/storage/storage_context.test.tsx b/x-pack/plugins/ml/public/application/contexts/storage/storage_context.test.tsx new file mode 100644 index 000000000000..6aeeb396f38c --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/storage/storage_context.test.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { MlStorageContextProvider, useStorage } from './storage_context'; +import { MlStorageKey } from '../../../../common/types/storage'; + +const mockSet = jest.fn(); +const mockRemove = jest.fn(); + +jest.mock('../kibana', () => ({ + useMlKibana: () => { + return { + services: { + storage: { + set: mockSet, + get: jest.fn((key: MlStorageKey) => { + switch (key) { + case 'ml.gettingStarted.isDismissed': + return true; + default: + return; + } + }), + remove: mockRemove, + }, + }, + }; + }, +})); + +describe('useStorage', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('returns the default value', () => { + const { result } = renderHook(() => useStorage('ml.jobSelectorFlyout.applyTimeRange', true), { + wrapper: MlStorageContextProvider, + }); + + expect(result.current[0]).toBe(true); + }); + + test('returns the value from storage', () => { + const { result } = renderHook(() => useStorage('ml.gettingStarted.isDismissed', false), { + wrapper: MlStorageContextProvider, + }); + + expect(result.current[0]).toBe(true); + }); + + test('updates the storage value', async () => { + const { result, waitForNextUpdate } = renderHook( + () => useStorage('ml.gettingStarted.isDismissed'), + { + wrapper: MlStorageContextProvider, + } + ); + + const [value, setValue] = result.current; + + expect(value).toBe(true); + + await act(async () => { + setValue(false); + await waitForNextUpdate(); + }); + + expect(result.current[0]).toBe(false); + expect(mockSet).toHaveBeenCalledWith('ml.gettingStarted.isDismissed', false); + }); + + test('removes the storage value', async () => { + const { result, waitForNextUpdate } = renderHook( + () => useStorage('ml.gettingStarted.isDismissed'), + { + wrapper: MlStorageContextProvider, + } + ); + + const [value, setValue] = result.current; + + expect(value).toBe(true); + + await act(async () => { + setValue(undefined); + await waitForNextUpdate(); + }); + + expect(result.current[0]).toBe(undefined); + expect(mockRemove).toHaveBeenCalledWith('ml.gettingStarted.isDismissed'); + }); + + test('updates the value on storage event', async () => { + const { result, waitForNextUpdate } = renderHook( + () => useStorage('ml.gettingStarted.isDismissed'), + { + wrapper: MlStorageContextProvider, + } + ); + + expect(result.current[0]).toBe(true); + + await act(async () => { + window.dispatchEvent( + new StorageEvent('storage', { + key: 'test_key', + newValue: 'test_value', + }) + ); + }); + + expect(result.current[0]).toBe(true); + + await act(async () => { + window.dispatchEvent( + new StorageEvent('storage', { + key: 'ml.gettingStarted.isDismissed', + newValue: null, + }) + ); + await waitForNextUpdate(); + }); + + expect(result.current[0]).toBe(undefined); + + await act(async () => { + window.dispatchEvent( + new StorageEvent('storage', { + key: 'ml.gettingStarted.isDismissed', + newValue: 'false', + }) + ); + await waitForNextUpdate(); + }); + + expect(result.current[0]).toBe(false); + }); +}); diff --git a/x-pack/plugins/ml/public/application/contexts/storage/storage_context.tsx b/x-pack/plugins/ml/public/application/contexts/storage/storage_context.tsx index ccd46c446ed2..c2b00a176c08 100644 --- a/x-pack/plugins/ml/public/application/contexts/storage/storage_context.tsx +++ b/x-pack/plugins/ml/public/application/contexts/storage/storage_context.tsx @@ -69,7 +69,8 @@ export const MlStorageContextProvider: FC = ({ children }) => { setState((prev) => { return { ...prev, - [event.key as MlStorageKey]: event.newValue, + [event.key as MlStorageKey]: + typeof event.newValue === 'string' ? JSON.parse(event.newValue) : event.newValue, }; }); } else { @@ -106,21 +107,32 @@ export const MlStorageContextProvider: FC = ({ children }) => { * @param key * @param initValue */ -export function useStorage( +export function useStorage>( key: K, - initValue?: TMlStorageMapped -): [TMlStorageMapped | undefined, (value: TMlStorageMapped) => void] { - const { value, setValue } = useContext(MlStorageContext); + initValue?: T +): [ + typeof initValue extends undefined + ? TMlStorageMapped | undefined + : Exclude, undefined>, + (value: TMlStorageMapped) => void +] { + const { value, setValue, removeValue } = useContext(MlStorageContext); const resultValue = useMemo(() => { - return (value?.[key] ?? initValue) as TMlStorageMapped; + return (value?.[key] ?? initValue) as typeof initValue extends undefined + ? TMlStorageMapped | undefined + : Exclude, undefined>; }, [value, key, initValue]); const setVal = useCallback( (v: TMlStorageMapped) => { - setValue(key, v); + if (isDefined(v)) { + setValue(key, v); + } else { + removeValue(key); + } }, - [setValue, key] + [setValue, removeValue, key] ); return [resultValue, setVal]; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx index 7f109defdff7..93fa7e908f35 100644 --- a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_swimlane_to_dashboard_controls.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { DashboardSavedObject } from '@kbn/dashboard-plugin/public'; +import { DashboardAttributes } from '@kbn/dashboard-plugin/common'; import type { Query } from '@kbn/es-query'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { getDefaultSwimlanePanelTitle } from '../../../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable'; @@ -30,7 +30,7 @@ export interface DashboardItem { id: string; title: string; description: string | undefined; - attributes: DashboardSavedObject; + attributes: DashboardAttributes; } export type EuiTableProps = EuiInMemoryTableProps; diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx index ac023017d43f..8bb7705938d8 100644 --- a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx +++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/use_dashboards_table.tsx @@ -8,7 +8,7 @@ import type { EuiInMemoryTableProps } from '@elastic/eui'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { debounce } from 'lodash'; -import type { DashboardSavedObject } from '@kbn/dashboard-plugin/public'; +import type { DashboardAttributes } from '@kbn/dashboard-plugin/common'; import { useDashboardService } from '../../services/dashboard_service'; import { useMlKibana } from '../../contexts/kibana'; @@ -16,7 +16,7 @@ export interface DashboardItem { id: string; title: string; description: string | undefined; - attributes: DashboardSavedObject; + attributes: DashboardAttributes; } export type EuiTableProps = EuiInMemoryTableProps; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.tsx b/x-pack/plugins/ml/public/application/explorer/explorer.tsx index b9feffb1ec11..5fcab05f068b 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer.tsx @@ -78,7 +78,7 @@ import { useMlKibana, useMlLocator } from '../contexts/kibana'; import { useMlContext } from '../contexts/ml'; import { useAnomalyExplorerContext } from './anomaly_explorer_context'; import { ML_ANOMALY_EXPLORER_PANELS } from '../../../common/types/storage'; -import { useStorage } from '../contexts/ml/use_storage'; +import { useStorage } from '../contexts/storage'; interface ExplorerPageProps { jobSelectorProps: JobSelectorProps; diff --git a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx index 811b1c43bce3..9ea6aa1b70f0 100644 --- a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx +++ b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx @@ -162,6 +162,7 @@ export const NotificationsList: FC = () => { const columns: Array> = [ { + id: 'timestamp', field: 'timestamp', name: , sortable: true, @@ -175,7 +176,7 @@ export const NotificationsList: FC = () => { name: , sortable: true, truncateText: false, - 'data-test-subj': 'mlNotificationLabel', + 'data-test-subj': 'mlNotificationLevel', render: (value: MlNotificationMessageLevel) => { return {value}; }, @@ -194,7 +195,7 @@ export const NotificationsList: FC = () => { }, { field: 'job_id', - name: , + name: , sortable: true, truncateText: false, 'data-test-subj': 'mlNotificationEntity', @@ -320,6 +321,7 @@ export const NotificationsList: FC = () => { }, }, }, + 'data-test-subj': 'mlNotificationsSearchBarInput', }} filters={filters} onChange={(e) => { diff --git a/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx b/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx index 457bca80ee2c..7b1f380c9065 100644 --- a/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx @@ -9,7 +9,7 @@ import React, { FC } from 'react'; import { EuiButton, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useMlKibana } from '../../contexts/kibana'; -import { useStorage } from '../../contexts/ml/use_storage'; +import { useStorage } from '../../contexts/storage'; import { ML_GETTING_STARTED_CALLOUT_DISMISSED } from '../../../../common/types/storage'; const feedbackLink = 'https://www.elastic.co/community/'; diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.ts index ff7b696551f4..abd97722f86b 100644 --- a/x-pack/plugins/ml/public/application/services/dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/services/dashboard_service.ts @@ -7,7 +7,8 @@ import { SavedObjectsClientContract } from '@kbn/core/public'; import { useMemo } from 'react'; -import { DashboardSavedObject, DashboardAppLocator } from '@kbn/dashboard-plugin/public'; +import { DashboardAppLocator } from '@kbn/dashboard-plugin/public'; +import type { DashboardAttributes } from '@kbn/dashboard-plugin/common'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import { useMlKibana } from '../contexts/kibana'; @@ -22,7 +23,7 @@ export function dashboardServiceProvider( * Fetches dashboards */ async fetchDashboards(query?: string) { - return await savedObjectClient.find({ + return await savedObjectClient.find({ type: 'dashboard', perPage: 1000, search: query ? `${query}*` : '', diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx index 0df10505e73c..23084c8d8886 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/series_controls/series_controls.tsx @@ -26,7 +26,7 @@ import { PartitionFieldConfig, PartitionFieldsConfig, } from '../../../../../common/types/storage'; -import { useStorage } from '../../../contexts/ml/use_storage'; +import { useStorage } from '../../../contexts/storage'; import { EntityFieldType } from '../../../../../common/types/anomalies'; import { FieldDefinition } from '../../../services/results_service/result_service_rx'; import { getViewableDetectors } from '../../timeseriesexplorer_utils/get_viewable_detectors'; diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/allocated_models.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/allocated_models.tsx index 0da7778c6e2b..d1808c2c8213 100644 --- a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/allocated_models.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/allocated_models.tsx @@ -190,6 +190,7 @@ export const AllocatedModels: FC = ({ })} onTableChange={() => {}} data-test-subj={'mlNodesAllocatedModels'} + css={{ overflow: 'auto' }} /> ); }; diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx index 4c9690486278..a6b6adc4fbc2 100644 --- a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx @@ -17,6 +17,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { cloneDeep } from 'lodash'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; +import { css } from '@emotion/react'; import { NodeItem } from './nodes_list'; import { useListItemsFormatter } from '../models_management/expanded_row'; import { AllocatedModels } from './allocated_models'; @@ -44,10 +45,12 @@ export const ExpandedRow: FC = ({ item }) => { attributes['ml.max_jvm_size'] = bytesFormatter(attributes['ml.max_jvm_size']); return ( - <> - - - +
+ @@ -85,25 +88,26 @@ export const ExpandedRow: FC = ({ item }) => { /> + - {allocatedModels.length > 0 ? ( - - - -
- -
-
- + {allocatedModels.length > 0 ? ( + <> + + + +
+ +
+
+ - -
-
- ) : null} - - + + + + ) : null} +
); }; diff --git a/x-pack/plugins/ml/server/routes/schemas/notifications_schema.ts b/x-pack/plugins/ml/server/routes/schemas/notifications_schema.ts index a82691ee5281..153ffd0aeea8 100644 --- a/x-pack/plugins/ml/server/routes/schemas/notifications_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/notifications_schema.ts @@ -8,26 +8,10 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const getNotificationsQuerySchema = schema.object({ - /** - * Message level, e.g. info, error - */ - level: schema.maybe(schema.string()), - /** - * Message type, e.g. anomaly_detector - */ - type: schema.maybe(schema.string()), /** * Search string for the message content */ queryString: schema.maybe(schema.string()), - /** - * Page numer, zero-indexed - */ - from: schema.number({ defaultValue: 0 }), - /** - * Number of messages to return - */ - size: schema.number({ defaultValue: 10 }), /** * Sort field */ diff --git a/x-pack/plugins/observability/e2e/synthetics_runner.ts b/x-pack/plugins/observability/e2e/synthetics_runner.ts index 25c236c6da58..a24771c091c0 100644 --- a/x-pack/plugins/observability/e2e/synthetics_runner.ts +++ b/x-pack/plugins/observability/e2e/synthetics_runner.ts @@ -10,7 +10,7 @@ import Url from 'url'; import { run as syntheticsRun } from '@elastic/synthetics'; import { PromiseType } from 'utility-types'; -import { createApmUsers } from '@kbn/apm-plugin/scripts/create_apm_users/create_apm_users'; +import { createApmUsers } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/create_apm_users'; import { esArchiverUnload } from './tasks/es_archiver'; @@ -23,6 +23,7 @@ export interface ArgParams { export class SyntheticsRunner { public getService: any; public kibanaUrl: string; + private elasticsearchUrl: string; public testFilesLoaded: boolean = false; @@ -31,6 +32,7 @@ export class SyntheticsRunner { constructor(getService: any, params: ArgParams) { this.getService = getService; this.kibanaUrl = this.getKibanaUrl(); + this.elasticsearchUrl = this.getElasticsearchUrl(); this.params = params; } @@ -40,7 +42,7 @@ export class SyntheticsRunner { async createTestUsers() { await createApmUsers({ - elasticsearch: { username: 'elastic', password: 'changeme' }, + elasticsearch: { node: this.elasticsearchUrl, username: 'elastic', password: 'changeme' }, kibana: { hostname: this.kibanaUrl }, }); } @@ -79,6 +81,16 @@ export class SyntheticsRunner { }); } + getElasticsearchUrl() { + const config = this.getService('config'); + + return Url.format({ + protocol: config.get('servers.elasticsearch.protocol'), + hostname: config.get('servers.elasticsearch.hostname'), + port: config.get('servers.elasticsearch.port'), + }); + } + async run() { if (!this.testFilesLoaded) { throw new Error('Test files not loaded'); diff --git a/x-pack/plugins/observability/server/assets/constants.ts b/x-pack/plugins/observability/server/assets/constants.ts index 4c0cc0e2e6f8..182ca89712dc 100644 --- a/x-pack/plugins/observability/server/assets/constants.ts +++ b/x-pack/plugins/observability/server/assets/constants.ts @@ -9,11 +9,7 @@ export const SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME = 'slo-observability.sli-mappi export const SLO_COMPONENT_TEMPLATE_SETTINGS_NAME = 'slo-observability.sli-settings'; export const SLO_INDEX_TEMPLATE_NAME = 'slo-observability.sli'; export const SLO_RESOURCES_VERSION = 1; - -export const getSLOIngestPipelineName = (spaceId: string) => - `${SLO_INDEX_TEMPLATE_NAME}.monthly-${spaceId}`; - -export const getSLODestinationIndexName = (spaceId: string) => - `${SLO_INDEX_TEMPLATE_NAME}-v${SLO_RESOURCES_VERSION}-${spaceId}`; +export const SLO_INGEST_PIPELINE_NAME = `${SLO_INDEX_TEMPLATE_NAME}.monthly`; +export const SLO_DESTINATION_INDEX_NAME = `${SLO_INDEX_TEMPLATE_NAME}-v${SLO_RESOURCES_VERSION}`; export const getSLOTransformId = (sloId: string) => `slo-${sloId}`; diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 1d9d3cbf455f..ff5fd246bea1 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -144,7 +144,6 @@ export class ObservabilityPlugin implements Plugin { const start = () => core.getStartServices().then(([coreStart]) => coreStart); - const { spacesService } = plugins.spaces; const { ruleDataService } = plugins.ruleRegistry; registerRoutes({ @@ -155,7 +154,6 @@ export class ObservabilityPlugin implements Plugin { logger: this.initContext.logger.get(), repository: getGlobalObservabilityServerRouteRepository(config), ruleDataService, - spacesService, }); return { diff --git a/x-pack/plugins/observability/server/routes/register_routes.ts b/x-pack/plugins/observability/server/routes/register_routes.ts index e0e9e94f5cb2..1c45eb600147 100644 --- a/x-pack/plugins/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability/server/routes/register_routes.ts @@ -14,7 +14,6 @@ import { CoreSetup, CoreStart, Logger, RouteRegistrar } from '@kbn/core/server'; import Boom from '@hapi/boom'; import { errors } from '@elastic/elasticsearch'; import { RuleDataPluginService } from '@kbn/rule-registry-plugin/server'; -import { SpacesServiceStart } from '@kbn/spaces-plugin/server'; import { ObservabilityRequestHandlerContext } from '../types'; import { AbstractObservabilityServerRouteRepository } from './types'; import { getHTTPResponseCode, ObservabilityError } from '../errors'; @@ -24,7 +23,6 @@ export function registerRoutes({ core, logger, ruleDataService, - spacesService, }: { core: { setup: CoreSetup; @@ -33,7 +31,6 @@ export function registerRoutes({ repository: AbstractObservabilityServerRouteRepository; logger: Logger; ruleDataService: RuleDataPluginService; - spacesService: SpacesServiceStart; }) { const routes = Object.values(repository); @@ -67,7 +64,6 @@ export function registerRoutes({ logger, params: decodedParams, ruleDataService, - spacesService, })) as any; if (data === undefined) { diff --git a/x-pack/plugins/observability/server/routes/slo/route.ts b/x-pack/plugins/observability/server/routes/slo/route.ts index 3f04d5e0b13c..63ef4e1e07e6 100644 --- a/x-pack/plugins/observability/server/routes/slo/route.ts +++ b/x-pack/plugins/observability/server/routes/slo/route.ts @@ -38,19 +38,13 @@ const createSLORoute = createObservabilityServerRoute({ tags: [], }, params: createSLOParamsSchema, - handler: async ({ context, request, params, logger, spacesService }) => { + handler: async ({ context, params, logger }) => { const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; - const spaceId = spacesService.getSpaceId(request); - const resourceInstaller = new DefaultResourceInstaller(esClient, logger, spaceId); + const resourceInstaller = new DefaultResourceInstaller(esClient, logger); const repository = new KibanaSavedObjectsSLORepository(soClient); - const transformManager = new DefaultTransformManager( - transformGenerators, - esClient, - logger, - spaceId - ); + const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); const createSLO = new CreateSLO(resourceInstaller, repository, transformManager); const response = await createSLO.execute(params.body); @@ -65,18 +59,12 @@ const deleteSLORoute = createObservabilityServerRoute({ tags: [], }, params: deleteSLOParamsSchema, - handler: async ({ context, request, params, logger, spacesService }) => { + handler: async ({ context, params, logger }) => { const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; - const spaceId = spacesService.getSpaceId(request); const repository = new KibanaSavedObjectsSLORepository(soClient); - const transformManager = new DefaultTransformManager( - transformGenerators, - esClient, - logger, - spaceId - ); + const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); const deleteSLO = new DeleteSLO(repository, transformManager, esClient); diff --git a/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts b/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts index b1a68b9c04ae..2e43c81f6d38 100644 --- a/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts @@ -33,6 +33,7 @@ describe('DeleteSLO', () => { await deleteSLO.execute(slo.id); + expect(mockRepository.findById).toHaveBeenCalledWith(slo.id); expect(mockTransformManager.stop).toHaveBeenCalledWith(getSLOTransformId(slo.id)); expect(mockTransformManager.uninstall).toHaveBeenCalledWith(getSLOTransformId(slo.id)); expect(mockEsClient.deleteByQuery).toHaveBeenCalledWith( diff --git a/x-pack/plugins/observability/server/services/slo/delete_slo.ts b/x-pack/plugins/observability/server/services/slo/delete_slo.ts index 59e5df8a5975..a7d931174a59 100644 --- a/x-pack/plugins/observability/server/services/slo/delete_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/delete_slo.ts @@ -19,6 +19,9 @@ export class DeleteSLO { ) {} public async execute(sloId: string): Promise { + // ensure the slo exists on the request's space. + await this.repository.findById(sloId); + const sloTransformId = getSLOTransformId(sloId); await this.transformManager.stop(sloTransformId); await this.transformManager.uninstall(sloTransformId); diff --git a/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts b/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts index f8746f38cd24..90749176513d 100644 --- a/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts +++ b/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts @@ -9,7 +9,7 @@ import { IngestGetPipelineResponse } from '@elastic/elasticsearch/lib/api/typesW import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { loggerMock } from '@kbn/logging-mocks'; import { - getSLOIngestPipelineName, + SLO_INGEST_PIPELINE_NAME, SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME, SLO_COMPONENT_TEMPLATE_SETTINGS_NAME, SLO_INDEX_TEMPLATE_NAME, @@ -17,17 +17,12 @@ import { } from '../../assets/constants'; import { DefaultResourceInstaller } from './resource_installer'; -const SPACE_ID = 'space-id'; describe('resourceInstaller', () => { describe("when the common resources don't exist", () => { it('installs the common resources', async () => { const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); mockClusterClient.indices.existsIndexTemplate.mockResponseOnce(false); - const installer = new DefaultResourceInstaller( - mockClusterClient, - loggerMock.create(), - SPACE_ID - ); + const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create()); await installer.ensureCommonResourcesInstalled(); @@ -44,7 +39,7 @@ describe('resourceInstaller', () => { expect.objectContaining({ name: SLO_INDEX_TEMPLATE_NAME }) ); expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledWith( - expect.objectContaining({ id: getSLOIngestPipelineName(SPACE_ID) }) + expect.objectContaining({ id: SLO_INGEST_PIPELINE_NAME }) ); }); }); @@ -55,13 +50,9 @@ describe('resourceInstaller', () => { mockClusterClient.indices.existsIndexTemplate.mockResponseOnce(true); mockClusterClient.ingest.getPipeline.mockResponseOnce({ // @ts-ignore _meta not typed properly - [getSLOIngestPipelineName(SPACE_ID)]: { _meta: { version: SLO_RESOURCES_VERSION } }, + [SLO_INGEST_PIPELINE_NAME]: { _meta: { version: SLO_RESOURCES_VERSION } }, } as IngestGetPipelineResponse); - const installer = new DefaultResourceInstaller( - mockClusterClient, - loggerMock.create(), - SPACE_ID - ); + const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create()); await installer.ensureCommonResourcesInstalled(); diff --git a/x-pack/plugins/observability/server/services/slo/resource_installer.ts b/x-pack/plugins/observability/server/services/slo/resource_installer.ts index 8ed8108b3c4b..abc02052097b 100644 --- a/x-pack/plugins/observability/server/services/slo/resource_installer.ts +++ b/x-pack/plugins/observability/server/services/slo/resource_installer.ts @@ -13,7 +13,7 @@ import type { import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { - getSLOIngestPipelineName, + SLO_INGEST_PIPELINE_NAME, SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME, SLO_COMPONENT_TEMPLATE_SETTINGS_NAME, SLO_INDEX_TEMPLATE_NAME, @@ -29,11 +29,7 @@ export interface ResourceInstaller { } export class DefaultResourceInstaller implements ResourceInstaller { - constructor( - private esClient: ElasticsearchClient, - private logger: Logger, - private spaceId: string - ) {} + constructor(private esClient: ElasticsearchClient, private logger: Logger) {} public async ensureCommonResourcesInstalled(): Promise { const alreadyInstalled = await this.areResourcesAlreadyInstalled(); @@ -64,8 +60,8 @@ export class DefaultResourceInstaller implements ResourceInstaller { await this.createOrUpdateIngestPipelineTemplate( getSLOPipelineTemplate( - getSLOIngestPipelineName(this.spaceId), - this.getPipelinePrefix(SLO_RESOURCES_VERSION, this.spaceId) + SLO_INGEST_PIPELINE_NAME, + this.getPipelinePrefix(SLO_RESOURCES_VERSION) ) ); } catch (err) { @@ -74,10 +70,10 @@ export class DefaultResourceInstaller implements ResourceInstaller { } } - private getPipelinePrefix(version: number, spaceId: string): string { + private getPipelinePrefix(version: number): string { // Following https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme - // slo-observability.sli--. - return `${SLO_INDEX_TEMPLATE_NAME}-v${version}-${spaceId}.`; + // slo-observability.sli-. + return `${SLO_INDEX_TEMPLATE_NAME}-v${version}.`; } private async areResourcesAlreadyInstalled(): Promise { @@ -87,12 +83,11 @@ export class DefaultResourceInstaller implements ResourceInstaller { let ingestPipelineExists = false; try { - const pipelineName = getSLOIngestPipelineName(this.spaceId); - const pipeline = await this.esClient.ingest.getPipeline({ id: pipelineName }); + const pipeline = await this.esClient.ingest.getPipeline({ id: SLO_INGEST_PIPELINE_NAME }); ingestPipelineExists = // @ts-ignore _meta is not defined on the type - pipeline && pipeline[pipelineName]._meta.version === SLO_RESOURCES_VERSION; + pipeline && pipeline[SLO_INGEST_PIPELINE_NAME]._meta.version === SLO_RESOURCES_VERSION; } catch (err) { return false; } diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap index 239c3c93503b..7b7f49061fd5 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap @@ -20,8 +20,8 @@ Object { "version": 1, }, "dest": Object { - "index": "slo-observability.sli-v1-my-namespace", - "pipeline": "slo-observability.sli.monthly-my-namespace", + "index": "slo-observability.sli-v1", + "pipeline": "slo-observability.sli.monthly", }, "frequency": "1m", "pivot": Object { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap index 1fdaec28f897..8b73f76a8082 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap @@ -20,8 +20,8 @@ Object { "version": 1, }, "dest": Object { - "index": "slo-observability.sli-v1-my-namespace", - "pipeline": "slo-observability.sli.monthly-my-namespace", + "index": "slo-observability.sli-v1", + "pipeline": "slo-observability.sli.monthly", }, "frequency": "1m", "pivot": Object { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts index 1e8fadf365d7..ba984e542619 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts @@ -13,7 +13,7 @@ const generator = new ApmTransactionDurationTransformGenerator(); describe('APM Transaction Duration Transform Generator', () => { it('returns the correct transform params with every specified indicator params', async () => { const anSLO = createSLO(createAPMTransactionDurationIndicator()); - const transform = generator.getTransformParams(anSLO, 'my-namespace'); + const transform = generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot({ transform_id: expect.any(String), @@ -34,7 +34,7 @@ describe('APM Transaction Duration Transform Generator', () => { transaction_type: '*', }) ); - const transform = generator.getTransformParams(anSLO, 'my-namespace'); + const transform = generator.getTransformParams(anSLO); expect(transform.source.query).toMatchSnapshot(); }); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts index 4c4642173763..bc45e12abbb3 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts @@ -12,8 +12,8 @@ import { } from '@elastic/elasticsearch/lib/api/types'; import { ALL_VALUE } from '../../../types/schema'; import { - getSLODestinationIndexName, - getSLOIngestPipelineName, + SLO_DESTINATION_INDEX_NAME, + SLO_INGEST_PIPELINE_NAME, getSLOTransformId, } from '../../../assets/constants'; import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template'; @@ -27,7 +27,7 @@ import { TransformGenerator } from '.'; const APM_SOURCE_INDEX = 'metrics-apm*'; export class ApmTransactionDurationTransformGenerator implements TransformGenerator { - public getTransformParams(slo: SLO, spaceId: string): TransformPutTransformRequest { + public getTransformParams(slo: SLO): TransformPutTransformRequest { if (!apmTransactionDurationSLOSchema.is(slo)) { throw new Error(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -35,7 +35,7 @@ export class ApmTransactionDurationTransformGenerator implements TransformGenera return getSLOTransformTemplate( this.buildTransformId(slo), this.buildSource(slo), - this.buildDestination(slo, spaceId), + this.buildDestination(), this.buildGroupBy(), this.buildAggregations(slo) ); @@ -104,10 +104,10 @@ export class ApmTransactionDurationTransformGenerator implements TransformGenera }; } - private buildDestination(slo: APMTransactionDurationSLO, spaceId: string) { + private buildDestination() { return { - pipeline: getSLOIngestPipelineName(spaceId), - index: getSLODestinationIndexName(spaceId), + pipeline: SLO_INGEST_PIPELINE_NAME, + index: SLO_DESTINATION_INDEX_NAME, }; } diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts index afa904fa1f8c..2bc88c576f8c 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts @@ -13,7 +13,7 @@ const generator = new ApmTransactionErrorRateTransformGenerator(); describe('APM Transaction Error Rate Transform Generator', () => { it('returns the correct transform params with every specified indicator params', async () => { const anSLO = createSLO(createAPMTransactionErrorRateIndicator()); - const transform = generator.getTransformParams(anSLO, 'my-namespace'); + const transform = generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot({ transform_id: expect.any(String), @@ -27,7 +27,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { it("uses default values when 'good_status_codes' is not specified", async () => { const anSLO = createSLO(createAPMTransactionErrorRateIndicator({ good_status_codes: [] })); - const transform = generator.getTransformParams(anSLO, 'my-namespace'); + const transform = generator.getTransformParams(anSLO); expect(transform.pivot?.aggregations).toMatchSnapshot(); }); @@ -41,7 +41,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { transaction_type: '*', }) ); - const transform = generator.getTransformParams(anSLO, 'my-namespace'); + const transform = generator.getTransformParams(anSLO); expect(transform.source.query).toMatchSnapshot(); }); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts index 6740bee2b707..23a9a03f6e14 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts @@ -14,8 +14,8 @@ import { ALL_VALUE } from '../../../types/schema'; import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template'; import { TransformGenerator } from '.'; import { - getSLODestinationIndexName, - getSLOIngestPipelineName, + SLO_DESTINATION_INDEX_NAME, + SLO_INGEST_PIPELINE_NAME, getSLOTransformId, } from '../../../assets/constants'; import { @@ -29,7 +29,7 @@ const ALLOWED_STATUS_CODES = ['2xx', '3xx', '4xx', '5xx']; const DEFAULT_GOOD_STATUS_CODES = ['2xx', '3xx', '4xx']; export class ApmTransactionErrorRateTransformGenerator implements TransformGenerator { - public getTransformParams(slo: SLO, spaceId: string): TransformPutTransformRequest { + public getTransformParams(slo: SLO): TransformPutTransformRequest { if (!apmTransactionErrorRateSLOSchema.is(slo)) { throw new Error(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -37,7 +37,7 @@ export class ApmTransactionErrorRateTransformGenerator implements TransformGener return getSLOTransformTemplate( this.buildTransformId(slo), this.buildSource(slo), - this.buildDestination(slo, spaceId), + this.buildDestination(), this.buildGroupBy(), this.buildAggregations(slo) ); @@ -106,10 +106,10 @@ export class ApmTransactionErrorRateTransformGenerator implements TransformGener }; } - private buildDestination(slo: APMTransactionErrorRateSLO, spaceId: string) { + private buildDestination() { return { - pipeline: getSLOIngestPipelineName(spaceId), - index: getSLODestinationIndexName(spaceId), + pipeline: SLO_INGEST_PIPELINE_NAME, + index: SLO_DESTINATION_INDEX_NAME, }; } diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts index 21a917ea1af6..3965e809373c 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts @@ -9,5 +9,5 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typ import { SLO } from '../../../types/models'; export interface TransformGenerator { - getTransformParams(slo: SLO, spaceId: string): TransformPutTransformRequest; + getTransformParams(slo: SLO): TransformPutTransformRequest; } diff --git a/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts b/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts index 1badb6b08e49..434e6841ff0e 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts @@ -23,8 +23,6 @@ import { import { SLO, SLITypes } from '../../types/models'; import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo'; -const SPACE_ID = 'space-id'; - describe('TransformManager', () => { let esClientMock: ElasticsearchClientMock; let loggerMock: jest.Mocked; @@ -41,7 +39,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_duration': new DummyTransformGenerator(), }; - const service = new DefaultTransformManager(generators, esClientMock, loggerMock, SPACE_ID); + const service = new DefaultTransformManager(generators, esClientMock, loggerMock); await expect( service.install( @@ -63,12 +61,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_duration': new FailTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); await expect( transformManager.install( @@ -92,12 +85,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); const slo = createSLO(createAPMTransactionErrorRateIndicator()); const transformId = await transformManager.install(slo); @@ -113,12 +101,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); await transformManager.start('slo-transform-id'); @@ -132,12 +115,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); await transformManager.stop('slo-transform-id'); @@ -151,12 +129,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); await transformManager.uninstall('slo-transform-id'); @@ -171,12 +144,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); await transformManager.uninstall('slo-transform-id'); diff --git a/x-pack/plugins/observability/server/services/slo/transform_manager.ts b/x-pack/plugins/observability/server/services/slo/transform_manager.ts index ab7799a4a00c..154660fccaf9 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_manager.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_manager.ts @@ -24,8 +24,7 @@ export class DefaultTransformManager implements TransformManager { constructor( private generators: Record, private esClient: ElasticsearchClient, - private logger: Logger, - private spaceId: string + private logger: Logger ) {} async install(slo: SLO): Promise { @@ -35,7 +34,7 @@ export class DefaultTransformManager implements TransformManager { throw new Error(`Unsupported SLO type: ${slo.indicator.type}`); } - const transformParams = generator.getTransformParams(slo, this.spaceId); + const transformParams = generator.getTransformParams(slo); try { await retryTransientEsErrors(() => this.esClient.transform.putTransform(transformParams), { logger: this.logger, diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index 69e7f0ba51af..a5b111569e31 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -29,11 +29,16 @@ import { const technicalPreviewLabel = i18n.translate( 'xpack.observability.uiSettings.technicalPreviewLabel', - { - defaultMessage: 'technical preview', - } + { defaultMessage: 'technical preview' } ); +function feedbackLink({ href }: { href: string }) { + return `
${i18n.translate( + 'xpack.observability.uiSettings.giveFeedBackLabel', + { defaultMessage: 'Give feedback' } + )}`; +} + type UiSettings = UiSettingsParams & { showInLabs?: boolean }; /** @@ -167,12 +172,7 @@ export const uiSettings: Record = { '{technicalPreviewLabel} Enable the Service groups feature on APM UI. {feedbackLink}.', values: { technicalPreviewLabel: `[${technicalPreviewLabel}]`, - feedbackLink: - '' + - i18n.translate('xpack.observability.enableServiceGroups.feedbackLinkText', { - defaultMessage: 'Give feedback', - }) + - '', + feedbackLink: feedbackLink({ href: 'https://ela.st/feedback-service-groups' }), }, }), schema: schema.boolean(), @@ -206,15 +206,7 @@ export const uiSettings: Record = { '{technicalPreviewLabel} Default APM Service Inventory page sort (for Services without Machine Learning applied) to sort by Service Name. {feedbackLink}.', values: { technicalPreviewLabel: `[${technicalPreviewLabel}]`, - feedbackLink: - '' + - i18n.translate( - 'xpack.observability.apmServiceInventoryOptimizedSorting.feedbackLinkText', - { - defaultMessage: 'Give feedback', - } - ) + - '', + feedbackLink: feedbackLink({ href: 'https://ela.st/feedback-apm-page-performance' }), }, } ), @@ -245,12 +237,7 @@ export const uiSettings: Record = { '{technicalPreviewLabel} Enable the APM Trace Explorer feature, that allows you to search and inspect traces with KQL or EQL. {feedbackLink}.', values: { technicalPreviewLabel: `[${technicalPreviewLabel}]`, - feedbackLink: - '' + - i18n.translate('xpack.observability.apmTraceExplorerTabDescription.feedbackLinkText', { - defaultMessage: 'Give feedback', - }) + - '', + feedbackLink: feedbackLink({ href: 'https://ela.st/feedback-trace-explorer' }), }, }), schema: schema.boolean(), @@ -269,12 +256,7 @@ export const uiSettings: Record = { '{technicalPreviewLabel} Enable the APM Operations Breakdown feature, that displays aggregates for backend operations. {feedbackLink}.', values: { technicalPreviewLabel: `[${technicalPreviewLabel}]`, - feedbackLink: - '' + - i18n.translate('xpack.observability.apmOperationsBreakdownDescription.feedbackLinkText', { - defaultMessage: 'Give feedback', - }) + - '', + feedbackLink: feedbackLink({ href: 'https://ela.st/feedback-operations-breakdown' }), }, }), schema: schema.boolean(), @@ -318,12 +300,7 @@ export const uiSettings: Record = { '{technicalPreviewLabel} Display Amazon Lambda metrics in the service metrics tab. {feedbackLink}', values: { technicalPreviewLabel: `[${technicalPreviewLabel}]`, - feedbackLink: - '' + - i18n.translate('xpack.observability.awsLambdaDescription', { - defaultMessage: 'Send feedback', - }) + - '', + feedbackLink: feedbackLink({ href: 'https://ela.st/feedback-aws-lambda' }), }, }), schema: schema.boolean(), diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts index e5c6b5013b13..34d04289ad67 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts @@ -8,7 +8,6 @@ import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver'; import { login } from '../../tasks/login'; import { - checkResults, findAndClickButton, findFormFieldByRowsLabelAndType, inputQuery, @@ -59,6 +58,28 @@ describe('Alert Event Details', () => { cy.getBySel('ruleSwitch').should('have.attr', 'aria-checked', 'true'); }); + it('enables to add detection action with osquery', () => { + cy.visit('/app/security/rules'); + cy.contains(RULE_NAME).click(); + cy.contains('Edit rule settings').click(); + cy.getBySel('edit-rule-actions-tab').wait(500).click(); + cy.contains('Perform no actions').get('select').select('On each rule execution'); + cy.contains('Response actions are run on each rule execution'); + cy.getBySel('.osquery-ResponseActionTypeSelectOption').click(); + cy.get(LIVE_QUERY_EDITOR); + cy.contains('Save changes').click(); + cy.contains('Query is a required field'); + inputQuery('select * from uptime'); + cy.wait(1000); // wait for the validation to trigger - cypress is way faster than users ;) + + // getSavedQueriesDropdown().type(`users{downArrow}{enter}`); + cy.contains('Save changes').click(); + cy.contains(`${RULE_NAME} was saved`).should('exist'); + cy.contains('Edit rule settings').click(); + cy.getBySel('edit-rule-actions-tab').wait(500).click(); + cy.contains('select * from uptime'); + }); + it('should be able to run live query and add to timeline (-depending on the previous test)', () => { const TIMELINE_NAME = 'Untitled timeline'; cy.visit('/app/security/alerts'); @@ -94,38 +115,20 @@ describe('Alert Event Details', () => { cy.contains('Cancel').click(); cy.contains(TIMELINE_NAME).click(); cy.getBySel('draggableWrapperKeyboardHandler').contains('action_id: "'); - }); - it('enables to add detection action with osquery', () => { - cy.visit('/app/security/rules'); - cy.contains(RULE_NAME).click(); - cy.contains('Edit rule settings').click(); - cy.getBySel('edit-rule-actions-tab').wait(500).click(); - cy.contains('Perform no actions').get('select').select('On each rule execution'); - cy.contains('Response actions are run on each rule execution'); - cy.getBySel('.osquery-ResponseActionTypeSelectOption').click(); - cy.get(LIVE_QUERY_EDITOR); - cy.contains('Save changes').click(); - cy.contains('Query is a required field'); - inputQuery('select * from uptime'); - cy.wait(1000); // wait for the validation to trigger - cypress is way faster than users ;) - - // getSavedQueriesDropdown().type(`users{downArrow}{enter}`); - cy.contains('Save changes').click(); - cy.contains(`${RULE_NAME} was saved`).should('exist'); - cy.contains('Edit rule settings').click(); - cy.getBySel('edit-rule-actions-tab').wait(500).click(); - cy.contains('select * from uptime'); + // timeline unsaved changes modal + cy.visit('/app/osquery'); + closeModalIfVisible(); }); // TODO think on how to get these actions triggered faster (because now they are not triggered during the test). - it.skip('sees osquery results from last action', () => { - cy.visit('/app/security/alerts'); - cy.getBySel('header-page-title').contains('Alerts').should('exist'); - cy.getBySel('expand-event').first().click({ force: true }); - cy.contains('Osquery Results').click(); - cy.getBySel('osquery-results').should('exist'); - cy.contains('select * from uptime'); - cy.getBySel('osqueryResultsTable').within(() => { - checkResults(); - }); - }); + // it.skip('sees osquery results from last action', () => { + // cy.visit('/app/security/alerts'); + // cy.getBySel('header-page-title').contains('Alerts').should('exist'); + // cy.getBySel('expand-event').first().click({ force: true }); + // cy.contains('Osquery Results').click(); + // cy.getBySel('osquery-results').should('exist'); + // cy.contains('select * from uptime'); + // cy.getBySel('osqueryResultsTable').within(() => { + // checkResults(); + // }); + // }); }); diff --git a/x-pack/plugins/osquery/cypress/integration/all/cases.spec.ts b/x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts similarity index 96% rename from x-pack/plugins/osquery/cypress/integration/all/cases.spec.ts rename to x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts index cb16dd1dfb8f..ce586e4d4994 100644 --- a/x-pack/plugins/osquery/cypress/integration/all/cases.spec.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/cases.cy.ts @@ -35,7 +35,7 @@ describe('Add to Cases', () => { cy.contains('Test Obs case').click(); checkResults(); cy.contains('attached Osquery results'); - cy.contains('SELECT * FROM users;'); + cy.contains('select * from uptime;'); cy.contains('View in Discover').should('exist'); cy.contains('View in Lens').should('exist'); cy.contains('Add to Case').should('not.exist'); @@ -66,7 +66,7 @@ describe('Add to Cases', () => { cy.contains('Test Security Case').click(); checkResults(); cy.contains('attached Osquery results'); - cy.contains('SELECT * FROM users;'); + cy.contains('select * from uptime;'); cy.contains('View in Discover').should('exist'); cy.contains('View in Lens').should('exist'); cy.contains('Add to Case').should('not.exist'); diff --git a/x-pack/plugins/osquery/kibana.json b/x-pack/plugins/osquery/kibana.json index 6d956fdce9fe..63e7718368ce 100644 --- a/x-pack/plugins/osquery/kibana.json +++ b/x-pack/plugins/osquery/kibana.json @@ -12,6 +12,7 @@ "requiredPlugins": [ "actions", "data", + "licensing", "dataViews", "discover", "features", diff --git a/x-pack/plugins/osquery/public/cases/add_to_cases.tsx b/x-pack/plugins/osquery/public/cases/add_to_cases.tsx new file mode 100644 index 000000000000..bc46a76edf36 --- /dev/null +++ b/x-pack/plugins/osquery/public/cases/add_to_cases.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useKibana } from '../common/lib/kibana'; +import type { AddToCaseButtonProps } from './add_to_cases_button'; +import { AddToCaseButton } from './add_to_cases_button'; + +const CASES_OWNER: string[] = []; + +export const AddToCaseWrapper: React.FC = React.memo((props) => { + const { cases } = useKibana().services; + const casePermissions = cases.helpers.canUseCases(); + const CasesContext = cases.ui.getCasesContext(); + + return ( + + {' '} + + ); +}); + +AddToCaseWrapper.displayName = 'AddToCaseWrapper'; diff --git a/x-pack/plugins/osquery/public/cases/add_to_cases_button.tsx b/x-pack/plugins/osquery/public/cases/add_to_cases_button.tsx index 9f2df87f3561..7124ebd95549 100644 --- a/x-pack/plugins/osquery/public/cases/add_to_cases_button.tsx +++ b/x-pack/plugins/osquery/public/cases/add_to_cases_button.tsx @@ -19,7 +19,7 @@ const ADD_TO_CASE = i18n.translate( } ); -interface IProps { +export interface AddToCaseButtonProps { queryId: string; agentIds?: string[]; actionId: string; @@ -28,7 +28,7 @@ interface IProps { iconProps?: Record; } -export const AddToCaseButton: React.FC = ({ +export const AddToCaseButton: React.FC = ({ actionId, agentIds = [], queryId = '', diff --git a/x-pack/plugins/osquery/public/discover/pack_view_in_discover.tsx b/x-pack/plugins/osquery/public/discover/pack_view_in_discover.tsx new file mode 100644 index 000000000000..c6506126c83f --- /dev/null +++ b/x-pack/plugins/osquery/public/discover/pack_view_in_discover.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import moment from 'moment-timezone'; +import { usePackQueryLastResults } from '../packs/use_pack_query_last_results'; +import { ViewResultsActionButtonType } from '../live_queries/form/pack_queries_status_table'; +import { ViewResultsInDiscoverAction } from './view_results_in_discover'; + +interface PackViewInActionProps { + item: { + id: string; + interval: number; + action_id?: string; + agents: string[]; + }; + actionId?: string; +} +const PackViewInDiscoverActionComponent: React.FC = ({ item }) => { + const { action_id: actionId, agents: agentIds, interval } = item; + const { data: lastResultsData } = usePackQueryLastResults({ + actionId, + interval, + }); + + const startDate = lastResultsData?.['@timestamp'] + ? moment(lastResultsData?.['@timestamp'][0]).subtract(interval, 'seconds').toISOString() + : `now-${interval}s`; + const endDate = lastResultsData?.['@timestamp'] + ? moment(lastResultsData?.['@timestamp'][0]).toISOString() + : 'now'; + + return ( + + ); +}; + +export const PackViewInDiscoverAction = React.memo(PackViewInDiscoverActionComponent); diff --git a/x-pack/plugins/osquery/public/discover/view_results_in_discover.tsx b/x-pack/plugins/osquery/public/discover/view_results_in_discover.tsx new file mode 100644 index 000000000000..2106f11b8922 --- /dev/null +++ b/x-pack/plugins/osquery/public/discover/view_results_in_discover.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FilterStateStore } from '@kbn/es-query'; +import { useKibana } from '../common/lib/kibana'; +import { useLogsDataView } from '../common/hooks/use_logs_data_view'; +import { ViewResultsActionButtonType } from '../live_queries/form/pack_queries_status_table'; + +interface ViewResultsInDiscoverActionProps { + actionId?: string; + agentIds?: string[]; + buttonType: ViewResultsActionButtonType; + endDate?: string; + startDate?: string; + mode?: string; +} + +const ViewResultsInDiscoverActionComponent: React.FC = ({ + actionId, + agentIds, + buttonType, + endDate, + startDate, +}) => { + const { discover, application } = useKibana().services; + const locator = discover?.locator; + const discoverPermissions = application.capabilities.discover; + const { data: logsDataView } = useLogsDataView({ skip: !actionId }); + + const [discoverUrl, setDiscoverUrl] = useState(''); + + useEffect(() => { + const getDiscoverUrl = async () => { + if (!locator || !logsDataView) return; + + const agentIdsQuery = agentIds?.length + ? { + bool: { + minimum_should_match: 1, + should: agentIds.map((agentId) => ({ match_phrase: { 'agent.id': agentId } })), + }, + } + : null; + + const newUrl = await locator.getUrl({ + indexPatternId: logsDataView.id, + filters: [ + { + meta: { + index: logsDataView.id, + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'action_id', + params: { query: actionId }, + }, + query: { match_phrase: { action_id: actionId } }, + $state: { store: FilterStateStore.APP_STATE }, + }, + ...(agentIdsQuery + ? [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: 'agent IDs', + disabled: false, + index: logsDataView.id, + key: 'query', + negate: false, + type: 'custom', + value: JSON.stringify(agentIdsQuery), + }, + query: agentIdsQuery, + }, + ] + : []), + ], + refreshInterval: { + pause: true, + value: 0, + }, + timeRange: + startDate && endDate + ? { + to: endDate, + from: startDate, + mode: 'absolute', + } + : { + to: 'now', + from: 'now-1d', + mode: 'relative', + }, + }); + setDiscoverUrl(newUrl); + }; + + getDiscoverUrl(); + }, [actionId, agentIds, endDate, startDate, locator, logsDataView]); + + if (!discoverPermissions.show) { + return null; + } + + if (buttonType === ViewResultsActionButtonType.button) { + return ( + + {VIEW_IN_DISCOVER} + + ); + } + + return ( + + + + ); +}; + +const VIEW_IN_DISCOVER = i18n.translate( + 'xpack.osquery.pack.queriesTable.viewDiscoverResultsActionAriaLabel', + { + defaultMessage: 'View in Discover', + } +); + +export const ViewResultsInDiscoverAction = React.memo(ViewResultsInDiscoverActionComponent); diff --git a/x-pack/plugins/osquery/public/form/results_type_field.tsx b/x-pack/plugins/osquery/public/form/results_type_field.tsx index 55bbe69397f9..ccc1961259c3 100644 --- a/x-pack/plugins/osquery/public/form/results_type_field.tsx +++ b/x-pack/plugins/osquery/public/form/results_type_field.tsx @@ -34,7 +34,7 @@ const DIFFERENTIAL_OPTION = { inputDisplay: ( ), }; diff --git a/x-pack/plugins/osquery/public/lens/pack_view_in_lens.tsx b/x-pack/plugins/osquery/public/lens/pack_view_in_lens.tsx new file mode 100644 index 000000000000..6e1cac3eda86 --- /dev/null +++ b/x-pack/plugins/osquery/public/lens/pack_view_in_lens.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import moment from 'moment-timezone'; +import { usePackQueryLastResults } from '../packs/use_pack_query_last_results'; +import { ViewResultsActionButtonType } from '../live_queries/form/pack_queries_status_table'; +import { ViewResultsInLensAction } from './view_results_in_lens'; + +interface PackViewInActionProps { + item: { + id: string; + interval: number; + action_id?: string; + agents: string[]; + }; + actionId?: string; +} +const PackViewInLensActionComponent: React.FC = ({ item }) => { + const { action_id: actionId, agents: agentIds, interval } = item; + const { data: lastResultsData } = usePackQueryLastResults({ + actionId, + interval, + }); + + const startDate = lastResultsData?.['@timestamp'] + ? moment(lastResultsData?.['@timestamp'][0]).subtract(interval, 'seconds').toISOString() + : `now-${interval}s`; + const endDate = lastResultsData?.['@timestamp'] + ? moment(lastResultsData?.['@timestamp'][0]).toISOString() + : 'now'; + + return ( + + ); +}; + +export const PackViewInLensAction = React.memo(PackViewInLensActionComponent); diff --git a/x-pack/plugins/osquery/public/lens/view_results_in_lens.tsx b/x-pack/plugins/osquery/public/lens/view_results_in_lens.tsx new file mode 100644 index 000000000000..080c078f6a29 --- /dev/null +++ b/x-pack/plugins/osquery/public/lens/view_results_in_lens.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 React, { useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import type { + PersistedIndexPatternLayer, + PieVisualizationState, + TermsIndexPatternColumn, + TypedLensByValueInput, +} from '@kbn/lens-plugin/public'; +import { DOCUMENT_FIELD_NAME as RECORDS_FIELD } from '@kbn/lens-plugin/common/constants'; +import { FilterStateStore } from '@kbn/es-query'; +import { ViewResultsActionButtonType } from '../live_queries/form/pack_queries_status_table'; +import type { LogsDataView } from '../common/hooks/use_logs_data_view'; +import { useKibana } from '../common/lib/kibana'; +import { useLogsDataView } from '../common/hooks/use_logs_data_view'; + +interface ViewResultsInLensActionProps { + actionId?: string; + agentIds?: string[]; + buttonType: ViewResultsActionButtonType; + endDate?: string; + startDate?: string; + mode?: string; +} + +const ViewResultsInLensActionComponent: React.FC = ({ + actionId, + agentIds, + buttonType, + endDate, + startDate, + mode, +}) => { + const lensService = useKibana().services.lens; + const { data: logsDataView } = useLogsDataView({ skip: !actionId }); + + const handleClick = useCallback( + (event) => { + event.preventDefault(); + + if (logsDataView) { + lensService?.navigateToPrefilledEditor( + { + id: '', + timeRange: { + from: startDate ?? 'now-1d', + to: endDate ?? 'now', + mode: mode ?? (startDate || endDate) ? 'absolute' : 'relative', + }, + attributes: getLensAttributes(logsDataView, actionId, agentIds), + }, + { + openInNewTab: true, + skipAppLeave: true, + } + ); + } + }, + [actionId, agentIds, endDate, lensService, logsDataView, mode, startDate] + ); + + const isDisabled = useMemo(() => !actionId || !logsDataView, [actionId, logsDataView]); + + if (buttonType === ViewResultsActionButtonType.button) { + return ( + + {VIEW_IN_LENS} + + ); + } + + return ( + + + + ); +}; + +function getLensAttributes( + logsDataView: LogsDataView, + actionId?: string, + agentIds?: string[] +): TypedLensByValueInput['attributes'] { + const dataLayer: PersistedIndexPatternLayer = { + columnOrder: ['8690befd-fd69-4246-af4a-dd485d2a3b38', 'ed999e9d-204c-465b-897f-fe1a125b39ed'], + columns: { + '8690befd-fd69-4246-af4a-dd485d2a3b38': { + sourceField: 'type', + isBucketed: true, + dataType: 'string', + scale: 'ordinal', + operationType: 'terms', + label: 'Top values of type', + params: { + otherBucket: true, + size: 5, + missingBucket: false, + orderBy: { + columnId: 'ed999e9d-204c-465b-897f-fe1a125b39ed', + type: 'column', + }, + orderDirection: 'desc', + }, + } as TermsIndexPatternColumn, + 'ed999e9d-204c-465b-897f-fe1a125b39ed': { + sourceField: RECORDS_FIELD, + isBucketed: false, + dataType: 'number', + scale: 'ratio', + operationType: 'count', + label: 'Count of records', + }, + }, + incompleteColumns: {}, + }; + + const xyConfig: PieVisualizationState = { + shape: 'pie', + layers: [ + { + layerType: 'data', + legendDisplay: 'default', + nestedLegend: false, + layerId: 'layer1', + metric: 'ed999e9d-204c-465b-897f-fe1a125b39ed', + numberDisplay: 'percent', + primaryGroups: ['8690befd-fd69-4246-af4a-dd485d2a3b38'], + categoryDisplay: 'default', + }, + ], + }; + + const agentIdsQuery = agentIds?.length + ? { + bool: { + minimum_should_match: 1, + should: agentIds?.map((agentId) => ({ match_phrase: { 'agent.id': agentId } })), + }, + } + : undefined; + + return { + visualizationType: 'lnsPie', + title: `Action ${actionId} results`, + references: [ + { + id: logsDataView.id, + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: logsDataView.id, + name: 'indexpattern-datasource-layer-layer1', + type: 'index-pattern', + }, + { + name: 'filter-index-pattern-0', + id: logsDataView.id, + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer1: dataLayer, + }, + }, + }, + filters: [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + index: 'filter-index-pattern-0', + negate: false, + alias: null, + disabled: false, + params: { + query: actionId, + }, + type: 'phrase', + key: 'action_id', + }, + query: { + match_phrase: { + action_id: actionId, + }, + }, + }, + ...(agentIdsQuery + ? [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: 'agent IDs', + disabled: false, + index: 'filter-index-pattern-0', + key: 'query', + negate: false, + type: 'custom', + value: JSON.stringify(agentIdsQuery), + }, + query: agentIdsQuery, + }, + ] + : []), + ], + query: { language: 'kuery', query: '' }, + visualization: xyConfig, + }, + }; +} + +const VIEW_IN_LENS = i18n.translate( + 'xpack.osquery.pack.queriesTable.viewLensResultsActionAriaLabel', + { + defaultMessage: 'View in Lens', + } +); + +export const ViewResultsInLensAction = React.memo(ViewResultsInLensActionComponent); diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 4a6a204cec0b..b870d1385752 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -12,6 +12,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useForm as useHookForm, FormProvider } from 'react-hook-form'; import { isEmpty, find, pickBy } from 'lodash'; +import { AddToCaseWrapper } from '../../cases/add_to_cases'; import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline'; import { QueryPackSelectable } from './query_pack_selectable'; import type { SavedQuerySOFormData } from '../../saved_queries/form/use_saved_query_form'; @@ -25,7 +26,6 @@ import type { AgentSelection } from '../../agents/types'; import { LiveQueryQueryField } from './live_query_query_field'; import { AgentsTableField } from './agents_table_field'; import { savedQueryDataSerializer } from '../../saved_queries/form/use_saved_query_form'; -import { AddToCaseButton } from '../../cases/add_to_cases_button'; import { PackFieldWrapper } from '../../shared_components/osquery_response_action_type/pack_field_wrapper'; export interface LiveQueryFormFields { @@ -213,7 +213,7 @@ const LiveQueryFormComponent: React.FC = ({ (payload) => { if (liveQueryActionId) { return ( - ({ match_phrase: { 'agent.id': agentId } })), - }, - } - : undefined; - - return { - visualizationType: 'lnsPie', - title: `Action ${actionId} results`, - references: [ - { - id: logsDataView.id, - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }, - { - id: logsDataView.id, - name: 'indexpattern-datasource-layer-layer1', - type: 'index-pattern', - }, - { - name: 'filter-index-pattern-0', - id: logsDataView.id, - type: 'index-pattern', - }, - ], - state: { - datasourceStates: { - indexpattern: { - layers: { - layer1: dataLayer, - }, - }, - }, - filters: [ - { - $state: { store: FilterStateStore.APP_STATE }, - meta: { - index: 'filter-index-pattern-0', - negate: false, - alias: null, - disabled: false, - params: { - query: actionId, - }, - type: 'phrase', - key: 'action_id', - }, - query: { - match_phrase: { - action_id: actionId, - }, - }, - }, - ...(agentIdsQuery - ? [ - { - $state: { store: FilterStateStore.APP_STATE }, - meta: { - alias: 'agent IDs', - disabled: false, - index: 'filter-index-pattern-0', - key: 'query', - negate: false, - type: 'custom', - value: JSON.stringify(agentIdsQuery), - }, - query: agentIdsQuery, - }, - ] - : []), - ], - query: { language: 'kuery', query: '' }, - visualization: xyConfig, - }, - }; -} - -const ViewResultsInLensActionComponent: React.FC = ({ - actionId, - agentIds, - buttonType, - endDate, - startDate, - mode, -}) => { - const lensService = useKibana().services.lens; - const isLensAvailable = lensService?.canUseEditor(); - const { data: logsDataView } = useLogsDataView({ skip: !actionId }); - - const handleClick = useCallback( - (event) => { - event.preventDefault(); - - if (logsDataView) { - lensService?.navigateToPrefilledEditor( - { - id: '', - timeRange: { - from: startDate ?? 'now-1d', - to: endDate ?? 'now', - mode: mode ?? (startDate || endDate) ? 'absolute' : 'relative', - }, - attributes: getLensAttributes(logsDataView, actionId, agentIds), - }, - { - openInNewTab: true, - skipAppLeave: true, - } - ); - } - }, - [actionId, agentIds, endDate, lensService, logsDataView, mode, startDate] - ); - - const isDisabled = useMemo(() => !actionId || !logsDataView, [actionId, logsDataView]); - - if (!isLensAvailable) { - return null; - } - - if (buttonType === ViewResultsActionButtonType.button) { - return ( - - {VIEW_IN_LENS} - - ); - } - - return ( - - - - ); -}; - -export const ViewResultsInLensAction = React.memo(ViewResultsInLensActionComponent); - -const ViewResultsInDiscoverActionComponent: React.FC = ({ - actionId, - agentIds, - buttonType, - endDate, - startDate, -}) => { - const { discover, application } = useKibana().services; - const locator = discover?.locator; - const discoverPermissions = application.capabilities.discover; - const { data: logsDataView } = useLogsDataView({ skip: !actionId }); - - const [discoverUrl, setDiscoverUrl] = useState(''); - - useEffect(() => { - const getDiscoverUrl = async () => { - if (!locator || !logsDataView) return; - - const agentIdsQuery = agentIds?.length - ? { - bool: { - minimum_should_match: 1, - should: agentIds.map((agentId) => ({ match_phrase: { 'agent.id': agentId } })), - }, - } - : null; - - const newUrl = await locator.getUrl({ - indexPatternId: logsDataView.id, - filters: [ - { - meta: { - index: logsDataView.id, - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'action_id', - params: { query: actionId }, - }, - query: { match_phrase: { action_id: actionId } }, - $state: { store: FilterStateStore.APP_STATE }, - }, - ...(agentIdsQuery - ? [ - { - $state: { store: FilterStateStore.APP_STATE }, - meta: { - alias: 'agent IDs', - disabled: false, - index: logsDataView.id, - key: 'query', - negate: false, - type: 'custom', - value: JSON.stringify(agentIdsQuery), - }, - query: agentIdsQuery, - }, - ] - : []), - ], - refreshInterval: { - pause: true, - value: 0, - }, - timeRange: - startDate && endDate - ? { - to: endDate, - from: startDate, - mode: 'absolute', - } - : { - to: 'now', - from: 'now-1d', - mode: 'relative', - }, - }); - setDiscoverUrl(newUrl); - }; - - getDiscoverUrl(); - }, [actionId, agentIds, endDate, startDate, locator, logsDataView]); - - if (!discoverPermissions.show) { - return null; - } - - if (buttonType === ViewResultsActionButtonType.button) { - return ( - - {VIEW_IN_DISCOVER} - - ); - } - - return ( - - - - ); -}; - -export const ViewResultsInDiscoverAction = React.memo(ViewResultsInDiscoverActionComponent); - interface DocsColumnResultsProps { count?: number; isLive?: boolean; @@ -450,72 +99,6 @@ const AgentsColumnResults: React.FC = ({ ); -interface PackViewInActionProps { - item: { - id: string; - interval: number; - action_id?: string; - agents: string[]; - }; - actionId?: string; -} - -const PackViewInDiscoverActionComponent: React.FC = ({ item }) => { - const { action_id: actionId, agents: agentIds, interval } = item; - const { data: lastResultsData } = usePackQueryLastResults({ - actionId, - interval, - }); - - const startDate = lastResultsData?.['@timestamp'] - ? moment(lastResultsData?.['@timestamp'][0]).subtract(interval, 'seconds').toISOString() - : `now-${interval}s`; - const endDate = lastResultsData?.['@timestamp'] - ? moment(lastResultsData?.['@timestamp'][0]).toISOString() - : 'now'; - - return ( - - ); -}; - -const PackViewInDiscoverAction = React.memo(PackViewInDiscoverActionComponent); - -const PackViewInLensActionComponent: React.FC = ({ item }) => { - const { action_id: actionId, agents: agentIds, interval } = item; - const { data: lastResultsData } = usePackQueryLastResults({ - actionId, - interval, - }); - - const startDate = lastResultsData?.['@timestamp'] - ? moment(lastResultsData?.['@timestamp'][0]).subtract(interval, 'seconds').toISOString() - : `now-${interval}s`; - const endDate = lastResultsData?.['@timestamp'] - ? moment(lastResultsData?.['@timestamp'][0]).toISOString() - : 'now'; - - return ( - - ); -}; - -const PackViewInLensAction = React.memo(PackViewInLensActionComponent); - type PackQueryStatusItem = Partial<{ action_id: string; id: string; @@ -542,10 +125,12 @@ interface PackQueriesStatusTableProps { actionId, isIcon, isDisabled, + queryId, }: { actionId?: string; isIcon?: boolean; isDisabled?: boolean; + queryId?: string; }) => ReactElement; showResultsHeader?: boolean; } @@ -562,10 +147,6 @@ const PackQueriesStatusTableComponent: React.FC = ( showResultsHeader, }) => { const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>({}); - const { cases, timelines, appName } = useKibana().services; - const casePermissions = cases.helpers.canUseCases(); - const CasesContext = cases.ui.getCasesContext(); - const renderIDColumn = useCallback( (id: string) => ( @@ -619,11 +200,15 @@ const PackQueriesStatusTableComponent: React.FC = ( const renderLensResultsAction = useCallback((item) => , []); const handleAddToCase = useCallback( - (payload: { actionId: string; isIcon?: boolean }) => + (payload: { actionId?: string; isIcon?: boolean; queryId: string }) => // eslint-disable-next-line react/display-name () => { if (addToCase) { - return addToCase({ actionId: payload.actionId, isIcon: payload?.isIcon }); + return addToCase({ + actionId: payload.actionId, + isIcon: payload.isIcon, + queryId: payload.queryId, + }); } return <>; @@ -648,7 +233,7 @@ const PackQueriesStatusTableComponent: React.FC = ( agentIds={agentIds} failedAgentsCount={item?.failed ?? 0} addToTimeline={addToTimeline} - addToCase={addToCase && handleAddToCase({ actionId: item.action_id })} + addToCase={addToCase && handleAddToCase({ queryId: item.action_id, actionId })} /> @@ -658,7 +243,7 @@ const PackQueriesStatusTableComponent: React.FC = ( return itemIdToExpandedRowMapValues; }); }, - [startDate, expirationDate, agentIds, addToTimeline, addToCase, handleAddToCase] + [startDate, expirationDate, agentIds, addToTimeline, addToCase, handleAddToCase, actionId] ); const renderToggleResultsAction = useCallback( @@ -677,28 +262,37 @@ const PackQueriesStatusTableComponent: React.FC = ( const getItemId = useCallback((item: PackItem) => get(item, 'id'), []); - const columns = useMemo(() => { - const resultActions = [ - { - render: renderDiscoverResultsAction, - }, - { - render: renderLensResultsAction, - }, - { - available: () => !!addToCase, - render: (item: { action_id: string }) => - addToCase && - addToCase({ actionId: item.action_id, isIcon: true, isDisabled: !item.action_id }), - }, - { - available: () => addToTimeline && timelines && appName === SECURITY_APP_NAME, - render: (item: { action_id: string }) => - addToTimeline && addToTimeline({ query: ['action_id', item.action_id], isIcon: true }), - }, - ]; + const renderResultActions = useCallback( + (row: { action_id: string }) => { + const resultActions = [ + { + render: renderDiscoverResultsAction, + }, + { + render: renderLensResultsAction, + }, + { + render: (item: { action_id: string }) => + addToTimeline && addToTimeline({ query: ['action_id', item.action_id], isIcon: true }), + }, + { + render: (item: { action_id: string }) => + addToCase && + addToCase({ + actionId, + queryId: item.action_id, + isIcon: true, + isDisabled: !item.action_id, + }), + }, + ]; - return [ + return resultActions.map((action) => action.render(row)); + }, + [actionId, addToCase, addToTimeline, renderDiscoverResultsAction, renderLensResultsAction] + ); + const columns = useMemo( + () => [ { field: 'id', name: i18n.translate('xpack.osquery.pack.queriesTable.idColumnTitle', { @@ -734,7 +328,7 @@ const PackQueriesStatusTableComponent: React.FC = ( defaultMessage: 'View results', }), width: '90px', - actions: resultActions, + render: renderResultActions, }, { id: 'actions', @@ -747,21 +341,16 @@ const PackQueriesStatusTableComponent: React.FC = ( }, ], }, - ]; - }, [ - renderDiscoverResultsAction, - renderLensResultsAction, - renderIDColumn, - renderQueryColumn, - renderDocsColumn, - renderAgentsColumn, - renderToggleResultsAction, - addToCase, - addToTimeline, - timelines, - appName, - ]); - + ], + [ + renderIDColumn, + renderQueryColumn, + renderDocsColumn, + renderAgentsColumn, + renderResultActions, + renderToggleResultsAction, + ] + ); const sorting = useMemo( () => ({ sort: { @@ -798,7 +387,7 @@ const PackQueriesStatusTableComponent: React.FC = ( ); return ( - + <> {showResultsHeader && ( )} @@ -811,7 +400,7 @@ const PackQueriesStatusTableComponent: React.FC = ( itemIdToExpandedRowMap={itemIdToExpandedRowMap} isExpandable /> - + ); }; diff --git a/x-pack/plugins/osquery/public/live_queries/form/pack_results_header.tsx b/x-pack/plugins/osquery/public/live_queries/form/pack_results_header.tsx index 854548fdb58e..b4892a0387e8 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/pack_results_header.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/pack_results_header.tsx @@ -35,6 +35,7 @@ const StyledIconsList = styled(EuiFlexItem)` export const PackResultsHeader = ({ actionId, addToCase }: PackResultsHeadersProps) => ( <> + diff --git a/x-pack/plugins/osquery/public/packs/form/index.tsx b/x-pack/plugins/osquery/public/packs/form/index.tsx index 594c539c389c..ac3892fe9748 100644 --- a/x-pack/plugins/osquery/public/packs/form/index.tsx +++ b/x-pack/plugins/osquery/public/packs/form/index.tsx @@ -155,7 +155,7 @@ const PackFormComponent: React.FC = ({ - + diff --git a/x-pack/plugins/osquery/public/packs/queries/query_flyout.tsx b/x-pack/plugins/osquery/public/packs/queries/query_flyout.tsx index d709b4f639e6..65d829e7b7e8 100644 --- a/x-pack/plugins/osquery/public/packs/queries/query_flyout.tsx +++ b/x-pack/plugins/osquery/public/packs/queries/query_flyout.tsx @@ -79,7 +79,7 @@ const QueryFlyoutComponent: React.FC = ({ resetField('version', { defaultValue: savedQuery.version ? [savedQuery.version] : [] }); resetField('interval', { defaultValue: savedQuery.interval ? savedQuery.interval : 3600 }); resetField('snapshot', { defaultValue: savedQuery.snapshot ?? true }); - resetField('removed'); + resetField('removed', { defaultValue: savedQuery.removed }); resetField('ecs_mapping', { defaultValue: savedQuery.ecs_mapping ?? {} }); } }, diff --git a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx index a4c58074c76e..bf35a529137f 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx @@ -10,13 +10,18 @@ import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { AddToCaseButton } from '../../../cases/add_to_cases_button'; +import styled from 'styled-components'; +import { AddToCaseWrapper } from '../../../cases/add_to_cases'; import { useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { useLiveQueryDetails } from '../../../actions/use_live_query_details'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; import { PackQueriesStatusTable } from '../../../live_queries/form/pack_queries_status_table'; +const StyledTableWrapper = styled(EuiFlexItem)` + padding-left: 10px; +`; + const LiveQueryDetailsPageComponent = () => { const { actionId } = useParams<{ actionId: string }>(); useBreadcrumbs('live_query_details', { liveQueryId: actionId }); @@ -55,7 +60,7 @@ const LiveQueryDetailsPageComponent = () => { }, [data?.status]); const addToCaseButton = useCallback( (payload) => ( - { return ( - + + + ); }; diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx index 49501bc24b70..f4de8b04cb47 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx @@ -10,13 +10,10 @@ import React, { useMemo } from 'react'; import type { ReactElement } from 'react'; import type { ECSMapping } from '@kbn/osquery-io-ts-types'; -import { useKibana } from '../../../common/lib/kibana'; import type { AddToTimelinePayload } from '../../../timelines/get_add_to_timeline'; import { ResultsTable } from '../../../results/results_table'; import { ActionResultsSummary } from '../../../action_results/action_results_summary'; -const CASES_OWNER: string[] = []; - interface ResultTabsProps { actionId: string; agentIds?: string[]; @@ -38,10 +35,6 @@ const ResultTabsComponent: React.FC = ({ addToTimeline, addToCase, }) => { - const { cases } = useKibana().services; - const casePermissions = cases.helpers.canUseCases(); - const CasesContext = cases.ui.getCasesContext(); - const tabs = useMemo( () => [ { @@ -85,16 +78,14 @@ const ResultTabsComponent: React.FC = ({ ); return ( - - - + ); }; diff --git a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx index 3392f2af037c..76bdd77d17b1 100644 --- a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx @@ -72,11 +72,6 @@ export const savedQueryDataSerializer = (payload: SavedQueryFormData): SavedQuer draft.interval = draft.interval + ''; } - if (draft.snapshot) { - delete draft.snapshot; - delete draft.removed; - } - return draft; }); diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx index 6bb8d6f5a563..3095428d478d 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx @@ -6,17 +6,12 @@ */ import { EuiLoadingContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { OsqueryEmptyPrompt, OsqueryNotAvailablePrompt } from '../prompts'; import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline'; -import { - AGENT_STATUS_ERROR, - EMPTY_PROMPT, - NOT_AVAILABLE, - PERMISSION_DENIED, - SHORT_EMPTY_TITLE, -} from './translations'; +import { AGENT_STATUS_ERROR, PERMISSION_DENIED, SHORT_EMPTY_TITLE } from './translations'; import { useKibana } from '../../common/lib/kibana'; import { LiveQuery } from '../../live_queries'; import { OsqueryIcon } from '../../components/osquery_icon'; @@ -39,22 +34,11 @@ const OsqueryActionComponent: React.FC = ({ }) => { const permissions = useKibana().services.application.capabilities.osquery; - const emptyPrompt = useMemo( - () => ( - } - title={

{SHORT_EMPTY_TITLE}

} - titleSize="xs" - body={

{EMPTY_PROMPT}

} - /> - ), - [] - ); const { osqueryAvailable, agentFetched, isLoading, policyFetched, policyLoading, agentData } = useIsOsqueryAvailable(agentId); if (agentId && agentFetched && !agentData) { - return emptyPrompt; + return ; } if ( @@ -91,14 +75,7 @@ const OsqueryActionComponent: React.FC = ({ } if (agentId && !osqueryAvailable) { - return ( - } - title={

{SHORT_EMPTY_TITLE}

} - titleSize="xs" - body={

{NOT_AVAILABLE}

} - /> - ); + return ; } if (agentId && agentData?.status !== 'online') { diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.test.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.test.tsx index e33207a12d90..772df753c897 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.test.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.test.tsx @@ -20,7 +20,7 @@ import { DETAILS_ID, DETAILS_QUERY, DETAILS_TIMESTAMP, - mockCasesContext, + getMockedKibanaConfig, } from './test_utils'; jest.mock('../../common/lib/kibana'); @@ -43,34 +43,8 @@ const defaultProps = { queryId: '', }; const mockKibana = (permissionType: unknown = defaultPermissions) => { - useKibanaMock.mockReturnValue({ - services: { - application: { - capabilities: permissionType, - }, - cases: { - helpers: { - canUseCases: jest.fn(), - }, - ui: { - getCasesContext: jest.fn().mockImplementation(() => mockCasesContext), - }, - }, - data: { - dataViews: { - getCanSaveSync: jest.fn(), - hasData: { - hasESData: jest.fn(), - hasUserDataView: jest.fn(), - hasDataView: jest.fn(), - }, - }, - }, - notifications: { - toasts: jest.fn(), - }, - }, - } as unknown as ReturnType); + const mockedKibana = getMockedKibanaConfig(permissionType); + useKibanaMock.mockReturnValue(mockedKibana); }; const renderWithContext = (Element: React.ReactElement) => diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.tsx index 998de8a15cfc..b0d0691c14f7 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_result.tsx @@ -6,9 +6,10 @@ */ import { EuiComment, EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; import { FormattedRelative } from '@kbn/i18n-react'; +import { AddToCaseWrapper } from '../../cases/add_to_cases'; import type { OsqueryActionResultsProps } from './types'; import { useLiveQueryDetails } from '../../actions/use_live_query_details'; import { ATTACHED_QUERY } from '../../agents/translations'; @@ -22,7 +23,6 @@ interface OsqueryResultProps extends Omit export const OsqueryResult = ({ actionId, - queryId, ruleName, addToTimeline, agentIds, @@ -30,10 +30,22 @@ export const OsqueryResult = ({ }: OsqueryResultProps) => { const { data } = useLiveQueryDetails({ actionId, - // isLive, - // ...(queryId ? { queryIds: [queryId] } : {}), }); + const addToCaseButton = useCallback( + (payload) => ( + + ), + [data?.agents, actionId] + ); + return (
@@ -51,6 +63,7 @@ export const OsqueryResult = ({ expirationDate={data?.expiration} agentIds={agentIds} addToTimeline={addToTimeline} + addToCase={addToCaseButton} /> diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_results.test.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_results.test.tsx index 9e2b74e00070..5190d10dc1c7 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_results.test.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/osquery_results.test.tsx @@ -17,7 +17,7 @@ import * as useAllLiveQueries from '../../actions/use_all_live_queries'; import * as useLiveQueryDetails from '../../actions/use_live_query_details'; import { PERMISSION_DENIED } from '../osquery_action/translations'; import * as privileges from '../../action_results/use_action_privileges'; -import { defaultLiveQueryDetails, DETAILS_QUERY, mockCasesContext } from './test_utils'; +import { defaultLiveQueryDetails, DETAILS_QUERY, getMockedKibanaConfig } from './test_utils'; jest.mock('../../common/lib/kibana'); @@ -49,34 +49,8 @@ const defaultPermissions = { }; const mockKibana = (permissionType: unknown = defaultPermissions) => { - useKibanaMock.mockReturnValue({ - services: { - application: { - capabilities: permissionType, - }, - cases: { - helpers: { - canUseCases: jest.fn(), - }, - ui: { - getCasesContext: jest.fn().mockImplementation(() => mockCasesContext), - }, - }, - data: { - dataViews: { - getCanSaveSync: jest.fn(), - hasData: { - hasESData: jest.fn(), - hasUserDataView: jest.fn(), - hasDataView: jest.fn(), - }, - }, - }, - notifications: { - toasts: jest.fn(), - }, - }, - } as unknown as ReturnType); + const mockedKibana = getMockedKibanaConfig(permissionType); + useKibanaMock.mockReturnValue(mockedKibana); }; const renderWithContext = (Element: React.ReactElement) => diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_results/test_utils.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_results/test_utils.tsx index 4de58a4ed9aa..c1c991becc8a 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_results/test_utils.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_results/test_utils.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import type { useKibana } from '../../common/lib/kibana'; export const DETAILS_QUERY = 'select * from uptime'; export const DETAILS_ID = 'test-id'; @@ -38,4 +39,41 @@ export const defaultLiveQueryDetails = { }, } as never; +export const getMockedKibanaConfig = (permissionType: unknown) => + ({ + services: { + application: { + capabilities: permissionType, + }, + cases: { + helpers: { + canUseCases: jest.fn().mockImplementation(() => ({ + read: true, + update: true, + push: true, + })), + }, + ui: { + getCasesContext: jest.fn().mockImplementation(() => mockCasesContext), + }, + hooks: { + getUseCasesAddToExistingCaseModal: jest.fn(), + }, + }, + data: { + dataViews: { + getCanSaveSync: jest.fn(), + hasData: { + hasESData: jest.fn(), + hasUserDataView: jest.fn(), + hasDataView: jest.fn(), + }, + }, + }, + notifications: { + toasts: jest.fn(), + }, + }, + } as unknown as ReturnType); + export const mockCasesContext: React.FC = (props) => <>{props?.children ?? null}; diff --git a/x-pack/plugins/osquery/public/shared_components/prompts.tsx b/x-pack/plugins/osquery/public/shared_components/prompts.tsx new file mode 100644 index 000000000000..992de7c9ab14 --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/prompts.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { OsqueryIcon } from '../components/osquery_icon'; +import { EMPTY_PROMPT, NOT_AVAILABLE, SHORT_EMPTY_TITLE } from './osquery_action/translations'; + +export const OsqueryEmptyPrompt = () => ( + } + title={

{SHORT_EMPTY_TITLE}

} + titleSize="xs" + body={

{EMPTY_PROMPT}

} + /> +); + +export const OsqueryNotAvailablePrompt = () => ( + } + title={

{SHORT_EMPTY_TITLE}

} + titleSize="xs" + body={

{NOT_AVAILABLE}

} + /> +); diff --git a/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts b/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts index c60c6d476974..3dda3cdb36b0 100644 --- a/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts +++ b/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts @@ -144,7 +144,10 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte } set(draft, `inputs[0].config.osquery.value.packs.${packSO.attributes.name}`, { - queries: convertSOQueriesToPack(queries, { removeMultiLines: true }), + queries: convertSOQueriesToPack(queries, { + removeMultiLines: true, + removeResultType: true, + }), }); return draft; diff --git a/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts b/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts index c113cbeb36c3..5b0d76131ee2 100644 --- a/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts +++ b/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts @@ -285,6 +285,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte { queries: convertSOQueriesToPack(updatedPackSO.attributes.queries, { removeMultiLines: true, + removeResultType: true, }), } ); @@ -315,7 +316,9 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte draft, `inputs[0].config.osquery.value.packs.${updatedPackSO.attributes.name}`, { - queries: updatedPackSO.attributes.queries, + queries: convertSOQueriesToPack(updatedPackSO.attributes.queries, { + removeResultType: true, + }), } ); diff --git a/x-pack/plugins/osquery/server/routes/pack/utils.test.ts b/x-pack/plugins/osquery/server/routes/pack/utils.test.ts index 05aff2807393..3ddb074a6edb 100644 --- a/x-pack/plugins/osquery/server/routes/pack/utils.test.ts +++ b/x-pack/plugins/osquery/server/routes/pack/utils.test.ts @@ -42,15 +42,45 @@ describe('Pack utils', () => { describe('convertSOQueriesToPack', () => { test('converts to pack with empty ecs_mapping', () => { const convertedQueries = convertSOQueriesToPack(getTestQueries()); - expect(convertedQueries).toStrictEqual(getTestQueries({})); + expect(convertedQueries).toStrictEqual(getTestQueries()); }); test('converts to pack with converting query to single line', () => { const convertedQueries = convertSOQueriesToPack(getTestQueries(), { removeMultiLines: true }); - expect(convertedQueries).toStrictEqual(oneLiner); + expect(convertedQueries).toStrictEqual({ + ...oneLiner, + }); }); test('converts to object with pack names after query.id', () => { const convertedQueries = convertSOQueriesToPack(getTestQueries({ id: 'testId' })); expect(convertedQueries).toStrictEqual(getTestQueries({}, 'testId')); }); + test('converts with results snapshot set false', () => { + const convertedQueries = convertSOQueriesToPack( + getTestQueries({ snapshot: false, removed: true }), + { removeResultType: true } + ); + expect(convertedQueries).toStrictEqual(getTestQueries({ removed: true, snapshot: false })); + }); + test('converts with results snapshot set true and removed false', () => { + const convertedQueries = convertSOQueriesToPack( + getTestQueries({ snapshot: true, removed: true }), + { removeResultType: true } + ); + expect(convertedQueries).toStrictEqual(getTestQueries({})); + }); + test('converts with results snapshot set true but removed false', () => { + const convertedQueries = convertSOQueriesToPack( + getTestQueries({ snapshot: true, removed: false }), + { removeResultType: true } + ); + expect(convertedQueries).toStrictEqual(getTestQueries({})); + }); + test('converts with both results set to false', () => { + const convertedQueries = convertSOQueriesToPack( + getTestQueries({ snapshot: false, removed: false }), + { removeResultType: true } + ); + expect(convertedQueries).toStrictEqual(getTestQueries({ removed: false, snapshot: false })); + }); }); }); diff --git a/x-pack/plugins/osquery/server/routes/pack/utils.ts b/x-pack/plugins/osquery/server/routes/pack/utils.ts index fe1ded84f446..4342cdb3ead8 100644 --- a/x-pack/plugins/osquery/server/routes/pack/utils.ts +++ b/x-pack/plugins/osquery/server/routes/pack/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isEmpty, pick, reduce } from 'lodash'; +import { isEmpty, pick, reduce, isArray } from 'lodash'; import { DEFAULT_PLATFORM } from '../../../common/constants'; import { removeMultilines } from '../../../common/utils/build_query/remove_multilines'; import { convertECSMappingToArray, convertECSMappingToObject } from '../utils'; @@ -37,18 +37,29 @@ export const convertPackQueriesToSO = (queries) => }> ); -// @ts-expect-error update types -export const convertSOQueriesToPack = (queries, options?: { removeMultiLines?: boolean }) => +export const convertSOQueriesToPack = ( + // @ts-expect-error update types + queries, + options?: { removeMultiLines?: boolean; removeResultType?: boolean } +) => reduce( queries, // eslint-disable-next-line @typescript-eslint/naming-convention - (acc, { id: queryId, ecs_mapping, query, platform, ...rest }, key) => { + (acc, { id: queryId, ecs_mapping, query, platform, removed, snapshot, ...rest }, key) => { + const resultType = !snapshot ? { removed, snapshot } : {}; const index = queryId ? queryId : key; acc[index] = { ...rest, query: options?.removeMultiLines ? removeMultilines(query) : query, - ...(!isEmpty(ecs_mapping) ? { ecs_mapping: convertECSMappingToObject(ecs_mapping) } : {}), + ...(!isEmpty(ecs_mapping) + ? isArray(ecs_mapping) + ? { ecs_mapping: convertECSMappingToObject(ecs_mapping) } + : { ecs_mapping } + : {}), ...(platform === DEFAULT_PLATFORM || platform === undefined ? {} : { platform }), + ...(options?.removeResultType + ? resultType + : { ...(snapshot ? { snapshot } : {}), ...(removed ? { removed } : {}) }), }; return acc; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts index 206719927281..4ad6146328a1 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isEmpty, pickBy, some } from 'lodash'; +import { isEmpty, pickBy, some, isBoolean } from 'lodash'; import type { IRouter } from '@kbn/core/server'; import { PLUGIN_ID } from '../../../common'; import type { CreateSavedQueryRequestSchemaDecoded } from '../../../common/schemas/routes/saved_query/create_saved_query_request_schema'; @@ -76,7 +76,7 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp updated_by: currentUser, updated_at: new Date().toISOString(), }, - (value) => !isEmpty(value) || value === false + (value) => !isEmpty(value) || isBoolean(value) ) ); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 8272c9220e10..cd18a28e0d37 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -90,6 +90,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", ] `); }); @@ -174,6 +175,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", @@ -218,6 +220,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", @@ -316,6 +319,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", @@ -374,6 +378,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", @@ -392,6 +397,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getRuleExecutionKPI", ] `); }); @@ -480,6 +486,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", @@ -498,6 +505,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/getAuthorizedAlertsIndices", diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index 542dfd1267d4..a11a4fa77bcd 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -17,7 +17,14 @@ enum AlertingEntity { } const readOperations: Record = { - rule: ['get', 'getRuleState', 'getAlertSummary', 'getExecutionLog', 'find'], + rule: [ + 'get', + 'getRuleState', + 'getAlertSummary', + 'getExecutionLog', + 'find', + 'getRuleExecutionKPI', + ], alert: ['get', 'find', 'getAuthorizedAlertsIndices'], }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_metrics.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_metrics.ts index c6bb71970e59..b15c76119e44 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_metrics.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_metrics.ts @@ -12,8 +12,17 @@ export type DurationMetric = t.TypeOf; export const DurationMetric = PositiveInteger; export type RuleExecutionMetrics = t.TypeOf; + +/** + @property total_search_duration_ms - "total time spent performing ES searches as measured by Kibana; + includes network latency and time spent serializing/deserializing request/response", + @property total_indexing_duration_ms - "total time spent indexing documents during current rule execution cycle", + @property total_enrichment_duration_ms - total time spent enriching documents during current rule execution cycle + @property execution_gap_duration_s - "duration in seconds of execution gap" +*/ export const RuleExecutionMetrics = t.partial({ total_search_duration_ms: DurationMetric, total_indexing_duration_ms: DurationMetric, + total_enrichment_duration_ms: DurationMetric, execution_gap_duration_s: DurationMetric, }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts index a74845721a02..0140e5f8d926 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts @@ -9,7 +9,6 @@ import * as t from 'io-ts'; import { NonEmptyArray, TimeDuration, enumeration } from '@kbn/securitysolution-io-ts-types'; import { - throttle, action_group as actionGroup, action_params as actionParams, action_id as actionId, @@ -41,6 +40,18 @@ export enum BulkActionEditType { 'set_schedule' = 'set_schedule', } +export const throttleForBulkActions = t.union([ + t.literal('rule'), + TimeDuration({ + allowedDurations: [ + [1, 'h'], + [1, 'd'], + [7, 'd'], + ], + }), +]); +export type ThrottleForBulkActions = t.TypeOf; + const bulkActionEditPayloadTags = t.type({ type: t.union([ t.literal(BulkActionEditType.add_tags), @@ -96,7 +107,7 @@ const bulkActionEditPayloadRuleActions = t.type({ t.literal(BulkActionEditType.set_rule_actions), ]), value: t.type({ - throttle, + throttle: throttleForBulkActions, actions: t.array(normalizedRuleAction), }), }); @@ -106,8 +117,8 @@ export type BulkActionEditPayloadRuleActions = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts index 08adac7c9ede..8b982e6e6f46 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts @@ -28,6 +28,8 @@ export const NoParametersRequestSchema = { body: schema.object({ ...BaseActionRequestSchema }), }; +export type BaseActionRequestBody = TypeOf; + export const KillOrSuspendProcessRequestSchema = { body: schema.object({ ...BaseActionRequestSchema, diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts index 3434e95f29b4..b9c0dcff2054 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts @@ -107,6 +107,40 @@ describe('Endpoint Authz service', () => { ); }); }); + + describe('endpoint rbac is enabled', () => { + describe('canIsolateHost', () => { + it('should be true if packagePrivilege.writeHostIsolation is true', () => { + fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction = + true; + const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true); + expect(authz.canIsolateHost).toBe(true); + }); + + it('should be false if packagePrivilege.writeHostIsolation is false', () => { + fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction = + false; + const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true); + expect(authz.canIsolateHost).toBe(false); + }); + }); + + describe('canUnIsolateHost', () => { + it('should be true if packagePrivilege.writeHostIsolation is true', () => { + fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction = + true; + const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true); + expect(authz.canUnIsolateHost).toBe(true); + }); + + it('should be false if packagePrivilege.writeHostIsolation is false', () => { + fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction = + false; + const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true); + expect(authz.canUnIsolateHost).toBe(false); + }); + }); + }); }); describe('getEndpointAuthzInitialState()', () => { diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts index 6f578ec24d85..bc40bd11f5d7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts @@ -28,14 +28,18 @@ export const calculateEndpointAuthz = ( const isPlatinumPlusLicense = licenseService.isPlatinumPlus(); const isEnterpriseLicense = licenseService.isEnterprise(); const hasEndpointManagementAccess = userRoles.includes('superuser'); + const canIsolateHost = isEndpointRbacEnabled + ? fleetAuthz.packagePrivileges?.endpoint?.actions?.writeHostIsolation?.executePackageAction || + false + : hasEndpointManagementAccess; return { canAccessFleet: fleetAuthz?.fleet.all ?? userRoles.includes('superuser'), canAccessEndpointManagement: hasEndpointManagementAccess, canCreateArtifactsByPolicy: hasEndpointManagementAccess && isPlatinumPlusLicense, // Response Actions - canIsolateHost: isPlatinumPlusLicense && hasEndpointManagementAccess, - canUnIsolateHost: hasEndpointManagementAccess, + canIsolateHost: isPlatinumPlusLicense && canIsolateHost, + canUnIsolateHost: canIsolateHost, canKillProcess: hasEndpointManagementAccess && isEnterpriseLicense, canSuspendProcess: hasEndpointManagementAccess && isEnterpriseLicense, canGetRunningProcesses: hasEndpointManagementAccess && isEnterpriseLicense, diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index bcbdcfb3b66b..91eb10c5f45a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -253,6 +253,7 @@ export interface PendingActionsResponse { } export type PendingActionsRequestQuery = TypeOf; + export interface ActionDetails { /** The action id */ id: string; diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_data_view.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_data_view.cy.ts new file mode 100644 index 000000000000..766c8d9483f0 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_data_view.cy.ts @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + RULES_BULK_EDIT_DATA_VIEWS_WARNING, + RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX, +} from '../../screens/rules_bulk_edit'; + +import { DATA_VIEW_DETAILS, INDEX_PATTERNS_DETAILS } from '../../screens/rule_details'; + +import { + waitForRulesTableToBeLoaded, + goToRuleDetails, + selectNumberOfRules, +} from '../../tasks/alerts_detection_rules'; + +import { + typeIndexPatterns, + waitForBulkEditActionToFinish, + submitBulkEditForm, + checkOverwriteDataViewCheckbox, + checkOverwriteIndexPatternsCheckbox, + openBulkEditAddIndexPatternsForm, + openBulkEditDeleteIndexPatternsForm, +} from '../../tasks/rules_bulk_edit'; + +import { hasIndexPatterns, getDetails, assertDetailsNotExist } from '../../tasks/rule_details'; +import { login, visitWithoutDateRange } from '../../tasks/login'; + +import { SECURITY_DETECTIONS_RULES_URL } from '../../urls/navigation'; +import { + createCustomRule, + createCustomIndicatorRule, + createEventCorrelationRule, + createThresholdRule, + createNewTermsRule, + createSavedQueryRule, +} from '../../tasks/api_calls/rules'; +import { cleanKibana, deleteAlertsAndRules, postDataView } from '../../tasks/common'; + +import { + getEqlRule, + getNewThreatIndicatorRule, + getNewRule, + getNewThresholdRule, + getNewTermsRule, +} from '../../objects/rule'; + +import { esArchiverResetKibana } from '../../tasks/es_archiver'; + +const DATA_VIEW_ID = 'auditbeat'; + +const expectedIndexPatterns = ['index-1-*', 'index-2-*']; + +const expectedNumberOfCustomRulesToBeEdited = 6; + +const indexDataSource = { dataView: DATA_VIEW_ID, type: 'dataView' } as const; + +const defaultRuleData = { + dataSource: indexDataSource, +}; + +describe('Detection rules, bulk edit, data view', () => { + before(() => { + cleanKibana(); + login(); + }); + beforeEach(() => { + deleteAlertsAndRules(); + esArchiverResetKibana(); + + postDataView(DATA_VIEW_ID); + + createCustomRule({ ...getNewRule(), ...defaultRuleData }, '1'); + createEventCorrelationRule({ ...getEqlRule(), ...defaultRuleData }, '2'); + createCustomIndicatorRule({ ...getNewThreatIndicatorRule(), ...defaultRuleData }, '3'); + createThresholdRule({ ...getNewThresholdRule(), ...defaultRuleData }, '4'); + createNewTermsRule({ ...getNewTermsRule(), ...defaultRuleData }, '5'); + createSavedQueryRule({ ...getNewRule(), ...defaultRuleData, savedId: 'mocked' }, '6'); + + visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL); + + waitForRulesTableToBeLoaded(); + }); + + it('Add index patterns to custom rules with configured data view', () => { + selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); + + openBulkEditAddIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + + // check if rule still has data view and index patterns field does not exist + goToRuleDetails(); + getDetails(DATA_VIEW_DETAILS).contains(DATA_VIEW_ID); + assertDetailsNotExist(INDEX_PATTERNS_DETAILS); + }); + + it('Add index patterns to custom rules with configured data view when data view checkbox is checked', () => { + selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); + + openBulkEditAddIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + + // click on data view overwrite checkbox, ensure warning is displayed + cy.get(RULES_BULK_EDIT_DATA_VIEWS_WARNING).should('not.exist'); + checkOverwriteDataViewCheckbox(); + cy.get(RULES_BULK_EDIT_DATA_VIEWS_WARNING).should('be.visible'); + + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + + // check if rule has been updated with index patterns and data view does not exist + goToRuleDetails(); + hasIndexPatterns(expectedIndexPatterns.join('')); + assertDetailsNotExist(DATA_VIEW_DETAILS); + }); + + it('Overwrite index patterns in custom rules with configured data view', () => { + selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); + + openBulkEditAddIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + checkOverwriteIndexPatternsCheckbox(); + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + + // check if rule still has data view and index patterns field does not exist + goToRuleDetails(); + getDetails(DATA_VIEW_DETAILS).contains(DATA_VIEW_ID); + assertDetailsNotExist(INDEX_PATTERNS_DETAILS); + }); + + it('Overwrite index patterns in custom rules with configured data view when data view checkbox is checked', () => { + selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); + + openBulkEditAddIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + checkOverwriteIndexPatternsCheckbox(); + checkOverwriteDataViewCheckbox(); + + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + + // check if rule has been overwritten with index patterns and data view does not exist + goToRuleDetails(); + hasIndexPatterns(expectedIndexPatterns.join('')); + assertDetailsNotExist(DATA_VIEW_DETAILS); + }); + + it('Delete index patterns in custom rules with configured data view', () => { + selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); + + openBulkEditDeleteIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + + // in delete form data view checkbox is absent + cy.get(RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX).should('not.exist'); + + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + + // check if rule still has data view and index patterns field does not exist + goToRuleDetails(); + getDetails(DATA_VIEW_DETAILS).contains(DATA_VIEW_ID); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_saved_query_rule.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_saved_query_rule.cy.ts index 9b1d5c71a6fa..5027fe09a8d3 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_saved_query_rule.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_saved_query_rule.cy.ts @@ -25,8 +25,8 @@ import { import { goToRuleDetails, editFirstRule } from '../../tasks/alerts_detection_rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; -import { createSavedQuery } from '../../tasks/api_calls/saved_queries'; -import { cleanKibana, deleteAlertsAndRules, deleteSavedQueries } from '../../tasks/common'; +import { createSavedQuery, deleteSavedQueries } from '../../tasks/api_calls/saved_queries'; +import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common'; import { createAndEnableRule, fillAboutRuleAndContinue, diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts index b5c6b5cd341e..02ccff0c265d 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts @@ -122,14 +122,14 @@ describe('Related integrations', () => { visit(DETECTIONS_RULE_MANAGEMENT_URL); }); - it('should display a badge with the installed integrations on the rule management page', () => { + it.skip('should display a badge with the installed integrations on the rule management page', () => { cy.get(INTEGRATIONS_POPOVER).should( 'have.text', `${rule.enabledIntegrations}/${rule.integrations.length} integrations` ); }); - it('should display a popover when clicking the badge with the installed integrations on the rule management page', () => { + it.skip('should display a popover when clicking the badge with the installed integrations on the rule management page', () => { openIntegrationsPopover(); cy.get(INTEGRATIONS_POPOVER_TITLE).should( @@ -148,7 +148,7 @@ describe('Related integrations', () => { }); }); - it('should display the integrations on the definition section', () => { + it.skip('should display the integrations on the definition section', () => { goToTheRuleDetailsOf(rule.name); cy.get(INTEGRATIONS).should('have.length', rule.integrations.length); diff --git a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts index 34e4f9515b27..6f4056034a05 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts @@ -32,6 +32,9 @@ export const RULES_BULK_EDIT_INDEX_PATTERNS = '[data-test-subj="bulkEditRulesInd export const RULES_BULK_EDIT_OVERWRITE_INDEX_PATTERNS_CHECKBOX = '[data-test-subj="bulkEditRulesOverwriteIndexPatterns"]'; +export const RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX = + '[data-test-subj="bulkEditRulesOverwriteRulesWithDataViews"]'; + export const RULES_BULK_EDIT_TAGS = '[data-test-subj="bulkEditRulesTags"]'; export const RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX = @@ -48,6 +51,9 @@ export const RULES_BULK_EDIT_TIMELINE_TEMPLATES_SELECTOR = export const RULES_BULK_EDIT_TIMELINE_TEMPLATES_WARNING = '[data-test-subj="bulkEditRulesTimelineTemplateWarning"]'; +export const RULES_BULK_EDIT_DATA_VIEWS_WARNING = + '[data-test-subj="bulkEditRulesDataViewsWarning"]'; + export const RULES_BULK_EDIT_SCHEDULES_WARNING = '[data-test-subj="bulkEditRulesSchedulesWarning"]'; export const UPDATE_SCHEDULE_INTERVAL_INPUT = diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 4fb076f11f44..80fb77013acb 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -53,7 +53,8 @@ export const createCustomRule = ( severity: rule.severity.toLocaleLowerCase(), type: 'query', from: 'now-50000h', - index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : '', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, query: rule.customQuery, language: 'kuery', enabled: false, @@ -71,83 +72,80 @@ export const createCustomRule = ( }); export const createEventCorrelationRule = (rule: CustomRule, ruleId = 'rule_testing') => { - if (rule.dataSource.type === 'indexPatterns') { - cy.request({ - method: 'POST', - url: 'api/detection_engine/rules', - body: { - rule_id: ruleId, - risk_score: parseInt(rule.riskScore, 10), - description: rule.description, - interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, - from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, - name: rule.name, - severity: rule.severity.toLocaleLowerCase(), - type: 'eql', - index: rule.dataSource.index, - query: rule.customQuery, - language: 'eql', - enabled: true, - tags: rule.tags, - }, - headers: { 'kbn-xsrf': 'cypress-creds' }, - }); - } + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, + from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'eql', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, + query: rule.customQuery, + language: 'eql', + enabled: true, + tags: rule.tags, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); }; export const createThresholdRule = (rule: ThresholdRule, ruleId = 'rule_testing') => { - if (rule.dataSource.type === 'indexPatterns') { - cy.request({ - method: 'POST', - url: 'api/detection_engine/rules', - body: { - rule_id: ruleId, - risk_score: parseInt(rule.riskScore, 10), - description: rule.description, - interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, - from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, - name: rule.name, - severity: rule.severity.toLocaleLowerCase(), - type: 'threshold', - index: rule.dataSource.index, - query: rule.customQuery, - threshold: { - field: [rule.thresholdField], - value: parseInt(rule.threshold, 10), - cardinality: [], - }, - enabled: true, - tags: rule.tags, + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, + from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'threshold', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, + query: rule.customQuery, + threshold: { + field: [rule.thresholdField], + value: parseInt(rule.threshold, 10), + cardinality: [], }, - headers: { 'kbn-xsrf': 'cypress-creds' }, - }); - } + enabled: true, + tags: rule.tags, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); }; export const createNewTermsRule = (rule: NewTermsRule, ruleId = 'rule_testing') => { - if (rule.dataSource.type === 'indexPatterns') { - cy.request({ - method: 'POST', - url: 'api/detection_engine/rules', - body: { - rule_id: ruleId, - risk_score: parseInt(rule.riskScore, 10), - description: rule.description, - interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, - from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, - name: rule.name, - severity: rule.severity.toLocaleLowerCase(), - type: 'new_terms', - index: rule.dataSource.index, - query: rule.customQuery, - new_terms_fields: rule.newTermsFields, - history_window_start: `now-${rule.historyWindowSize.interval}${rule.historyWindowSize.type}`, - enabled: true, - tags: rule.tags, - }, - headers: { 'kbn-xsrf': 'cypress-creds' }, - }); - } + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, + from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'new_terms', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, + query: rule.customQuery, + new_terms_fields: rule.newTermsFields, + history_window_start: `now-${rule.historyWindowSize.interval}${rule.historyWindowSize.type}`, + enabled: true, + tags: rule.tags, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); }; export const createSavedQueryRule = ( @@ -166,7 +164,8 @@ export const createSavedQueryRule = ( severity: rule.severity.toLocaleLowerCase(), type: 'saved_query', from: 'now-50000h', - index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : '', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, saved_id: rule.savedId, language: 'kuery', enabled: false, @@ -184,49 +183,48 @@ export const createSavedQueryRule = ( }); export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') => { - if (rule.dataSource.type === 'indexPatterns') { - cy.request({ - method: 'POST', - url: 'api/detection_engine/rules', - body: { - rule_id: ruleId, - risk_score: parseInt(rule.riskScore, 10), - description: rule.description, - // Default interval is 1m, our tests config overwrite this to 1s - // See https://github.com/elastic/kibana/pull/125396 for details - interval: '10s', - name: rule.name, - severity: rule.severity.toLocaleLowerCase(), - type: 'threat_match', - timeline_id: rule.timeline.templateTimelineId, - timeline_title: rule.timeline.title, - threat_mapping: [ - { - entries: [ - { - field: rule.indicatorMappingField, - type: 'mapping', - value: rule.indicatorIndexField, - }, - ], - }, - ], - threat_query: '*:*', - threat_language: 'kuery', - threat_filters: [], - threat_index: rule.indicatorIndexPattern, - threat_indicator_path: rule.threatIndicatorPath, - from: 'now-50000h', - index: rule.dataSource.index, - query: rule.customQuery || '*:*', - language: 'kuery', - enabled: true, - tags: rule.tags, - }, - headers: { 'kbn-xsrf': 'cypress-creds' }, - failOnStatusCode: false, - }); - } + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + // Default interval is 1m, our tests config overwrite this to 1s + // See https://github.com/elastic/kibana/pull/125396 for details + interval: '10s', + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'threat_match', + timeline_id: rule.timeline.templateTimelineId, + timeline_title: rule.timeline.title, + threat_mapping: [ + { + entries: [ + { + field: rule.indicatorMappingField, + type: 'mapping', + value: rule.indicatorIndexField, + }, + ], + }, + ], + threat_query: '*:*', + threat_language: 'kuery', + threat_filters: [], + threat_index: rule.indicatorIndexPattern, + threat_indicator_path: rule.threatIndicatorPath, + from: 'now-50000h', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, + query: rule.customQuery || '*:*', + language: 'kuery', + enabled: true, + tags: rule.tags, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); }; export const createCustomRuleEnabled = ( diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/saved_queries.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/saved_queries.ts index 8c4aea51ec04..08867cf55d4c 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/saved_queries.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/saved_queries.ts @@ -33,3 +33,20 @@ export const createSavedQuery = ( }, headers: { 'kbn-xsrf': 'cypress-creds' }, }); + +export const deleteSavedQueries = () => { + const kibanaIndexUrl = `${Cypress.env('ELASTICSEARCH_URL')}/.kibana_\*`; + cy.request('POST', `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed`, { + query: { + bool: { + filter: [ + { + match: { + type: 'query', + }, + }, + ], + }, + }, + }); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts index 23f04d0fd3c2..cd9525e95b0b 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common.ts @@ -179,23 +179,6 @@ export const deleteCases = () => { }); }; -export const deleteSavedQueries = () => { - const kibanaIndexUrl = `${Cypress.env('ELASTICSEARCH_URL')}/.kibana_\*`; - cy.request('POST', `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed`, { - query: { - bool: { - filter: [ - { - match: { - type: 'query', - }, - }, - ], - }, - }, - }); -}; - export const postDataView = (dataSource: string) => { cy.request({ method: 'POST', diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index 75668b49aa20..0bd06911acf7 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -158,6 +158,9 @@ export const goBackToAllRulesTable = () => { export const getDetails = (title: string | RegExp) => cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION); +export const assertDetailsNotExist = (title: string | RegExp) => + cy.get(DETAILS_TITLE).contains(title).should('not.exist'); + export const hasIndexPatterns = (indexPatterns: string) => { cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns); diff --git a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts index 5b3ae403f4a0..0000a84f2668 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts @@ -30,6 +30,7 @@ import { APPLY_TIMELINE_RULE_BULK_MENU_ITEM, RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX, RULES_BULK_EDIT_OVERWRITE_INDEX_PATTERNS_CHECKBOX, + RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX, RULES_BULK_EDIT_TIMELINE_TEMPLATES_SELECTOR, UPDATE_SCHEDULE_MENU_ITEM, UPDATE_SCHEDULE_INTERVAL_INPUT, @@ -159,6 +160,14 @@ export const checkOverwriteIndexPatternsCheckbox = () => { .should('be.checked'); }; +export const checkOverwriteDataViewCheckbox = () => { + cy.get(RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX) + .should('have.text', 'Apply changes to rules configured with data views') + .click() + .get('input') + .should('be.checked'); +}; + export const selectTimelineTemplate = (timelineTitle: string) => { cy.get(RULES_BULK_EDIT_TIMELINE_TEMPLATES_SELECTOR).click(); cy.get(TIMELINE_SEARCHBOX).type(`${timelineTitle}{enter}`).should('not.exist'); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx index c12643a30f94..b6a3995534d1 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx @@ -5,16 +5,24 @@ * 2.0. */ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { shallow } from 'enzyme'; import React from 'react'; import type { DraggableStateSnapshot, DraggingStyle } from 'react-beautiful-dnd'; -import { waitFor } from '@testing-library/react'; + import '../../mock/match_media'; +import { TimelineId } from '../../../../common/types'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; import { mockDataProviders } from '../../../timelines/components/timeline/data_providers/mock/mock_data_providers'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/components/row_renderers_browser/constants'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; -import { ConditionalPortal, DraggableWrapper, getStyle } from './draggable_wrapper'; +import { + ConditionalPortal, + disableHoverActions, + DraggableWrapper, + getStyle, +} from './draggable_wrapper'; import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../../lib/kibana'); @@ -27,6 +35,26 @@ jest.mock('@elastic/eui', () => { }; }); +const timelineIdsWithHoverActions = [ + undefined, + TimelineId.active, + TimelineId.alternateTest, + TimelineId.casePage, + TimelineId.detectionsPage, + TimelineId.detectionsRulesDetailsPage, + TimelineId.hostsPageEvents, + TimelineId.hostsPageSessions, + TimelineId.kubernetesPageSessions, + TimelineId.networkPageEvents, + TimelineId.test, + TimelineId.usersPageEvents, +]; + +const timelineIdsNoHoverActions = [ + TimelineId.rulePreview, + ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, +]; + describe('DraggableWrapper', () => { const dataProvider = mockDataProviders[0]; const message = 'draggable wrapper content'; @@ -36,6 +64,15 @@ describe('DraggableWrapper', () => { jest.useFakeTimers(); }); + afterEach(() => { + const portal = document.querySelector('[data-euiportal="true"]'); + if (portal != null) { + portal.innerHTML = ''; + } + + jest.useRealTimers(); + }); + describe('rendering', () => { test('it renders against the snapshot', () => { const wrapper = shallow( @@ -103,6 +140,56 @@ describe('DraggableWrapper', () => { expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(true); }); }); + + timelineIdsWithHoverActions.forEach((timelineId) => { + test(`it renders hover actions (by default) when 'isDraggable' is false and timelineId is '${timelineId}'`, async () => { + const isDraggable = false; + + const { container } = render( + + + message} + timelineId={timelineId} + /> + + + ); + + fireEvent.mouseEnter(container.querySelector('[data-test-subj="withHoverActionsButton"]')!); + + await waitFor(() => { + expect(screen.getByTestId('hover-actions-copy-button')).toBeInTheDocument(); + }); + }); + }); + + timelineIdsNoHoverActions.forEach((timelineId) => { + test(`it does NOT render hover actions when 'isDraggable' is false and timelineId is '${timelineId}'`, async () => { + const isDraggable = false; + + const { container } = render( + + + message} + timelineId={timelineId} + /> + + + ); + + fireEvent.mouseEnter(container.querySelector('[data-test-subj="withHoverActionsButton"]')!); + + await waitFor(() => { + expect(screen.queryByTestId('hover-actions-copy-button')).not.toBeInTheDocument(); + }); + }); + }); }); describe('text truncation styling', () => { @@ -192,4 +279,18 @@ describe('ConditionalPortal', () => { expect(getStyle(style, snapshot)).toHaveProperty('transitionDuration', '0.00000001s'); }); }); + + describe('disableHoverActions', () => { + timelineIdsNoHoverActions.forEach((timelineId) => + test(`it returns true when timelineId is ${timelineId}`, () => { + expect(disableHoverActions(timelineId)).toBe(true); + }) + ); + + timelineIdsWithHoverActions.forEach((timelineId) => + test(`it returns false when timelineId is ${timelineId}`, () => { + expect(disableHoverActions(timelineId)).toBe(false); + }) + ); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index f972bcf463b5..887f1635c08c 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -18,6 +18,7 @@ import { Draggable, Droppable } from 'react-beautiful-dnd'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { TimelineId } from '../../../../common/types'; import { dragAndDropActions } from '../../store/drag_and_drop'; import type { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/components/row_renderers_browser/constants'; @@ -108,6 +109,9 @@ interface Props { onFilterAdded?: () => void; } +export const disableHoverActions = (timelineId: string | undefined): boolean => + [TimelineId.rulePreview, ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID].includes(timelineId ?? ''); + /** * Wraps a draggable component to handle registration / unregistration of the * data provider associated with the item being dropped @@ -370,7 +374,7 @@ const DraggableWrapperComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx index 2058cd4a7c61..0b13363a653a 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx @@ -14,6 +14,12 @@ import type { EventsQueryTabBodyComponentProps } from './events_query_tab_body'; import { EventsQueryTabBody, ALERTS_EVENTS_HISTOGRAM_ID } from './events_query_tab_body'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; import * as tGridActions from '@kbn/timelines-plugin/public/store/t_grid/actions'; +import { licenseService } from '../../hooks/use_license'; + +const mockGetDefaultControlColumn = jest.fn(); +jest.mock('../../../timelines/components/timeline/body/control_columns', () => ({ + getDefaultControlColumn: (props: number) => mockGetDefaultControlColumn(props), +})); jest.mock('../../lib/kibana', () => { const original = jest.requireActual('../../lib/kibana'); @@ -47,6 +53,19 @@ jest.mock('../../containers/use_full_screen', () => ({ }), })); +jest.mock('../../hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + isEnterprise: jest.fn(() => false), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); + describe('EventsQueryTabBody', () => { const commonProps: EventsQueryTabBodyComponentProps = { indexNames: ['test-index'], @@ -69,6 +88,7 @@ describe('EventsQueryTabBody', () => { ); expect(queryByText('MockedStatefulEventsViewer')).toBeInTheDocument(); + expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(4); }); it('renders the matrix histogram when globalFullScreen is false', () => { @@ -147,4 +167,28 @@ describe('EventsQueryTabBody', () => { expect(spy).toHaveBeenCalled(); }); + + it('only have 4 columns on Action bar for non-Enterprise user', () => { + render( + + + + ); + + expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(4); + }); + + it('shows 5 columns on Action bar for Enterprise user', () => { + const licenseServiceMock = licenseService as jest.Mocked; + + licenseServiceMock.isEnterprise.mockReturnValue(true); + + render( + + + + ); + + expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(5); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx index 7b96f3886159..0d0143eab2e3 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx @@ -29,7 +29,7 @@ import { LabelField } from './label_field'; import OsqueryLogo from './osquery_icon/osquery.svg'; import { OsqueryFlyout } from '../../../../../detections/components/osquery/osquery_flyout'; import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; -import { convertECSMappingToObject } from './utils'; +import { OsqueryNotAvailablePrompt } from './not_available_prompt'; const StyledEuiButton = styled(EuiButton)` > span > img { @@ -49,7 +49,12 @@ const OsqueryEditorComponent = ({ }; }>) => { const isEditMode = node != null; - const { osquery } = useKibana().services; + const { + osquery, + application: { + capabilities: { osquery: osqueryPermissions }, + }, + } = useKibana().services; const formMethods = useForm<{ label: string; query: string; @@ -70,7 +75,7 @@ const OsqueryEditorComponent = ({ { query: data.query, label: data.label, - ecs_mapping: convertECSMappingToObject(data.ecs_mapping), + ecs_mapping: data.ecs_mapping, }, (value) => !isEmpty(value) ) @@ -83,6 +88,17 @@ const OsqueryEditorComponent = ({ [onSave] ); + const noOsqueryPermissions = useMemo( + () => + (!osqueryPermissions.runSavedQueries || !osqueryPermissions.readSavedQueries) && + !osqueryPermissions.writeLiveQueries, + [ + osqueryPermissions.readSavedQueries, + osqueryPermissions.runSavedQueries, + osqueryPermissions.writeLiveQueries, + ] + ); + const OsqueryActionForm = useMemo(() => { if (osquery?.LiveQueryField) { const { LiveQueryField } = osquery; @@ -98,6 +114,10 @@ const OsqueryEditorComponent = ({ return null; }, [formMethods, osquery]); + if (noOsqueryPermissions) { + return ; + } + return ( <> diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/not_available_prompt.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/not_available_prompt.tsx new file mode 100644 index 000000000000..90db73811bd4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/not_available_prompt.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCode, EuiEmptyPrompt } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { i18n } from '@kbn/i18n'; + +export const PERMISSION_DENIED = i18n.translate( + 'xpack.securitySolution.markdown.osquery.permissionDenied', + { + defaultMessage: 'Permission denied', + } +); + +export const OsqueryNotAvailablePrompt = () => ( + {PERMISSION_DENIED}} + titleSize="xs" + body={ +

+ {'osquery'}, + }} + /> +

+ } + /> +); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/utils.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/utils.ts deleted file mode 100644 index 77e2f14c5142..000000000000 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/utils.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEmpty, reduce } from 'lodash'; - -export const convertECSMappingToObject = ( - ecsMapping: Array<{ - key: string; - result: { - type: string; - value: string; - }; - }> -) => - reduce( - ecsMapping, - (acc, value) => { - if (!isEmpty(value?.key) && !isEmpty(value.result?.type) && !isEmpty(value.result?.value)) { - acc[value.key] = { - [value.result.type]: value.result.value, - }; - } - - return acc; - }, - {} as Record - ); diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx index a89a61897232..47a1bc4fa651 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx @@ -14,6 +14,7 @@ import { TimelineId } from '@kbn/timelines-plugin/common'; import type { SessionsComponentsProps } from './types'; import type { TimelineModel } from '../../../timelines/store/timeline/model'; import { useGetUserCasesPermissions } from '../../lib/kibana'; +import { licenseService } from '../../hooks/use_license'; jest.mock('../../lib/kibana'); @@ -47,10 +48,28 @@ type Props = Partial & { entityType: EntityType; }; +const mockGetDefaultControlColumn = jest.fn(); +jest.mock('../../../timelines/components/timeline/body/control_columns', () => ({ + getDefaultControlColumn: (props: number) => mockGetDefaultControlColumn(props), +})); + const TEST_PREFIX = 'security_solution:sessions_viewer:sessions_view'; const callFilters = jest.fn(); +jest.mock('../../hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + isEnterprise: jest.fn(() => false), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); + // creating a dummy component for testing TGrid to avoid mocking all the implementation details // but still test if the TGrid will render properly const SessionsViewerTGrid: React.FC = ({ columns, start, end, id, filters, entityType }) => { @@ -144,4 +163,42 @@ describe('SessionsView', () => { ]); }); }); + it('Action tab should have 4 columns for non Enterprise users', async () => { + render( + + + + ); + + await waitFor(() => { + expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(4); + }); + }); + + it('Action tab should have 5 columns for Enterprise or above users', async () => { + const licenseServiceMock = licenseService as jest.Mocked; + + licenseServiceMock.isEnterprise.mockReturnValue(true); + render( + + + + ); + + await waitFor(() => { + expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(5); + }); + }); + + it('Action tab should have 5 columns when accessed via K8S dahsboard', async () => { + render( + + + + ); + + await waitFor(() => { + expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(5); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts index 29da5688357b..a0d23820025b 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts @@ -7,15 +7,19 @@ import type { RenderHookResult, RenderResult } from '@testing-library/react-hooks'; import { act, renderHook } from '@testing-library/react-hooks'; -import { useCurrentUser, useKibana } from '../../../lib/kibana'; -import { useEndpointPrivileges } from './use_endpoint_privileges'; + import { securityMock } from '@kbn/security-plugin/public/mocks'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import { createFleetAuthzMock } from '@kbn/fleet-plugin/common'; + +import type { EndpointPrivileges } from '../../../../../common/endpoint/types'; +import { useCurrentUser, useKibana } from '../../../lib/kibana'; import { licenseService } from '../../../hooks/use_license'; +import { useEndpointPrivileges } from './use_endpoint_privileges'; import { getEndpointPrivilegesInitialStateMock } from './mocks'; -import type { EndpointPrivileges } from '../../../../../common/endpoint/types'; import { getEndpointPrivilegesInitialState } from './utils'; +const useKibanaMock = useKibana as jest.Mocked; jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_license', () => { const licenseServiceInstance = { @@ -47,6 +51,7 @@ describe('When using useEndpointPrivileges hook', () => { }); (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); + useKibanaMock().services.fleet!.authz = createFleetAuthzMock(); licenseServiceMock.isPlatinumPlus.mockReturnValue(true); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap new file mode 100644 index 000000000000..747487203066 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap @@ -0,0 +1,276 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`authenticationLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-3fd0c5d5-f762-4a27-8c56-14eee0223e13", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-bef502be-e5ff-442f-9e3e-229f86ca2afa", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "6f4dbdc7-35b6-4e20-ac53-1272167e3919", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "3fd0c5d5-f762-4a27-8c56-14eee0223e13": Object { + "columnOrder": Array [ + "b41a2958-650b-470a-84c4-c6fd8f0c6d37", + "5417777d-d9d9-4268-9cdc-eb29b873bd65", + ], + "columns": Object { + "5417777d-d9d9-4268-9cdc-eb29b873bd65": Object { + "customLabel": true, + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "event.outcome : \\"success\\"", + }, + "isBucketed": false, + "label": "Success", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + "b41a2958-650b-470a-84c4-c6fd8f0c6d37": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + }, + "incompleteColumns": Object {}, + }, + "bef502be-e5ff-442f-9e3e-229f86ca2afa": Object { + "columnOrder": Array [ + "cded27f7-8ef8-458c-8d9b-70db48ae340d", + "a3bf9dc1-c8d2-42d6-9e60-31892a4c509e", + ], + "columns": Object { + "a3bf9dc1-c8d2-42d6-9e60-31892a4c509e": Object { + "customLabel": true, + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "event.outcome : \\"failure\\"", + }, + "isBucketed": false, + "label": "Failure", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + "cded27f7-8ef8-458c-8d9b-70db48ae340d": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "6f4dbdc7-35b6-4e20-ac53-1272167e3919", + "key": "query", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"must\\":[{\\"term\\":{\\"event.category\\":\\"authentication\\"}}]}}", + }, + "query": Object { + "bool": Object { + "must": Array [ + Object { + "term": Object { + "event.category": "authentication", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.name", + "negate": false, + "params": Object { + "query": "mockHost", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.name": "mockHost", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"query\\": {\\"bool\\": {\\"filter\\": [{\\"bool\\": {\\"should\\": [{\\"exists\\": {\\"field\\": \\"host.name\\"}}],\\"minimum_should_match\\": 1}}]}}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "host.name", + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "layers": Array [ + Object { + "accessors": Array [ + "5417777d-d9d9-4268-9cdc-eb29b873bd65", + ], + "layerId": "3fd0c5d5-f762-4a27-8c56-14eee0223e13", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "xAccessor": "b41a2958-650b-470a-84c4-c6fd8f0c6d37", + "yConfig": Array [ + Object { + "color": "#54b399", + "forAccessor": "5417777d-d9d9-4268-9cdc-eb29b873bd65", + }, + ], + }, + Object { + "accessors": Array [ + "a3bf9dc1-c8d2-42d6-9e60-31892a4c509e", + ], + "layerId": "bef502be-e5ff-442f-9e3e-229f86ca2afa", + "layerType": "data", + "seriesType": "bar_stacked", + "xAccessor": "cded27f7-8ef8-458c-8d9b-70db48ae340d", + "yConfig": Array [ + Object { + "color": "#da8b45", + "forAccessor": "a3bf9dc1-c8d2-42d6-9e60-31892a4c509e", + }, + ], + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "Authentication", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap new file mode 100644 index 000000000000..ac42b228012f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap @@ -0,0 +1,231 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getExternalAlertLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-a3c54471-615f-4ff9-9fda-69b5b2ea3eef", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "723c4653-681b-4105-956e-abef287bf025", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "a04472fc-94a3-4b8d-ae05-9d30ea8fbd6a", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "a3c54471-615f-4ff9-9fda-69b5b2ea3eef": Object { + "columnOrder": Array [ + "42334c6e-98d9-47a2-b4cb-a445abb44c93", + "37bdf546-3c11-4b08-8c5d-e37debc44f1d", + "0a923af2-c880-4aa3-aa93-a0b9c2801f6d", + ], + "columns": Object { + "0a923af2-c880-4aa3-aa93-a0b9c2801f6d": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + "37bdf546-3c11-4b08-8c5d-e37debc44f1d": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "42334c6e-98d9-47a2-b4cb-a445abb44c93": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.dataset", + "operationType": "terms", + "params": Object { + "missingBucket": false, + "orderBy": Object { + "columnId": "0a923af2-c880-4aa3-aa93-a0b9c2801f6d", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 10, + }, + "scale": "ordinal", + "sourceField": "event.dataset", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "a04472fc-94a3-4b8d-ae05-9d30ea8fbd6a", + "key": "event.kind", + "negate": false, + "params": Object { + "query": "alert", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "event.kind": "alert", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.name", + "negate": false, + "params": Object { + "query": "mockHost", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.name": "mockHost", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"query\\": {\\"bool\\": {\\"filter\\": [{\\"bool\\": {\\"should\\": [{\\"exists\\": {\\"field\\": \\"host.name\\"}}],\\"minimum_should_match\\": 1}}]}}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "host.name", + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "layers": Array [ + Object { + "accessors": Array [ + "0a923af2-c880-4aa3-aa93-a0b9c2801f6d", + ], + "layerId": "a3c54471-615f-4ff9-9fda-69b5b2ea3eef", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "splitAccessor": "42334c6e-98d9-47a2-b4cb-a445abb44c93", + "xAccessor": "37bdf546-3c11-4b08-8c5d-e37debc44f1d", + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "External alerts", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.test.ts new file mode 100644 index 000000000000..808789db397e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { authenticationLensAttributes } from './authentication'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'hosts', + tabName: 'events', + }, + ]), +})); + +describe('authenticationLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: authenticationLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts index 199f78d372cd..15d7f029ae61 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { + AUTHENCICATION_FAILURE_CHART_LABEL, + AUTHENCICATION_SUCCESS_CHART_LABEL, +} from '../../translations'; import type { LensAttributes } from '../../types'; export const authenticationLensAttributes: LensAttributes = { @@ -110,7 +114,7 @@ export const authenticationLensAttributes: LensAttributes = { }, }, '5417777d-d9d9-4268-9cdc-eb29b873bd65': { - label: 'Success', + label: AUTHENCICATION_SUCCESS_CHART_LABEL, dataType: 'number', operationType: 'count', isBucketed: false, @@ -143,7 +147,7 @@ export const authenticationLensAttributes: LensAttributes = { }, }, 'a3bf9dc1-c8d2-42d6-9e60-31892a4c509e': { - label: 'Failure', + label: AUTHENCICATION_FAILURE_CHART_LABEL, dataType: 'number', operationType: 'count', isBucketed: false, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.test.ts new file mode 100644 index 000000000000..da890c4d49fe --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { getExternalAlertLensAttributes } from './external_alert'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'hosts', + tabName: 'events', + }, + ]), +})); + +describe('getExternalAlertLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getExternalAlertLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts index e24db03b3302..a815d442b043 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { COUNT, TOP_VALUE } from '../../translations'; import type { GetLensAttributes, LensAttributes } from '../../types'; export const getExternalAlertLensAttributes: GetLensAttributes = ( @@ -86,7 +87,7 @@ export const getExternalAlertLensAttributes: GetLensAttributes = ( }, }, '0a923af2-c880-4aa3-aa93-a0b9c2801f6d': { - label: 'Count of records', + label: COUNT, dataType: 'number', operationType: 'count', isBucketed: false, @@ -94,7 +95,7 @@ export const getExternalAlertLensAttributes: GetLensAttributes = ( sourceField: '___records___', }, '42334c6e-98d9-47a2-b4cb-a445abb44c93': { - label: `Top values of ${stackByField}`, // could be event.category + label: TOP_VALUE(`${stackByField}`), // could be event.category dataType: 'string', operationType: 'terms', scale: 'ordinal', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/event.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/event.test.ts.snap new file mode 100644 index 000000000000..7f3fdb6c6610 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/event.test.ts.snap @@ -0,0 +1,205 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getEventsHistogramLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "0039eb0c-9a1a-4687-ae54-0f4e239bec75": Object { + "columnOrder": Array [ + "34919782-4546-43a5-b668-06ac934d3acd", + "aac9d7d0-13a3-480a-892b-08207a787926", + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "columns": Object { + "34919782-4546-43a5-b668-06ac934d3acd": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.dataset", + "operationType": "terms", + "params": Object { + "missingBucket": false, + "orderBy": Object { + "columnId": "e09e0380-0740-4105-becc-0a4ca12e3944", + "type": "column", + }, + "orderDirection": "asc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 10, + }, + "scale": "ordinal", + "sourceField": "event.dataset", + }, + "aac9d7d0-13a3-480a-892b-08207a787926": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "e09e0380-0740-4105-becc-0a4ca12e3944": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.name", + "negate": false, + "params": Object { + "query": "mockHost", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.name": "mockHost", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"query\\": {\\"bool\\": {\\"filter\\": [{\\"bool\\": {\\"should\\": [{\\"exists\\": {\\"field\\": \\"host.name\\"}}],\\"minimum_should_match\\": 1}}]}}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "host.name", + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "layers": Array [ + Object { + "accessors": Array [ + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "layerId": "0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "splitAccessor": "34919782-4546-43a5-b668-06ac934d3acd", + "xAccessor": "aac9d7d0-13a3-480a-892b-08207a787926", + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "Host - events", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_host_area.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_host_area.test.ts.snap new file mode 100644 index 000000000000..c5e4e272ec9c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_host_area.test.ts.snap @@ -0,0 +1,196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiHostAreaLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-416b6fad-1923-4f6a-a2df-b223bb287e30", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "416b6fad-1923-4f6a-a2df-b223bb287e30": Object { + "columnOrder": Array [ + "5eea817b-67b7-4268-8ecb-7688d1094721", + "b00c65ea-32be-4163-bfc8-f795b1ef9d06", + ], + "columns": Object { + "5eea817b-67b7-4268-8ecb-7688d1094721": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "b00c65ea-32be-4163-bfc8-f795b1ef9d06": Object { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": "Unique count of host.name", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "host.name", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.name", + "negate": false, + "params": Object { + "query": "mockHost", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.name": "mockHost", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"query\\": {\\"bool\\": {\\"filter\\": [{\\"bool\\": {\\"should\\": [{\\"exists\\": {\\"field\\": \\"host.name\\"}}],\\"minimum_should_match\\": 1}}]}}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "host.name", + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": false, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "labelsOrientation": Object { + "x": 0, + "yLeft": 0, + "yRight": 0, + }, + "layers": Array [ + Object { + "accessors": Array [ + "b00c65ea-32be-4163-bfc8-f795b1ef9d06", + ], + "layerId": "416b6fad-1923-4f6a-a2df-b223bb287e30", + "layerType": "data", + "seriesType": "area", + "xAccessor": "5eea817b-67b7-4268-8ecb-7688d1094721", + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "area", + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "[Host] Hosts - area", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_host_metric.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_host_metric.test.ts.snap new file mode 100644 index 000000000000..3669de2d3010 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_host_metric.test.ts.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiHostMetricLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-416b6fad-1923-4f6a-a2df-b223bb287e30", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "416b6fad-1923-4f6a-a2df-b223bb287e30": Object { + "columnOrder": Array [ + "b00c65ea-32be-4163-bfc8-f795b1ef9d06", + ], + "columns": Object { + "b00c65ea-32be-4163-bfc8-f795b1ef9d06": Object { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": " ", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "host.name", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.name", + "negate": false, + "params": Object { + "query": "mockHost", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.name": "mockHost", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"query\\": {\\"bool\\": {\\"filter\\": [{\\"bool\\": {\\"should\\": [{\\"exists\\": {\\"field\\": \\"host.name\\"}}],\\"minimum_should_match\\": 1}}]}}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "host.name", + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "b00c65ea-32be-4163-bfc8-f795b1ef9d06", + "layerId": "416b6fad-1923-4f6a-a2df-b223bb287e30", + "layerType": "data", + }, + }, + "title": "[Host] Hosts - metric", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_area.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_area.test.ts.snap new file mode 100644 index 000000000000..acaf78556269 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_area.test.ts.snap @@ -0,0 +1,255 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUniqueIpsAreaLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-8be0156b-d423-4a39-adf1-f54d4c9f2e69", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-ca05ecdb-0fa4-49a8-9305-b23d91012a46", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "8be0156b-d423-4a39-adf1-f54d4c9f2e69": Object { + "columnOrder": Array [ + "a0cb6400-f708-46c3-ad96-24788f12dae4", + "d9a6eb6b-8b78-439e-98e7-a718f8ffbebe", + ], + "columns": Object { + "a0cb6400-f708-46c3-ad96-24788f12dae4": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "d9a6eb6b-8b78-439e-98e7-a718f8ffbebe": Object { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": "Src.", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "source.ip", + }, + }, + "incompleteColumns": Object {}, + }, + "ca05ecdb-0fa4-49a8-9305-b23d91012a46": Object { + "columnOrder": Array [ + "f95e74e6-99dd-4b11-8faf-439b4d959df9", + "e7052671-fb9e-481f-8df3-7724c98cfc6f", + ], + "columns": Object { + "e7052671-fb9e-481f-8df3-7724c98cfc6f": Object { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": "Dest.", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "destination.ip", + }, + "f95e74e6-99dd-4b11-8faf-439b4d959df9": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.name", + "negate": false, + "params": Object { + "query": "mockHost", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.name": "mockHost", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"query\\": {\\"bool\\": {\\"filter\\": [{\\"bool\\": {\\"should\\": [{\\"exists\\": {\\"field\\": \\"host.name\\"}}],\\"minimum_should_match\\": 1}}]}}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "host.name", + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "labelsOrientation": Object { + "x": 0, + "yLeft": 0, + "yRight": 0, + }, + "layers": Array [ + Object { + "accessors": Array [ + "d9a6eb6b-8b78-439e-98e7-a718f8ffbebe", + ], + "layerId": "8be0156b-d423-4a39-adf1-f54d4c9f2e69", + "layerType": "data", + "seriesType": "area", + "xAccessor": "a0cb6400-f708-46c3-ad96-24788f12dae4", + "yConfig": Array [ + Object { + "color": "#d36186", + "forAccessor": "d9a6eb6b-8b78-439e-98e7-a718f8ffbebe", + }, + ], + }, + Object { + "accessors": Array [ + "e7052671-fb9e-481f-8df3-7724c98cfc6f", + ], + "layerId": "ca05ecdb-0fa4-49a8-9305-b23d91012a46", + "layerType": "data", + "seriesType": "area", + "xAccessor": "f95e74e6-99dd-4b11-8faf-439b4d959df9", + "yConfig": Array [ + Object { + "color": "#9170b8", + "forAccessor": "e7052671-fb9e-481f-8df3-7724c98cfc6f", + }, + ], + }, + ], + "legend": Object { + "isVisible": false, + "position": "right", + "showSingleSeries": false, + }, + "preferredSeriesType": "area", + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "[Host] Unique IPs - area", + "type": "lens", + "updated_at": "2022-02-09T17:44:03.359Z", + "version": "WzI5MTI5OSwzXQ==", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_bar.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_bar.test.ts.snap new file mode 100644 index 000000000000..9f702ecb0641 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_bar.test.ts.snap @@ -0,0 +1,265 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUniqueIpsBarLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-8be0156b-d423-4a39-adf1-f54d4c9f2e69", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-ec84ba70-2adb-4647-8ef0-8ad91a0e6d4e", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "8be0156b-d423-4a39-adf1-f54d4c9f2e69": Object { + "columnOrder": Array [ + "f8bfa719-5c1c-4bf2-896e-c318d77fc08e", + "32f66676-f4e1-48fd-b7f8-d4de38318601", + ], + "columns": Object { + "32f66676-f4e1-48fd-b7f8-d4de38318601": Object { + "dataType": "number", + "isBucketed": false, + "label": "Unique count of source.ip", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "source.ip", + }, + "f8bfa719-5c1c-4bf2-896e-c318d77fc08e": Object { + "dataType": "string", + "isBucketed": true, + "label": "Filters", + "operationType": "filters", + "params": Object { + "filters": Array [ + Object { + "input": Object { + "language": "kuery", + "query": "", + }, + "label": "Src.", + }, + ], + }, + "scale": "ordinal", + }, + }, + "incompleteColumns": Object {}, + }, + "ec84ba70-2adb-4647-8ef0-8ad91a0e6d4e": Object { + "columnOrder": Array [ + "c72aad6a-fc9c-43dc-9194-e13ca3ee8aff", + "b7e59b08-96e6-40d1-84fd-e97b977d1c47", + ], + "columns": Object { + "b7e59b08-96e6-40d1-84fd-e97b977d1c47": Object { + "dataType": "number", + "isBucketed": false, + "label": "Unique count of destination.ip", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "destination.ip", + }, + "c72aad6a-fc9c-43dc-9194-e13ca3ee8aff": Object { + "customLabel": true, + "dataType": "string", + "isBucketed": true, + "label": "Dest.", + "operationType": "filters", + "params": Object { + "filters": Array [ + Object { + "input": Object { + "language": "kuery", + "query": "", + }, + "label": "Dest.", + }, + ], + }, + "scale": "ordinal", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.name", + "negate": false, + "params": Object { + "query": "mockHost", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.name": "mockHost", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"query\\": {\\"bool\\": {\\"filter\\": [{\\"bool\\": {\\"should\\": [{\\"exists\\": {\\"field\\": \\"host.name\\"}}],\\"minimum_should_match\\": 1}}]}}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "host.name", + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "labelsOrientation": Object { + "x": 0, + "yLeft": 0, + "yRight": 0, + }, + "layers": Array [ + Object { + "accessors": Array [ + "32f66676-f4e1-48fd-b7f8-d4de38318601", + ], + "layerId": "8be0156b-d423-4a39-adf1-f54d4c9f2e69", + "layerType": "data", + "seriesType": "bar_horizontal_stacked", + "xAccessor": "f8bfa719-5c1c-4bf2-896e-c318d77fc08e", + "yConfig": Array [ + Object { + "color": "#d36186", + "forAccessor": "32f66676-f4e1-48fd-b7f8-d4de38318601", + }, + ], + }, + Object { + "accessors": Array [ + "b7e59b08-96e6-40d1-84fd-e97b977d1c47", + ], + "layerId": "ec84ba70-2adb-4647-8ef0-8ad91a0e6d4e", + "layerType": "data", + "seriesType": "bar_horizontal_stacked", + "xAccessor": "c72aad6a-fc9c-43dc-9194-e13ca3ee8aff", + "yConfig": Array [ + Object { + "color": "#9170b8", + "forAccessor": "b7e59b08-96e6-40d1-84fd-e97b977d1c47", + }, + ], + }, + ], + "legend": Object { + "isVisible": false, + "position": "right", + "showSingleSeries": false, + }, + "preferredSeriesType": "bar_horizontal_stacked", + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "[Host] Unique IPs - bar", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_destination_metric.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_destination_metric.test.ts.snap new file mode 100644 index 000000000000..ebeb85e27a44 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_destination_metric.test.ts.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUniqueIpsDestinationMetricLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-8be0156b-d423-4a39-adf1-f54d4c9f2e69", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "8be0156b-d423-4a39-adf1-f54d4c9f2e69": Object { + "columnOrder": Array [ + "d9a6eb6b-8b78-439e-98e7-a718f8ffbebe", + ], + "columns": Object { + "d9a6eb6b-8b78-439e-98e7-a718f8ffbebe": Object { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": " ", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "destination.ip", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.name", + "negate": false, + "params": Object { + "query": "mockHost", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.name": "mockHost", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"query\\": {\\"bool\\": {\\"filter\\": [{\\"bool\\": {\\"should\\": [{\\"exists\\": {\\"field\\": \\"host.name\\"}}],\\"minimum_should_match\\": 1}}]}}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "host.name", + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "d9a6eb6b-8b78-439e-98e7-a718f8ffbebe", + "layerId": "8be0156b-d423-4a39-adf1-f54d4c9f2e69", + "layerType": "data", + }, + }, + "title": "[Host] Unique IPs - destination metric", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_source_metric.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_source_metric.test.ts.snap new file mode 100644 index 000000000000..f8ec7bb8c70d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/__snapshots__/kpi_unique_ips_source_metric.test.ts.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUniqueIpsSourceMetricLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-8be0156b-d423-4a39-adf1-f54d4c9f2e69", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "8be0156b-d423-4a39-adf1-f54d4c9f2e69": Object { + "columnOrder": Array [ + "d9a6eb6b-8b78-439e-98e7-a718f8ffbebe", + ], + "columns": Object { + "d9a6eb6b-8b78-439e-98e7-a718f8ffbebe": Object { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": " ", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "source.ip", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.name", + "negate": false, + "params": Object { + "query": "mockHost", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.name": "mockHost", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"query\\": {\\"bool\\": {\\"filter\\": [{\\"bool\\": {\\"should\\": [{\\"exists\\": {\\"field\\": \\"host.name\\"}}],\\"minimum_should_match\\": 1}}]}}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "host.name", + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "d9a6eb6b-8b78-439e-98e7-a718f8ffbebe", + "layerId": "8be0156b-d423-4a39-adf1-f54d4c9f2e69", + "layerType": "data", + }, + }, + "title": "[Host] Unique IPs - source metric", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/event.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/event.test.ts new file mode 100644 index 000000000000..107b63716f40 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/event.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { getEventsHistogramLensAttributes } from './events'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'hosts', + tabName: 'events', + }, + ]), +})); + +describe('getEventsHistogramLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getEventsHistogramLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_area.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_area.test.ts new file mode 100644 index 000000000000..524844487294 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_area.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiHostAreaLensAttributes } from './kpi_host_area'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'hosts', + tabName: 'events', + }, + ]), +})); + +describe('kpiHostAreaLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiHostAreaLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_area.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_area.ts index f4486b77390b..64f62133e940 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_area.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_area.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { UNIQUE_COUNT } from '../../translations'; import type { LensAttributes } from '../../types'; export const kpiHostAreaLensAttributes: LensAttributes = { @@ -32,7 +33,7 @@ export const kpiHostAreaLensAttributes: LensAttributes = { customLabel: true, dataType: 'number', isBucketed: false, - label: ' ', + label: UNIQUE_COUNT('host.name'), operationType: 'unique_count', scale: 'ratio', sourceField: 'host.name', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_metric.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_metric.test.ts new file mode 100644 index 000000000000..06a884c15e4d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_metric.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiHostMetricLensAttributes } from './kpi_host_metric'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'hosts', + tabName: 'events', + }, + ]), +})); + +describe('kpiHostMetricLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiHostMetricLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_metric.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_metric.ts index c5fcae45df0f..00ab0239acb4 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_metric.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_host_metric.ts @@ -40,7 +40,7 @@ export const kpiHostMetricLensAttributes: LensAttributes = { }, }, title: '[Host] Hosts - metric', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', references: [ { id: '{dataViewId}', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_area.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_area.test.ts new file mode 100644 index 000000000000..63f50b141b5b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_area.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUniqueIpsAreaLensAttributes } from './kpi_unique_ips_area'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'hosts', + tabName: 'events', + }, + ]), +})); + +describe('kpiUniqueIpsAreaLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUniqueIpsAreaLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_bar.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_bar.test.ts new file mode 100644 index 000000000000..f04f0de2b8be --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_bar.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUniqueIpsBarLensAttributes } from './kpi_unique_ips_bar'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'hosts', + tabName: 'events', + }, + ]), +})); + +describe('kpiUniqueIpsBarLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUniqueIpsBarLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_bar.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_bar.ts index b55fcb64ba49..cf7dbf21913b 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_bar.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_bar.ts @@ -6,7 +6,7 @@ */ import type { LensAttributes } from '../../types'; -import { SOURCE_CHART_LABEL, DESTINATION_CHART_LABEL } from '../../translations'; +import { SOURCE_CHART_LABEL, DESTINATION_CHART_LABEL, UNIQUE_COUNT } from '../../translations'; export const kpiUniqueIpsBarLensAttributes: LensAttributes = { description: '', @@ -23,7 +23,7 @@ export const kpiUniqueIpsBarLensAttributes: LensAttributes = { '32f66676-f4e1-48fd-b7f8-d4de38318601': { dataType: 'number', isBucketed: false, - label: 'Unique count of source.ip', + label: UNIQUE_COUNT('source.ip'), operationType: 'unique_count', scale: 'ratio', sourceField: 'source.ip', @@ -50,7 +50,7 @@ export const kpiUniqueIpsBarLensAttributes: LensAttributes = { 'b7e59b08-96e6-40d1-84fd-e97b977d1c47': { dataType: 'number', isBucketed: false, - label: 'Unique count of destination.ip', + label: UNIQUE_COUNT('destination.ip'), operationType: 'unique_count', scale: 'ratio', sourceField: 'destination.ip', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_destination_metric.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_destination_metric.test.ts new file mode 100644 index 000000000000..9dd67a2d5ab4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_destination_metric.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUniqueIpsDestinationMetricLensAttributes } from './kpi_unique_ips_destination_metric'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'hosts', + tabName: 'events', + }, + ]), +})); + +describe('kpiUniqueIpsDestinationMetricLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUniqueIpsDestinationMetricLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_destination_metric.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_destination_metric.ts index ae18c08be800..5c4aa31f6583 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_destination_metric.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_destination_metric.ts @@ -40,7 +40,7 @@ export const kpiUniqueIpsDestinationMetricLensAttributes: LensAttributes = { }, }, title: '[Host] Unique IPs - destination metric', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', references: [ { id: '{dataViewId}', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_source_metric.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_source_metric.test.ts new file mode 100644 index 000000000000..6e69495e63a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_source_metric.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUniqueIpsSourceMetricLensAttributes } from './kpi_unique_ips_source_metric'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'hosts', + tabName: 'events', + }, + ]), +})); + +describe('kpiUniqueIpsSourceMetricLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUniqueIpsSourceMetricLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_source_metric.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_source_metric.ts index 8a0b778975ab..4d308b95d796 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_source_metric.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_source_metric.ts @@ -40,7 +40,7 @@ export const kpiUniqueIpsSourceMetricLensAttributes: LensAttributes = { }, }, title: '[Host] Unique IPs - source metric', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', references: [ { id: '{dataViewId}', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap new file mode 100644 index 000000000000..6e0f9c2bbd51 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap @@ -0,0 +1,245 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dnsTopDomainsLensAttributes should render 1`] = ` +Object { + "description": "Security Solution Network DNS", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-b1c3efc6-c886-4fba-978f-3b6bb5e7948a", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "filter-index-pattern-0", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "b1c3efc6-c886-4fba-978f-3b6bb5e7948a": Object { + "columnOrder": Array [ + "e8842815-2a45-4c74-86de-c19a391e2424", + "d1452b87-0e9e-4fc0-a725-3727a18e0b37", + "2a4d5e20-f570-48e4-b9ab-ff3068919377", + ], + "columns": Object { + "2a4d5e20-f570-48e4-b9ab-ff3068919377": Object { + "dataType": "number", + "isBucketed": false, + "label": "Unique count of dns.question.registered_domain", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "dns.question.registered_domain", + }, + "d1452b87-0e9e-4fc0-a725-3727a18e0b37": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "e8842815-2a45-4c74-86de-c19a391e2424": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of dns.question.name", + "operationType": "terms", + "params": Object { + "missingBucket": false, + "orderBy": Object { + "columnId": "2a4d5e20-f570-48e4-b9ab-ff3068919377", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "size": 6, + }, + "scale": "ordinal", + "sourceField": "dns.question.name", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "indexRefName": "filter-index-pattern-0", + "key": "dns.question.type", + "negate": true, + "params": Object { + "query": "PTR", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "dns.question.type": "PTR", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"source.ip\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"destination.ip\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "source.ip", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "destination.ip", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "labelsOrientation": Object { + "x": 0, + "yLeft": 0, + "yRight": 0, + }, + "layers": Array [ + Object { + "accessors": Array [ + "2a4d5e20-f570-48e4-b9ab-ff3068919377", + ], + "layerId": "b1c3efc6-c886-4fba-978f-3b6bb5e7948a", + "layerType": "data", + "position": "top", + "seriesType": "bar", + "showGridlines": false, + "splitAccessor": "e8842815-2a45-4c74-86de-c19a391e2424", + "xAccessor": "d1452b87-0e9e-4fc0-a725-3727a18e0b37", + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "bar", + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "Top domains by dns.question.registered_domain", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_dns_queries.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_dns_queries.test.ts.snap new file mode 100644 index 000000000000..39f16779abaf --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_dns_queries.test.ts.snap @@ -0,0 +1,189 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiDnsQueriesLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-cea37c70-8f91-43bf-b9fe-72d8c049f6a3", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "196d783b-3779-4c39-898e-6606fe633d05", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "cea37c70-8f91-43bf-b9fe-72d8c049f6a3": Object { + "columnOrder": Array [ + "0374e520-eae0-4ac1-bcfe-37565e7fc9e3", + ], + "columns": Object { + "0374e520-eae0-4ac1-bcfe-37565e7fc9e3": Object { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": "", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "196d783b-3779-4c39-898e-6606fe633d05", + "key": "query", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"dns.question.name\\"}},{\\"term\\":{\\"suricata.eve.dns.type\\":{\\"value\\":\\"query\\"}}},{\\"exists\\":{\\"field\\":\\"zeek.dns.query\\"}}],\\"minimum_should_match\\":1}}", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "dns.question.name", + }, + }, + Object { + "term": Object { + "suricata.eve.dns.type": Object { + "value": "query", + }, + }, + }, + Object { + "exists": Object { + "field": "zeek.dns.query", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"source.ip\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"destination.ip\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "source.ip", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "destination.ip", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "0374e520-eae0-4ac1-bcfe-37565e7fc9e3", + "colorMode": "None", + "layerId": "cea37c70-8f91-43bf-b9fe-72d8c049f6a3", + "layerType": "data", + }, + }, + "title": "[Network] DNS metric", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_network_events.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_network_events.test.ts.snap new file mode 100644 index 000000000000..03bacfac49ad --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_network_events.test.ts.snap @@ -0,0 +1,193 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiNetworkEventsLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-eaadfec7-deaa-4aeb-a403-3b4e516416d2", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "861af17d-be25-45a3-a82d-d6e697b76e51", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "09617767-f732-410e-af53-bebcbd0bf4b9", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "eaadfec7-deaa-4aeb-a403-3b4e516416d2": Object { + "columnOrder": Array [ + "370ebd07-5ce0-4f46-a847-0e363c50d037", + ], + "columns": Object { + "370ebd07-5ce0-4f46-a847-0e363c50d037": Object { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": " ", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "security-solution-default", + "key": "source.ip", + "negate": false, + "type": "exists", + "value": "exists", + }, + "query": Object { + "exists": Object { + "field": "source.ip", + }, + }, + }, + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "security-solution-default", + "key": "destination.ip", + "negate": false, + "type": "exists", + "value": "exists", + }, + "query": Object { + "exists": Object { + "field": "destination.ip", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"source.ip\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"destination.ip\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "source.ip", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "destination.ip", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "370ebd07-5ce0-4f46-a847-0e363c50d037", + "layerId": "eaadfec7-deaa-4aeb-a403-3b4e516416d2", + "layerType": "data", + }, + }, + "title": "[Network] Network events", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_tls_handshakes.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_tls_handshakes.test.ts.snap new file mode 100644 index 000000000000..6e695484fdc0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_tls_handshakes.test.ts.snap @@ -0,0 +1,212 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiTlsHandshakesLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-1f48a633-8eee-45ae-9471-861227e9ca03", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "1f48a633-8eee-45ae-9471-861227e9ca03": Object { + "columnOrder": Array [ + "21052b6b-5504-4084-a2e2-c17f772345cf", + ], + "columns": Object { + "21052b6b-5504-4084-a2e2-c17f772345cf": Object { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": " ", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "32ee22d9-2e77-4aee-8073-87750e92c3ee", + "key": "query", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"source.ip\\"}},{\\"exists\\":{\\"field\\":\\"destination.ip\\"}}],\\"minimum_should_match\\":1}}", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "source.ip", + }, + }, + Object { + "exists": Object { + "field": "destination.ip", + }, + }, + ], + }, + }, + }, + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "1e93f984-9374-4755-a198-de57751533c6", + "key": "query", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"tls.version\\"}},{\\"exists\\":{\\"field\\":\\"suricata.eve.tls.version\\"}},{\\"exists\\":{\\"field\\":\\"zeek.ssl.version\\"}}],\\"minimum_should_match\\":1}}", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "tls.version", + }, + }, + Object { + "exists": Object { + "field": "suricata.eve.tls.version", + }, + }, + Object { + "exists": Object { + "field": "zeek.ssl.version", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"source.ip\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"destination.ip\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "source.ip", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "destination.ip", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "21052b6b-5504-4084-a2e2-c17f772345cf", + "layerId": "1f48a633-8eee-45ae-9471-861227e9ca03", + "layerType": "data", + }, + }, + "title": "[Network] TLS handshakes", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_flow_ids.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_flow_ids.test.ts.snap new file mode 100644 index 000000000000..1e3f1f63c40c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_flow_ids.test.ts.snap @@ -0,0 +1,176 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUniqueFlowIdsLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-5d46d48f-6ce8-46be-a797-17ad50642564", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "5d46d48f-6ce8-46be-a797-17ad50642564": Object { + "columnOrder": Array [ + "a27f3503-9c73-4fc1-86bb-12461dae4b70", + ], + "columns": Object { + "a27f3503-9c73-4fc1-86bb-12461dae4b70": Object { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": " ", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "network.community_id", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "c01edc8a-90ce-4d49-95f0-76954a034eb2", + "key": "query", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"source.ip\\"}},{\\"exists\\":{\\"field\\":\\"destination.ip\\"}}],\\"minimum_should_match\\":1}}", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "source.ip", + }, + }, + Object { + "exists": Object { + "field": "destination.ip", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"source.ip\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"destination.ip\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "source.ip", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "destination.ip", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "a27f3503-9c73-4fc1-86bb-12461dae4b70", + "layerId": "5d46d48f-6ce8-46be-a797-17ad50642564", + "layerType": "data", + }, + }, + "title": "[Network] Unique flow IDs", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_area.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_area.test.ts.snap new file mode 100644 index 000000000000..2415dcc6c750 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_area.test.ts.snap @@ -0,0 +1,261 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUniquePrivateIpsAreaLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-38aa6532-6bf9-4c8f-b2a6-da8d32f7d0d7", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-72dc4b99-b07d-4dc9-958b-081d259e11fa", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "38aa6532-6bf9-4c8f-b2a6-da8d32f7d0d7": Object { + "columnOrder": Array [ + "662cd5e5-82bf-4325-a703-273f84b97e09", + "5f317308-cfbb-4ee5-bfb9-07653184fabf", + ], + "columns": Object { + "5f317308-cfbb-4ee5-bfb9-07653184fabf": Object { + "customLabel": true, + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "\\"source.ip\\": \\"10.0.0.0/8\\" or \\"source.ip\\": \\"192.168.0.0/16\\" or \\"source.ip\\": \\"172.16.0.0/12\\" or \\"source.ip\\": \\"fd00::/8\\"", + }, + "isBucketed": false, + "label": "Src.", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "source.ip", + }, + "662cd5e5-82bf-4325-a703-273f84b97e09": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + }, + "incompleteColumns": Object {}, + }, + "72dc4b99-b07d-4dc9-958b-081d259e11fa": Object { + "columnOrder": Array [ + "36444b8c-7e10-4069-8298-6c1b46912be2", + "ac1eb80c-ddde-46c4-a90c-400261926762", + ], + "columns": Object { + "36444b8c-7e10-4069-8298-6c1b46912be2": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "ac1eb80c-ddde-46c4-a90c-400261926762": Object { + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "\\"destination.ip\\": \\"10.0.0.0/8\\" or \\"destination.ip\\": \\"192.168.0.0/16\\" or \\"destination.ip\\": \\"172.16.0.0/12\\" or \\"destination.ip\\": \\"fd00::/8\\"", + }, + "isBucketed": false, + "label": "Dest.", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "destination.ip", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"source.ip\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"destination.ip\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "source.ip", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "destination.ip", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "labelsOrientation": Object { + "x": 0, + "yLeft": 0, + "yRight": 0, + }, + "layers": Array [ + Object { + "accessors": Array [ + "5f317308-cfbb-4ee5-bfb9-07653184fabf", + ], + "layerId": "38aa6532-6bf9-4c8f-b2a6-da8d32f7d0d7", + "layerType": "data", + "seriesType": "area", + "xAccessor": "662cd5e5-82bf-4325-a703-273f84b97e09", + "yConfig": Array [ + Object { + "color": "#d36186", + "forAccessor": "5f317308-cfbb-4ee5-bfb9-07653184fabf", + }, + ], + }, + Object { + "accessors": Array [ + "ac1eb80c-ddde-46c4-a90c-400261926762", + ], + "layerId": "72dc4b99-b07d-4dc9-958b-081d259e11fa", + "layerType": "data", + "seriesType": "area", + "xAccessor": "36444b8c-7e10-4069-8298-6c1b46912be2", + "yConfig": Array [ + Object { + "color": "#9170b8", + "forAccessor": "ac1eb80c-ddde-46c4-a90c-400261926762", + }, + ], + }, + ], + "legend": Object { + "isVisible": false, + "position": "right", + "showSingleSeries": false, + }, + "preferredSeriesType": "area", + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "[Network] Unique private IPs - area chart", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_bar.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_bar.test.ts.snap new file mode 100644 index 000000000000..2ea658869183 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_bar.test.ts.snap @@ -0,0 +1,276 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUniquePrivateIpsBarLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-e406bf4f-942b-41ac-b516-edb5cef06ec8", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-38aa6532-6bf9-4c8f-b2a6-da8d32f7d0d7", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "38aa6532-6bf9-4c8f-b2a6-da8d32f7d0d7": Object { + "columnOrder": Array [ + "4607c585-3af3-43b9-804f-e49b27796d79", + "d27e0966-daf9-41f4-9033-230cf1e76dc9", + ], + "columns": Object { + "4607c585-3af3-43b9-804f-e49b27796d79": Object { + "dataType": "string", + "isBucketed": true, + "label": "Filters", + "operationType": "filters", + "params": Object { + "filters": Array [ + Object { + "input": Object { + "language": "kuery", + "query": "", + }, + "label": "Dest.", + }, + ], + }, + "scale": "ordinal", + }, + "d27e0966-daf9-41f4-9033-230cf1e76dc9": Object { + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "\\"destination.ip\\": \\"10.0.0.0/8\\" or \\"destination.ip\\": \\"192.168.0.0/16\\" or \\"destination.ip\\": \\"172.16.0.0/12\\" or \\"destination.ip\\": \\"fd00::/8\\"", + }, + "isBucketed": false, + "label": "Unique count of destination.ip", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "destination.ip", + }, + }, + "incompleteColumns": Object {}, + }, + "e406bf4f-942b-41ac-b516-edb5cef06ec8": Object { + "columnOrder": Array [ + "d9c438c5-f776-4436-9d20-d62dc8c03be8", + "5acd4c9d-dc3b-4b21-9632-e4407944c36d", + ], + "columns": Object { + "5acd4c9d-dc3b-4b21-9632-e4407944c36d": Object { + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "source.ip: \\"10.0.0.0/8\\" or source.ip: \\"192.168.0.0/16\\" or source.ip: \\"172.16.0.0/12\\" or source.ip: \\"fd00::/8\\"", + }, + "isBucketed": false, + "label": "Unique count of source.ip", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "source.ip", + }, + "d9c438c5-f776-4436-9d20-d62dc8c03be8": Object { + "dataType": "string", + "isBucketed": true, + "label": "Filters", + "operationType": "filters", + "params": Object { + "filters": Array [ + Object { + "input": Object { + "language": "kuery", + "query": "", + }, + "label": "Src.", + }, + ], + }, + "scale": "ordinal", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"source.ip\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"destination.ip\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "source.ip", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "destination.ip", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "labelsOrientation": Object { + "x": 0, + "yLeft": 0, + "yRight": 0, + }, + "layers": Array [ + Object { + "accessors": Array [ + "5acd4c9d-dc3b-4b21-9632-e4407944c36d", + ], + "layerId": "e406bf4f-942b-41ac-b516-edb5cef06ec8", + "layerType": "data", + "position": "top", + "seriesType": "bar_horizontal_stacked", + "showGridlines": false, + "xAccessor": "d9c438c5-f776-4436-9d20-d62dc8c03be8", + "yConfig": Array [ + Object { + "color": "#d36186", + "forAccessor": "5acd4c9d-dc3b-4b21-9632-e4407944c36d", + }, + ], + }, + Object { + "accessors": Array [ + "d27e0966-daf9-41f4-9033-230cf1e76dc9", + ], + "layerId": "38aa6532-6bf9-4c8f-b2a6-da8d32f7d0d7", + "layerType": "data", + "seriesType": "bar_horizontal_stacked", + "xAccessor": "4607c585-3af3-43b9-804f-e49b27796d79", + "yConfig": Array [ + Object { + "color": "#9170b8", + "forAccessor": "d27e0966-daf9-41f4-9033-230cf1e76dc9", + }, + ], + }, + ], + "legend": Object { + "isVisible": false, + "position": "right", + "showSingleSeries": false, + }, + "preferredSeriesType": "bar_horizontal_stacked", + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "[Network] Unique private IPs - bar chart", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_destination_metric.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_destination_metric.test.ts.snap new file mode 100644 index 000000000000..37311a980c6b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_destination_metric.test.ts.snap @@ -0,0 +1,149 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUniquePrivateIpsDestinationMetricLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-cea37c70-8f91-43bf-b9fe-72d8c049f6a3", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "cea37c70-8f91-43bf-b9fe-72d8c049f6a3": Object { + "columnOrder": Array [ + "bd17c23e-4f83-4108-8005-2669170d064b", + ], + "columns": Object { + "bd17c23e-4f83-4108-8005-2669170d064b": Object { + "customLabel": true, + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "\\"destination.ip\\": \\"10.0.0.0/8\\" or \\"destination.ip\\": \\"192.168.0.0/16\\" or \\"destination.ip\\": \\"172.16.0.0/12\\" or \\"destination.ip\\": \\"fd00::/8\\"", + }, + "isBucketed": false, + "label": "", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "destination.ip", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"source.ip\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"destination.ip\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "source.ip", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "destination.ip", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "bd17c23e-4f83-4108-8005-2669170d064b", + "layerId": "cea37c70-8f91-43bf-b9fe-72d8c049f6a3", + "layerType": "data", + }, + }, + "title": "[Network] Unique private IPs - destination metric", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_source_metric.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_source_metric.test.ts.snap new file mode 100644 index 000000000000..2f7ba7d2997b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/kpi_unique_private_ips_source_metric.test.ts.snap @@ -0,0 +1,149 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUniquePrivateIpsSourceMetricLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-cea37c70-8f91-43bf-b9fe-72d8c049f6a3", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "cea37c70-8f91-43bf-b9fe-72d8c049f6a3": Object { + "columnOrder": Array [ + "bd17c23e-4f83-4108-8005-2669170d064b", + ], + "columns": Object { + "bd17c23e-4f83-4108-8005-2669170d064b": Object { + "customLabel": true, + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "source.ip: \\"10.0.0.0/8\\" or source.ip: \\"192.168.0.0/16\\" or source.ip: \\"172.16.0.0/12\\" or source.ip: \\"fd00::/8\\"", + }, + "isBucketed": false, + "label": "", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "source.ip", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": "", + "disabled": false, + "key": "bool", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"source.ip\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\": \\"destination.ip\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "source.ip", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "destination.ip", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "bd17c23e-4f83-4108-8005-2669170d064b", + "layerId": "cea37c70-8f91-43bf-b9fe-72d8c049f6a3", + "layerType": "data", + }, + }, + "title": "[Network] Unique private IPs - source metric", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.test.ts new file mode 100644 index 000000000000..a726a44e34e3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { dnsTopDomainsLensAttributes } from './dns_top_domains'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'network', + tabName: 'events', + }, + ]), +})); + +describe('dnsTopDomainsLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: dnsTopDomainsLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts index 07a3badc3b96..ef75bea77c3e 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TOP_VALUE, UNIQUE_COUNT } from '../../translations'; import type { LensAttributes } from '../../types'; /* Exported from Kibana Saved Object */ @@ -104,7 +105,7 @@ export const dnsTopDomainsLensAttributes: LensAttributes = { }, }, '2a4d5e20-f570-48e4-b9ab-ff3068919377': { - label: 'Unique count of dns.question.registered_domain', + label: UNIQUE_COUNT('dns.question.registered_domain'), dataType: 'number', operationType: 'unique_count', scale: 'ratio', @@ -112,7 +113,7 @@ export const dnsTopDomainsLensAttributes: LensAttributes = { isBucketed: false, }, 'e8842815-2a45-4c74-86de-c19a391e2424': { - label: 'Top values of dns.question.name', + label: TOP_VALUE('dns.question.name'), dataType: 'string', operationType: 'terms', scale: 'ordinal', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_dns_queries.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_dns_queries.test.ts new file mode 100644 index 000000000000..b19b5c1f2f1b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_dns_queries.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiDnsQueriesLensAttributes } from './kpi_dns_queries'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'network', + tabName: 'events', + }, + ]), +})); + +describe('kpiDnsQueriesLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiDnsQueriesLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_dns_queries.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_dns_queries.ts index 1d6cddb7f1b6..681cd278214b 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_dns_queries.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_dns_queries.ts @@ -10,7 +10,7 @@ import type { LensAttributes } from '../../types'; export const kpiDnsQueriesLensAttributes: LensAttributes = { title: '[Network] DNS metric', description: '', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', state: { visualization: { layerId: 'cea37c70-8f91-43bf-b9fe-72d8c049f6a3', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_network_events.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_network_events.test.ts new file mode 100644 index 000000000000..4ca26f222021 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_network_events.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiNetworkEventsLensAttributes } from './kpi_network_events'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'network', + tabName: 'events', + }, + ]), +})); + +describe('kpiNetworkEventsLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiNetworkEventsLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_network_events.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_network_events.ts index 013ad35b31ec..534ffeb2024e 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_network_events.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_network_events.ts @@ -10,7 +10,7 @@ import type { LensAttributes } from '../../types'; export const kpiNetworkEventsLensAttributes: LensAttributes = { title: '[Network] Network events', description: '', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', state: { visualization: { layerId: 'eaadfec7-deaa-4aeb-a403-3b4e516416d2', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_tls_handshakes.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_tls_handshakes.test.ts new file mode 100644 index 000000000000..f06f478ca0e2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_tls_handshakes.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiTlsHandshakesLensAttributes } from './kpi_tls_handshakes'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'network', + tabName: 'events', + }, + ]), +})); + +describe('kpiTlsHandshakesLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiTlsHandshakesLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_tls_handshakes.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_tls_handshakes.ts index 343c61dbd2be..367fe6fd40f6 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_tls_handshakes.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_tls_handshakes.ts @@ -9,7 +9,7 @@ import type { LensAttributes } from '../../types'; export const kpiTlsHandshakesLensAttributes: LensAttributes = { title: '[Network] TLS handshakes', description: '', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', state: { visualization: { layerId: '1f48a633-8eee-45ae-9471-861227e9ca03', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_flow_ids.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_flow_ids.test.ts new file mode 100644 index 000000000000..64b9be02a1d1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_flow_ids.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUniqueFlowIdsLensAttributes } from './kpi_unique_flow_ids'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'network', + tabName: 'events', + }, + ]), +})); + +describe('kpiUniqueFlowIdsLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUniqueFlowIdsLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_flow_ids.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_flow_ids.ts index 3646e3c0a70b..5f31645c75ec 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_flow_ids.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_flow_ids.ts @@ -10,7 +10,7 @@ import type { LensAttributes } from '../../types'; export const kpiUniqueFlowIdsLensAttributes: LensAttributes = { title: '[Network] Unique flow IDs', description: '', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', state: { visualization: { layerId: '5d46d48f-6ce8-46be-a797-17ad50642564', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_area.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_area.test.ts new file mode 100644 index 000000000000..8bb98ddaf95c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_area.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUniquePrivateIpsAreaLensAttributes } from './kpi_unique_private_ips_area'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'network', + tabName: 'events', + }, + ]), +})); + +describe('kpiUniquePrivateIpsAreaLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUniquePrivateIpsAreaLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_area.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_area.ts index 2d3792e39937..394bc227e871 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_area.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_area.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { DESTINATION_CHART_LABEL, SOURCE_CHART_LABEL } from '../../translations'; import type { LensAttributes } from '../../types'; export const kpiUniquePrivateIpsAreaLensAttributes: LensAttributes = { @@ -97,7 +98,7 @@ export const kpiUniquePrivateIpsAreaLensAttributes: LensAttributes = { }, }, '5f317308-cfbb-4ee5-bfb9-07653184fabf': { - label: 'Src.', + label: SOURCE_CHART_LABEL, dataType: 'number', operationType: 'unique_count', scale: 'ratio', @@ -131,7 +132,7 @@ export const kpiUniquePrivateIpsAreaLensAttributes: LensAttributes = { }, }, 'ac1eb80c-ddde-46c4-a90c-400261926762': { - label: 'Dest.', + label: DESTINATION_CHART_LABEL, dataType: 'number', operationType: 'unique_count', scale: 'ratio', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_bar.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_bar.test.ts new file mode 100644 index 000000000000..894144d383b5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_bar.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUniquePrivateIpsBarLensAttributes } from './kpi_unique_private_ips_bar'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'network', + tabName: 'events', + }, + ]), +})); + +describe('kpiUniquePrivateIpsBarLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUniquePrivateIpsBarLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_bar.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_bar.ts index bf4ad0e70408..fe4a698aedf5 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_bar.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_bar.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SOURCE_CHART_LABEL, DESTINATION_CHART_LABEL } from '../../translations'; +import { SOURCE_CHART_LABEL, DESTINATION_CHART_LABEL, UNIQUE_COUNT } from '../../translations'; import type { LensAttributes } from '../../types'; export const kpiUniquePrivateIpsBarLensAttributes: LensAttributes = { @@ -90,7 +90,7 @@ export const kpiUniquePrivateIpsBarLensAttributes: LensAttributes = { 'e406bf4f-942b-41ac-b516-edb5cef06ec8': { columns: { '5acd4c9d-dc3b-4b21-9632-e4407944c36d': { - label: SOURCE_CHART_LABEL, + label: UNIQUE_COUNT('source.ip'), dataType: 'number', isBucketed: false, operationType: 'unique_count', @@ -130,7 +130,7 @@ export const kpiUniquePrivateIpsBarLensAttributes: LensAttributes = { '38aa6532-6bf9-4c8f-b2a6-da8d32f7d0d7': { columns: { 'd27e0966-daf9-41f4-9033-230cf1e76dc9': { - label: DESTINATION_CHART_LABEL, + label: UNIQUE_COUNT('destination.ip'), dataType: 'number', isBucketed: false, operationType: 'unique_count', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_destination_metric.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_destination_metric.test.ts new file mode 100644 index 000000000000..7d65e042554b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_destination_metric.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUniquePrivateIpsDestinationMetricLensAttributes } from './kpi_unique_private_ips_destination_metric'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'network', + tabName: 'events', + }, + ]), +})); + +describe('kpiUniquePrivateIpsDestinationMetricLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUniquePrivateIpsDestinationMetricLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_destination_metric.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_destination_metric.ts index a2bccef3b624..6e3d440619e7 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_destination_metric.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_destination_metric.ts @@ -48,7 +48,7 @@ export const kpiUniquePrivateIpsDestinationMetricLensAttributes: LensAttributes }, }, title: '[Network] Unique private IPs - destination metric', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', references: [ { id: '{dataViewId}', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_source_metric.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_source_metric.test.ts new file mode 100644 index 000000000000..88042d866325 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_source_metric.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUniquePrivateIpsSourceMetricLensAttributes } from './kpi_unique_private_ips_source_metric'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'network', + tabName: 'events', + }, + ]), +})); + +describe('kpiUniquePrivateIpsSourceMetricLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUniquePrivateIpsSourceMetricLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_source_metric.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_source_metric.ts index a95745c7b96e..3f1110d70630 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_source_metric.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_source_metric.ts @@ -47,7 +47,7 @@ export const kpiUniquePrivateIpsSourceMetricLensAttributes: LensAttributes = { }, }, title: '[Network] Unique private IPs - source metric', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', references: [ { id: '{dataViewId}', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_total_users_area.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_total_users_area.test.ts.snap new file mode 100644 index 000000000000..11df964f2eca --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_total_users_area.test.ts.snap @@ -0,0 +1,151 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiTotalUsersAreaLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-416b6fad-1923-4f6a-a2df-b223bb287e30", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "416b6fad-1923-4f6a-a2df-b223bb287e30": Object { + "columnOrder": Array [ + "5eea817b-67b7-4268-8ecb-7688d1094721", + "b00c65ea-32be-4163-bfc8-f795b1ef9d06", + ], + "columns": Object { + "5eea817b-67b7-4268-8ecb-7688d1094721": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "b00c65ea-32be-4163-bfc8-f795b1ef9d06": Object { + "customLabel": true, + "dataType": "number", + "isBucketed": false, + "label": "Unique count of user.name", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "user.name", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": false, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "labelsOrientation": Object { + "x": 0, + "yLeft": 0, + "yRight": 0, + }, + "layers": Array [ + Object { + "accessors": Array [ + "b00c65ea-32be-4163-bfc8-f795b1ef9d06", + ], + "layerId": "416b6fad-1923-4f6a-a2df-b223bb287e30", + "layerType": "data", + "seriesType": "area", + "xAccessor": "5eea817b-67b7-4268-8ecb-7688d1094721", + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "preferredSeriesType": "area", + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "[User] Users - area", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_total_users_metric.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_total_users_metric.test.ts.snap new file mode 100644 index 000000000000..b53e1bd24d30 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_total_users_metric.test.ts.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiTotalUsersMetricLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-416b6fad-1923-4f6a-a2df-b223bb287e30", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "416b6fad-1923-4f6a-a2df-b223bb287e30": Object { + "columnOrder": Array [ + "3e51b035-872c-4b44-824b-fe069c222e91", + ], + "columns": Object { + "3e51b035-872c-4b44-824b-fe069c222e91": Object { + "dataType": "number", + "isBucketed": false, + "label": " ", + "operationType": "unique_count", + "scale": "ratio", + "sourceField": "user.name", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "3e51b035-872c-4b44-824b-fe069c222e91", + "layerId": "416b6fad-1923-4f6a-a2df-b223bb287e30", + "layerType": "data", + }, + }, + "title": "[User] Users - metric", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentication_metric_failure.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentication_metric_failure.test.ts.snap new file mode 100644 index 000000000000..37c20b7e8026 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentication_metric_failure.test.ts.snap @@ -0,0 +1,126 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUserAuthenticationsMetricFailureLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4590dafb-4ac7-45aa-8641-47a3ff0b817c", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "4590dafb-4ac7-45aa-8641-47a3ff0b817c": Object { + "columnOrder": Array [ + "0eb97c09-a351-4280-97da-944e4bd30dd7", + ], + "columns": Object { + "0eb97c09-a351-4280-97da-944e4bd30dd7": Object { + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "event.outcome : \\"failure\\" ", + }, + "isBucketed": false, + "label": "", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "indexRefName": "filter-index-pattern-0", + "key": "query", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"term\\":{\\"event.category\\":\\"authentication\\"}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "event.category": "authentication", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "0eb97c09-a351-4280-97da-944e4bd30dd7", + "layerId": "4590dafb-4ac7-45aa-8641-47a3ff0b817c", + "layerType": "data", + }, + }, + "title": "[Host] User authentications - metric failure ", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentications_area.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentications_area.test.ts.snap new file mode 100644 index 000000000000..1954bccfaffb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentications_area.test.ts.snap @@ -0,0 +1,240 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUserAuthenticationsAreaLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "{dataViewId}", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-31213ae3-905b-4e88-b987-0cccb1f3209f", + "type": "{dataViewId}", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4590dafb-4ac7-45aa-8641-47a3ff0b817c", + "type": "{dataViewId}", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "31213ae3-905b-4e88-b987-0cccb1f3209f": Object { + "columnOrder": Array [ + "33a6163d-0c0a-451d-aa38-8ca6010dd5bf", + "2b27c80e-a20d-46f1-8fb2-79626ef4563c", + ], + "columns": Object { + "2b27c80e-a20d-46f1-8fb2-79626ef4563c": Object { + "customLabel": true, + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "event.outcome: \\"failure\\" ", + }, + "isBucketed": false, + "label": "Fail", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + "33a6163d-0c0a-451d-aa38-8ca6010dd5bf": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + }, + "incompleteColumns": Object {}, + }, + "4590dafb-4ac7-45aa-8641-47a3ff0b817c": Object { + "columnOrder": Array [ + "49a42fe6-ebe8-4adb-8eed-1966a5297b7e", + "0eb97c09-a351-4280-97da-944e4bd30dd7", + ], + "columns": Object { + "0eb97c09-a351-4280-97da-944e4bd30dd7": Object { + "customLabel": true, + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "event.outcome : \\"success\\" ", + }, + "isBucketed": false, + "label": "Succ.", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + "49a42fe6-ebe8-4adb-8eed-1966a5297b7e": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "indexRefName": "filter-index-pattern-0", + "key": "query", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"term\\":{\\"event.category\\":\\"authentication\\"}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "event.category": "authentication", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": true, + "yLeft": false, + "yRight": true, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "labelsOrientation": Object { + "x": 0, + "yLeft": 0, + "yRight": 0, + }, + "layers": Array [ + Object { + "accessors": Array [ + "0eb97c09-a351-4280-97da-944e4bd30dd7", + ], + "layerId": "4590dafb-4ac7-45aa-8641-47a3ff0b817c", + "layerType": "data", + "seriesType": "area", + "xAccessor": "49a42fe6-ebe8-4adb-8eed-1966a5297b7e", + "yConfig": Array [ + Object { + "color": "#54b399", + "forAccessor": "0eb97c09-a351-4280-97da-944e4bd30dd7", + }, + ], + }, + Object { + "accessors": Array [ + "2b27c80e-a20d-46f1-8fb2-79626ef4563c", + ], + "layerId": "31213ae3-905b-4e88-b987-0cccb1f3209f", + "layerType": "data", + "seriesType": "area", + "xAccessor": "33a6163d-0c0a-451d-aa38-8ca6010dd5bf", + "yConfig": Array [ + Object { + "color": "#e7664c", + "forAccessor": "2b27c80e-a20d-46f1-8fb2-79626ef4563c", + }, + ], + }, + ], + "legend": Object { + "isVisible": false, + "position": "right", + "showSingleSeries": false, + }, + "preferredSeriesType": "area", + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "[Host] User authentications - area ", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentications_bar.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentications_bar.test.ts.snap new file mode 100644 index 000000000000..5335dca6057a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentications_bar.test.ts.snap @@ -0,0 +1,240 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUserAuthenticationsBarLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-31213ae3-905b-4e88-b987-0cccb1f3209f", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-b9acd453-f476-4467-ad38-203e37b73e55", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "31213ae3-905b-4e88-b987-0cccb1f3209f": Object { + "columnOrder": Array [ + "430e690c-9992-414f-9bce-00812d99a5e7", + "938b445a-a291-4bbc-84fe-4f47b69c20e4", + ], + "columns": Object { + "430e690c-9992-414f-9bce-00812d99a5e7": Object { + "dataType": "string", + "isBucketed": true, + "label": "Filters", + "operationType": "filters", + "params": Object { + "filters": Array [ + Object { + "input": Object { + "language": "kuery", + "query": "event.outcome : \\"success\\" ", + }, + "label": "Succ.", + }, + ], + }, + "scale": "ordinal", + }, + "938b445a-a291-4bbc-84fe-4f47b69c20e4": Object { + "dataType": "number", + "isBucketed": false, + "label": "Succ.", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + "b9acd453-f476-4467-ad38-203e37b73e55": Object { + "columnOrder": Array [ + "e959c351-a3a2-4525-b244-9623f215a8fd", + "c8165fc3-7180-4f1b-8c87-bc3ea04c6df7", + ], + "columns": Object { + "c8165fc3-7180-4f1b-8c87-bc3ea04c6df7": Object { + "dataType": "number", + "isBucketed": false, + "label": "Fail", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + "e959c351-a3a2-4525-b244-9623f215a8fd": Object { + "customLabel": true, + "dataType": "string", + "isBucketed": true, + "label": "Fail", + "operationType": "filters", + "params": Object { + "filters": Array [ + Object { + "input": Object { + "language": "kuery", + "query": "event.outcome:\\"failure\\" ", + }, + "label": "Fail", + }, + ], + }, + "scale": "ordinal", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "indexRefName": "filter-index-pattern-0", + "key": "query", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"term\\":{\\"event.category\\":\\"authentication\\"}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "event.category": "authentication", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "fittingFunction": "None", + "gridlinesVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "labelsOrientation": Object { + "x": 0, + "yLeft": 0, + "yRight": 0, + }, + "layers": Array [ + Object { + "accessors": Array [ + "938b445a-a291-4bbc-84fe-4f47b69c20e4", + ], + "layerId": "31213ae3-905b-4e88-b987-0cccb1f3209f", + "layerType": "data", + "seriesType": "bar_horizontal_stacked", + "xAccessor": "430e690c-9992-414f-9bce-00812d99a5e7", + "yConfig": Array [], + }, + Object { + "accessors": Array [ + "c8165fc3-7180-4f1b-8c87-bc3ea04c6df7", + ], + "layerId": "b9acd453-f476-4467-ad38-203e37b73e55", + "layerType": "data", + "seriesType": "bar_horizontal_stacked", + "xAccessor": "e959c351-a3a2-4525-b244-9623f215a8fd", + "yConfig": Array [ + Object { + "color": "#e7664c", + "forAccessor": "c8165fc3-7180-4f1b-8c87-bc3ea04c6df7", + }, + ], + }, + ], + "legend": Object { + "isVisible": false, + "position": "right", + "showSingleSeries": false, + }, + "preferredSeriesType": "bar_horizontal_stacked", + "tickLabelsVisibilitySettings": Object { + "x": true, + "yLeft": true, + "yRight": true, + }, + "valueLabels": "hide", + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "[Host] User authentications - bar ", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentications_metric_success.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentications_metric_success.test.ts.snap new file mode 100644 index 000000000000..4cadcaf19e91 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/__snapshots__/kpi_user_authentications_metric_success.test.ts.snap @@ -0,0 +1,127 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`kpiUserAuthenticationsMetricSuccessLensAttributes should render 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-current-indexpattern", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4590dafb-4ac7-45aa-8641-47a3ff0b817c", + "type": "index-pattern", + }, + ], + "state": Object { + "datasourceStates": Object { + "indexpattern": Object { + "layers": Object { + "4590dafb-4ac7-45aa-8641-47a3ff0b817c": Object { + "columnOrder": Array [ + "0eb97c09-a351-4280-97da-944e4bd30dd7", + ], + "columns": Object { + "0eb97c09-a351-4280-97da-944e4bd30dd7": Object { + "customLabel": true, + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "event.outcome : \\"success\\" ", + }, + "isBucketed": false, + "label": " ", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "indexRefName": "filter-index-pattern-0", + "key": "query", + "negate": false, + "type": "custom", + "value": "{\\"bool\\":{\\"filter\\":[{\\"term\\":{\\"event.category\\":\\"authentication\\"}}]}}", + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "event.category": "authentication", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "auditbeat-mytest-*", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "auditbeat-mytest-*", + }, + }, + ], + }, + }, + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "accessor": "0eb97c09-a351-4280-97da-944e4bd30dd7", + "layerId": "4590dafb-4ac7-45aa-8641-47a3ff0b817c", + "layerType": "data", + }, + }, + "title": "[Host] User authentications - metric success ", + "visualizationType": "lnsLegacyMetric", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_area.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_area.test.ts new file mode 100644 index 000000000000..43cb5be3a973 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_area.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiTotalUsersAreaLensAttributes } from './kpi_total_users_area'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'users', + tabName: 'events', + }, + ]), +})); + +describe('kpiTotalUsersAreaLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiTotalUsersAreaLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_area.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_area.ts index d958f9304b8a..c97748077a6b 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_area.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_area.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { UNIQUE_COUNT } from '../../translations'; import type { LensAttributes } from '../../types'; export const kpiTotalUsersAreaLensAttributes: LensAttributes = { @@ -32,7 +33,7 @@ export const kpiTotalUsersAreaLensAttributes: LensAttributes = { customLabel: true, dataType: 'number', isBucketed: false, - label: ' ', + label: UNIQUE_COUNT('user.name'), operationType: 'unique_count', scale: 'ratio', sourceField: 'user.name', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric.test.ts new file mode 100644 index 000000000000..0a5ec9891d26 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiTotalUsersMetricLensAttributes } from './kpi_total_users_metric'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'users', + tabName: 'events', + }, + ]), +})); + +describe('kpiTotalUsersMetricLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiTotalUsersMetricLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric.ts index 08e5756337dc..faa6b62e18b6 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric.ts @@ -19,7 +19,7 @@ export const kpiTotalUsersMetricLensAttributes: LensAttributes = { '3e51b035-872c-4b44-824b-fe069c222e91': { dataType: 'number', isBucketed: false, - label: 'Unique count of user.name', + label: ' ', operationType: 'unique_count', scale: 'ratio', sourceField: 'user.name', @@ -39,7 +39,7 @@ export const kpiTotalUsersMetricLensAttributes: LensAttributes = { }, }, title: '[User] Users - metric', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', references: [ { id: '{dataViewId}', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentication_metric_failure.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentication_metric_failure.test.ts new file mode 100644 index 000000000000..e251ccf51c5e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentication_metric_failure.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUserAuthenticationsMetricFailureLensAttributes } from './kpi_user_authentication_metric_failure'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'users', + tabName: 'events', + }, + ]), +})); + +describe('kpiUserAuthenticationsMetricFailureLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUserAuthenticationsMetricFailureLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentication_metric_failure.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentication_metric_failure.ts index e4690f66998c..3f421f8a1c30 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentication_metric_failure.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentication_metric_failure.ts @@ -10,7 +10,7 @@ import type { LensAttributes } from '../../types'; export const kpiUserAuthenticationsMetricFailureLensAttributes: LensAttributes = { title: '[Host] User authentications - metric failure ', description: '', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', state: { visualization: { accessor: '0eb97c09-a351-4280-97da-944e4bd30dd7', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_area.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_area.test.ts new file mode 100644 index 000000000000..b51d3e474a70 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_area.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUserAuthenticationsAreaLensAttributes } from './kpi_user_authentications_area'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'users', + tabName: 'events', + }, + ]), +})); + +describe('kpiUserAuthenticationsAreaLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUserAuthenticationsAreaLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_area.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_area.ts index cf9902bb2413..d96ea21489bb 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_area.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_area.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { FAIL_CHART_LABEL, SUCCESS_CHART_LABEL } from '../../translations'; import type { LensAttributes } from '../../types'; export const kpiUserAuthenticationsAreaLensAttributes: LensAttributes = { @@ -124,7 +125,7 @@ export const kpiUserAuthenticationsAreaLensAttributes: LensAttributes = { query: 'event.outcome: "failure" ', }, isBucketed: false, - label: 'Fail', + label: FAIL_CHART_LABEL, operationType: 'count', scale: 'ratio', sourceField: '___records___', @@ -157,7 +158,7 @@ export const kpiUserAuthenticationsAreaLensAttributes: LensAttributes = { query: 'event.outcome : "success" ', }, isBucketed: false, - label: 'Succ.', + label: SUCCESS_CHART_LABEL, operationType: 'count', scale: 'ratio', sourceField: '___records___', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_bar.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_bar.test.ts new file mode 100644 index 000000000000..41590d330cd4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_bar.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUserAuthenticationsBarLensAttributes } from './kpi_user_authentications_bar'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'users', + tabName: 'events', + }, + ]), +})); + +describe('kpiUserAuthenticationsBarLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUserAuthenticationsBarLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_metric_success.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_metric_success.test.ts new file mode 100644 index 000000000000..570e03325ec3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_metric_success.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../mocks'; + +import { useLensAttributes } from '../../use_lens_attributes'; + +import { kpiUserAuthenticationsMetricSuccessLensAttributes } from './kpi_user_authentications_metric_success'; + +jest.mock('../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + selectedPatterns: ['auditbeat-mytest-*'], + dataViewId: 'security-solution-my-test', + }), +})); + +jest.mock('../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockHost', + pageName: 'users', + tabName: 'events', + }, + ]), +})); + +describe('kpiUserAuthenticationsMetricSuccessLensAttributes', () => { + it('should render', () => { + const { result } = renderHook( + () => + useLensAttributes({ + lensAttributes: kpiUserAuthenticationsMetricSuccessLensAttributes, + stackByField: 'event.dataset', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_metric_success.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_metric_success.ts index 66f30bf2378a..3af6f5734d45 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_metric_success.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/users/kpi_user_authentications_metric_success.ts @@ -10,7 +10,7 @@ import type { LensAttributes } from '../../types'; export const kpiUserAuthenticationsMetricSuccessLensAttributes: LensAttributes = { title: '[Host] User authentications - metric success ', description: '', - visualizationType: 'lnsMetric', + visualizationType: 'lnsLegacyMetric', state: { visualization: { accessor: '0eb97c09-a351-4280-97da-944e4bd30dd7', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx new file mode 100644 index 000000000000..7f4e7e4e6d6e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { cloneDeep } from 'lodash/fp'; + +import { + TestProviders, + mockGlobalState, + SUB_PLUGINS_REDUCER, + kibanaObservable, + createSecuritySolutionStorageMock, +} from '../../mock'; +import type { State } from '../../store'; +import { createStore } from '../../store'; + +export const queryFromSearchBar = { + query: 'host.name: *', + language: 'kql', +}; + +export const filterFromSearchBar = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.id', + params: { + query: '123', + }, + }, + query: { + match_phrase: { + 'host.id': '123', + }, + }, + }, +]; + +export const mockCreateStoreWithQueryFilters = () => { + const { storage } = createSecuritySolutionStorageMock(); + + const state: State = mockGlobalState; + + const myState = cloneDeep(state); + + myState.inputs = { + ...myState.inputs, + global: { + ...myState.inputs.global, + query: queryFromSearchBar, + filters: filterFromSearchBar, + }, + }; + return createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); +}; + +export const wrapper = ({ children }: { children: React.ReactElement }) => ( + {children} +); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts index 9814a98817ef..4dcceb29323b 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/translations.ts @@ -67,9 +67,41 @@ export const SUCCESS_CHART_LABEL = i18n.translate( } ); +export const AUTHENCICATION_SUCCESS_CHART_LABEL = i18n.translate( + 'xpack.securitySolution.visualizationActions.userAuthentications.authentication.successChartLabel', + { + defaultMessage: 'Success', + } +); + export const FAIL_CHART_LABEL = i18n.translate( 'xpack.securitySolution.visualizationActions.userAuthentications.failChartLabel', { defaultMessage: 'Fail', } ); + +export const AUTHENCICATION_FAILURE_CHART_LABEL = i18n.translate( + 'xpack.securitySolution.visualizationActions.userAuthentications.authentication.failureChartLabel', + { + defaultMessage: 'Failure', + } +); + +export const UNIQUE_COUNT = (field: string) => + i18n.translate('xpack.securitySolution.visualizationActions.uniqueCountLabel', { + values: { field }, + + defaultMessage: 'Unique count of {field}', + }); + +export const TOP_VALUE = (field: string) => + i18n.translate('xpack.securitySolution.visualizationActions.topValueLabel', { + values: { field }, + + defaultMessage: 'Top values of {field}', + }); + +export const COUNT = i18n.translate('xpack.securitySolution.visualizationActions.countLabel', { + defaultMessage: 'Count of records', +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx index 7db97ab5526a..76e2f54c62ce 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx @@ -6,21 +6,12 @@ */ import { renderHook } from '@testing-library/react-hooks'; -import React from 'react'; -import { cloneDeep } from 'lodash/fp'; -import { - TestProviders, - mockGlobalState, - SUB_PLUGINS_REDUCER, - kibanaObservable, - createSecuritySolutionStorageMock, -} from '../../mock'; import { getExternalAlertLensAttributes } from './lens_attributes/common/external_alert'; import { useLensAttributes } from './use_lens_attributes'; import { hostNameExistsFilter, getHostDetailsPageFilter, getIndexFilters } from './utils'; -import type { State } from '../../store'; -import { createStore } from '../../store'; + +import { filterFromSearchBar, queryFromSearchBar, wrapper } from './mocks'; jest.mock('../../containers/sourcerer', () => ({ useSourcererDataView: jest.fn().mockReturnValue({ @@ -40,51 +31,7 @@ jest.mock('../../utils/route/use_route_spy', () => ({ })); describe('useLensAttributes', () => { - const state: State = mockGlobalState; - const { storage } = createSecuritySolutionStorageMock(); - const queryFromSearchBar = { - query: 'host.name: *', - language: 'kql', - }; - - const filterFromSearchBar = [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.id', - params: { - query: '123', - }, - }, - query: { - match_phrase: { - 'host.id': '123', - }, - }, - }, - ]; - let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - beforeEach(() => { - const myState = cloneDeep(state); - myState.inputs = { - ...myState.inputs, - global: { - ...myState.inputs.global, - query: queryFromSearchBar, - filters: filterFromSearchBar, - }, - }; - store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - }); - it('should add query', () => { - const wrapper = ({ children }: { children: React.ReactElement }) => ( - {children} - ); const { result } = renderHook( () => useLensAttributes({ @@ -94,13 +41,10 @@ describe('useLensAttributes', () => { { wrapper } ); - expect(result?.current?.state.query).toEqual({ query: 'host.name: *', language: 'kql' }); + expect(result?.current?.state.query).toEqual(queryFromSearchBar); }); it('should add filters', () => { - const wrapper = ({ children }: { children: React.ReactElement }) => ( - {children} - ); const { result } = renderHook( () => useLensAttributes({ @@ -120,9 +64,6 @@ describe('useLensAttributes', () => { }); it('should add data view id to references', () => { - const wrapper = ({ children }: { children: React.ReactElement }) => ( - {children} - ); const { result } = renderHook( () => useLensAttributes({ diff --git a/x-pack/plugins/security_solution/public/common/hooks/translations.ts b/x-pack/plugins/security_solution/public/common/hooks/translations.ts index 1f939282b5c7..54ed3a79d017 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/translations.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/translations.ts @@ -39,10 +39,3 @@ export const EQL_TIME_INTERVAL_NOT_DEFINED = i18n.translate( defaultMessage: 'Time intervals are not defined.', } ); - -export const SAVED_QUERY_LOAD_ERROR_TOAST = i18n.translate( - 'xpack.securitySolution.hooks.useGetSavedQuery.errorToastMessage', - { - defaultMessage: 'Failed to load the saved query', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/throttle_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/throttle_description.tsx index 69110b2e5360..c0b6780ea8bc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/throttle_description.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/throttle_description.tsx @@ -6,10 +6,13 @@ */ import { find } from 'lodash/fp'; -import { THROTTLE_OPTIONS, DEFAULT_THROTTLE_OPTION } from '../throttle_select_field'; +import { + THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING, + DEFAULT_THROTTLE_OPTION, +} from '../throttle_select_field'; export const buildThrottleDescription = (value = DEFAULT_THROTTLE_OPTION.value, title: string) => { - const throttleOption = find(['value', value], THROTTLE_OPTIONS); + const throttleOption = find(['value', value], THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING); return { title, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx index a5316eabb11f..7d7c9534c168 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx @@ -70,6 +70,7 @@ const savedQueryToFieldValue = (savedQuery: SavedQuery): FieldValueQueryBar => ( filters: savedQuery.attributes.filters ?? [], query: savedQuery.attributes.query, saved_id: savedQuery.id, + title: savedQuery.attributes.title, }); export const QueryBarDefineRule = ({ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 23042cd1165e..21bf8125540b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -739,8 +739,8 @@ const StepDefineRuleComponent: FC = ({ <> = ({ 'data-test-subj': 'detectionEngineStepDefineRuleShouldLoadQueryDynamically', euiFieldProps: { disabled: isLoading, + label: formQuery?.title + ? i18n.getSavedQueryCheckboxLabel(formQuery.title) + : undefined, }, }} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx index bc3937013b6a..b23b496eae82 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx @@ -629,12 +629,6 @@ export const schema: FormSchema = { }, shouldLoadQueryDynamically: { type: FIELD_TYPES.CHECKBOX, - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldShouldLoadQueryDynamicallyLabel', - { - defaultMessage: 'Load the saved query dynamically on each rule execution', - } - ), defaultValue: false, }, }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx index b28d06777fd3..4650da2ce603 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx @@ -78,13 +78,22 @@ export const EQL_QUERY_BAR_LABEL = i18n.translate( } ); -export const SAVED_QUERY_CHECKBOX_LABEL = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryCheckboxLabel', +export const SAVED_QUERY_FORM_ROW_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryFormRowLabel', { defaultMessage: 'Saved query', } ); +export const getSavedQueryCheckboxLabel = (savedQueryName: string) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldShouldLoadQueryDynamicallyLabel', + { + defaultMessage: 'Load saved query "{savedQueryName}" dynamically on each rule execution', + values: { savedQueryName }, + } + ); + export const THREAT_MATCH_INDEX_HELPER_TEXT = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index 5cdf89df572c..fe2c5d8fa4ee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -33,7 +33,7 @@ import { Form, UseField, useForm, useFormData } from '../../../../shared_imports import { StepContentWrapper } from '../step_content_wrapper'; import { ThrottleSelectField, - THROTTLE_OPTIONS, + THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING, DEFAULT_THROTTLE_OPTION, } from '../throttle_select_field'; import { RuleActionsField } from '../rule_actions_field'; @@ -62,11 +62,14 @@ const GhostFormField = () => <>; const getThrottleOptions = (throttle?: string | null) => { // Add support for throttle options set by the API - if (throttle && findIndex(['value', throttle], THROTTLE_OPTIONS) < 0) { - return [...THROTTLE_OPTIONS, { value: throttle, text: throttle }]; + if ( + throttle && + findIndex(['value', throttle], THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING) < 0 + ) { + return [...THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING, { value: throttle, text: throttle }]; } - return THROTTLE_OPTIONS; + return THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING; }; const DisplayActionsHeader = () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.tsx index 4bc15d418a38..264e72fafb60 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/throttle_select_field/index.tsx @@ -13,15 +13,19 @@ import { } from '../../../../../common/constants'; import { SelectField } from '../../../../shared_imports'; -export const THROTTLE_OPTIONS = [ - { value: NOTIFICATION_THROTTLE_NO_ACTIONS, text: 'Perform no actions' }, +export const THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS = [ { value: NOTIFICATION_THROTTLE_RULE, text: 'On each rule execution' }, { value: '1h', text: 'Hourly' }, { value: '1d', text: 'Daily' }, { value: '7d', text: 'Weekly' }, ]; -export const DEFAULT_THROTTLE_OPTION = THROTTLE_OPTIONS[0]; +export const THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING = [ + { value: NOTIFICATION_THROTTLE_NO_ACTIONS, text: 'Perform no actions' }, + ...THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS, +]; + +export const DEFAULT_THROTTLE_OPTION = THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING[0]; type ThrottleSelectField = typeof SelectField; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/rule_actions_form.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/rule_actions_form.tsx index 8a9e52c9d552..0ec448a1ab1f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/rule_actions_form.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/rule_actions_form.tsx @@ -12,6 +12,7 @@ import type { RuleAction, ActionTypeRegistryContract, } from '@kbn/triggers-actions-ui-plugin/public'; + import type { FormSchema } from '../../../../../../../shared_imports'; import { useForm, @@ -21,9 +22,12 @@ import { getUseField, Field, } from '../../../../../../../shared_imports'; -import type { BulkActionEditPayload } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { BulkActionEditType } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; -import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../../../common/constants'; +import type { + BulkActionEditPayload, + ThrottleForBulkActions, +} from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { NOTIFICATION_THROTTLE_RULE } from '../../../../../../../../common/constants'; import { BulkEditFormWrapper } from './bulk_edit_form_wrapper'; import { bulkAddRuleActions as i18n } from '../translations'; @@ -32,7 +36,7 @@ import { useKibana } from '../../../../../../../common/lib/kibana'; import { ThrottleSelectField, - THROTTLE_OPTIONS, + THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS, } from '../../../../../../components/rules/throttle_select_field'; import { getAllActionMessageParams } from '../../../helpers'; @@ -42,7 +46,7 @@ import { validateRuleActionsField } from '../../../../../../containers/detection const CommonUseField = getUseField({ component: Field }); export interface RuleActionsFormData { - throttle: string; + throttle: ThrottleForBulkActions; actions: RuleAction[]; overwrite: boolean; } @@ -68,7 +72,7 @@ const getFormSchema = ( }); const defaultFormData: RuleActionsFormData = { - throttle: NOTIFICATION_THROTTLE_NO_ACTIONS, + throttle: NOTIFICATION_THROTTLE_RULE, actions: [], overwrite: false, }; @@ -93,7 +97,7 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction defaultValue: defaultFormData, }); - const [{ overwrite, throttle }] = useFormData({ form, watch: ['overwrite', 'throttle'] }); + const [{ overwrite }] = useFormData({ form, watch: ['overwrite', 'throttle'] }); const handleSubmit = useCallback(async () => { const { data, isValid } = await form.submit(); @@ -121,7 +125,7 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction dataTestSubj: 'bulkEditRulesRuleActionThrottle', hasNoInitialSelection: false, euiFieldProps: { - options: THROTTLE_OPTIONS, + options: THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS, }, }), [] @@ -129,8 +133,6 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction const messageVariables = useMemo(() => getAllActionMessageParams(), []); - const showActionsSelect = throttle !== NOTIFICATION_THROTTLE_NO_ACTIONS; - return ( -
  • - - - - ), - overwriteActionsCheckbox: ( - - - - ), - }} - /> -
  • {i18n.RULE_VARIABLES_DETAIL}
  • @@ -193,18 +171,14 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction /> - {showActionsSelect && ( - <> - - - - )} + + = ({ const [dataViewOptions, setDataViewOptions] = useState<{ [x: string]: DataViewListItem }>({}); // load saved query only if rule type === 'saved_query', as other rule types still can have saved_id property that is not used + // Rule schema allows to save any rule with saved_id property, but it only used for saved_query rule type + // In future we might look in possibility to restrict rule schema (breaking change!) and remove saved_id from the rest of rules through migration const savedQueryId = rule?.type === 'saved_query' ? rule?.saved_id : undefined; const { isSavedQueryLoading, savedQueryBar } = useGetSavedQuery(savedQueryId); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index 1a50d570ea17..b1c7e0033f5f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -69,3 +69,10 @@ export const DELETED_RULE = i18n.translate( defaultMessage: 'Deleted rule', } ); + +export const SAVED_QUERY_LOAD_ERROR_TOAST = i18n.translate( + 'xpack.securitySolution.hooks.useGetSavedQuery.errorToastMessage', + { + defaultMessage: 'Failed to load the saved query', + } +); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_get_saved_query.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/use_get_saved_query.ts similarity index 81% rename from x-pack/plugins/security_solution/public/common/hooks/use_get_saved_query.ts rename to x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/use_get_saved_query.ts index 932fe61e2ce4..4c3c4c6c42fe 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_get_saved_query.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/use_get_saved_query.ts @@ -7,12 +7,13 @@ import { useEffect, useMemo } from 'react'; -import { useSavedQueryServices } from '../utils/saved_query_services'; -import type { DefineStepRule } from '../../detections/pages/detection_engine/rules/types'; +import { useSavedQueryServices } from '../../../../../common/utils/saved_query_services'; +import type { DefineStepRule } from '../types'; + +import { useFetch, REQUEST_NAMES } from '../../../../../common/hooks/use_fetch'; +import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; -import { useFetch, REQUEST_NAMES } from './use_fetch'; import { SAVED_QUERY_LOAD_ERROR_TOAST } from './translations'; -import { useAppToasts } from './use_app_toasts'; export const useGetSavedQuery = (savedQueryId: string | undefined) => { const savedQueryServices = useSavedQueryServices(); diff --git a/x-pack/plugins/security_solution/public/jest.config.js b/x-pack/plugins/security_solution/public/jest.config.js index 5eb349b2c16e..afa3e1b47efd 100644 --- a/x-pack/plugins/security_solution/public/jest.config.js +++ b/x-pack/plugins/security_solution/public/jest.config.js @@ -14,7 +14,17 @@ module.exports = { coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public', coverageReporters: ['text', 'html'], - collectCoverageFrom: ['/x-pack/plugins/security_solution/public/**/*.{ts,tsx}'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/public/**/*.{ts,tsx}', + '!/x-pack/plugins/security_solution/public/*.test.{ts,tsx}', + '!/x-pack/plugins/security_solution/public/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*', + '!/x-pack/plugins/security_solution/public/*mock*.{ts,tsx}', + '!/x-pack/plugins/security_solution/public/*.test.{ts,tsx}', + '!/x-pack/plugins/security_solution/public/*.d.ts', + '!/x-pack/plugins/security_solution/public/*.config.ts', + '!/x-pack/plugins/security_solution/public/index.{js,ts,tsx}', + ], + // See: https://github.com/elastic/kibana/issues/117255, the moduleNameMapper creates mocks to avoid memory leaks from kibana core. moduleNameMapper: { 'core/server$': '/x-pack/plugins/security_solution/server/__mocks__/core.mock.ts', diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx index 9160732e32b3..678033531225 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.test.tsx @@ -12,7 +12,7 @@ import { act, fireEvent, waitFor, waitForElementToBeRemoved } from '@testing-lib import userEvent from '@testing-library/user-event'; import type { ArtifactListPageRenderingSetup } from './mocks'; import { getArtifactListPageRenderingSetup } from './mocks'; -import { getDeferred } from '../mocks'; +import { getDeferred } from '../../mocks/utils'; jest.mock('../../../common/components/user_privileges'); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx index fc687282ccca..0586034d1555 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/artifact_list_page.tsx @@ -11,6 +11,7 @@ import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-t import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import type { EuiFlyoutSize } from '@elastic/eui/src/components/flyout/flyout'; import { useLocation } from 'react-router-dom'; +import { useIsMounted } from '@kbn/securitysolution-hook-utils'; import type { ServerApiError } from '../../../common/types'; import { AdministrationListPage } from '../administration_list_page'; @@ -45,7 +46,6 @@ import { useToasts } from '../../../common/lib/kibana'; import { useMemoizedRouteState } from '../../common/hooks'; import { BackToExternalAppSecondaryButton } from '../back_to_external_app_secondary_button'; import { BackToExternalAppButton } from '../back_to_external_app_button'; -import { useIsMounted } from '../../hooks/use_is_mounted'; type ArtifactEntryCardType = typeof ArtifactEntryCard; @@ -221,7 +221,7 @@ export const ArtifactListPage = memo( ); const handleArtifactDeleteModalOnSuccess = useCallback(() => { - if (isMounted) { + if (isMounted()) { setSelectedItemForDelete(undefined); refetchListData(); } diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_delete_modal.test.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_delete_modal.test.ts index d0fb3e3c59df..57ea165f0b85 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_delete_modal.test.ts +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_delete_modal.test.ts @@ -11,7 +11,7 @@ import type { ArtifactListPageRenderingSetup } from '../mocks'; import { getArtifactListPageRenderingSetup } from '../mocks'; import { act, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { getDeferred } from '../../mocks'; +import { getDeferred } from '../../../mocks/utils'; describe('When displaying the Delete artifact modal in the Artifact List Page', () => { let renderResult: ReturnType; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx index ee3ac4a907ee..5179bb7b76be 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx @@ -19,7 +19,7 @@ import type { trustedAppsAllHttpMocks } from '../../../mocks'; import { useUserPrivileges as _useUserPrivileges } from '../../../../common/components/user_privileges'; import { entriesToConditionEntries } from '../../../../common/utils/exception_list_items/mappers'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { getDeferred } from '../../mocks'; +import { getDeferred } from '../../../mocks/utils'; jest.mock('../../../../common/components/user_privileges'); const useUserPrivileges = _useUserPrivileges as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx index 4d9f53c73341..1261d01c7af4 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.tsx @@ -24,6 +24,7 @@ import { import type { EuiFlyoutSize } from '@elastic/eui/src/components/flyout/flyout'; import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { useIsMounted } from '@kbn/securitysolution-hook-utils'; import { useUrlParams } from '../../../hooks/use_url_params'; import { useIsFlyoutOpened } from '../hooks/use_is_flyout_opened'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; @@ -39,7 +40,6 @@ import { useKibana, useToasts } from '../../../../common/lib/kibana'; import { createExceptionListItemForCreate } from '../../../../../common/endpoint/service/artifacts/utils'; import { useWithArtifactSubmitData } from '../hooks/use_with_artifact_submit_data'; import { useIsArtifactAllowedPerPolicyUsage } from '../hooks/use_is_artifact_allowed_per_policy_usage'; -import { useIsMounted } from '../../../hooks/use_is_mounted'; import { useGetArtifact } from '../../../hooks/artifacts'; import type { PolicyData } from '../../../../../common/endpoint/types'; @@ -271,7 +271,7 @@ export const ArtifactFlyout = memo( const handleFormComponentOnChange: ArtifactFormComponentProps['onChange'] = useCallback( ({ item: updatedItem, isValid }) => { - if (isMounted) { + if (isMounted()) { setFormState({ item: updatedItem, isValid, @@ -289,7 +289,7 @@ export const ArtifactFlyout = memo( : labels.flyoutCreateSubmitSuccess(result) ); - if (isMounted) { + if (isMounted()) { // Close the flyout // `undefined` will cause params to be dropped from url setUrlParams({ ...urlParams, itemId: undefined, show: undefined }, true); @@ -307,12 +307,12 @@ export const ArtifactFlyout = memo( submitHandler(formState.item, formMode) .then(handleSuccess) .catch((submitHandlerError) => { - if (isMounted) { + if (isMounted()) { setExternalSubmitHandlerError(submitHandlerError); } }) .finally(() => { - if (isMounted) { + if (isMounted()) { setExternalIsSubmittingData(false); } }); @@ -326,7 +326,7 @@ export const ArtifactFlyout = memo( useEffect(() => { if (isEditFlow && !hasItemDataForEdit && !error && isInitializing && !isLoadingItemForEdit) { fetchItemForEdit().then(({ data: editItemData }) => { - if (editItemData && isMounted) { + if (editItemData && isMounted()) { setFormState(createFormInitialState(apiClient.listId, editItemData)); } }); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_with_artifact_list_data.ts b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_with_artifact_list_data.ts index 813e205b64c9..ddae258fef89 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_with_artifact_list_data.ts +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/hooks/use_with_artifact_list_data.ts @@ -8,8 +8,8 @@ import { useEffect, useMemo, useState } from 'react'; import type { Pagination } from '@elastic/eui'; import { useQuery } from '@tanstack/react-query'; +import { useIsMounted } from '@kbn/securitysolution-hook-utils'; import type { ServerApiError } from '../../../../common/types'; -import { useIsMounted } from '../../../hooks/use_is_mounted'; import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants'; import { useUrlParams } from '../../../hooks/use_url_params'; import type { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; @@ -98,7 +98,7 @@ export const useWithArtifactListData = ( // Once we know if data exists, update the page initializing state. // This should only ever happen at most once; useEffect(() => { - if (isMounted) { + if (isMounted()) { if (isPageInitializing && !isLoadingDataExists) { setIsPageInitializing(false); } @@ -107,7 +107,7 @@ export const useWithArtifactListData = ( // Update the uiPagination once the query succeeds useEffect(() => { - if (isMounted && listData && !isLoadingListData && isSuccessListData) { + if (isMounted() && listData && !isLoadingListData && isSuccessListData) { setUiPagination((prevState) => { return { ...prevState, @@ -134,7 +134,7 @@ export const useWithArtifactListData = ( // >> Check if data exists again (which should return true useEffect(() => { if ( - isMounted && + isMounted() && !isLoadingListData && !isLoadingDataExists && !listDataError && diff --git a/x-pack/plugins/security_solution/public/management/components/console/types.ts b/x-pack/plugins/security_solution/public/management/components/console/types.ts index 2d863e7878be..5b90a18f27ce 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/types.ts @@ -168,7 +168,7 @@ export type CommandExecutionComponent< /** The arguments that could have been entered by the user */ TArgs extends SupportedArguments = any, /** Internal store for the Command execution */ - TStore extends object = Record, + TStore extends object = any, /** The metadata defined on the Command Definition */ TMeta = any > = ComponentType>; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.test.tsx index 29b6fd044657..97689a790afa 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.test.tsx @@ -129,7 +129,7 @@ describe('When using processes action from response actions console', () => { enterConsoleCommand(renderResult, 'processes'); await waitFor(() => { - expect(renderResult.getByTestId('getProcessesErrorCallout').textContent).toMatch( + expect(renderResult.getByTestId('getProcesses-actionFailure').textContent).toMatch( /error one \| error two/ ); }); @@ -145,7 +145,7 @@ describe('When using processes action from response actions console', () => { enterConsoleCommand(renderResult, 'processes'); await waitFor(() => { - expect(renderResult.getByTestId('performGetProcessesErrorCallout').textContent).toMatch( + expect(renderResult.getByTestId('getProcesses-apiFailure').textContent).toMatch( /this is an error/ ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.tsx index c778f03fcb5f..d7b05ae721ab 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/get_processes_action.tsx @@ -5,21 +5,17 @@ * 2.0. */ -import React, { memo, useEffect, useMemo } from 'react'; +import React, { memo, useMemo } from 'react'; import styled from 'styled-components'; import { EuiBasicTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { IHttpFetchError } from '@kbn/core-http-browser'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { useConsoleActionSubmitter } from './hooks/use_console_action_submitter'; import type { - ActionDetails, GetProcessesActionOutputContent, + ProcessesRequestBody, } from '../../../../common/endpoint/types'; -import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details'; -import type { EndpointCommandDefinitionMeta } from './types'; -import type { CommandExecutionComponentProps } from '../console/types'; import { useSendGetEndpointProcessesRequest } from '../../hooks/endpoint/use_send_get_endpoint_processes_request'; -import { ActionError } from './action_error'; +import type { ActionRequestComponentProps } from './types'; // @ts-expect-error TS2769 const StyledEuiBasicTable = styled(EuiBasicTable)` @@ -43,181 +39,90 @@ const StyledEuiBasicTable = styled(EuiBasicTable)` } `; -export const GetProcessesActionResult = memo< - CommandExecutionComponentProps< - { comment?: string }, - { - actionId?: string; - actionRequestSent?: boolean; - completedActionDetails?: ActionDetails; - apiError?: IHttpFetchError; - }, - EndpointCommandDefinitionMeta - > ->(({ command, setStore, store, status, setStatus, ResultComponent }) => { - const endpointId = command.commandDefinition?.meta?.endpointId; - const { actionId, completedActionDetails, apiError } = store; - - const isPending = status === 'pending'; - const isError = status === 'error'; - const actionRequestSent = Boolean(store.actionRequestSent); - - const { - mutate: getProcesses, - data: getProcessesData, - isSuccess: isGetProcessesSuccess, - error: processesActionRequestError, - } = useSendGetEndpointProcessesRequest(); - - const { data: actionDetails } = useGetActionDetails( - actionId ?? '-', - { - enabled: Boolean(actionId) && isPending, - refetchInterval: isPending ? 3000 : false, - } - ); - - // Send get processes request if not yet done - useEffect(() => { - if (!actionRequestSent && endpointId) { - getProcesses({ - endpoint_ids: [endpointId], - comment: command.args.args?.comment?.[0], - }); - - setStore((prevState) => { - return { ...prevState, actionRequestSent: true }; - }); - } - }, [actionRequestSent, command.args.args?.comment, endpointId, getProcesses, setStore]); +export const GetProcessesActionResult = memo( + ({ command, setStore, store, status, setStatus, ResultComponent }) => { + const endpointId = command.commandDefinition?.meta?.endpointId; + const actionCreator = useSendGetEndpointProcessesRequest(); + + const actionRequestBody = useMemo(() => { + return endpointId + ? { + endpoint_ids: [endpointId], + comment: command.args.args?.comment?.[0], + } + : undefined; + }, [command.args.args?.comment, endpointId]); + + const { result, actionDetails: completedActionDetails } = useConsoleActionSubmitter< + ProcessesRequestBody, + GetProcessesActionOutputContent + >({ + ResultComponent, + setStore, + store, + status, + setStatus, + actionCreator, + actionRequestBody, + dataTestSubj: 'getProcesses', + }); + + const columns = useMemo( + () => [ + { + field: 'user', + name: i18n.translate( + 'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.user', + { defaultMessage: 'USER' } + ), + width: '10%', + }, + { + field: 'pid', + name: i18n.translate( + 'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.pid', + { defaultMessage: 'PID' } + ), + width: '5%', + }, + { + field: 'entity_id', + name: i18n.translate( + 'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.enityId', + { defaultMessage: 'ENTITY ID' } + ), + width: '30%', + }, + + { + field: 'command', + name: i18n.translate( + 'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.command', + { defaultMessage: 'COMMAND' } + ), + width: '55%', + }, + ], + [] + ); - // If get processes request was created, store the action id if necessary - useEffect(() => { - if (isPending) { - if (isGetProcessesSuccess && actionId !== getProcessesData?.data.id) { - setStore((prevState) => { - return { ...prevState, actionId: getProcessesData?.data.id }; - }); - } else if (processesActionRequestError) { - setStatus('error'); - setStore((prevState) => { - return { ...prevState, apiError: processesActionRequestError }; - }); + const tableEntries = useMemo(() => { + if (endpointId) { + return completedActionDetails?.outputs?.[endpointId]?.content.entries ?? []; } - } - }, [ - actionId, - getProcessesData?.data.id, - processesActionRequestError, - isGetProcessesSuccess, - setStatus, - setStore, - isPending, - ]); - - useEffect(() => { - if (actionDetails?.data.isCompleted && isPending) { - setStatus('success'); - setStore((prevState) => { - return { - ...prevState, - completedActionDetails: actionDetails?.data, - }; - }); - } - }, [actionDetails?.data, setStatus, setStore, isPending]); - - const columns = useMemo( - () => [ - { - field: 'user', - name: i18n.translate( - 'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.user', - { defaultMessage: 'USER' } - ), - width: '10%', - }, - { - field: 'pid', - name: i18n.translate( - 'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.pid', - { defaultMessage: 'PID' } - ), - width: '5%', - }, - { - field: 'entity_id', - name: i18n.translate( - 'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.enityId', - { defaultMessage: 'ENTITY ID' } - ), - width: '30%', - }, - - { - field: 'command', - name: i18n.translate( - 'xpack.securitySolution.endpointResponseActions.getProcesses.table.header.command', - { defaultMessage: 'COMMAND' } - ), - width: '55%', - }, - ], - [] - ); + return []; + }, [completedActionDetails?.outputs, endpointId]); - const tableEntries = useMemo(() => { - if (endpointId) { - return completedActionDetails?.outputs?.[endpointId]?.content.entries ?? []; + if (!completedActionDetails || !completedActionDetails.wasSuccessful) { + return result; } - return []; - }, [completedActionDetails?.outputs, endpointId]); - - // Show nothing if still pending - if (isPending) { - return ; - } - // Show errors if perform action fails - if (isError && apiError) { + // Show results return ( - - + + ); } - - // Show errors - if (completedActionDetails?.errors) { - return ( - - ); - } - - // Show results - return ( - - - - ); -}); +); GetProcessesActionResult.displayName = 'GetProcessesActionResult'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/hooks.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/hooks.tsx deleted file mode 100644 index bbc390bb2ba5..000000000000 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/hooks.tsx +++ /dev/null @@ -1,100 +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 { useEffect, useRef } from 'react'; -import { useIsMounted } from '../../hooks/use_is_mounted'; -import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details'; -import { ACTION_DETAILS_REFRESH_INTERVAL } from './constants'; -import type { ActionRequestState, ActionRequestComponentProps } from './types'; -import type { useSendIsolateEndpointRequest } from '../../hooks/endpoint/use_send_isolate_endpoint_request'; -import type { useSendReleaseEndpointRequest } from '../../hooks/endpoint/use_send_release_endpoint_request'; - -export const useUpdateActionState = ({ - actionRequestApi, - actionRequest, - command, - endpointId, - setStatus, - setStore, - isPending, -}: Pick & { - actionRequestApi: ReturnType< - typeof useSendIsolateEndpointRequest | typeof useSendReleaseEndpointRequest - >; - actionRequest?: ActionRequestState; - endpointId?: string; - isPending: boolean; -}) => { - const isMounted = useIsMounted(); - const actionRequestSent = Boolean(actionRequest?.requestSent); - const { data: actionDetails } = useGetActionDetails(actionRequest?.actionId ?? '-', { - enabled: Boolean(actionRequest?.actionId) && isPending, - refetchInterval: isPending ? ACTION_DETAILS_REFRESH_INTERVAL : false, - }); - - // keep a reference to track the console's mounted state - // in order to update the store and cause a re-render on action request API response - const latestIsMounted = useRef(false); - latestIsMounted.current = isMounted; - - // Create action request - useEffect(() => { - if (!actionRequestSent && endpointId && isMounted) { - const request: ActionRequestState = { - requestSent: true, - actionId: undefined, - }; - - actionRequestApi - .mutateAsync({ - endpoint_ids: [endpointId], - comment: command.args.args?.comment?.[0], - }) - .then((response) => { - request.actionId = response.data.id; - - if (latestIsMounted.current) { - setStore((prevState) => { - return { ...prevState, actionRequest: { ...request } }; - }); - } - }); - - setStore((prevState) => { - return { ...prevState, actionRequest: request }; - }); - } - }, [ - actionRequestApi, - actionRequestSent, - command.args.args?.comment, - endpointId, - isMounted, - setStore, - ]); - - useEffect(() => { - // update the console's mounted state ref - latestIsMounted.current = isMounted; - // set to false when unmounted/console is hidden - return () => { - latestIsMounted.current = false; - }; - }, [isMounted]); - - useEffect(() => { - if (actionDetails?.data.isCompleted && isPending) { - setStatus('success'); - setStore((prevState) => { - return { - ...prevState, - completedActionDetails: actionDetails.data, - }; - }); - } - }, [actionDetails?.data, actionDetails?.data.isCompleted, setStatus, setStore, isPending]); -}; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/hooks/use_console_action_submitter.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/hooks/use_console_action_submitter.test.tsx new file mode 100644 index 000000000000..7ef506ea30f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/hooks/use_console_action_submitter.test.tsx @@ -0,0 +1,275 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { + UseConsoleActionSubmitterOptions, + ConsoleActionSubmitter, + CommandResponseActionApiState, +} from './use_console_action_submitter'; +import { useConsoleActionSubmitter } from './use_console_action_submitter'; +import type { AppContextTestRender } from '../../../../common/mock/endpoint'; +import { createAppRootMockRenderer } from '../../../../common/mock/endpoint'; +import { EndpointActionGenerator } from '../../../../../common/endpoint/data_generators/endpoint_action_generator'; +import React, { useState } from 'react'; +import type { CommandExecutionResultProps } from '../../console'; +import type { DeferredInterface } from '../../../mocks/utils'; +import { getDeferred } from '../../../mocks/utils'; +import type { ActionDetails } from '../../../../../common/endpoint/types'; +import { act, waitFor } from '@testing-library/react'; +import { responseActionsHttpMocks } from '../../../mocks/response_actions_http_mocks'; + +describe('When using `useConsoleActionSubmitter()` hook', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let renderArgs: UseConsoleActionSubmitterOptions; + let updateHookRenderArgs: () => void; + let hookRenderResultStorage: jest.Mock<(args: ConsoleActionSubmitter) => void>; + let releaseSuccessActionRequestApiResponse: DeferredInterface['resolve']; + let releaseFailedActionRequestApiResponse: DeferredInterface['reject']; + let apiMocks: ReturnType; + + const ActionSubmitterTestComponent = () => { + const [hookOptions, setHookOptions] = useState(renderArgs); + + updateHookRenderArgs = () => { + new Promise((r) => { + setTimeout(r, 1); + }).then(() => { + setHookOptions({ + ...renderArgs, + }); + }); + }; + + const { result, actionDetails } = useConsoleActionSubmitter(hookOptions); + + hookRenderResultStorage({ result, actionDetails }); + + return
    {result}
    ; + }; + + const getOutputTextContent = (): string => { + return renderResult.getByTestId('testContainer').textContent ?? ''; + }; + + beforeEach(() => { + const { render: renderComponent, coreStart } = createAppRootMockRenderer(); + const actionGenerator = new EndpointActionGenerator(); + const deferred = getDeferred(); + + apiMocks = responseActionsHttpMocks(coreStart.http); + + hookRenderResultStorage = jest.fn(); + releaseSuccessActionRequestApiResponse = () => + deferred.resolve(actionGenerator.generateActionDetails({ id: '123' })); + releaseFailedActionRequestApiResponse = deferred.reject; + + let status: UseConsoleActionSubmitterOptions['status'] = 'pending'; + let commandStore: CommandResponseActionApiState = {}; + + renderArgs = { + dataTestSubj: 'test', + actionRequestBody: { + endpoint_ids: ['123'], + }, + actionCreator: { + mutateAsync: jest.fn(async () => { + return { + data: await deferred.promise, + }; + }), + } as unknown as UseConsoleActionSubmitterOptions['actionCreator'], + get status() { + return status; + }, + setStatus: jest.fn((newStatus) => { + status = newStatus; + updateHookRenderArgs(); + }), + get store() { + return commandStore; + }, + setStore: jest.fn((newStoreOrCallback: object | ((prevStore: object) => object)) => { + if (typeof newStoreOrCallback === 'function') { + commandStore = newStoreOrCallback(commandStore); + } else { + commandStore = newStoreOrCallback; + } + + updateHookRenderArgs(); + }), + ResultComponent: jest.fn( + ({ children, showAs, 'data-test-subj': dataTestSubj }: CommandExecutionResultProps) => { + return ( +
    + {children} +
    + ); + } + ), + }; + + render = () => { + renderResult = renderComponent(); + return renderResult; + }; + }); + + afterEach(() => { + renderResult.unmount(); + }); + + it('should return expected interface while its still pending', () => { + render(); + + expect(hookRenderResultStorage).toHaveBeenLastCalledWith({ + result: expect.anything(), + action: undefined, + }); + + expect(renderResult.getByTestId('test-pending')).not.toBeNull(); + }); + + it('should update command state when request is sent', () => { + render(); + + expect(renderArgs.store?.actionApiState?.request.sent).toBe(true); + expect(renderArgs.store?.actionApiState?.request.actionId).toBe(undefined); + }); + + it('should store the action id when action request api is successful', async () => { + render(); + + releaseSuccessActionRequestApiResponse(); + + await waitFor(() => { + expect(renderArgs.store?.actionApiState?.request.actionId).toBe('123'); + }); + }); + + it('should store action request api error', async () => { + render(); + const error = new Error('oh oh. request failed'); + + act(() => { + releaseFailedActionRequestApiResponse(error); + }); + + await waitFor(() => { + expect(renderArgs.store?.actionApiState?.request.actionId).toBe(undefined); + expect(renderArgs.store?.actionApiState?.request.error).toBe(error); + }); + + await waitFor(() => { + expect(getOutputTextContent()).toEqual( + 'The following error was encountered:oh oh. request failed' + ); + }); + }); + + it('should still store the action id if component is unmounted while action request API is in flight', async () => { + render(); + renderResult.unmount(); + + expect(renderArgs.store.actionApiState?.request.sent).toBe(true); + + const requestState = renderArgs.store.actionApiState?.request; + releaseSuccessActionRequestApiResponse(); + + await waitFor(() => { + // this check just ensure that we mutated the state when the api returned success instead of + // dispatching a `setStore()`. + expect(renderArgs.store.actionApiState?.request === requestState).toBe(true); + + expect(renderArgs.store.actionApiState?.request.actionId).toEqual('123'); + }); + }); + + it('should call action details api once we have an action id', async () => { + render(); + + expect(apiMocks.responseProvider.actionDetails).not.toHaveBeenCalled(); + + releaseSuccessActionRequestApiResponse(); + + await waitFor(() => { + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledWith({ + path: '/api/endpoint/action/123', + }); + }); + }); + + it('should continue to show pending message until action completes', async () => { + apiMocks.responseProvider.actionDetails.mockImplementation(() => { + return { + data: new EndpointActionGenerator().generateActionDetails({ + id: '123', + isCompleted: false, + }), + }; + }); + render(); + releaseSuccessActionRequestApiResponse(); + + await waitFor(() => { + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledWith({ + path: '/api/endpoint/action/123', + }); + }); + + expect(renderResult.getByTestId('test-pending')).not.toBeNull(); + + expect(hookRenderResultStorage).toHaveBeenLastCalledWith({ + result: expect.anything(), + actionDetails: undefined, + }); + }); + + it('should store action details api error', async () => { + const error = new Error('on oh. getting action details failed'); + apiMocks.responseProvider.actionDetails.mockImplementation(() => { + throw error; + }); + + render(); + releaseSuccessActionRequestApiResponse(); + + await waitFor(() => { + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledWith({ + path: '/api/endpoint/action/123', + }); + }); + + expect(renderArgs.store.actionApiState?.actionDetailsError).toBe(error); + + expect(renderResult.getByTestId('test-apiFailure').textContent).toEqual( + 'The following error was encountered:on oh. getting action details failed' + ); + }); + + it('should store action details once action completes', async () => { + const actionDetails = new EndpointActionGenerator().generateActionDetails({ id: '123' }); + apiMocks.responseProvider.actionDetails.mockReturnValue({ data: actionDetails }); + + render(); + releaseSuccessActionRequestApiResponse(); + + await waitFor(() => { + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledWith({ + path: '/api/endpoint/action/123', + }); + }); + + expect(renderArgs.store.actionApiState?.actionDetails).toBe(actionDetails); + expect(hookRenderResultStorage).toHaveBeenLastCalledWith({ + result: expect.anything(), + actionDetails, + }); + + expect(renderResult.getByTestId('test-success').textContent).toEqual(''); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/hooks/use_console_action_submitter.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/hooks/use_console_action_submitter.tsx new file mode 100644 index 000000000000..7183b5cc61ef --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/hooks/use_console_action_submitter.tsx @@ -0,0 +1,292 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useMemo } from 'react'; +import type { UseMutationResult } from '@tanstack/react-query'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useIsMounted } from '@kbn/securitysolution-hook-utils'; +import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; +import type { BaseActionRequestBody } from '../../../../../common/endpoint/schema/actions'; +import { ActionSuccess } from '../action_success'; +import { ActionError } from '../action_error'; +import { FormattedError } from '../../formatted_error'; +import { useGetActionDetails } from '../../../hooks/endpoint/use_get_action_details'; +import { ACTION_DETAILS_REFRESH_INTERVAL } from '../constants'; +import type { + ActionDetails, + Immutable, + ResponseActionApiResponse, +} from '../../../../../common/endpoint/types'; +import type { CommandExecutionComponentProps } from '../../console'; + +export interface ConsoleActionSubmitter { + /** + * The ui to be returned to the console. This UI will display different states of the action, + * including pending, error conditions and generic success messages. + */ + result: JSX.Element; + actionDetails: Immutable> | undefined; +} + +/** + * Command store state for response action api state. + */ +export interface CommandResponseActionApiState { + actionApiState?: { + request: { + sent: boolean; + actionId: string | undefined; + error: IHttpFetchError | undefined; + }; + actionDetails: ActionDetails | undefined; + actionDetailsError: IHttpFetchError | undefined; + }; +} + +export interface UseConsoleActionSubmitterOptions< + TReqBody extends BaseActionRequestBody = BaseActionRequestBody, + TActionOutputContent extends object = object +> extends Pick< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + CommandExecutionComponentProps>, + 'ResultComponent' | 'setStore' | 'store' | 'status' | 'setStatus' + > { + actionCreator: UseMutationResult; + /** + * The API request body. If `undefined`, then API will not be called. + */ + actionRequestBody: TReqBody | undefined; + + dataTestSubj?: string; +} + +/** + * generic hook for use with Response Action commands. It will create the action, store its ID and + * continuously pull the Action's Details until it completes. It handles all aspects of UI display + * for the different states of the command (pending -> success/failure) + * + * @param actionCreator + * @param actionRequestBody + * @param setStatus + * @param status + * @param setStore + * @param store + * @param ResultComponent + * @param dataTestSubj + */ +export const useConsoleActionSubmitter = < + TReqBody extends BaseActionRequestBody = BaseActionRequestBody, + TActionOutputContent extends object = object +>({ + actionCreator, + actionRequestBody, + setStatus, + status, + setStore, + store, + ResultComponent, + dataTestSubj, +}: UseConsoleActionSubmitterOptions< + TReqBody, + TActionOutputContent +>): ConsoleActionSubmitter => { + const isMounted = useIsMounted(); + const getTestId = useTestIdGenerator(dataTestSubj); + const isPending = status === 'pending'; + + const currentActionState = useMemo< + Immutable>['actionApiState']> + >( + () => + store.actionApiState ?? { + request: { + sent: false, + error: undefined, + actionId: undefined, + }, + actionDetails: undefined, + actionDetailsError: undefined, + }, + [store.actionApiState] + ); + + const { actionDetails, actionDetailsError } = currentActionState; + const { + actionId, + sent: actionRequestSent, + error: actionRequestError, + } = currentActionState.request; + + const { data: apiActionDetailsResponse, error: apiActionDetailsError } = + useGetActionDetails(actionId ?? '-', { + enabled: Boolean(actionId) && isPending, + refetchInterval: isPending ? ACTION_DETAILS_REFRESH_INTERVAL : false, + }); + + // Create the action request if not yet done + useEffect(() => { + if (!actionRequestSent && actionRequestBody && isMounted()) { + const updatedRequestState: Required< + CommandResponseActionApiState + >['actionApiState']['request'] = { + ...( + currentActionState as Required< + CommandResponseActionApiState + >['actionApiState'] + ).request, + sent: true, + }; + + // The object defined above (`updatedRequestState`) is saved to the command state right away. + // the creation of the Action request (below) will mutate this object to store the Action ID + // once the API response is received. We do this to ensure that the action is not created more + // than once if the user happens to close the console prior to the response being returned. + // Once a response is received, we check if the component is mounted, and if so, then we send + // another update to the command store which will cause it to re-render and start checking for + // action completion. + actionCreator + .mutateAsync(actionRequestBody) + .then((response) => { + updatedRequestState.actionId = response.data.id; + }) + .catch((err) => { + updatedRequestState.error = err; + }) + .finally(() => { + // If the component is mounted, then set the store with the updated data (causes a rerender) + if (isMounted()) { + setStore((prevState) => { + return { + ...prevState, + actionApiState: { + ...(prevState.actionApiState ?? currentActionState), + request: { ...updatedRequestState }, + }, + }; + }); + } + }); + + setStore((prevState) => { + return { + ...prevState, + actionApiState: { + ...(prevState.actionApiState ?? currentActionState), + request: updatedRequestState, + }, + }; + }); + } + }, [ + actionCreator, + actionRequestBody, + actionRequestSent, + currentActionState, + isMounted, + setStore, + ]); + + // If an error was returned while attempting to create the action request, + // then set command status to error + useEffect(() => { + if (actionRequestError && isPending) { + setStatus('error'); + } + }, [actionRequestError, isPending, setStatus]); + + // If an error was return by the Action Details API, then store it and set the status to error + useEffect(() => { + if (apiActionDetailsError && isPending) { + setStatus('error'); + setStore((prevState) => { + return { + ...prevState, + actionApiState: { + ...(prevState.actionApiState ?? currentActionState), + actionDetails: undefined, + actionDetailsError: apiActionDetailsError, + }, + }; + }); + } + }, [apiActionDetailsError, currentActionState, isPending, setStatus, setStore]); + + // If the action details indicates complete, then update the action's console state and set the status to success + useEffect(() => { + if (apiActionDetailsResponse?.data.isCompleted && isPending) { + setStatus(apiActionDetailsResponse?.data.wasSuccessful ? 'success' : 'error'); + setStore((prevState) => { + return { + ...prevState, + actionApiState: { + ...(prevState.actionApiState ?? currentActionState), + actionDetails: apiActionDetailsResponse.data, + }, + // Unclear why I needed to cast this here. For some reason the `ActionDetails['outputs']` is + // reporting a type error for the `content` property, although the types seem to line up. + } as typeof prevState; + }); + } + }, [apiActionDetailsResponse, currentActionState, isPending, setStatus, setStore]); + + // Calculate the action's UI result based on the different API responses + const result = useMemo(() => { + if (isPending) { + return ; + } + + const apiError = actionRequestError || actionDetailsError; + + if (apiError) { + return ( + + + + + ); + } + + if (actionDetails) { + // Response action failures + if (actionDetails.errors) { + return ( + + ); + } + + return ( + + ); + } + + return <>; + }, [ + isPending, + actionRequestError, + actionDetailsError, + actionDetails, + ResultComponent, + getTestId, + ]); + + return { + result, + actionDetails: currentActionState.actionDetails, + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.test.tsx index 63e05d420dea..110ddbb53b1b 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.test.tsx @@ -16,9 +16,9 @@ import { getEndpointResponseActionsConsoleCommands } from './endpoint_response_a import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks'; import { enterConsoleCommand } from '../console/mocks'; import { waitFor } from '@testing-library/react'; -import { getDeferred } from '../mocks'; import type { ResponderCapabilities } from '../../../../common/endpoint/constants'; import { RESPONDER_CAPABILITIES } from '../../../../common/endpoint/constants'; +import { getDeferred } from '../../mocks/utils'; describe('When using isolate action from response actions console', () => { let render: ( @@ -115,7 +115,7 @@ describe('When using isolate action from response actions console', () => { enterConsoleCommand(renderResult, 'isolate'); await waitFor(() => { - expect(renderResult.getByTestId('isolateSuccessCallout')).toBeTruthy(); + expect(renderResult.getByTestId('isolate-success')).toBeTruthy(); }); }); @@ -130,7 +130,7 @@ describe('When using isolate action from response actions console', () => { enterConsoleCommand(renderResult, 'isolate'); await waitFor(() => { - expect(renderResult.getByTestId('isolateErrorCallout').textContent).toMatch( + expect(renderResult.getByTestId('isolate-actionFailure').textContent).toMatch( /error one \| error two/ ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.tsx index ec11d022650c..8df7692cf3ac 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/isolate_action.tsx @@ -5,47 +5,37 @@ * 2.0. */ -import React, { memo } from 'react'; +import { memo, useMemo } from 'react'; +import { useConsoleActionSubmitter } from './hooks/use_console_action_submitter'; import type { ActionRequestComponentProps } from './types'; import { useSendIsolateEndpointRequest } from '../../hooks/endpoint/use_send_isolate_endpoint_request'; -import { ActionError } from './action_error'; -import { useUpdateActionState } from './hooks'; export const IsolateActionResult = memo( ({ command, setStore, store, status, setStatus, ResultComponent }) => { - const endpointId = command.commandDefinition?.meta?.endpointId; - const { completedActionDetails, actionRequest } = store; - const isPending = status === 'pending'; const isolateHostApi = useSendIsolateEndpointRequest(); - useUpdateActionState({ - actionRequestApi: isolateHostApi, - actionRequest, - command, - endpointId, - setStatus, - setStore, - isPending, - }); - - // Show nothing if still pending - if (isPending) { - return ; - } + const actionRequestBody = useMemo(() => { + const endpointId = command.commandDefinition?.meta?.endpointId; + const comment = command.args.args?.comment?.[0]; - // Show errors - if (completedActionDetails?.errors) { - return ( - - ); - } + return endpointId + ? { + endpoint_ids: [endpointId], + comment, + } + : undefined; + }, [command.args.args?.comment, command.commandDefinition?.meta?.endpointId]); - // Show Success - return ; + return useConsoleActionSubmitter({ + ResultComponent, + setStore, + store, + status, + setStatus, + actionCreator: isolateHostApi, + actionRequestBody, + dataTestSubj: 'isolate', + }).result; } ); IsolateActionResult.displayName = 'IsolateActionResult'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx index 827a4d619175..f888df2099b1 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.test.tsx @@ -195,7 +195,7 @@ describe('When using the kill-process action from response actions console', () enterConsoleCommand(renderResult, 'kill-process --pid 123'); await waitFor(() => { - expect(renderResult.getByTestId('killProcessSuccessCallout')).toBeTruthy(); + expect(renderResult.getByTestId('killProcess-success')).toBeTruthy(); }); }); @@ -204,7 +204,7 @@ describe('When using the kill-process action from response actions console', () enterConsoleCommand(renderResult, 'kill-process --entityId 123wer'); await waitFor(() => { - expect(renderResult.getByTestId('killProcessSuccessCallout')).toBeTruthy(); + expect(renderResult.getByTestId('killProcess-success')).toBeTruthy(); }); }); @@ -219,7 +219,7 @@ describe('When using the kill-process action from response actions console', () enterConsoleCommand(renderResult, 'kill-process --pid 123'); await waitFor(() => { - expect(renderResult.getByTestId('killProcessErrorCallout').textContent).toMatch( + expect(renderResult.getByTestId('killProcess-actionFailure').textContent).toMatch( /error one \| error two/ ); }); @@ -234,7 +234,7 @@ describe('When using the kill-process action from response actions console', () enterConsoleCommand(renderResult, 'kill-process --pid 123'); await waitFor(() => { - expect(renderResult.getByTestId('killProcessAPIErrorCallout').textContent).toMatch( + expect(renderResult.getByTestId('killProcess-apiFailure').textContent).toMatch( /this is an error/ ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.tsx index 3e4dcf61d169..bf501c31b9e8 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/kill_process_action.tsx @@ -5,130 +5,40 @@ * 2.0. */ -import React, { memo, useEffect } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { memo, useMemo } from 'react'; +import type { KillOrSuspendProcessRequestBody } from '../../../../common/endpoint/types'; import { parsedPidOrEntityIdParameter } from './utils'; -import { ActionSuccess } from './action_success'; -import type { - ActionDetails, - KillProcessActionOutputContent, -} from '../../../../common/endpoint/types'; -import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details'; -import type { EndpointCommandDefinitionMeta } from './types'; import { useSendKillProcessRequest } from '../../hooks/endpoint/use_send_kill_process_endpoint_request'; -import type { CommandExecutionComponentProps } from '../console/types'; -import { ActionError } from './action_error'; -import { ACTION_DETAILS_REFRESH_INTERVAL } from './constants'; +import type { ActionRequestComponentProps } from './types'; +import { useConsoleActionSubmitter } from './hooks/use_console_action_submitter'; export const KillProcessActionResult = memo< - CommandExecutionComponentProps< - { comment?: string; pid?: string; entityId?: string }, - { - actionId?: string; - actionRequestSent?: boolean; - completedActionDetails?: ActionDetails; - apiError?: IHttpFetchError; - }, - EndpointCommandDefinitionMeta - > + ActionRequestComponentProps<{ pid?: string[]; entityId?: string[] }> >(({ command, setStore, store, status, setStatus, ResultComponent }) => { - const endpointId = command.commandDefinition?.meta?.endpointId; - const { actionId, completedActionDetails, apiError } = store; - const isPending = status === 'pending'; - const isError = status === 'error'; - const actionRequestSent = Boolean(store.actionRequestSent); + const actionCreator = useSendKillProcessRequest(); - const { mutate, data, isSuccess, error } = useSendKillProcessRequest(); - - const { data: actionDetails } = useGetActionDetails( - actionId ?? '-', - { - enabled: Boolean(actionId) && isPending, - refetchInterval: isPending ? ACTION_DETAILS_REFRESH_INTERVAL : false, - } - ); - - // Send Kill request if not yet done - useEffect(() => { + const actionRequestBody = useMemo(() => { + const endpointId = command.commandDefinition?.meta?.endpointId; const parameters = parsedPidOrEntityIdParameter(command.args.args); - if (!actionRequestSent && endpointId && parameters) { - mutate({ - endpoint_ids: [endpointId], - comment: command.args.args?.comment?.[0], - parameters, - }); - setStore((prevState) => { - return { ...prevState, actionRequestSent: true }; - }); - } - }, [actionRequestSent, command.args.args, endpointId, mutate, setStore]); - - // If kill-process request was created, store the action id if necessary - useEffect(() => { - if (isPending) { - if (isSuccess && actionId !== data.data.id) { - setStore((prevState) => { - return { ...prevState, actionId: data.data.id }; - }); - } else if (error) { - setStatus('error'); - setStore((prevState) => { - return { ...prevState, apiError: error }; - }); - } - } - }, [actionId, data?.data.id, isSuccess, error, setStore, setStatus, isPending]); - - useEffect(() => { - if (actionDetails?.data.isCompleted && isPending) { - setStatus('success'); - setStore((prevState) => { - return { - ...prevState, - completedActionDetails: actionDetails.data, - }; - }); - } - }, [actionDetails?.data, setStatus, setStore, isPending]); - - // Show API errors if perform action fails - if (isError && apiError) { - return ( - - - - ); - } - - // Show nothing if still pending - if (isPending || !completedActionDetails) { - return ; - } - - // Show errors - if (completedActionDetails?.errors) { - return ( - - ); - } - - // Show Success - return ( - - ); + return endpointId + ? { + endpoint_ids: [endpointId], + comment: command.args.args?.comment?.[0], + parameters, + } + : undefined; + }, [command.args.args, command.commandDefinition?.meta?.endpointId]); + + return useConsoleActionSubmitter({ + ResultComponent, + setStore, + store, + status, + setStatus, + actionCreator, + actionRequestBody, + dataTestSubj: 'killProcess', + }).result; }); KillProcessActionResult.displayName = 'KillProcessActionResult'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx index 19e3be94469e..d1c1ea264f86 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.test.tsx @@ -16,9 +16,9 @@ import { getEndpointResponseActionsConsoleCommands } from './endpoint_response_a import { enterConsoleCommand } from '../console/mocks'; import { waitFor } from '@testing-library/react'; import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks'; -import { getDeferred } from '../mocks'; import type { ResponderCapabilities } from '../../../../common/endpoint/constants'; import { RESPONDER_CAPABILITIES } from '../../../../common/endpoint/constants'; +import { getDeferred } from '../../mocks/utils'; describe('When using the release action from response actions console', () => { let render: ( @@ -116,7 +116,7 @@ describe('When using the release action from response actions console', () => { enterConsoleCommand(renderResult, 'release'); await waitFor(() => { - expect(renderResult.getByTestId('releaseSuccessCallout')).toBeTruthy(); + expect(renderResult.getByTestId('release-success')).toBeTruthy(); }); }); @@ -131,7 +131,7 @@ describe('When using the release action from response actions console', () => { enterConsoleCommand(renderResult, 'release'); await waitFor(() => { - expect(renderResult.getByTestId('releaseErrorCallout').textContent).toMatch( + expect(renderResult.getByTestId('release-actionFailure').textContent).toMatch( /error one \| error two/ ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.tsx index f789b4867132..9b0f371ca003 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/release_action.tsx @@ -5,48 +5,37 @@ * 2.0. */ -import React, { memo } from 'react'; +import { memo, useMemo } from 'react'; import type { ActionRequestComponentProps } from './types'; import { useSendReleaseEndpointRequest } from '../../hooks/endpoint/use_send_release_endpoint_request'; -import { ActionError } from './action_error'; -import { useUpdateActionState } from './hooks'; +import { useConsoleActionSubmitter } from './hooks/use_console_action_submitter'; export const ReleaseActionResult = memo( ({ command, setStore, store, status, setStatus, ResultComponent }) => { - const endpointId = command.commandDefinition?.meta?.endpointId; - const { completedActionDetails, actionRequest } = store; - const isPending = status === 'pending'; - const releaseHostApi = useSendReleaseEndpointRequest(); - useUpdateActionState({ - actionRequestApi: releaseHostApi, - actionRequest, - command, - endpointId, - setStatus, - setStore, - isPending, - }); - - // Show nothing if still pending - if (isPending) { - return ; - } + const actionRequestBody = useMemo(() => { + const endpointId = command.commandDefinition?.meta?.endpointId; + const comment = command.args.args?.comment?.[0]; - // Show errors - if (completedActionDetails?.errors) { - return ( - - ); - } + return endpointId + ? { + endpoint_ids: [endpointId], + comment, + } + : undefined; + }, [command.args.args?.comment, command.commandDefinition?.meta?.endpointId]); - // Show Success - return ; + return useConsoleActionSubmitter({ + ResultComponent, + setStore, + store, + status, + setStatus, + actionCreator: releaseHostApi, + actionRequestBody, + dataTestSubj: 'release', + }).result; } ); ReleaseActionResult.displayName = 'ReleaseActionResult'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.test.tsx index 9446fb5dcba6..7479e52edfb0 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.test.tsx @@ -186,7 +186,7 @@ describe('When using the suspend-process action from response actions console', enterConsoleCommand(renderResult, 'suspend-process --pid 123'); await waitFor(() => { - expect(renderResult.getByTestId('suspendProcessSuccessCallout')).toBeTruthy(); + expect(renderResult.getByTestId('suspendProcess-success')).toBeTruthy(); }); }); @@ -195,7 +195,7 @@ describe('When using the suspend-process action from response actions console', enterConsoleCommand(renderResult, 'suspend-process --entityId 123wer'); await waitFor(() => { - expect(renderResult.getByTestId('suspendProcessSuccessCallout')).toBeTruthy(); + expect(renderResult.getByTestId('suspendProcess-success')).toBeTruthy(); }); }); @@ -210,7 +210,7 @@ describe('When using the suspend-process action from response actions console', enterConsoleCommand(renderResult, 'suspend-process --pid 123'); await waitFor(() => { - expect(renderResult.getByTestId('suspendProcessErrorCallout').textContent).toMatch( + expect(renderResult.getByTestId('suspendProcess-actionFailure').textContent).toMatch( /error one \| error two/ ); }); @@ -225,7 +225,7 @@ describe('When using the suspend-process action from response actions console', enterConsoleCommand(renderResult, 'suspend-process --pid 123'); await waitFor(() => { - expect(renderResult.getByTestId('suspendProcessAPIErrorCallout').textContent).toMatch( + expect(renderResult.getByTestId('suspendProcess-apiFailure').textContent).toMatch( /this is an error/ ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.tsx index a60e7eb6bd65..f8401a81fa11 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/suspend_process_action.tsx @@ -5,130 +5,46 @@ * 2.0. */ -import React, { memo, useEffect } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { memo, useMemo } from 'react'; import { parsedPidOrEntityIdParameter } from './utils'; -import { ActionSuccess } from './action_success'; import type { - ActionDetails, SuspendProcessActionOutputContent, + KillOrSuspendProcessRequestBody, } from '../../../../common/endpoint/types'; -import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details'; -import type { EndpointCommandDefinitionMeta } from './types'; import { useSendSuspendProcessRequest } from '../../hooks/endpoint/use_send_suspend_process_endpoint_request'; -import type { CommandExecutionComponentProps } from '../console/types'; -import { ActionError } from './action_error'; -import { ACTION_DETAILS_REFRESH_INTERVAL } from './constants'; +import type { ActionRequestComponentProps } from './types'; +import { useConsoleActionSubmitter } from './hooks/use_console_action_submitter'; export const SuspendProcessActionResult = memo< - CommandExecutionComponentProps< - { comment?: string; pid?: string; entityId?: string }, - { - actionId?: string; - actionRequestSent?: boolean; - completedActionDetails?: ActionDetails; - apiError?: IHttpFetchError; - }, - EndpointCommandDefinitionMeta - > + ActionRequestComponentProps<{ pid?: string[]; entityId?: string[] }> >(({ command, setStore, store, status, setStatus, ResultComponent }) => { - const endpointId = command.commandDefinition?.meta?.endpointId; - const { actionId, completedActionDetails, apiError } = store; - const isPending = status === 'pending'; - const isError = status === 'error'; - const actionRequestSent = Boolean(store.actionRequestSent); + const actionCreator = useSendSuspendProcessRequest(); - const { mutate, data, isSuccess, error } = useSendSuspendProcessRequest(); - - const { data: actionDetails } = useGetActionDetails( - actionId ?? '-', - { - enabled: Boolean(actionId) && isPending, - refetchInterval: isPending ? ACTION_DETAILS_REFRESH_INTERVAL : false, - } - ); - - // Send Suspend request if not yet done - useEffect(() => { + const actionRequestBody = useMemo(() => { + const endpointId = command.commandDefinition?.meta?.endpointId; const parameters = parsedPidOrEntityIdParameter(command.args.args); - if (!actionRequestSent && endpointId && parameters) { - mutate({ - endpoint_ids: [endpointId], - comment: command.args.args?.comment?.[0], - parameters, - }); - setStore((prevState) => { - return { ...prevState, actionRequestSent: true }; - }); - } - }, [actionRequestSent, command.args.args, endpointId, mutate, setStore]); - - // If suspend-process request was created, store the action id if necessary - useEffect(() => { - if (isPending) { - if (isSuccess && actionId !== data.data.id) { - setStore((prevState) => { - return { ...prevState, actionId: data.data.id }; - }); - } else if (error) { - setStatus('error'); - setStore((prevState) => { - return { ...prevState, apiError: error }; - }); - } - } - }, [actionId, data?.data.id, isSuccess, error, setStore, setStatus, isPending]); - - useEffect(() => { - if (actionDetails?.data.isCompleted && isPending) { - setStatus('success'); - setStore((prevState) => { - return { - ...prevState, - completedActionDetails: actionDetails.data, - }; - }); - } - }, [actionDetails?.data, setStatus, setStore, isPending]); - - // Show API errors if perform action fails - if (isError && apiError) { - return ( - - - - ); - } - - // Show nothing if still pending - if (isPending || !completedActionDetails) { - return ; - } - - // Show errors - if (completedActionDetails?.errors) { - return ( - - ); - } - - // Show Success - return ( - - ); + return endpointId + ? { + endpoint_ids: [endpointId], + comment: command.args.args?.comment?.[0], + parameters, + } + : undefined; + }, [command.args.args, command.commandDefinition?.meta?.endpointId]); + + return useConsoleActionSubmitter< + KillOrSuspendProcessRequestBody, + SuspendProcessActionOutputContent + >({ + ResultComponent, + setStore, + store, + status, + setStatus, + actionCreator, + actionRequestBody, + dataTestSubj: 'suspendProcess', + }).result; }); SuspendProcessActionResult.displayName = 'SuspendProcessActionResult'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/types.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/types.ts index 34c306ed0a11..92cc3e8c9017 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/types.ts @@ -5,8 +5,9 @@ * 2.0. */ +import type { CommandResponseActionApiState } from './hooks/use_console_action_submitter'; import type { ManagedConsoleExtensionComponentProps } from '../console'; -import type { ActionDetails, HostMetadata } from '../../../../common/endpoint/types'; +import type { HostMetadata } from '../../../../common/endpoint/types'; import type { CommandExecutionComponentProps } from '../console/types'; export interface EndpointCommandDefinitionMeta { @@ -17,16 +18,9 @@ export type EndpointResponderExtensionComponentProps = ManagedConsoleExtensionCo endpoint: HostMetadata; }>; -export interface ActionRequestState { - requestSent: boolean; - actionId?: string; -} - -export type ActionRequestComponentProps = CommandExecutionComponentProps< - { comment?: string }, - { - actionRequest?: ActionRequestState; - completedActionDetails?: ActionDetails; - }, - EndpointCommandDefinitionMeta ->; +export type ActionRequestComponentProps = + CommandExecutionComponentProps< + { comment?: string } & TArgs, + CommandResponseActionApiState, + EndpointCommandDefinitionMeta + >; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/utils.test.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/utils.test.ts index ab84e9de959f..48b6753645c9 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/utils.test.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/utils.test.ts @@ -19,9 +19,9 @@ describe('Endpoint Responder - Utilities', () => { expect(parameters).toEqual({ entity_id: '123qwe' }); }); - it('should return undefined if no params are defined', () => { + it('should return entity id with emtpy string if no params are defined', () => { const parameters = parsedPidOrEntityIdParameter({}); - expect(parameters).toEqual(undefined); + expect(parameters).toEqual({ entity_id: '' }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/utils.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/utils.ts index 9ebcd090bd2a..0b8e59d0353f 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/utils.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/utils.ts @@ -4,17 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { EndpointActionDataParameterTypes } from '../../../../common/endpoint/types'; +import type { ResponseActionParametersWithPidOrEntityId } from '../../../../common/endpoint/types'; export const parsedPidOrEntityIdParameter = (parameters: { pid?: string[]; entityId?: string[]; -}): EndpointActionDataParameterTypes => { +}): ResponseActionParametersWithPidOrEntityId => { if (parameters.pid) { return { pid: Number(parameters.pid[0]) }; - } else if (parameters.entityId) { - return { entity_id: parameters.entityId[0] }; } - return undefined; + return { + entity_id: parameters?.entityId?.[0] ?? '', + }; }; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx index bb2c1b7761d5..34cc5a3ca0f9 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx @@ -159,7 +159,6 @@ export const getCommandKey = ( } }; -// TODO: add more filter names here export type FilterName = keyof typeof FILTER_NAMES; export const useActionsLogFilter = ({ filterName, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx index 30148c9643ba..556c76529633 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.test.tsx @@ -393,6 +393,26 @@ describe('Response actions history', () => { expect(noTrays).toEqual([]); }); + it('should contain relevant details in each expanded row', async () => { + render(); + const { getAllByTestId } = renderResult; + + const expandButtons = getAllByTestId(`${testPrefix}-expand-button`); + expandButtons.map((button) => userEvent.click(button)); + const trays = getAllByTestId(`${testPrefix}-details-tray`); + expect(trays).toBeTruthy(); + expect(Array.from(trays[0].querySelectorAll('dt')).map((title) => title.textContent)).toEqual( + [ + 'Command placed', + 'Execution started on', + 'Execution completed', + 'Input', + 'Parameters', + 'Output:', + ] + ); + }); + it('should refresh data when autoRefresh is toggled on', async () => { render(); const { getByTestId } = renderResult; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx index 2a9362830c76..ac9a0829bb7b 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx @@ -5,112 +5,29 @@ * 2.0. */ -import { - EuiAvatar, - EuiBasicTable, - EuiButtonIcon, - EuiDescriptionList, - EuiEmptyPrompt, - EuiFacetButton, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiScreenReaderOnly, - EuiI18nNumber, - EuiText, - EuiCodeBlock, - EuiToolTip, - RIGHT_ALIGNMENT, -} from '@elastic/eui'; -import { euiStyled, css } from '@kbn/kibana-react-plugin/common'; +import { EuiBasicTable, EuiEmptyPrompt, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; -import type { HorizontalAlignment, CriteriaWithPagination } from '@elastic/eui'; +import type { CriteriaWithPagination } from '@elastic/eui'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import type { ResponseActions, ResponseActionStatus, } from '../../../../common/endpoint/service/response_actions/constants'; -import { getEmptyValue } from '../../../common/components/empty_value'; -import { FormattedDate } from '../../../common/components/formatted_date'; + import type { ActionListApiResponse } from '../../../../common/endpoint/types'; import type { EndpointActionListRequestQuery } from '../../../../common/endpoint/schema/actions'; import { ManagementEmptyStateWrapper } from '../management_empty_state_wrapper'; import { useGetEndpointActionList } from '../../hooks'; -import { OUTPUT_MESSAGES, TABLE_COLUMN_NAMES, UX_MESSAGES } from './translations'; -import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../common/constants'; +import { UX_MESSAGES } from './translations'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; import { ActionsLogFilters } from './components/actions_log_filters'; -import { - getActionStatus, - getUiCommand, - getCommandKey, - useDateRangePicker, -} from './components/hooks'; -import { StatusBadge } from './components/status_badge'; +import { getCommandKey, useDateRangePicker } from './components/hooks'; import { useActionHistoryUrlParams } from './components/use_action_history_url_params'; import { useUrlPagination } from '../../hooks/use_url_pagination'; import { ManagementPageLoader } from '../management_page_loader'; import { ActionsLogEmptyState } from './components/actions_log_empty_state'; - -const emptyValue = getEmptyValue(); - -// Truncated usernames -const StyledFacetButton = euiStyled(EuiFacetButton)` - .euiText { - margin-top: 0.38rem; - overflow-y: visible !important; - } -`; - -const customDescriptionListCss = css` - &.euiDescriptionList { - > .euiDescriptionList__title { - color: ${(props) => props.theme.eui.euiColorDarkShade}; - font-size: ${(props) => props.theme.eui.euiFontSizeXS}; - margin-top: ${(props) => props.theme.eui.euiSizeS}; - } - - > .euiDescriptionList__description { - font-weight: ${(props) => props.theme.eui.euiFontWeightSemiBold}; - margin-top: ${(props) => props.theme.eui.euiSizeS}; - } - } -`; - -const StyledDescriptionList = euiStyled(EuiDescriptionList).attrs({ - compressed: true, - type: 'column', -})` - ${customDescriptionListCss} -`; - -// output section styles -const topSpacingCss = css` - ${(props) => `${props.theme.eui.euiCodeBlockPaddingModifiers.paddingMedium} 0`} -`; -const dashedBorderCss = css` - ${(props) => `1px dashed ${props.theme.eui.euiColorDisabled}`}; -`; -const StyledDescriptionListOutput = euiStyled(EuiDescriptionList).attrs({ compressed: true })` - ${customDescriptionListCss} - dd { - margin: ${topSpacingCss}; - padding: ${topSpacingCss}; - border-top: ${dashedBorderCss}; - border-bottom: ${dashedBorderCss}; - } -`; - -// code block styles -const StyledEuiCodeBlock = euiStyled(EuiCodeBlock).attrs({ - transparentBackground: true, - paddingSize: 'none', -})` - code { - color: ${(props) => props.theme.eui.euiColorDarkShade} !important; - } -`; +import { useResponseActionsLogTable } from './use_response_actions_log_table'; export const ResponseActionsLog = memo< Pick & { @@ -131,9 +48,6 @@ export const ResponseActionsLog = memo< } = useActionHistoryUrlParams(); const getTestId = useTestIdGenerator('response-actions-list'); - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{ - [k: ActionListApiResponse['data'][number]['id']]: React.ReactNode; - }>({}); // Used to decide if display global loader or not (only the fist time tha page loads) const [isFirstAttempt, setIsFirstAttempt] = useState(true); @@ -183,6 +97,19 @@ export const ResponseActionsLog = memo< { retry: false } ); + // total actions + const totalItemCount = useMemo(() => actionList?.total ?? 0, [actionList]); + + // table columns and expanded row state + const { itemIdToExpandedRowMap, recordRangeLabel, responseActionListColumns, tablePagination } = + useResponseActionsLogTable({ + showHostNames, + pageIndex: isFlyout ? (queryParams.page || 1) - 1 : paginationFromUrlParams.page - 1, + pageSize: isFlyout ? queryParams.pageSize || 10 : paginationFromUrlParams.pageSize, + queryParams, + totalItemCount, + }); + // Hide page header when there is no actions index calling the setIsDataInResponse with false value. // Otherwise, it shows the page header calling the setIsDataInResponse with true value and it also keeps track // if the API request was done for the first time. @@ -248,310 +175,6 @@ export const ResponseActionsLog = memo< [setQueryParams] ); - // total actions - const totalItemCount = useMemo(() => actionList?.total ?? 0, [actionList]); - - // expanded tray contents - const toggleDetails = useCallback( - (item: ActionListApiResponse['data'][number]) => { - const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - if (itemIdToExpandedRowMapValues[item.id]) { - delete itemIdToExpandedRowMapValues[item.id]; - } else { - const { - startedAt, - completedAt, - isCompleted, - wasSuccessful, - isExpired, - command: _command, - parameters, - } = item; - - const parametersList = parameters - ? Object.entries(parameters).map(([key, value]) => { - return `${key}:${value}`; - }) - : undefined; - - const command = getUiCommand(_command); - const dataList = [ - { - title: OUTPUT_MESSAGES.expandSection.placedAt, - description: `${startedAt}`, - }, - { - title: OUTPUT_MESSAGES.expandSection.startedAt, - description: `${startedAt}`, - }, - { - title: OUTPUT_MESSAGES.expandSection.completedAt, - description: `${completedAt ?? emptyValue}`, - }, - { - title: OUTPUT_MESSAGES.expandSection.input, - description: `${command}`, - }, - { - title: OUTPUT_MESSAGES.expandSection.parameters, - description: parametersList ? parametersList : emptyValue, - }, - ].map(({ title, description }) => { - return { - title: {title}, - description: {description}, - }; - }); - - const outputList = [ - { - title: ( - {`${OUTPUT_MESSAGES.expandSection.output}:`} - ), - description: ( - // codeblock for output - - {isExpired - ? OUTPUT_MESSAGES.hasExpired(command) - : isCompleted - ? wasSuccessful - ? OUTPUT_MESSAGES.wasSuccessful(command) - : OUTPUT_MESSAGES.hasFailed(command) - : OUTPUT_MESSAGES.isPending(command)} - - ), - }, - ]; - - itemIdToExpandedRowMapValues[item.id] = ( - <> - - - - - - - - - - ); - } - setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); - }, - [getTestId, itemIdToExpandedRowMap] - ); - // memoized callback for toggleDetails - const onClickCallback = useCallback( - (actionListDataItem: ActionListApiResponse['data'][number]) => () => - toggleDetails(actionListDataItem), - [toggleDetails] - ); - - // table column - const responseActionListColumns = useMemo(() => { - const columns = [ - { - field: 'startedAt', - name: TABLE_COLUMN_NAMES.time, - width: !showHostNames ? '21%' : '15%', - truncateText: true, - render: (startedAt: ActionListApiResponse['data'][number]['startedAt']) => { - return ( - - ); - }, - }, - { - field: 'command', - name: TABLE_COLUMN_NAMES.command, - width: !showHostNames ? '21%' : '10%', - truncateText: true, - render: (_command: ActionListApiResponse['data'][number]['command']) => { - const command = getUiCommand(_command); - return ( - - - {command} - - - ); - }, - }, - { - field: 'createdBy', - name: TABLE_COLUMN_NAMES.user, - width: !showHostNames ? '21%' : '14%', - truncateText: true, - render: (userId: ActionListApiResponse['data'][number]['createdBy']) => { - return ( - - } - > - - - {userId} - - - - ); - }, - }, - // conditional hostnames column - { - field: 'hosts', - name: TABLE_COLUMN_NAMES.hosts, - width: '20%', - truncateText: true, - render: (_hosts: ActionListApiResponse['data'][number]['hosts']) => { - const hosts = _hosts && Object.values(_hosts); - // join hostnames if the action is for multiple agents - // and skip empty strings for names if any - const _hostnames = hosts - .reduce((acc, host) => { - if (host.name.trim()) { - acc.push(host.name); - } - return acc; - }, []) - .join(', '); - - let hostnames = _hostnames; - if (!_hostnames) { - if (hosts.length > 1) { - // when action was for a single agent and no host name - hostnames = UX_MESSAGES.unenrolled.hosts; - } else if (hosts.length === 1) { - // when action was for a multiple agents - // and none of them have a host name - hostnames = UX_MESSAGES.unenrolled.host; - } - } - return ( - - - {hostnames} - - - ); - }, - }, - { - field: 'comment', - name: TABLE_COLUMN_NAMES.comments, - width: !showHostNames ? '21%' : '30%', - truncateText: true, - render: (comment: ActionListApiResponse['data'][number]['comment']) => { - return ( - - - {comment ?? emptyValue} - - - ); - }, - }, - { - field: 'status', - name: TABLE_COLUMN_NAMES.status, - width: !showHostNames ? '15%' : '10%', - render: (_status: ActionListApiResponse['data'][number]['status']) => { - const status = getActionStatus(_status); - - return ( - - - - ); - }, - }, - { - field: '', - align: RIGHT_ALIGNMENT as HorizontalAlignment, - width: '40px', - isExpander: true, - name: ( - - {UX_MESSAGES.screenReaderExpand} - - ), - render: (actionListDataItem: ActionListApiResponse['data'][number]) => { - return ( - - ); - }, - }, - ]; - // filter out the `hosts` column - // if showHostNames is FALSE - if (!showHostNames) { - return columns.filter((column) => column.field !== 'hosts'); - } - return columns; - }, [showHostNames, getTestId, itemIdToExpandedRowMap, onClickCallback]); - - // table pagination - const tablePagination = useMemo(() => { - const pageIndex = isFlyout ? (queryParams.page || 1) - 1 : paginationFromUrlParams.page - 1; - const pageSize = isFlyout ? queryParams.pageSize || 10 : paginationFromUrlParams.pageSize; - return { - // this controls the table UI page - // to match 0-based table paging - pageIndex, - pageSize, - totalItemCount, - pageSizeOptions: MANAGEMENT_PAGE_SIZE_OPTIONS as number[], - }; - }, [ - isFlyout, - paginationFromUrlParams.page, - paginationFromUrlParams.pageSize, - queryParams.page, - queryParams.pageSize, - totalItemCount, - ]); - // handle onChange const handleTableOnChange = useCallback( ({ page: _page }: CriteriaWithPagination) => { @@ -563,16 +186,12 @@ export const ResponseActionsLog = memo< page: index + 1, pageSize: size, }; - if (isFlyout) { - setQueryParams((prevState) => ({ - ...prevState, - ...pagingArgs, - })); - } else { - setQueryParams((prevState) => ({ - ...prevState, - ...pagingArgs, - })); + + setQueryParams((prevState) => ({ + ...prevState, + ...pagingArgs, + })); + if (!isFlyout) { setPaginationOnUrlParams({ ...pagingArgs, }); @@ -582,42 +201,6 @@ export const ResponseActionsLog = memo< [isFlyout, reFetchEndpointActionList, setQueryParams, setPaginationOnUrlParams] ); - // compute record ranges - const pagedResultsCount = useMemo(() => { - const page = queryParams.page ?? 1; - const perPage = queryParams?.pageSize ?? 10; - - const totalPages = Math.ceil(totalItemCount / perPage); - const fromCount = perPage * page - perPage + 1; - const toCount = - page === totalPages || totalPages === 1 ? totalItemCount : fromCount + perPage - 1; - return { fromCount, toCount }; - }, [queryParams.page, queryParams.pageSize, totalItemCount]); - - // create range label to display - const recordRangeLabel = useMemo( - () => ( - - - - {'-'} - - - ), - total: , - recordsLabel: {UX_MESSAGES.recordsLabel(totalItemCount)}, - }} - /> - - ), - [getTestId, pagedResultsCount.fromCount, pagedResultsCount.toCount, totalItemCount] - ); - if (error?.body?.statusCode === 404 && error?.body?.message === 'index_not_found_exception') { return ; } else if (isFetching && isFirstAttempt) { @@ -675,7 +258,7 @@ export const ResponseActionsLog = memo< columns={responseActionListColumns} itemId="id" itemIdToExpandedRowMap={itemIdToExpandedRowMap} - isExpandable={true} + isExpandable pagination={tablePagination} onChange={handleTableOnChange} loading={isFetching} diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/use_response_actions_log_table.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/use_response_actions_log_table.tsx new file mode 100644 index 000000000000..e4dd30b46812 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/use_response_actions_log_table.tsx @@ -0,0 +1,441 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useMemo, useState } from 'react'; +import type { HorizontalAlignment } from '@elastic/eui'; + +import { + EuiI18nNumber, + EuiAvatar, + EuiButtonIcon, + EuiCodeBlock, + EuiDescriptionList, + EuiFacetButton, + EuiFlexGroup, + EuiFlexItem, + RIGHT_ALIGNMENT, + EuiScreenReaderOnly, + EuiText, + EuiToolTip, +} from '@elastic/eui'; +import { css, euiStyled } from '@kbn/kibana-react-plugin/common'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { ActionListApiResponse } from '../../../../common/endpoint/types'; +import type { EndpointActionListRequestQuery } from '../../../../common/endpoint/schema/actions'; +import { FormattedDate } from '../../../common/components/formatted_date'; +import { OUTPUT_MESSAGES, TABLE_COLUMN_NAMES, UX_MESSAGES } from './translations'; +import { getActionStatus, getUiCommand } from './components/hooks'; +import { getEmptyValue } from '../../../common/components/empty_value'; +import { StatusBadge } from './components/status_badge'; +import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; +import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../common/constants'; + +const emptyValue = getEmptyValue(); + +// Truncated usernames +const StyledFacetButton = euiStyled(EuiFacetButton)` + .euiText { + margin-top: 0.38rem; + overflow-y: visible !important; + } +`; + +const customDescriptionListCss = css` + &.euiDescriptionList { + > .euiDescriptionList__title { + color: ${(props) => props.theme.eui.euiColorDarkShade}; + font-size: ${(props) => props.theme.eui.euiFontSizeXS}; + margin-top: ${(props) => props.theme.eui.euiSizeS}; + } + + > .euiDescriptionList__description { + font-weight: ${(props) => props.theme.eui.euiFontWeightSemiBold}; + margin-top: ${(props) => props.theme.eui.euiSizeS}; + } + } +`; + +const StyledDescriptionList = euiStyled(EuiDescriptionList).attrs({ + compressed: true, + type: 'column', +})` + ${customDescriptionListCss} +`; + +// output section styles +const topSpacingCss = css` + ${(props) => `${props.theme.eui.euiCodeBlockPaddingModifiers.paddingMedium} 0`} +`; +const dashedBorderCss = css` + ${(props) => `1px dashed ${props.theme.eui.euiColorDisabled}`}; +`; +const StyledDescriptionListOutput = euiStyled(EuiDescriptionList).attrs({ compressed: true })` + ${customDescriptionListCss} + dd { + margin: ${topSpacingCss}; + padding: ${topSpacingCss}; + border-top: ${dashedBorderCss}; + border-bottom: ${dashedBorderCss}; + } +`; + +// code block styles +const StyledEuiCodeBlock = euiStyled(EuiCodeBlock).attrs({ + transparentBackground: true, + paddingSize: 'none', +})` + code { + color: ${(props) => props.theme.eui.euiColorDarkShade} !important; + } +`; + +export const useResponseActionsLogTable = ({ + pageIndex, + pageSize, + queryParams, + showHostNames, + totalItemCount, +}: { + pageIndex: number; + pageSize: number; + queryParams: EndpointActionListRequestQuery; + showHostNames: boolean; + totalItemCount: number; +}) => { + const getTestId = useTestIdGenerator('response-actions-list'); + + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{ + [k: ActionListApiResponse['data'][number]['id']]: React.ReactNode; + }>({}); + + // expanded tray contents + const toggleDetails = useCallback( + (item: ActionListApiResponse['data'][number]) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + if (itemIdToExpandedRowMapValues[item.id]) { + delete itemIdToExpandedRowMapValues[item.id]; + } else { + const { + startedAt, + completedAt, + isCompleted, + wasSuccessful, + isExpired, + command: _command, + parameters, + } = item; + + const parametersList = parameters + ? Object.entries(parameters).map(([key, value]) => { + return `${key}:${value}`; + }) + : undefined; + + const command = getUiCommand(_command); + const dataList = [ + { + title: OUTPUT_MESSAGES.expandSection.placedAt, + description: `${startedAt}`, + }, + { + title: OUTPUT_MESSAGES.expandSection.startedAt, + description: `${startedAt}`, + }, + { + title: OUTPUT_MESSAGES.expandSection.completedAt, + description: `${completedAt ?? emptyValue}`, + }, + { + title: OUTPUT_MESSAGES.expandSection.input, + description: `${command}`, + }, + { + title: OUTPUT_MESSAGES.expandSection.parameters, + description: parametersList ? parametersList : emptyValue, + }, + ].map(({ title, description }) => { + return { + title: {title}, + description: {description}, + }; + }); + + const outputList = [ + { + title: ( + {`${OUTPUT_MESSAGES.expandSection.output}:`} + ), + description: ( + // codeblock for output + + {isExpired + ? OUTPUT_MESSAGES.hasExpired(command) + : isCompleted + ? wasSuccessful + ? OUTPUT_MESSAGES.wasSuccessful(command) + : OUTPUT_MESSAGES.hasFailed(command) + : OUTPUT_MESSAGES.isPending(command)} + + ), + }, + ]; + + itemIdToExpandedRowMapValues[item.id] = ( + <> + + + + + + + + + + ); + } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }, + [getTestId, itemIdToExpandedRowMap] + ); + // memoized callback for toggleDetails + const onClickCallback = useCallback( + (actionListDataItem: ActionListApiResponse['data'][number]) => () => + toggleDetails(actionListDataItem), + [toggleDetails] + ); + + const responseActionListColumns = useMemo(() => { + const columns = [ + { + field: 'startedAt', + name: TABLE_COLUMN_NAMES.time, + width: !showHostNames ? '21%' : '15%', + truncateText: true, + render: (startedAt: ActionListApiResponse['data'][number]['startedAt']) => { + return ( + + ); + }, + }, + { + field: 'command', + name: TABLE_COLUMN_NAMES.command, + width: !showHostNames ? '21%' : '10%', + truncateText: true, + render: (_command: ActionListApiResponse['data'][number]['command']) => { + const command = getUiCommand(_command); + return ( + + + {command} + + + ); + }, + }, + { + field: 'createdBy', + name: TABLE_COLUMN_NAMES.user, + width: !showHostNames ? '21%' : '14%', + truncateText: true, + render: (userId: ActionListApiResponse['data'][number]['createdBy']) => { + return ( + + } + > + + + {userId} + + + + ); + }, + }, + // conditional hostnames column + { + field: 'hosts', + name: TABLE_COLUMN_NAMES.hosts, + width: '20%', + truncateText: true, + render: (_hosts: ActionListApiResponse['data'][number]['hosts']) => { + const hosts = _hosts && Object.values(_hosts); + // join hostnames if the action is for multiple agents + // and skip empty strings for names if any + const _hostnames = hosts + .reduce((acc, host) => { + if (host.name.trim()) { + acc.push(host.name); + } + return acc; + }, []) + .join(', '); + + let hostnames = _hostnames; + if (!_hostnames) { + if (hosts.length > 1) { + // when action was for a single agent and no host name + hostnames = UX_MESSAGES.unenrolled.hosts; + } else if (hosts.length === 1) { + // when action was for a multiple agents + // and none of them have a host name + hostnames = UX_MESSAGES.unenrolled.host; + } + } + return ( + + + {hostnames} + + + ); + }, + }, + { + field: 'comment', + name: TABLE_COLUMN_NAMES.comments, + width: !showHostNames ? '21%' : '30%', + truncateText: true, + render: (comment: ActionListApiResponse['data'][number]['comment']) => { + return ( + + + {comment ?? emptyValue} + + + ); + }, + }, + { + field: 'status', + name: TABLE_COLUMN_NAMES.status, + width: !showHostNames ? '15%' : '10%', + render: (_status: ActionListApiResponse['data'][number]['status']) => { + const status = getActionStatus(_status); + + return ( + + + + ); + }, + }, + { + field: '', + align: RIGHT_ALIGNMENT as HorizontalAlignment, + width: '40px', + isExpander: true, + name: ( + + {UX_MESSAGES.screenReaderExpand} + + ), + render: (actionListDataItem: ActionListApiResponse['data'][number]) => { + return ( + + ); + }, + }, + ]; + // filter out the `hosts` column + // if showHostNames is FALSE + if (!showHostNames) { + return columns.filter((column) => column.field !== 'hosts'); + } + return columns; + }, [showHostNames, getTestId, itemIdToExpandedRowMap, onClickCallback]); + + // table pagination + const tablePagination = useMemo(() => { + return { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: MANAGEMENT_PAGE_SIZE_OPTIONS as number[], + }; + }, [pageIndex, pageSize, totalItemCount]); + + // compute record ranges + const pagedResultsCount = useMemo(() => { + const page = queryParams.page ?? 1; + const perPage = queryParams?.pageSize ?? 10; + + const totalPages = Math.ceil(totalItemCount / perPage); + const fromCount = perPage * page - perPage + 1; + const toCount = + page === totalPages || totalPages === 1 ? totalItemCount : fromCount + perPage - 1; + return { fromCount, toCount }; + }, [queryParams.page, queryParams.pageSize, totalItemCount]); + + // create range label to display + const recordRangeLabel = useMemo( + () => ( + + + + {'-'} + + + ), + total: , + recordsLabel: {UX_MESSAGES.recordsLabel(totalItemCount)}, + }} + /> + + ), + [getTestId, pagedResultsCount.fromCount, pagedResultsCount.toCount, totalItemCount] + ); + + return { itemIdToExpandedRowMap, responseActionListColumns, recordRangeLabel, tablePagination }; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/page_overlay/page_overlay.tsx b/x-pack/plugins/security_solution/public/management/components/page_overlay/page_overlay.tsx index b5c7629b76aa..2d3a9c9cb7f0 100644 --- a/x-pack/plugins/security_solution/public/management/components/page_overlay/page_overlay.tsx +++ b/x-pack/plugins/security_solution/public/management/components/page_overlay/page_overlay.tsx @@ -13,6 +13,7 @@ import classnames from 'classnames'; import { useLocation } from 'react-router-dom'; import type { EuiPortalProps } from '@elastic/eui/src/components/portal/portal'; import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; +import { useIsMounted } from '@kbn/securitysolution-hook-utils'; import { useHasFullScreenContent } from '../../../common/containers/use_full_screen'; import { FULL_SCREEN_CONTENT_OVERRIDES_CSS_STYLESHEET, @@ -22,7 +23,6 @@ import { SELECTOR_TIMELINE_IS_VISIBLE_CSS_CLASS_NAME, TIMELINE_EUI_THEME_ZINDEX_LEVEL, } from '../../../timelines/components/timeline/styles'; -import { useIsMounted } from '../../hooks/use_is_mounted'; const OverlayRootContainer = styled.div` border: none; @@ -246,7 +246,7 @@ export const PageOverlay = memo( // Capture the URL `pathname` that the overlay was opened for useEffect(() => { - if (isMounted) { + if (isMounted()) { setOpenedOnPathName((prevState) => { if (isHidden) { return null; @@ -270,7 +270,7 @@ export const PageOverlay = memo( // If `hideOnUrlPathNameChange` is true, then determine if the pathname changed and if so, call `onHide()` useEffect(() => { if ( - isMounted && + isMounted() && onHide && hideOnUrlPathnameChange && !isHidden && @@ -283,7 +283,7 @@ export const PageOverlay = memo( // Handle adding class names to the `document.body` DOM element useEffect(() => { - if (isMounted) { + if (isMounted()) { if (isHidden) { unSetDocumentBodyOverlayIsVisible(); unSetDocumentBodyLock(); diff --git a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts index 0804bef55b3e..cdb1041cda7e 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoints_list.test.ts @@ -11,6 +11,8 @@ import { useGetEndpointsList } from './use_get_endpoints_list'; import { HOST_METADATA_LIST_ROUTE } from '../../../../common/endpoint/constants'; import { useQuery as _useQuery } from '@tanstack/react-query'; import { endpointMetadataHttpMocks } from '../../pages/endpoint_hosts/mocks'; +import { EndpointStatus, HostStatus } from '../../../../common/endpoint/types'; +import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; const useQueryMock = _useQuery as jest.Mock; @@ -107,4 +109,127 @@ describe('useGetEndpointsList hook', () => { }) ); }); + + it('should also list inactive agents', async () => { + const getApiResponse = apiMocks.responseProvider.metadataList.getMockImplementation(); + + // set a few of the agents as inactive/unenrolled + apiMocks.responseProvider.metadataList.mockImplementation(() => { + if (getApiResponse) { + return { + ...getApiResponse(), + data: getApiResponse().data.map((item, i) => { + const isInactiveIndex = [0, 1, 3].includes(i); + return { + ...item, + host_status: isInactiveIndex ? HostStatus.INACTIVE : item.host_status, + metadata: { + ...item.metadata, + host: { + ...item.metadata.host, + hostname: isInactiveIndex + ? `${item.metadata.host.hostname}-inactive` + : item.metadata.host.hostname, + }, + Endpoint: { + ...item.metadata.Endpoint, + status: isInactiveIndex + ? EndpointStatus.unenrolled + : item.metadata.Endpoint.status, + }, + }, + }; + }), + }; + } + throw new Error('some error'); + }); + + // verify useGetEndpointsList hook returns the same inactive agents + const res = await renderReactQueryHook(() => useGetEndpointsList({ searchString: 'inactive' })); + expect( + res.data?.map((host) => host.name.split('-')[2]).filter((name) => name === 'inactive').length + ).toEqual(3); + }); + + it('should only list 50 agents when more than 50 in the metadata list API', async () => { + const getApiResponse = apiMocks.responseProvider.metadataList.getMockImplementation(); + + apiMocks.responseProvider.metadataList.mockImplementation(() => { + if (getApiResponse) { + const generator = new EndpointDocGenerator('seed'); + const total = 60; + const data = Array.from({ length: total }, () => { + const endpoint = { + metadata: generator.generateHostMetadata(), + host_status: HostStatus.UNHEALTHY, + }; + + generator.updateCommonInfo(); + + return endpoint; + }); + + return { + ...getApiResponse(), + data, + page: 0, + // this page size is not used by the hook (it uses the default of 50) + // this is only for the test + pageSize: 80, + total, + }; + } + throw new Error('some error'); + }); + + // verify useGetEndpointsList hook returns all 50 agents in the list + const res = await renderReactQueryHook(() => useGetEndpointsList({ searchString: '' })); + expect(res.data?.length).toEqual(50); + }); + + it('should only list 10 more agents when 50 or more agents are already selected', async () => { + const getApiResponse = apiMocks.responseProvider.metadataList.getMockImplementation(); + + apiMocks.responseProvider.metadataList.mockImplementation(() => { + if (getApiResponse) { + const generator = new EndpointDocGenerator('seed'); + const total = 61; + const data = Array.from({ length: total }, () => { + const endpoint = { + metadata: generator.generateHostMetadata(), + host_status: HostStatus.UNHEALTHY, + }; + + generator.updateCommonInfo(); + + return endpoint; + }); + + return { + ...getApiResponse(), + data, + page: 0, + // since we're mocking that all 50 agents are selected + // page size is set to max allowed + pageSize: 10000, + total, + }; + } + throw new Error('some error'); + }); + + // get the first 50 agents to select + const agentIdsToSelect = apiMocks.responseProvider + .metadataList() + .data.map((d) => d.metadata.agent.id) + .slice(0, 50); + + // call useGetEndpointsList with all 50 agents selected + const res = await renderReactQueryHook(() => + useGetEndpointsList({ searchString: '', selectedAgentIds: agentIdsToSelect }) + ); + // verify useGetEndpointsList hook returns 60 agents + expect(res.data?.length).toEqual(60); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/hooks/use_is_mounted.ts b/x-pack/plugins/security_solution/public/management/hooks/use_is_mounted.ts deleted file mode 100644 index 0c5a79b2ca2f..000000000000 --- a/x-pack/plugins/security_solution/public/management/hooks/use_is_mounted.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState } from 'react'; - -/** - * Track when a component is mounted/unmounted. Good for use in async processing that may update - * a component's internal state. - */ -export const useIsMounted = (): boolean => { - const [isMounted, setIsMounted] = useState(false); - - useEffect(() => { - setIsMounted(true); - - return () => { - setIsMounted(false); - }; - }, []); - - return isMounted; -}; diff --git a/x-pack/plugins/security_solution/public/management/components/mocks.tsx b/x-pack/plugins/security_solution/public/management/mocks/utils.ts similarity index 93% rename from x-pack/plugins/security_solution/public/management/components/mocks.tsx rename to x-pack/plugins/security_solution/public/management/mocks/utils.ts index 45c12df818fd..946d2d50b05d 100644 --- a/x-pack/plugins/security_solution/public/management/components/mocks.tsx +++ b/x-pack/plugins/security_solution/public/management/mocks/utils.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -interface DeferredInterface { + +export interface DeferredInterface { promise: Promise; resolve: (data: T) => void; reject: (e: Error) => void; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx index 6fc8a99ee732..35fe96d94d91 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx @@ -14,6 +14,9 @@ import { act } from '@testing-library/react'; import { endpointPageHttpMock } from '../../../mocks'; import { fireEvent } from '@testing-library/dom'; import { licenseService } from '../../../../../../common/hooks/use_license'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; +import { initialUserPrivilegesState } from '../../../../../../common/components/user_privileges/user_privileges_context'; +import { getUserPrivilegesMockDefaultValue } from '../../../../../../common/components/user_privileges/__mocks__'; jest.mock('../../../../../../common/lib/kibana/kibana_react', () => { const originalModule = jest.requireActual('../../../../../../common/lib/kibana/kibana_react'); @@ -31,6 +34,7 @@ jest.mock('../../../../../../common/lib/kibana/kibana_react', () => { }; }); jest.mock('../../../../../../common/hooks/use_license'); +jest.mock('../../../../../../common/components/user_privileges'); describe('When using the Endpoint Details Actions Menu', () => { let render: () => Promise>; @@ -59,6 +63,8 @@ describe('When using the Endpoint Details Actions Menu', () => { waitForAction = mockedContext.middlewareSpy.waitForAction; httpMocks = endpointPageHttpMock(mockedContext.coreStart.http); + (useUserPrivileges as jest.Mock).mockReturnValue(getUserPrivilegesMockDefaultValue()); + act(() => { mockedContext.history.push( '/administration/endpoints?selected_endpoint=5fe11314-678c-413e-87a2-b4a3461878ee' @@ -80,6 +86,10 @@ describe('When using the Endpoint Details Actions Menu', () => { }; }); + afterEach(() => { + (useUserPrivileges as jest.Mock).mockClear(); + }); + it('should not show the response actions history link', async () => { await render(); expect(renderResult.queryByTestId('actionsLink')).toBeNull(); @@ -121,18 +131,38 @@ describe('When using the Endpoint Details Actions Menu', () => { describe('and endpoint host is isolated', () => { beforeEach(() => setEndpointMetadataResponse(true)); - it('should display Unisolate action', async () => { - await render(); - expect(renderResult.getByTestId('unIsolateLink')).not.toBeNull(); + describe('and user has unisolate privilege', () => { + it('should display Unisolate action', async () => { + await render(); + expect(renderResult.getByTestId('unIsolateLink')).not.toBeNull(); + }); + + it('should navigate via router when unisolate is clicked', async () => { + await render(); + act(() => { + fireEvent.click(renderResult.getByTestId('unIsolateLink')); + }); + + expect(coreStart.application.navigateToApp).toHaveBeenCalled(); + }); }); - it('should navigate via router when unisolate is clicked', async () => { - await render(); - act(() => { - fireEvent.click(renderResult.getByTestId('unIsolateLink')); + describe('and user does not have unisolate privilege', () => { + beforeEach(() => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...initialUserPrivilegesState(), + endpointPrivileges: { + ...initialUserPrivilegesState().endpointPrivileges, + canIsolateHost: false, + canUnIsolateHost: false, + }, + }); }); - expect(coreStart.application.navigateToApp).toHaveBeenCalled(); + it('should not display unisolate action', async () => { + await render(); + expect(renderResult.queryByTestId('unIsolateLink')).toBeNull(); + }); }); }); @@ -143,12 +173,6 @@ describe('When using the Endpoint Details Actions Menu', () => { afterEach(() => licenseServiceMock.isPlatinumPlus.mockReturnValue(true)); - it('should not show the `isolate` action', async () => { - setEndpointMetadataResponse(); - await render(); - expect(renderResult.queryByTestId('isolateLink')).toBeNull(); - }); - it('should still show `unisolate` action for endpoints that are currently isolated', async () => { setEndpointMetadataResponse(true); await render(); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx index 40fd81c4ab58..9a9c884a3979 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx @@ -19,7 +19,6 @@ import { agentPolicies, uiQueryParams } from '../../store/selectors'; import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; import type { ContextMenuItemNavByRouterProps } from '../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router'; import { isEndpointHostIsolated } from '../../../../../common/utils/validators'; -import { useLicense } from '../../../../../common/hooks/use_license'; import { isIsolationSupported } from '../../../../../../common/endpoint/service/host_isolation/utils'; import { useDoesEndpointSupportResponder } from '../../../../../common/hooks/endpoint/use_does_endpoint_support_responder'; import { UPGRADE_ENDPOINT_FOR_RESPONDER } from '../../../../../common/translations'; @@ -36,7 +35,6 @@ export const useEndpointActionItems = ( endpointMetadata: MaybeImmutable | undefined, options?: Options ): ContextMenuItemNavByRouterProps[] => { - const isPlatinumPlus = useLicense().isPlatinumPlus(); const { getAppUrl } = useAppUrl(); const fleetAgentPolicies = useEndpointSelector(agentPolicies); const allCurrentUrlParams = useEndpointSelector(uiQueryParams); @@ -44,7 +42,8 @@ export const useEndpointActionItems = ( const isResponseActionsConsoleEnabled = useIsExperimentalFeatureEnabled( 'responseActionsConsoleEnabled' ); - const canAccessResponseConsole = useUserPrivileges().endpointPrivileges.canAccessResponseConsole; + const { canAccessResponseConsole, canIsolateHost, canUnIsolateHost } = + useUserPrivileges().endpointPrivileges; const isResponderCapabilitiesEnabled = useDoesEndpointSupportResponder(endpointMetadata); return useMemo(() => { @@ -82,8 +81,8 @@ export const useEndpointActionItems = ( const isolationActions = []; - if (isIsolated) { - // Un-isolate is always available to users regardless of license level + if (isIsolated && canUnIsolateHost) { + // Un-isolate is available to users regardless of license level if they have unisolate permissions isolationActions.push({ 'data-test-subj': 'unIsolateLink', icon: 'lockOpen', @@ -100,7 +99,7 @@ export const useEndpointActionItems = ( /> ), }); - } else if (isPlatinumPlus && isolationSupported) { + } else if (isolationSupported && canIsolateHost) { // For Platinum++ licenses, users also have ability to isolate isolationActions.push({ 'data-test-subj': 'isolateLink', @@ -260,10 +259,11 @@ export const useEndpointActionItems = ( endpointMetadata, fleetAgentPolicies, getAppUrl, - isPlatinumPlus, isResponseActionsConsoleEnabled, showEndpointResponseActionsConsole, options?.isEndpointList, isResponderCapabilitiesEnabled, + canIsolateHost, + canUnIsolateHost, ]); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index b85ad2cc7f6a..7d8d625d97e3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -1007,6 +1007,7 @@ describe('when on the endpoint list page', () => { let agentId: string; let agentPolicyId: string; let renderResult: ReturnType; + let endpointActionsButton: HTMLElement; // 2nd endpoint only has isolation capabilities const mockEndpointListApi = () => { @@ -1081,13 +1082,7 @@ describe('when on the endpoint list page', () => { beforeEach(async () => { mockEndpointListApi(); - (useUserPrivileges as jest.Mock).mockReturnValue({ - ...mockInitialUserPrivilegesState(), - endpointPrivileges: { - ...mockInitialUserPrivilegesState().endpointPrivileges, - canAccessResponseConsole: true, - }, - }); + (useUserPrivileges as jest.Mock).mockReturnValue(getUserPrivilegesMockDefaultValue()); reactTestingLibrary.act(() => { history.push(`${MANAGEMENT_PATH}/endpoints`); @@ -1097,9 +1092,7 @@ describe('when on the endpoint list page', () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies'); - const endpointActionsButton = ( - await renderResult.findAllByTestId('endpointTableRowActions') - )[0]; + endpointActionsButton = (await renderResult.findAllByTestId('endpointTableRowActions'))[0]; reactTestingLibrary.act(() => { reactTestingLibrary.fireEvent.click(endpointActionsButton); @@ -1108,7 +1101,6 @@ describe('when on the endpoint list page', () => { afterEach(() => { jest.clearAllMocks(); - (useUserPrivileges as jest.Mock).mockReturnValue(getUserPrivilegesMockDefaultValue()); }); it('shows the Responder option when all 3 processes capabilities are present in the endpoint', async () => { @@ -1141,6 +1133,24 @@ describe('when on the endpoint list page', () => { ); }); + it('hides isolate host option if canIsolateHost is false', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...mockInitialUserPrivilegesState(), + endpointPrivileges: { + ...mockInitialUserPrivilegesState().endpointPrivileges, + canIsolateHost: false, + }, + }); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(endpointActionsButton); + }); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(endpointActionsButton); + }); + const isolateLink = screen.queryByTestId('isolateLink'); + expect(isolateLink).toBeNull(); + }); + it('navigates to the Security Solution Host Details page', async () => { const hostLink = await renderResult.findByTestId('hostLink'); expect(hostLink.getAttribute('href')).toEqual( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx index 37f04ff804c1..b8f7a8c19fbc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/delete_modal/policy_artifacts_delete_modal.test.tsx @@ -20,7 +20,7 @@ import { PolicyArtifactsDeleteModal } from './policy_artifacts_delete_modal'; import { exceptionsListAllHttpMocks } from '../../../../../mocks/exceptions_list_http_mocks'; import { ExceptionsListApiClient } from '../../../../../services/exceptions_list/exceptions_list_api_client'; import { POLICY_ARTIFACT_DELETE_MODAL_LABELS } from './translations'; -import { getDeferred } from '../../../../../components/mocks'; +import { getDeferred } from '../../../../../mocks/utils'; const listType: Array = [ 'endpoint_events', diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.tsx index d1223ff14826..0617707505e5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.tsx @@ -8,7 +8,6 @@ import React, { memo, useState, useEffect, useRef, useCallback } from 'react'; import { EuiForm, - EuiCheckbox, EuiRadio, EuiSelect, EuiText, @@ -19,7 +18,6 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import styled from 'styled-components'; import type { PackagePolicyCreateExtensionComponentProps } from '@kbn/fleet-plugin/public'; -import { useLicense } from '../../../../../../common/hooks/use_license'; import { ALL_EVENTS, CLOUD_SECURITY, @@ -28,7 +26,6 @@ import { EDR_ESSENTIAL, ENDPOINT, INTERACTIVE_ONLY, - PREVENT_MALICIOUS_BEHAVIOR, } from './translations'; const PREFIX = 'endpoint_policy_create_extension'; @@ -70,11 +67,8 @@ const HelpTextWithPadding = styled.div` */ export const EndpointPolicyCreateExtension = memo( ({ newPolicy, onChange }) => { - const isPlatinumPlus = useLicense().isPlatinumPlus(); - // / Endpoint Radio Options (NGAV and EDRs) const [endpointPreset, setEndpointPreset] = useState('NGAV'); - const [behaviorProtectionChecked, setBehaviorProtectionChecked] = useState(false); const [selectedCloudEvent, setSelectedCloudEvent] = useState('ALL_EVENTS'); const [selectedEnvironment, setSelectedEnvironment] = useState('endpoint'); const initialRender = useRef(true); @@ -130,11 +124,6 @@ export const EndpointPolicyCreateExtension = memo) => { setSelectedEnvironment(e?.target?.value as Environment); @@ -170,9 +152,6 @@ export const EndpointPolicyCreateExtension = memo) => { setEndpointPreset(e.target.value as EndpointPreset); }, []); - const onChangeMaliciousBehavior = useCallback((e: React.ChangeEvent) => { - setBehaviorProtectionChecked((checked) => !checked); - }, []); const getEndpointPresetsProps = useCallback( (preset: EndpointPreset) => ({ @@ -217,7 +196,7 @@ export const EndpointPolicyCreateExtension = memo ), @@ -327,36 +306,6 @@ export const EndpointPolicyCreateExtension = memo - {isPlatinumPlus && ( - <> - - -

    - -

    -
    - - -

    - -

    -
    - - - - )} )} diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx index d8f087a7cc88..c1a52364a76f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx @@ -28,7 +28,7 @@ import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml import { useQueryInspector } from '../../../../common/components/page/manage_query'; const StyledEuiTitle = styled(EuiTitle)` - color: ${({ theme: { eui } }) => eui.euiColorVis9}; + color: ${({ theme: { eui } }) => eui.euiColorDanger}; `; const HOST_RISK_QUERY_ID = 'hostRiskScoreKpiQuery'; diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/index.tsx index 794b33c6b33e..49df0b1b0e45 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/host_risk_score/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React, { useEffect, useMemo, useState } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { RiskScoresDeprecated } from '../../../../common/components/risk_score/risk_score_deprecated'; @@ -35,6 +35,8 @@ import { EntityAnalyticsHostRiskScoreDisable } from '../../../../common/componen import { RiskScoreHeaderTitle } from '../../../../common/components/risk_score/risk_score_onboarding/risk_score_header_title'; import { RiskScoresNoDataDetected } from '../../../../common/components/risk_score/risk_score_onboarding/risk_score_no_data_detected'; import { useRefetchQueries } from '../../../../common/hooks/use_refetch_queries'; +import { Loader } from '../../../../common/components/loader'; +import { Panel } from '../../../../common/components/panel'; const TABLE_QUERY_ID = 'hostRiskDashboardTable'; const HOST_RISK_KPI_QUERY_ID = 'headerHostRiskScoreKpiQuery'; @@ -149,7 +151,7 @@ const EntityAnalyticsHostRiskScoresComponent = () => { return ( - + } titleSize="s" @@ -207,7 +209,10 @@ const EntityAnalyticsHostRiskScoresComponent = () => { )} - + {(isTableLoading || isKpiLoading) && ( + + )} + ); }; diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/index.tsx index 3b3905d4604a..034e62bb37ad 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/user_risk_score/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React, { useEffect, useMemo, useState } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { RiskScoresDeprecated } from '../../../../common/components/risk_score/risk_score_deprecated'; import { SeverityFilterGroup } from '../../../../common/components/severity/severity_filter_group'; @@ -35,6 +35,8 @@ import { EntityAnalyticsUserRiskScoreDisable } from '../../../../common/componen import { RiskScoreHeaderTitle } from '../../../../common/components/risk_score/risk_score_onboarding/risk_score_header_title'; import { RiskScoresNoDataDetected } from '../../../../common/components/risk_score/risk_score_onboarding/risk_score_no_data_detected'; import { useRefetchQueries } from '../../../../common/hooks/use_refetch_queries'; +import { Loader } from '../../../../common/components/loader'; +import { Panel } from '../../../../common/components/panel'; const TABLE_QUERY_ID = 'userRiskDashboardTable'; const USER_RISK_KPI_QUERY_ID = 'headerUserRiskScoreKpiQuery'; @@ -149,7 +151,7 @@ const EntityAnalyticsUserRiskScoresComponent = () => { return ( - + } titleSize="s" @@ -207,7 +209,10 @@ const EntityAnalyticsUserRiskScoresComponent = () => { )} - + {(isTableLoading || isKpiLoading) && ( + + )} + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index 004850ede7d8..7bc64b990064 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -7,12 +7,13 @@ import { mount } from 'enzyme'; import React from 'react'; - +import { TimelineId } from '../../../../../../common/types/timeline'; import { TestProviders, mockTimelineModel, mockTimelineData } from '../../../../../common/mock'; import { Actions, isAlert } from '.'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; +import { licenseService } from '../../../../../common/hooks/use_license'; jest.mock('../../../../../detections/components/user_info', () => ({ useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]), @@ -63,6 +64,19 @@ jest.mock('../../../../../common/lib/kibana', () => { }; }); +jest.mock('../../../../../common/hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + isEnterprise: jest.fn(() => false), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); + const defaultProps = { ariaRowindex: 2, checked: false, @@ -122,6 +136,7 @@ describe('Actions', () => { expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); }); + describe('Alert context menu enabled?', () => { test('it disables for eventType=raw', () => { const wrapper = mount( @@ -225,6 +240,65 @@ describe('Actions', () => { expect(wrapper.find('[data-test-subj="view-in-analyzer"]').exists()).toBe(false); }); + + test('it should not show session view button on action tabs for basic users', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entry_leader: { entity_id: ['test_id'] } }, + }; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="session-view-button"]').exists()).toEqual(false); + }); + + test('it should show session view button on action tabs when user access the session viewer via K8S dashboard', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entry_leader: { entity_id: ['test_id'] } }, + }; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="session-view-button"]').exists()).toEqual(true); + }); + + test('it should show session view button on action tabs for enterprise users', () => { + const licenseServiceMock = licenseService as jest.Mocked; + + licenseServiceMock.isEnterprise.mockReturnValue(true); + + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entry_leader: { entity_id: ['test_id'] } }, + }; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="session-view-button"]').exists()).toEqual(true); + }); }); describe('isAlert', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts new file mode 100644 index 000000000000..51326c8adbd1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list_handler.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { KibanaResponseFactory, RequestHandler, RouteConfig } from '@kbn/core/server'; +import { + coreMock, + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, + loggingSystemMock, + savedObjectsClientMock, +} from '@kbn/core/server/mocks'; +import type { EndpointActionListRequestQuery } from '../../../../common/endpoint/schema/actions'; +import { ENDPOINTS_ACTION_LIST_ROUTE } from '../../../../common/endpoint/constants'; +import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; +import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; +import { + createMockEndpointAppContextServiceSetupContract, + createMockEndpointAppContextServiceStartContract, + createRouteHandlerContext, +} from '../../mocks'; +import { registerActionListRoutes } from './list'; +import type { SecuritySolutionRequestHandlerContext } from '../../../types'; +import { doesLogsEndpointActionsIndexExist } from '../../utils'; +import { getActionList, getActionListByStatus } from '../../services'; + +jest.mock('../../utils'); +const mockDoesLogsEndpointActionsIndexExist = doesLogsEndpointActionsIndexExist as jest.Mock; + +jest.mock('../../services'); +const mockGetActionList = getActionList as jest.Mock; +const mockGetActionListByStatus = getActionListByStatus as jest.Mock; + +describe(' Action List Handler', () => { + let endpointAppContextService: EndpointAppContextService; + let mockResponse: jest.Mocked; + + let actionListHandler: ( + query?: EndpointActionListRequestQuery + ) => Promise>; + + beforeEach(() => { + const esClientMock = elasticsearchServiceMock.createScopedClusterClient(); + const routerMock = httpServiceMock.createRouter(); + endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); + endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); + mockDoesLogsEndpointActionsIndexExist.mockResolvedValue(true); + + registerActionListRoutes(routerMock, { + logFactory: loggingSystemMock.create(), + service: endpointAppContextService, + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); + + actionListHandler = async ( + query?: EndpointActionListRequestQuery + ): Promise> => { + const req = httpServerMock.createKibanaRequest({ + query, + }); + mockResponse = httpServerMock.createResponseFactory(); + const [, routeHandler]: [ + RouteConfig, + RequestHandler< + unknown, + EndpointActionListRequestQuery, + unknown, + SecuritySolutionRequestHandlerContext + > + ] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith(ENDPOINTS_ACTION_LIST_ROUTE) + )!; + await routeHandler( + coreMock.createCustomRequestHandlerContext( + createRouteHandlerContext(esClientMock, savedObjectsClientMock.create()) + ) as SecuritySolutionRequestHandlerContext, + req, + mockResponse + ); + + return mockResponse; + }; + }); + + afterEach(() => { + endpointAppContextService.stop(); + }); + + describe('Internals', () => { + it('should return `notFound` when actions index does not exist', async () => { + mockDoesLogsEndpointActionsIndexExist.mockResolvedValue(false); + await actionListHandler({ pageSize: 10, page: 1 }); + expect(mockResponse.notFound).toHaveBeenCalledWith({ + body: 'index_not_found_exception', + }); + }); + + it('should return `ok` when actions index exists', async () => { + await actionListHandler({ pageSize: 10, page: 1 }); + expect(mockResponse.ok).toHaveBeenCalled(); + }); + + it('should call `getActionListByStatus` when statuses filter values are provided', async () => { + await actionListHandler({ pageSize: 10, page: 1, statuses: ['failed', 'pending'] }); + expect(mockGetActionListByStatus).toBeCalledWith( + expect.objectContaining({ statuses: ['failed', 'pending'] }) + ); + }); + + it('should correctly format the request when calling `getActionListByStatus`', async () => { + await actionListHandler({ + agentIds: 'agentX', + commands: 'running-processes', + statuses: 'failed', + userIds: 'userX', + }); + expect(mockGetActionListByStatus).toBeCalledWith( + expect.objectContaining({ + elasticAgentIds: ['agentX'], + commands: ['running-processes'], + statuses: ['failed'], + userIds: ['userX'], + }) + ); + }); + + it('should call `getActionList` when statuses filter values are not provided', async () => { + await actionListHandler({ + pageSize: 10, + page: 1, + commands: ['isolate', 'kill-process'], + userIds: ['userX', 'userY'], + }); + expect(mockGetActionList).toBeCalledWith( + expect.objectContaining({ + commands: ['isolate', 'kill-process'], + userIds: ['userX', 'userY'], + }) + ); + }); + + it('should correctly format the request when calling `getActionList`', async () => { + await actionListHandler({ + page: 1, + pageSize: 10, + agentIds: 'agentX', + commands: 'isolate', + userIds: 'userX', + }); + + expect(mockGetActionList).toHaveBeenCalledWith( + expect.objectContaining({ + commands: ['isolate'], + elasticAgentIds: ['agentX'], + userIds: ['userX'], + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts index 8ffd33e070d9..a2da989a9c08 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts @@ -13,11 +13,7 @@ import type { LicenseService } from '../../../common/license/license'; import { isAtLeast } from '../../../common/license/license'; import { ProtectionModes } from '../../../common/endpoint/types'; import type { PolicyConfig } from '../../../common/endpoint/types'; -import type { - AnyPolicyCreateConfig, - PolicyCreateCloudConfig, - PolicyCreateEndpointConfig, -} from '../types'; +import type { AnyPolicyCreateConfig, PolicyCreateEndpointConfig } from '../types'; import { ENDPOINT_CONFIG_PRESET_EDR_ESSENTIAL, ENDPOINT_CONFIG_PRESET_NGAV } from '../constants'; /** @@ -32,7 +28,7 @@ export const createDefaultPolicy = ( : policyConfigFactoryWithoutPaidFeatures(); if (config?.type === 'cloud') { - return getCloudPolicyWithIntegrationConfig(policy, config); + return getCloudPolicyConfig(policy); } return getEndpointPolicyWithIntegrationConfig(policy, config); @@ -95,37 +91,20 @@ const getEndpointPolicyWithIntegrationConfig = ( /** * Retrieve policy for cloud based on the on the cloud integration config */ -const getCloudPolicyWithIntegrationConfig = ( - policy: PolicyConfig, - config: PolicyCreateCloudConfig -): PolicyConfig => { - /** - * Check if the protection is supported, then retrieve Behavior Protection mode based on cloud settings - */ - const getBehaviorProtectionMode = () => { - if (!policy.linux.behavior_protection.supported) { - return ProtectionModes.off; - } - - return config.cloudConfig.preventions.behavior_protection - ? ProtectionModes.prevent - : ProtectionModes.off; - }; - +const getCloudPolicyConfig = (policy: PolicyConfig): PolicyConfig => { + // Disabling all protections, since it's not yet supported on Cloud integrations const protections = { - // Disabling memory_protection, since it's not supported on Cloud integrations memory_protection: { supported: false, mode: ProtectionModes.off, }, malware: { ...policy.linux.malware, - // Disabling Malware protection, since it's not supported on Cloud integrations due to performance reasons mode: ProtectionModes.off, }, behavior_protection: { ...policy.linux.behavior_protection, - mode: getBehaviorProtectionMode(), + mode: ProtectionModes.off, }, }; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_integration_config.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_integration_config.ts index 76bd3e8af93f..bc5835856581 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_integration_config.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/validate_integration_config.ts @@ -46,18 +46,6 @@ const validateEndpointIntegrationConfig = ( } }; const validateCloudIntegrationConfig = (config: PolicyCreateCloudConfig, logger: Logger): void => { - if (!config?.cloudConfig?.preventions) { - logger.warn( - 'missing cloudConfig preventions: {preventions : behavior_protection: true / false}' - ); - throwError('invalid value for cloudConfig: missing preventions '); - } - if (typeof config.cloudConfig.preventions.behavior_protection !== 'boolean') { - logger.warn( - `invalid value for cloudConfig preventions behavior_protection: ${config.cloudConfig.preventions.behavior_protection}` - ); - throwError('invalid value for cloudConfig preventions behavior_protection'); - } if (!config?.eventFilters) { logger.warn( `eventFilters is required for cloud integration: {eventFilters : nonInteractiveSession: true / false}` diff --git a/x-pack/plugins/security_solution/server/fleet_integration/types.ts b/x-pack/plugins/security_solution/server/fleet_integration/types.ts index 2d012832f1ae..0c7a7c17951e 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/types.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/types.ts @@ -18,12 +18,6 @@ export interface PolicyCreateEventFilters { export interface PolicyCreateCloudConfig { type: 'cloud'; - cloudConfig: { - preventions: { - malware: boolean; - behavior_protection: boolean; - }; - }; eventFilters?: PolicyCreateEventFilters; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index a147118b782d..15b6ffe47d34 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -182,7 +182,6 @@ export const previewRulesRoute = async ( | 'getState' | 'replaceState' | 'scheduleActions' - | 'scheduleActionsWithSubGroup' | 'setContext' | 'getContext' | 'hasContext' diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts index 4116848b1ffc..3c712847851f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts @@ -222,6 +222,7 @@ const normalizeStatusChangeArgs = (args: StatusChangeArgs): NormalizedStatusChan ? { total_search_duration_ms: normalizeDurations(metrics.searchDurations), total_indexing_duration_ms: normalizeDurations(metrics.indexingDurations), + total_enrichment_duration_ms: normalizeDurations(metrics.enrichmentDurations), execution_gap_duration_s: normalizeGap(metrics.executionGap), } : undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts index 22392e699fce..5e48a43e949c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts @@ -115,5 +115,6 @@ export interface StatusChangeArgs { export interface MetricsArgs { searchDurations?: string[]; indexingDurations?: string[]; + enrichmentDurations?: string[]; executionGap?: Duration; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_type.ts index ac3b28e87e0d..1e0a2e74cadc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_type.ts @@ -58,6 +58,9 @@ const ruleExecutionMappings: SavedObjectsType['mappings'] = { total_indexing_duration_ms: { type: 'long', }, + total_enrichment_duration_ms: { + type: 'long', + }, execution_gap_duration_s: { type: 'long', }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index ae3f310d841c..05813ed1ee29 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -343,6 +343,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const warningMessages = result.warningMessages.concat(runResult.warningMessages); result = { bulkCreateTimes: result.bulkCreateTimes.concat(runResult.bulkCreateTimes), + enrichmentTimes: result.enrichmentTimes.concat(runResult.enrichmentTimes), createdSignals, createdSignalsCount: createdSignals.length, errors: result.errors.concat(runResult.errors), @@ -358,6 +359,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } else { result = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignals: [], createdSignalsCount: 0, errors: [], @@ -434,6 +436,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = metrics: { searchDurations: result.searchAfterTimes, indexingDurations: result.bulkCreateTimes, + enrichmentDurations: result.enrichmentTimes, }, }); } @@ -452,6 +455,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = metrics: { searchDurations: result.searchAfterTimes, indexingDurations: result.bulkCreateTimes, + enrichmentDurations: result.enrichmentTimes, }, }); } @@ -464,6 +468,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = metrics: { searchDurations: result.searchAfterTimes, indexingDurations: result.bulkCreateTimes, + enrichmentDurations: result.enrichmentTimes, }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts index 81f15364ff12..95bc32571f6e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts @@ -21,6 +21,7 @@ import type { export interface GenericBulkCreateResponse { success: boolean; bulkCreateDuration: string; + enrichmentDuration: string; createdItemsCount: number; createdItems: Array & { _id: string; _index: string }>; errors: string[]; @@ -45,6 +46,7 @@ export const bulkCreateFactory = return { errors: [], success: true, + enrichmentDuration: '0', bulkCreateDuration: '0', createdItemsCount: 0, createdItems: [], @@ -54,6 +56,24 @@ export const bulkCreateFactory = const start = performance.now(); + let enrichmentsTimeStart = 0; + let enrichmentsTimeFinish = 0; + let enrichAlertsWrapper: typeof enrichAlerts; + if (enrichAlerts) { + enrichAlertsWrapper = async (alerts, params) => { + enrichmentsTimeStart = performance.now(); + try { + const enrichedAlerts = await enrichAlerts(alerts, params); + return enrichedAlerts; + } catch (error) { + ruleExecutionLogger.error(`Enrichments failed ${error}`); + throw error; + } finally { + enrichmentsTimeFinish = performance.now(); + } + }; + } + const { createdAlerts, errors, alertsWereTruncated } = await alertWithPersistence( wrappedDocs.map((doc) => ({ _id: doc._id, @@ -62,7 +82,7 @@ export const bulkCreateFactory = })), refreshForBulkCreate, maxAlerts, - enrichAlerts + enrichAlertsWrapper ); const end = performance.now(); @@ -78,6 +98,7 @@ export const bulkCreateFactory = return { errors: Object.keys(errors), success: false, + enrichmentDuration: makeFloatString(enrichmentsTimeFinish - enrichmentsTimeStart), bulkCreateDuration: makeFloatString(end - start), createdItemsCount: createdAlerts.length, createdItems: createdAlerts, @@ -88,6 +109,7 @@ export const bulkCreateFactory = errors: [], success: true, bulkCreateDuration: makeFloatString(end - start), + enrichmentDuration: makeFloatString(enrichmentsTimeFinish - enrichmentsTimeStart), createdItemsCount: createdAlerts.length, createdItems: createdAlerts, alertsWereTruncated, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index 403d6542f4f0..e1af3077ec3e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -42,6 +42,7 @@ import type { IRuleExecutionLogForExecutors, IRuleExecutionLogService } from '.. export interface SecurityAlertTypeReturnValue { bulkCreateTimes: string[]; + enrichmentTimes: string[]; createdSignalsCount: number; createdSignals: unknown[]; errors: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts index 5d2dcd4a4b3d..8f23da386f5c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts @@ -10,6 +10,7 @@ import type { SecurityAlertTypeReturnValue } from '../types'; export const createResultObject = (state: TState) => { const result: SecurityAlertTypeReturnValue = { + enrichmentTimes: [], bulkCreateTimes: [], createdSignalsCount: 0, createdSignals: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_edit_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_edit_rules.ts index 273d6697437b..75c7db48333c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_edit_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_edit_rules.ts @@ -5,27 +5,27 @@ * 2.0. */ +import type { BulkEditError, RulesClient } from '@kbn/alerting-plugin/server'; import pMap from 'p-map'; -import type { RulesClient, BulkEditError } from '@kbn/alerting-plugin/server'; import type { BulkActionEditPayload, BulkActionEditPayloadRuleActions, } from '../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { BulkActionEditType } from '../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { enrichFilterWithRuleTypeMapping } from './enrich_filter_with_rule_type_mappings'; import type { MlAuthz } from '../../machine_learning/authz'; import { ruleParamsModifier } from './bulk_actions/rule_params_modifier'; import { splitBulkEditActions } from './bulk_actions/split_bulk_edit_actions'; import { validateBulkEditRule } from './bulk_actions/validations'; import { bulkEditActionToRulesClientOperation } from './bulk_actions/action_to_rules_client_operation'; + +import type { RuleAlertType } from './types'; import { - NOTIFICATION_THROTTLE_NO_ACTIONS, MAX_RULES_TO_UPDATE_IN_PARALLEL, + NOTIFICATION_THROTTLE_NO_ACTIONS, } from '../../../../common/constants'; -import { BulkActionEditType } from '../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { readRules } from './read_rules'; -import type { RuleAlertType } from './types'; - export interface BulkEditRulesArguments { rulesClient: RulesClient; actions: BulkActionEditPayload[]; @@ -67,39 +67,30 @@ export const bulkEditRules = async ({ // rulesClient bulkEdit currently doesn't support bulk mute/unmute. // this is a workaround to mitigate this, // until https://github.com/elastic/kibana/issues/139084 is resolved - // if rule actions has been applied: - // - we go through each rule - // - mute/unmute if needed, refetch rule - // calling mute for rule needed only when rule was unmuted before and throttle value is NOTIFICATION_THROTTLE_NO_ACTIONS + // if rule actions has been applied, we go through each rule, unmute it if necessary and refetch it // calling unmute needed only if rule was muted and throttle value is not NOTIFICATION_THROTTLE_NO_ACTIONS const ruleActions = attributesActions.filter((rule): rule is BulkActionEditPayloadRuleActions => [BulkActionEditType.set_rule_actions, BulkActionEditType.add_rule_actions].includes(rule.type) ); - // bulk edit actions are applying in a historical order. + // bulk edit actions are applied in historical order. // So, we need to find a rule action that will be applied the last, to be able to check if rule should be muted/unmuted const rulesAction = ruleActions.pop(); if (rulesAction) { - const muteOrUnmuteErrors: BulkEditError[] = []; - const rulesToMuteOrUnmute = await pMap( + const unmuteErrors: BulkEditError[] = []; + const rulesToUnmute = await pMap( result.rules, async (rule) => { try { if (rule.muteAll && rulesAction.value.throttle !== NOTIFICATION_THROTTLE_NO_ACTIONS) { await rulesClient.unmuteAll({ id: rule.id }); return (await readRules({ rulesClient, id: rule.id, ruleId: undefined })) ?? rule; - } else if ( - !rule.muteAll && - rulesAction.value.throttle === NOTIFICATION_THROTTLE_NO_ACTIONS - ) { - await rulesClient.muteAll({ id: rule.id }); - return (await readRules({ rulesClient, id: rule.id, ruleId: undefined })) ?? rule; } return rule; } catch (err) { - muteOrUnmuteErrors.push({ + unmuteErrors.push({ message: err.message, rule: { id: rule.id, @@ -115,8 +106,8 @@ export const bulkEditRules = async ({ return { ...result, - rules: rulesToMuteOrUnmute.filter((rule): rule is RuleAlertType => rule != null), - errors: [...result.errors, ...muteOrUnmuteErrors], + rules: rulesToUnmute.filter((rule): rule is RuleAlertType => rule != null), + errors: [...result.errors, ...unmuteErrors], }; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index bc22ca9ea44d..521fdf1e5a59 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -97,13 +97,10 @@ export const getFilter = async ({ index, exceptionFilter, }); - } else if (savedId && index != null) { - // if savedId present and we ending up here, then saved query failed to be fetched - // and we also didn't fall back to saved in rule query - throw Error(`Failed to fetch saved query. "${err.message}"`); } else { // user did not give any additional fall back mechanism for generating a rule // rethrow error for activity monitoring + err.message = `Failed to fetch saved query. "${err.message}"`; throw err; } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts index b73119d15077..a3cf4ebc95d7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts @@ -37,16 +37,6 @@ export const alertInstanceFactoryStub = < meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); }, - scheduleActionsWithSubGroup( - actionGroup: TActionGroupIds, - subgroup: string, - alertcontext: TInstanceContext - ) { - return new Alert('', { - state: {} as TInstanceState, - meta: { lastScheduledActions: { group: 'default', date: new Date() } }, - }); - }, setContext(alertContext: TInstanceContext) { return new Alert('', { state: {} as TInstanceState, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index d80e5cba026b..0ab6ec2a01dd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -65,6 +65,7 @@ export const createThreatSignals = async ({ let results: SearchAfterAndBulkCreateReturnType = { success: true, warning: false, + enrichmentTimes: [], bulkCreateTimes: [], searchAfterTimes: [], lastLookBackDate: null, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts index 981868589e4a..0bcdc8450a83 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts @@ -55,6 +55,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -67,6 +68,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -83,6 +85,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -95,6 +98,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -111,6 +115,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -123,6 +128,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -139,6 +145,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -151,6 +158,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -162,6 +170,7 @@ describe('utils', () => { expect.objectContaining({ searchAfterTimes: ['60'], bulkCreateTimes: ['50'], + enrichmentTimes: ['6'], }) ); }); @@ -172,6 +181,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -184,6 +194,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -296,6 +307,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -307,6 +319,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['30'], // max value from existingResult.searchAfterTimes bulkCreateTimes: ['25'], // max value from existingResult.bulkCreateTimes + enrichmentTimes: ['3'], // max value from existingResult.enrichmentTimes lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -323,6 +336,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -334,6 +348,7 @@ describe('utils', () => { warning: false, searchAfterTimes: [], bulkCreateTimes: [], + enrichmentTimes: [], lastLookBackDate: undefined, createdSignalsCount: 0, createdSignals: [], @@ -345,6 +360,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['30'], // max value from existingResult.searchAfterTimes bulkCreateTimes: ['25'], // max value from existingResult.bulkCreateTimes + enrichmentTimes: ['3'], // max value from existingResult.enrichmentTimes lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -362,6 +378,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], // max is 30 bulkCreateTimes: ['5', '15', '25'], // max is 25 + enrichmentTimes: ['1', '2', '3'], // max is 3 lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -373,6 +390,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), @@ -384,6 +402,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['40', '5', '15'], bulkCreateTimes: ['50', '5', '15'], + enrichmentTimes: ['4', '2', '3'], lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'), createdSignalsCount: 8, createdSignals: Array(8).fill(sampleSignalHit()), @@ -396,6 +415,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['70'], // max value between newResult1 and newResult2 + max array value of existingResult (40 + 30 = 70) bulkCreateTimes: ['75'], // max value between newResult1 and newResult2 + max array value of existingResult (50 + 25 = 75) + enrichmentTimes: ['7'], // max value between newResult1 and newResult2 + max array value of existingResult (4 + 3 = 7) lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'), // max lastLookBackDate createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3) createdSignals: Array(16).fill(sampleSignalHit()), @@ -413,6 +433,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], // max is 30 bulkCreateTimes: ['5', '15', '25'], // max is 25 + enrichmentTimes: ['1', '2', '3'], // max is 3 lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -424,6 +445,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), @@ -435,6 +457,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['40', '5', '15'], bulkCreateTimes: ['50', '5', '15'], + enrichmentTimes: ['5', '2', '3'], lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'), createdSignalsCount: 8, createdSignals: Array(8).fill(sampleSignalHit()), @@ -447,6 +470,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['70'], // max value between newResult1 and newResult2 + max array value of existingResult (40 + 30 = 70) bulkCreateTimes: ['75'], // max value between newResult1 and newResult2 + max array value of existingResult (50 + 25 = 75) + enrichmentTimes: ['8'], // max value between newResult1 and newResult2 + max array value of existingResult (50 + 3 = 8) lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'), // max lastLookBackDate createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3) createdSignals: Array(16).fill(sampleSignalHit()), @@ -464,6 +488,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], // max is 30 bulkCreateTimes: ['5', '15', '25'], // max is 25 + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -475,6 +500,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), @@ -486,6 +512,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['40', '5', '15'], bulkCreateTimes: ['50', '5', '15'], + enrichmentTimes: ['5', '2', '3'], lastLookBackDate: null, createdSignalsCount: 8, createdSignals: Array(8).fill(sampleSignalHit()), @@ -498,6 +525,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['70'], // max value between newResult1 and newResult2 + max array value of existingResult (40 + 30 = 70) bulkCreateTimes: ['75'], // max value between newResult1 and newResult2 + max array value of existingResult (50 + 25 = 75) + enrichmentTimes: ['8'], // max value between newResult1 and newResult2 + max array value of existingResult (5 + 3 = 8) lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), // max lastLookBackDate createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3) createdSignals: Array(16).fill(sampleSignalHit()), @@ -515,6 +543,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -527,6 +556,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['5', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -543,6 +573,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -555,6 +586,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -571,6 +603,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -583,6 +616,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -599,6 +633,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -611,6 +646,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -632,6 +668,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -644,6 +681,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts index bfba9a6fd22a..a73ead0bf946 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts @@ -70,6 +70,7 @@ export const combineResults = ( ): SearchAfterAndBulkCreateReturnType => ({ success: currentResult.success === false ? false : newResult.success, warning: currentResult.warning || newResult.warning, + enrichmentTimes: calculateAdditiveMax(currentResult.enrichmentTimes, newResult.enrichmentTimes), bulkCreateTimes: calculateAdditiveMax(currentResult.bulkCreateTimes, newResult.bulkCreateTimes), searchAfterTimes: calculateAdditiveMax( currentResult.searchAfterTimes, @@ -94,6 +95,7 @@ export const combineConcurrentResults = ( const maxedNewResult = newResult.reduce( (accum, item) => { const maxSearchAfterTime = calculateMax(accum.searchAfterTimes, item.searchAfterTimes); + const maxEnrichmentTimes = calculateMax(accum.enrichmentTimes, item.enrichmentTimes); const maxBulkCreateTimes = calculateMax(accum.bulkCreateTimes, item.bulkCreateTimes); const lastLookBackDate = calculateMaxLookBack(accum.lastLookBackDate, item.lastLookBackDate); return { @@ -101,6 +103,7 @@ export const combineConcurrentResults = ( warning: accum.warning || item.warning, searchAfterTimes: [maxSearchAfterTime], bulkCreateTimes: [maxBulkCreateTimes], + enrichmentTimes: [maxEnrichmentTimes], lastLookBackDate, createdSignalsCount: accum.createdSignalsCount + item.createdSignalsCount, createdSignals: [...accum.createdSignals, ...item.createdSignals], @@ -113,6 +116,7 @@ export const combineConcurrentResults = ( warning: false, searchAfterTimes: [], bulkCreateTimes: [], + enrichmentTimes: [], lastLookBackDate: undefined, createdSignalsCount: 0, createdSignals: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 93ddadf826d7..f786a28b5004 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -286,6 +286,7 @@ export interface SearchAfterAndBulkCreateReturnType { success: boolean; warning: boolean; searchAfterTimes: string[]; + enrichmentTimes: string[]; bulkCreateTimes: string[]; lastLookBackDate: Date | null | undefined; createdSignalsCount: number; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index d5603130400c..d80ed256a0de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -953,6 +953,7 @@ describe('utils', () => { }); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignalsCount: 0, createdSignals: [], errors: [], @@ -973,6 +974,7 @@ describe('utils', () => { }); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignalsCount: 0, createdSignals: [], errors: [], @@ -1291,6 +1293,7 @@ describe('utils', () => { const searchAfterReturnType = createSearchAfterReturnType(); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignalsCount: 0, createdSignals: [], errors: [], @@ -1306,6 +1309,7 @@ describe('utils', () => { test('createSearchAfterReturnType can override all values', () => { const searchAfterReturnType = createSearchAfterReturnType({ bulkCreateTimes: ['123'], + enrichmentTimes: [], createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), errors: ['error 1'], @@ -1317,6 +1321,7 @@ describe('utils', () => { }); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: ['123'], + enrichmentTimes: [], createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), errors: ['error 1'], @@ -1337,6 +1342,7 @@ describe('utils', () => { }); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), errors: ['error 1'], @@ -1355,6 +1361,7 @@ describe('utils', () => { const merged = mergeReturns([createSearchAfterReturnType(), createSearchAfterReturnType()]); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignalsCount: 0, createdSignals: [], errors: [], @@ -1411,6 +1418,7 @@ describe('utils', () => { const merged = mergeReturns([ createSearchAfterReturnType({ bulkCreateTimes: ['123'], + enrichmentTimes: [], createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: ['error 1', 'error 2'], @@ -1421,6 +1429,7 @@ describe('utils', () => { }), createSearchAfterReturnType({ bulkCreateTimes: ['456'], + enrichmentTimes: [], createdSignalsCount: 2, createdSignals: Array(2).fill(sampleSignalHit()), errors: ['error 3'], @@ -1433,6 +1442,7 @@ describe('utils', () => { ]); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: ['123', '456'], // concatenates the prev and next together + enrichmentTimes: [], createdSignalsCount: 5, // Adds the 3 and 2 together createdSignals: Array(5).fill(sampleSignalHit()), errors: ['error 1', 'error 2', 'error 3'], // concatenates the prev and next together @@ -1452,6 +1462,7 @@ describe('utils', () => { const next: GenericBulkCreateResponse = { success: false, bulkCreateDuration: '100', + enrichmentDuration: '0', createdItemsCount: 1, createdItems: [], errors: ['new error'], @@ -1469,6 +1480,7 @@ describe('utils', () => { const next: GenericBulkCreateResponse = { success: true, bulkCreateDuration: '0', + enrichmentDuration: '0', createdItemsCount: 0, createdItems: [], errors: ['error 1'], @@ -1484,6 +1496,7 @@ describe('utils', () => { const next: GenericBulkCreateResponse = { success: true, bulkCreateDuration: '0', + enrichmentDuration: '0', createdItemsCount: 0, createdItems: [], errors: ['error 2'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 6030400da2b7..24b5b068d568 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -649,6 +649,7 @@ export const createSearchAfterReturnType = ({ success, warning, searchAfterTimes, + enrichmentTimes, bulkCreateTimes, lastLookBackDate, createdSignalsCount, @@ -659,6 +660,7 @@ export const createSearchAfterReturnType = ({ success?: boolean | undefined; warning?: boolean; searchAfterTimes?: string[] | undefined; + enrichmentTimes?: string[] | undefined; bulkCreateTimes?: string[] | undefined; lastLookBackDate?: Date | undefined; createdSignalsCount?: number | undefined; @@ -670,6 +672,7 @@ export const createSearchAfterReturnType = ({ success: success ?? true, warning: warning ?? false, searchAfterTimes: searchAfterTimes ?? [], + enrichmentTimes: enrichmentTimes ?? [], bulkCreateTimes: bulkCreateTimes ?? [], lastLookBackDate: lastLookBackDate ?? null, createdSignalsCount: createdSignalsCount ?? 0, @@ -715,6 +718,7 @@ export const addToSearchAfterReturn = ({ current.createdSignalsCount += next.createdItemsCount; current.createdSignals.push(...next.createdItems); current.bulkCreateTimes.push(next.bulkCreateDuration); + current.enrichmentTimes.push(next.enrichmentDuration); current.errors = [...new Set([...current.errors, ...next.errors])]; }; @@ -727,6 +731,7 @@ export const mergeReturns = ( warning: existingWarning, searchAfterTimes: existingSearchAfterTimes, bulkCreateTimes: existingBulkCreateTimes, + enrichmentTimes: existingEnrichmentTimes, lastLookBackDate: existingLastLookBackDate, createdSignalsCount: existingCreatedSignalsCount, createdSignals: existingCreatedSignals, @@ -738,6 +743,7 @@ export const mergeReturns = ( success: newSuccess, warning: newWarning, searchAfterTimes: newSearchAfterTimes, + enrichmentTimes: newEnrichmentTimes, bulkCreateTimes: newBulkCreateTimes, lastLookBackDate: newLastLookBackDate, createdSignalsCount: newCreatedSignalsCount, @@ -750,6 +756,7 @@ export const mergeReturns = ( success: existingSuccess && newSuccess, warning: existingWarning || newWarning, searchAfterTimes: [...existingSearchAfterTimes, ...newSearchAfterTimes], + enrichmentTimes: [...existingEnrichmentTimes, ...newEnrichmentTimes], bulkCreateTimes: [...existingBulkCreateTimes, ...newBulkCreateTimes], lastLookBackDate: newLastLookBackDate ?? existingLastLookBackDate, createdSignalsCount: existingCreatedSignalsCount + newCreatedSignalsCount, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/artifact.ts b/x-pack/plugins/security_solution/server/lib/telemetry/artifact.ts index 46d0d6e665c4..07ec2b6f2e49 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/artifact.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/artifact.ts @@ -26,7 +26,7 @@ class Artifact implements IArtifact { this.receiver = receiver; this.esClusterInfo = await this.receiver.fetchClusterInfo(); const version = this.esClusterInfo?.version?.number; - this.manifestUrl = `${this.CDN_URL}/downloads/endpoint/manifest/artifacts-${version}.zip`; + this.manifestUrl = `${this.CDN_URL}/downloads/kibana/manifest/artifacts-${version}.zip`; } public async getArtifact(name: string): Promise { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/configuration.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/configuration.ts index f664fccbf75d..d266d2f1c769 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/configuration.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/configuration.ts @@ -29,7 +29,7 @@ export function createTelemetryConfigurationTaskConfig() { taskExecutionPeriod: TaskExecutionPeriod ) => { try { - const artifactName = 'telemetry-configuration-v1'; + const artifactName = 'telemetry-buffer-and-batch-sizes-v1'; const configArtifact = (await artifactService.getArtifact( artifactName )) as unknown as TelemetryConfiguration; diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index e14af8c55373..6f526051f17d 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -414,6 +414,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -500,6 +514,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -586,6 +614,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -672,6 +714,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -758,6 +814,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -844,6 +914,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -946,6 +1030,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1032,6 +1130,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1118,6 +1230,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1204,6 +1330,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1290,6 +1430,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1376,6 +1530,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1478,6 +1646,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1564,6 +1746,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1650,6 +1846,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1736,6 +1946,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1822,6 +2046,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1908,6 +2146,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', diff --git a/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts b/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts index eb32c58bda6c..3ad6b5740b53 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts @@ -144,6 +144,7 @@ export const getInitialSingleEventMetric = (): SingleEventMetric => ({ succeeded: 0, index_duration: getInitialMaxAvgMin(), search_duration: getInitialMaxAvgMin(), + enrichment_duration: getInitialMaxAvgMin(), gap_duration: getInitialMaxAvgMin(), gap_count: 0, }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts b/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts index 129d2b32d0b3..83eb7df28a02 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts @@ -155,6 +155,15 @@ export const getEventLogAllRules = (): SearchResponse ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threat_match: { @@ -2506,6 +2835,11 @@ export const getEventLogAllRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, machine_learning: { @@ -2529,6 +2863,11 @@ export const getEventLogAllRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, query: { @@ -2594,6 +2933,11 @@ export const getEventLogAllRulesResult = (): SingleEventLogStatusMetric => ({ avg: 4246.375, min: 2811, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 6, }, saved_query: { @@ -2617,6 +2961,11 @@ export const getEventLogAllRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threshold: { @@ -2640,6 +2989,11 @@ export const getEventLogAllRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, total: { @@ -2676,6 +3030,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threat_match: { @@ -2699,6 +3058,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, machine_learning: { @@ -2722,6 +3086,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, query: { @@ -2772,6 +3141,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 4141.75, min: 2811, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 4, }, saved_query: { @@ -2795,6 +3169,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threshold: { @@ -2818,6 +3197,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, total: { @@ -2854,6 +3238,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threat_match: { @@ -2877,6 +3266,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, machine_learning: { @@ -2900,6 +3294,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, query: { @@ -2940,6 +3339,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 4351, min: 3051, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 2, }, saved_query: { @@ -2963,6 +3367,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threshold: { @@ -2986,6 +3395,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, total: { diff --git a/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts b/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts index 499c79b11fcf..84fb656f793d 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts @@ -85,6 +85,7 @@ export interface SingleEventMetric { succeeded: number; index_duration: MaxAvgMin; search_duration: MaxAvgMin; + enrichment_duration: MaxAvgMin; gap_duration: MaxAvgMin; gap_count: number; } diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts index 09a988fbf02e..693e3579d7c3 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts @@ -68,6 +68,21 @@ describe('get_event_log_agg_by_rule_type_metrics', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }); }); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts index 6fe8103e29a0..957f56809d64 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts @@ -74,6 +74,21 @@ export const getEventLogAggByRuleTypeMetrics = ( field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }; }; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts index 22261ac48812..4673ca1b6bcb 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts @@ -74,6 +74,21 @@ describe('get_event_log_agg_by_rule_types_metrics', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }); @@ -139,6 +154,21 @@ describe('get_event_log_agg_by_rule_types_metrics', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }); @@ -204,6 +234,21 @@ describe('get_event_log_agg_by_rule_types_metrics', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, 'siem.indicatorRule': { @@ -263,6 +308,21 @@ describe('get_event_log_agg_by_rule_types_metrics', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts index 7d474769bd79..a87046660fe1 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts @@ -137,6 +137,21 @@ describe('get_event_log_agg_by_statuses', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }, @@ -246,6 +261,21 @@ describe('get_event_log_agg_by_statuses', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }, @@ -418,6 +448,21 @@ describe('get_event_log_agg_by_statuses', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, 'siem.thresholdRule': { @@ -477,6 +522,21 @@ describe('get_event_log_agg_by_statuses', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }, diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts index c64f0833fe85..9c5810011a97 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts @@ -85,6 +85,15 @@ describe('transform_single_rule_metric', () => { minTotalSearchDuration: { value: 12, }, + minTotalEnrichmentDuration: { + value: 4, + }, + maxTotalEnrichmentDuration: { + value: 2, + }, + avgTotalEnrichmentDuration: { + value: 12, + }, }, }); @@ -131,6 +140,11 @@ describe('transform_single_rule_metric', () => { avg: 2, min: 9, }, + enrichment_duration: { + max: 2, + avg: 12, + min: 4, + }, gap_count: 4, }); }); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts index bebd867fb195..5b3b6f8b7ebb 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts @@ -52,6 +52,11 @@ export const transformSingleRuleMetric = ({ avg: singleMetric.avgTotalSearchDuration.value ?? 0.0, min: singleMetric.minTotalSearchDuration.value ?? 0.0, }, + enrichment_duration: { + max: singleMetric?.maxTotalEnrichmentDuration?.value ?? 0.0, + avg: singleMetric?.avgTotalEnrichmentDuration?.value ?? 0.0, + min: singleMetric?.minTotalEnrichmentDuration?.value ?? 0.0, + }, gap_duration: { max: singleMetric.maxGapDuration.value ?? 0.0, avg: singleMetric.avgGapDuration.value ?? 0.0, diff --git a/x-pack/plugins/security_solution/server/usage/types.ts b/x-pack/plugins/security_solution/server/usage/types.ts index fe7711196303..a6db2e3d71e9 100644 --- a/x-pack/plugins/security_solution/server/usage/types.ts +++ b/x-pack/plugins/security_solution/server/usage/types.ts @@ -121,6 +121,15 @@ export interface SingleExecutionMetricAgg { minTotalSearchDuration: { value: number | null; }; + maxTotalEnrichmentDuration: { + value: number | null; + }; + avgTotalEnrichmentDuration: { + value: number | null; + }; + minTotalEnrichmentDuration: { + value: number | null; + }; } export interface EventLogTypeStatusAggs { diff --git a/x-pack/plugins/session_view/public/components/tty_player/hooks.test.tsx b/x-pack/plugins/session_view/public/components/tty_player/hooks.test.tsx index 004bd5bc757e..8b2161c3b121 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/hooks.test.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player/hooks.test.tsx @@ -167,6 +167,21 @@ describe('TTYPlayer/hooks', () => { expect(result.current.currentLine).toBe(initialProps.lines.length - 1); }); + it('should not print the first line twice after playback starts', async () => { + const { result, rerender } = renderHook((props) => useXtermPlayer(props), { + initialProps, + }); + + rerender({ ...initialProps, isPlaying: true }); + act(() => { + // advance render loop + jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS); + }); + rerender({ ...initialProps, isPlaying: false }); + + expect(result.current.terminal.buffer.active.getLine(0)?.translateToString(true)).toBe('256'); + }); + it('will allow a plain text search highlight on the last line printed', async () => { const { result: xTermResult } = renderHook((props) => useXtermPlayer(props), { initialProps, diff --git a/x-pack/plugins/session_view/public/components/tty_player/hooks.ts b/x-pack/plugins/session_view/public/components/tty_player/hooks.ts index 08f163903b0a..680d50283d5f 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/hooks.ts +++ b/x-pack/plugins/session_view/public/components/tty_player/hooks.ts @@ -281,13 +281,12 @@ export const useXtermPlayer = ({ useEffect(() => { if (isPlaying) { const timer = setTimeout(() => { - render(currentLine, false); - - if (currentLine === lines.length - 1) { + if (!hasNextPage && currentLine === lines.length - 1) { setIsPlaying(false); } else { const nextLine = Math.min(lines.length - 1, currentLine + TTY_LINES_PER_FRAME); setCurrentLine(nextLine); + render(nextLine, false); } }, playSpeed); diff --git a/x-pack/plugins/session_view/public/components/tty_player/index.tsx b/x-pack/plugins/session_view/public/components/tty_player/index.tsx index ba57fc9d845c..cb2746736c02 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/index.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player/index.tsx @@ -134,7 +134,12 @@ export const TTYPlayer = ({ - + diff --git a/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx b/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx index 2f952b48e9d5..06fa17a6c151 100644 --- a/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/tty_search_bar/index.test.tsx @@ -34,6 +34,7 @@ describe('TTYSearchBar component', () => { lines, seekToLine: jest.fn(), xTermSearchFn: jest.fn(), + setIsPlaying: jest.fn(), }; }); @@ -59,6 +60,7 @@ describe('TTYSearchBar component', () => { expect(props.xTermSearchFn).toHaveBeenCalledTimes(2); expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '', 0); expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '-h', 6); + expect(props.setIsPlaying).toHaveBeenCalledWith(false); }); it('calls seekToline and xTermSearchFn when currentMatch changes', async () => { @@ -85,6 +87,7 @@ describe('TTYSearchBar component', () => { expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '', 0); expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '-h', 6); expect(props.xTermSearchFn).toHaveBeenNthCalledWith(3, '-h', 13); + expect(props.setIsPlaying).toHaveBeenCalledTimes(3); }); it('calls xTermSearchFn with empty query when search is cleared', async () => { @@ -101,5 +104,6 @@ describe('TTYSearchBar component', () => { await new Promise((r) => setTimeout(r, 100)); expect(props.xTermSearchFn).toHaveBeenNthCalledWith(3, '', 0); + expect(props.setIsPlaying).toHaveBeenCalledWith(false); }); }); diff --git a/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx b/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx index e41b3b967cfa..18b829127ab2 100644 --- a/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx +++ b/x-pack/plugins/session_view/public/components/tty_search_bar/index.tsx @@ -19,15 +19,24 @@ export interface TTYSearchBarDeps { lines: IOLine[]; seekToLine(index: number): void; xTermSearchFn(query: string, index: number): void; + setIsPlaying(value: boolean): void; } -export const TTYSearchBar = ({ lines, seekToLine, xTermSearchFn }: TTYSearchBarDeps) => { +const STRIP_NEWLINES_REGEX = /^(\r\n|\r|\n|\n\r)/; + +export const TTYSearchBar = ({ + lines, + seekToLine, + xTermSearchFn, + setIsPlaying, +}: TTYSearchBarDeps) => { const [currentMatch, setCurrentMatch] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const jumpToMatch = useCallback( (match) => { if (match) { + setIsPlaying(false); const goToLine = lines.indexOf(match.line); seekToLine(goToLine); } @@ -40,7 +49,7 @@ export const TTYSearchBar = ({ lines, seekToLine, xTermSearchFn }: TTYSearchBarD clearTimeout(timeout); }; }, - [lines, seekToLine, xTermSearchFn, searchQuery] + [setIsPlaying, lines, seekToLine, xTermSearchFn, searchQuery] ); const searchResults = useMemo(() => { @@ -53,7 +62,7 @@ export const TTYSearchBar = ({ lines, seekToLine, xTermSearchFn }: TTYSearchBarD const cursorMovement = current.value.match(/^\x1b\[\d+;(\d+)(H|d)/); const regex = new RegExp(searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig'); const lineMatches = stripAnsi(current.value) - .replace(/^\r|\r?\n/, '') + .replace(STRIP_NEWLINES_REGEX, '') .matchAll(regex); if (lineMatches) { @@ -90,10 +99,14 @@ export const TTYSearchBar = ({ lines, seekToLine, xTermSearchFn }: TTYSearchBarD return matches; }, [searchQuery, lines, jumpToMatch, xTermSearchFn]); - const onSearch = useCallback((query) => { - setSearchQuery(query); - setCurrentMatch(null); - }, []); + const onSearch = useCallback( + (query) => { + setIsPlaying(false); + setSearchQuery(query); + setCurrentMatch(null); + }, + [setIsPlaying] + ); const onSetCurrentMatch = useCallback( (index) => { diff --git a/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts b/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts index f05003f650de..78e55e97d5b0 100644 --- a/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts @@ -74,7 +74,6 @@ export const browserFormatters: BrowserFormatMap = { [ConfigKey.IGNORE_HTTPS_ERRORS]: null, [ConfigKey.PROJECT_ID]: null, [ConfigKey.PLAYWRIGHT_OPTIONS]: null, - [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, [ConfigKey.ORIGINAL_SPACE]: null, [ConfigKey.TEXT_ASSERTION]: null, ...commonFormatters, diff --git a/x-pack/plugins/synthetics/common/formatters/common/formatters.ts b/x-pack/plugins/synthetics/common/formatters/common/formatters.ts index 89bf8793302b..751721e5d703 100644 --- a/x-pack/plugins/synthetics/common/formatters/common/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/common/formatters.ts @@ -29,6 +29,7 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.MONITOR_SOURCE_TYPE]: null, [ConfigKey.FORM_MONITOR_TYPE]: null, [ConfigKey.JOURNEY_ID]: null, + [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, }; export const arrayToJsonFormatter = (value: string[] = []) => diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts index 58e3a31df8a3..46bc0f135452 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -83,6 +83,7 @@ export const CommonFieldsCodec = t.intersection([ [ConfigKey.MONITOR_SOURCE_TYPE]: SourceTypeCodec, [ConfigKey.CONFIG_ID]: t.string, [ConfigKey.JOURNEY_ID]: t.string, + [ConfigKey.CUSTOM_HEARTBEAT_ID]: t.string, }), ]); @@ -218,7 +219,6 @@ export const EncryptedBrowserSimpleFieldsCodec = t.intersection([ [ConfigKey.PLAYWRIGHT_OPTIONS]: t.string, [ConfigKey.PROJECT_ID]: t.string, [ConfigKey.ORIGINAL_SPACE]: t.string, - [ConfigKey.CUSTOM_HEARTBEAT_ID]: t.string, [ConfigKey.TEXT_ASSERTION]: t.string, }), ]), diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts index 6abcf4b83213..cba2f506189e 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts @@ -30,7 +30,6 @@ export const ProjectMonitorCodec = t.intersection([ screenshot: ScreenshotOptionCodec, tags: t.union([t.string, t.array(t.string)]), ignoreHTTPSErrors: t.boolean, - apmServiceName: t.string, playwrightOptions: t.record(t.string, t.unknown), filter: t.interface({ match: t.string, diff --git a/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts b/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts index aa29b92b7ea2..e5714234de69 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts @@ -6,7 +6,7 @@ */ import { journey, step, expect, before } from '@elastic/synthetics'; -import { callKibana } from '@kbn/apm-plugin/scripts/create_apm_users/helpers/call_kibana'; +import { callKibana } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/helpers/call_kibana'; import { byTestId, waitForLoadingToFinish } from '@kbn/observability-plugin/e2e/utils'; import { loginPageProvider } from '../page_objects/login'; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts index a8b59b16a346..61f978327b8a 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/browser/normalizers.ts @@ -109,7 +109,6 @@ export const browserNormalizers: BrowserNormalizerMap = { [ConfigKey.IGNORE_HTTPS_ERRORS]: getBrowserNormalizer(ConfigKey.IGNORE_HTTPS_ERRORS), [ConfigKey.PROJECT_ID]: getBrowserNormalizer(ConfigKey.PROJECT_ID), [ConfigKey.PLAYWRIGHT_OPTIONS]: getBrowserNormalizer(ConfigKey.PLAYWRIGHT_OPTIONS), - [ConfigKey.CUSTOM_HEARTBEAT_ID]: getBrowserNormalizer(ConfigKey.CUSTOM_HEARTBEAT_ID), [ConfigKey.ORIGINAL_SPACE]: getBrowserNormalizer(ConfigKey.ORIGINAL_SPACE), [ConfigKey.TEXT_ASSERTION]: getBrowserNormalizer(ConfigKey.TEXT_ASSERTION), ...commonNormalizers, diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts index 14fab3caeb13..630ac70a8e05 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/fleet_package/common/normalizers.ts @@ -93,4 +93,5 @@ export const commonNormalizers: CommonNormalizerMap = { [ConfigKey.MONITOR_SOURCE_TYPE]: getCommonNormalizer(ConfigKey.MONITOR_SOURCE_TYPE), [ConfigKey.FORM_MONITOR_TYPE]: getCommonNormalizer(ConfigKey.FORM_MONITOR_TYPE), [ConfigKey.JOURNEY_ID]: getCommonNormalizer(ConfigKey.JOURNEY_ID), + [ConfigKey.CUSTOM_HEARTBEAT_ID]: getCommonNormalizer(ConfigKey.CUSTOM_HEARTBEAT_ID), }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts index 9c9d24a3f58f..7095e437aea2 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts @@ -69,7 +69,6 @@ export const browserFormatters: BrowserFormatMap = { [ConfigKey.PROJECT_ID]: null, [ConfigKey.PLAYWRIGHT_OPTIONS]: (fields) => stringToObjectFormatter(fields[ConfigKey.PLAYWRIGHT_OPTIONS] || ''), - [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, [ConfigKey.ORIGINAL_SPACE]: null, [ConfigKey.TEXT_ASSERTION]: null, ...commonFormatters, diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts index 16a0829cbb71..f9c3125041b4 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts @@ -31,6 +31,7 @@ export const commonFormatters: CommonFormatMap = { fields[ConfigKey.MONITOR_SOURCE_TYPE] || SourceType.UI, [ConfigKey.FORM_MONITOR_TYPE]: null, [ConfigKey.JOURNEY_ID]: null, + [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, }; export const arrayFormatter = (value: string[] = []) => (value.length ? value : null); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.test.ts index ed02f1037e32..9b32b61a59b3 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.test.ts @@ -67,9 +67,7 @@ describe('browser normalizers', () => { locations: ['us_central'], tags: ['tag1', 'tag2'], ignoreHTTPSErrors: true, - apmServiceName: 'cart-service', - type: DataStream.BROWSER, - }, + } as ProjectMonitor, // test that normalizers defaults to browser when type is omitted { id: 'test-id-2', screenshot: ScreenshotOption.ON, @@ -86,7 +84,6 @@ describe('browser normalizers', () => { locations: ['us_central', 'us_east'], tags: ['tag3', 'tag4'], ignoreHTTPSErrors: false, - apmServiceName: 'bean-service', type: DataStream.BROWSER, }, { @@ -106,7 +103,6 @@ describe('browser normalizers', () => { privateLocations: ['Germany'], tags: ['tag3', 'tag4'], ignoreHTTPSErrors: false, - apmServiceName: 'bean-service', type: DataStream.BROWSER, }, ]; @@ -118,6 +114,7 @@ describe('browser normalizers', () => { monitors, projectId, namespace: 'test-space', + version: '8.5.0', }); expect(actual).toEqual([ { @@ -145,7 +142,7 @@ describe('browser normalizers', () => { unit: 'm', }, screenshots: 'off', - 'service.name': 'cart-service', + 'service.name': '', 'source.project.content': 'test content 1', tags: ['tag1', 'tag2'], 'throttling.config': '5d/10u/20l', @@ -162,6 +159,7 @@ describe('browser normalizers', () => { timeout: null, }, unsupportedKeys: [], + errors: [], }, { normalizedFields: { @@ -201,7 +199,7 @@ describe('browser normalizers', () => { unit: 'm', }, screenshots: 'on', - 'service.name': 'bean-service', + 'service.name': '', 'source.project.content': 'test content 2', tags: ['tag3', 'tag4'], 'throttling.config': '10d/15u/18l', @@ -217,6 +215,7 @@ describe('browser normalizers', () => { timeout: null, }, unsupportedKeys: [], + errors: [], }, { normalizedFields: { @@ -263,7 +262,7 @@ describe('browser normalizers', () => { unit: 'm', }, screenshots: 'on', - 'service.name': 'bean-service', + 'service.name': '', 'source.project.content': 'test content 3', tags: ['tag3', 'tag4'], 'throttling.config': '10d/15u/18l', @@ -279,6 +278,7 @@ describe('browser normalizers', () => { timeout: null, }, unsupportedKeys: [], + errors: [], }, ]); }); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.ts index aae7031435c7..8eba2cd2651d 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/browser_monitor.ts @@ -10,20 +10,14 @@ import { ConfigKey, DataStream, FormMonitorType, - Locations, - PrivateLocation, - ProjectMonitor, } from '../../../../common/runtime_types'; -import { getNormalizeCommonFields, getValueInSeconds } from './common_fields'; import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; - -export interface NormalizedProjectProps { - locations: Locations; - privateLocations: PrivateLocation[]; - monitor: ProjectMonitor; - projectId: string; - namespace: string; -} +import { + NormalizedProjectProps, + NormalizerResult, + getNormalizeCommonFields, + getValueInSeconds, +} from './common_fields'; export const getNormalizeBrowserFields = ({ locations = [], @@ -31,7 +25,8 @@ export const getNormalizeBrowserFields = ({ monitor, projectId, namespace, -}: NormalizedProjectProps): { normalizedFields: BrowserFields; unsupportedKeys: string[] } => { + version, +}: NormalizedProjectProps): NormalizerResult => { const defaultFields = DEFAULT_FIELDS[DataStream.BROWSER]; const commonFields = getNormalizeCommonFields({ @@ -40,6 +35,7 @@ export const getNormalizeBrowserFields = ({ monitor, projectId, namespace, + version, }); const normalizedFields = { @@ -81,5 +77,6 @@ export const getNormalizeBrowserFields = ({ ...normalizedFields, }, unsupportedKeys: [], + errors: [], }; }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts new file mode 100644 index 000000000000..fee6f0ca2637 --- /dev/null +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { flattenAndFormatObject } from './common_fields'; + +describe('normalizeYamlConfig', () => { + it('does not continue flattening when encountering an array', () => { + const array = ['value1', 'value2']; + const supportedKeys: string[] = []; + const nestedObject = { + a: { + nested: { + key: array, + }, + }, + }; + expect(flattenAndFormatObject(nestedObject, '', supportedKeys)).toEqual({ + 'a.nested.key': array, + }); + }); + + it('does not continue flattening when encountering a supported key', () => { + const supportedKeys: string[] = ['a.supported.key']; + const object = { + with: { + further: { + nesting: '', + }, + }, + }; + const nestedObject = { + a: { + supported: { + key: object, + }, + }, + }; + expect(flattenAndFormatObject(nestedObject, '', supportedKeys)).toEqual({ + 'a.supported.key': object, + }); + }); + + it('flattens objects', () => { + const supportedKeys: string[] = []; + const nestedObject = { + a: { + nested: { + key: 'value1', + }, + }, + b: { + nested: { + key: 'value2', + }, + }, + }; + expect(flattenAndFormatObject(nestedObject, '', supportedKeys)).toEqual({ + 'a.nested.key': 'value1', + 'b.nested.key': 'value2', + }); + }); +}); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts index 31aebd0e8586..56045712606a 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts @@ -20,7 +20,27 @@ import { } from '../../../../common/runtime_types'; import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; import { DEFAULT_COMMON_FIELDS } from '../../../../common/constants/monitor_defaults'; -import { NormalizedProjectProps } from '.'; + +export interface NormalizedProjectProps { + locations: Locations; + privateLocations: PrivateLocation[]; + monitor: ProjectMonitor; + projectId: string; + namespace: string; + version: string; +} + +export interface Error { + id: string; + reason: string; + details: string; +} + +export interface NormalizerResult { + normalizedFields: MonitorTypeFields; + unsupportedKeys: string[]; + errors: Error[]; +} export const getNormalizeCommonFields = ({ locations = [], @@ -28,7 +48,7 @@ export const getNormalizeCommonFields = ({ monitor, projectId, namespace, -}: NormalizedProjectProps): CommonFields => { +}: NormalizedProjectProps): Partial => { const defaultFields = DEFAULT_COMMON_FIELDS; const normalizedFields = { @@ -45,18 +65,16 @@ export const getNormalizeCommonFields = ({ privateLocations, publicLocations: locations, }), - [ConfigKey.APM_SERVICE_NAME]: - monitor.apmServiceName || defaultFields[ConfigKey.APM_SERVICE_NAME], [ConfigKey.TAGS]: getOptionalListField(monitor.tags) || defaultFields[ConfigKey.TAGS], [ConfigKey.NAMESPACE]: formatKibanaNamespace(namespace) || defaultFields[ConfigKey.NAMESPACE], [ConfigKey.ORIGINAL_SPACE]: namespace || defaultFields[ConfigKey.NAMESPACE], [ConfigKey.CUSTOM_HEARTBEAT_ID]: getCustomHeartbeatId(monitor, projectId, namespace), [ConfigKey.ENABLED]: monitor.enabled ?? defaultFields[ConfigKey.ENABLED], + [ConfigKey.TIMEOUT]: monitor.timeout + ? getValueInSeconds(monitor.timeout) + : defaultFields[ConfigKey.TIMEOUT], }; - return { - ...defaultFields, - ...normalizedFields, - }; + return normalizedFields; }; export const getCustomHeartbeatId = ( @@ -94,6 +112,35 @@ export const getMonitorLocations = ({ ) as BrowserFields[ConfigKey.LOCATIONS]; }; +export const getUnsupportedKeysError = ( + monitor: ProjectMonitor, + unsupportedKeys: string[], + version: string +) => ({ + id: monitor.id, + reason: 'Unsupported Heartbeat option', + details: `The following Heartbeat options are not supported for ${ + monitor.type + } project monitors in ${version}: ${unsupportedKeys.join( + '|' + )}. You monitor was not created or updated.`, +}); + +export const getMultipleUrlsOrHostsError = ( + monitor: ProjectMonitor, + key: 'hosts' | 'urls', + version: string +) => ({ + id: monitor.id, + reason: 'Unsupported Heartbeat option', + details: `Multiple ${key} are not supported for ${ + monitor.type + } project monitors in ${version}. Please set only 1 ${key.slice( + 0, + -1 + )} per monitor. You monitor was not created or updated.`, +}); + export const getValueInSeconds = (value: string) => { const keyMap = { h: 60 * 60, @@ -136,7 +183,7 @@ export const getOptionalArrayField = (value: string[] | string = '') => { * @param {Object} [monitor] * @returns {Object} Returns an object containing synthetics-compatible configuration keys */ -const flattenAndFormatObject = (obj: Record, prefix = '', keys: string[]) => +export const flattenAndFormatObject = (obj: Record, prefix = '', keys: string[]) => Object.keys(obj).reduce>((acc, k) => { const pre = prefix.length ? prefix + '.' : ''; const key = pre + k; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.test.ts new file mode 100644 index 000000000000..922c5ca6dab0 --- /dev/null +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.test.ts @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Locations, LocationStatus, PrivateLocation } from '../../../../common/runtime_types'; +import { normalizeProjectMonitors } from '.'; + +describe('http normalizers', () => { + describe('normalize push monitors', () => { + const projectId = 'test-project-id'; + const locations: Locations = [ + { + id: 'us_central', + label: 'Test Location', + geo: { lat: 33.333, lon: 73.333 }, + url: 'test-url', + isServiceManaged: true, + status: LocationStatus.GA, + }, + { + id: 'us_east', + label: 'Test Location', + geo: { lat: 33.333, lon: 73.333 }, + url: 'test-url', + isServiceManaged: true, + status: LocationStatus.GA, + }, + ]; + const privateLocations: PrivateLocation[] = [ + { + id: 'germany', + label: 'Germany', + isServiceManaged: false, + concurrentMonitors: 1, + agentPolicyId: 'germany', + }, + ]; + const monitors = [ + { + locations: ['localhost'], + type: 'http', + enabled: false, + id: 'my-monitor-2', + name: 'My Monitor 2', + urls: ['http://localhost:9200', 'http://anotherurl:9200'], + schedule: 60, + timeout: '80s', + 'check.request': { + method: 'POST', + body: { + json: 'body', + }, + headers: { + 'a-header': 'a-header-value', + }, + }, + response: { + include_body: 'always', + }, + 'response.include_headers': false, + 'check.response': { + status: [200], + body: ['Saved', 'saved'], + }, + unsupportedKey: { + nestedUnsupportedKey: 'unsupportedValue', + }, + service: { + name: 'test service', + }, + ssl: { + supported_protocols: ['TLSv1.2', 'TLSv1.3'], + }, + }, + { + locations: ['localhost'], + type: 'http', + enabled: false, + id: 'my-monitor-3', + name: 'My Monitor 3', + urls: ['http://localhost:9200'], + schedule: 60, + timeout: '80s', + 'check.request': { + method: 'POST', + body: 'sometextbody', + headers: { + 'a-header': 'a-header-value', + }, + }, + response: { + include_body: 'always', + }, + tags: 'tag2,tag2', + 'response.include_headers': false, + 'check.response': { + status: [200], + body: { + positive: ['Saved', 'saved'], + }, + }, + 'service.name': 'test service', + 'ssl.supported_protocols': 'TLSv1.2,TLSv1.3', + }, + ]; + + it('properly normalizes http monitors', () => { + const actual = normalizeProjectMonitors({ + locations, + privateLocations, + monitors, + projectId, + namespace: 'test-space', + version: '8.5.0', + }); + expect(actual).toEqual([ + { + errors: [ + { + details: + 'Multiple urls are not supported for http project monitors in 8.5.0. Please set only 1 url per monitor. You monitor was not created or updated.', + id: 'my-monitor-2', + reason: 'Unsupported Heartbeat option', + }, + { + details: + 'The following Heartbeat options are not supported for http project monitors in 8.5.0: check.response.body|unsupportedKey.nestedUnsupportedKey. You monitor was not created or updated.', + id: 'my-monitor-2', + reason: 'Unsupported Heartbeat option', + }, + ], + normalizedFields: { + __ui: { + is_tls_enabled: false, + }, + 'check.request.body': { + type: 'json', + value: '{"json":"body"}', + }, + 'check.request.headers': { + 'a-header': 'a-header-value', + }, + 'check.request.method': 'POST', + 'check.response.body.negative': [], + 'check.response.body.positive': [], + 'check.response.headers': {}, + 'check.response.status': ['200'], + config_id: '', + custom_heartbeat_id: 'my-monitor-2-test-project-id-test-space', + enabled: false, + form_monitor_type: 'http', + journey_id: 'my-monitor-2', + locations: [], + max_redirects: '0', + name: 'My Monitor 2', + namespace: 'test_space', + origin: 'project', + original_space: 'test-space', + password: '', + project_id: 'test-project-id', + proxy_url: '', + 'response.include_body': 'always', + 'response.include_headers': false, + schedule: { + number: '60', + unit: 'm', + }, + 'service.name': 'test service', + 'ssl.certificate': '', + 'ssl.certificate_authorities': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.supported_protocols': ['TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + tags: [], + timeout: '80', + type: 'http', + urls: 'http://localhost:9200', + username: '', + }, + unsupportedKeys: ['check.response.body', 'unsupportedKey.nestedUnsupportedKey'], + }, + { + errors: [], + normalizedFields: { + __ui: { + is_tls_enabled: false, + }, + 'check.request.body': { + type: 'text', + value: 'sometextbody', + }, + 'check.request.headers': { + 'a-header': 'a-header-value', + }, + 'check.request.method': 'POST', + 'check.response.body.negative': [], + 'check.response.body.positive': ['Saved', 'saved'], + 'check.response.headers': {}, + 'check.response.status': ['200'], + config_id: '', + custom_heartbeat_id: 'my-monitor-3-test-project-id-test-space', + enabled: false, + form_monitor_type: 'http', + journey_id: 'my-monitor-3', + locations: [], + max_redirects: '0', + name: 'My Monitor 3', + namespace: 'test_space', + origin: 'project', + original_space: 'test-space', + password: '', + project_id: 'test-project-id', + proxy_url: '', + 'response.include_body': 'always', + 'response.include_headers': false, + schedule: { + number: '60', + unit: 'm', + }, + 'service.name': 'test service', + 'ssl.certificate': '', + 'ssl.certificate_authorities': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.supported_protocols': ['TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + tags: ['tag2', 'tag2'], + timeout: '80', + type: 'http', + urls: 'http://localhost:9200', + username: '', + }, + unsupportedKeys: [], + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts index 6f637d818667..d994e6151782 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts @@ -4,16 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { getNormalizeCommonFields } from './common_fields'; -import { NormalizedProjectProps } from './browser_monitor'; +import { get } from 'lodash'; import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; import { ConfigKey, DataStream, FormMonitorType, HTTPFields, + Mode, + TLSVersion, } from '../../../../common/runtime_types/monitor_management'; -import { normalizeYamlConfig, getValueInSeconds, getOptionalArrayField } from './common_fields'; +import { + NormalizedProjectProps, + NormalizerResult, + getNormalizeCommonFields, + normalizeYamlConfig, + getOptionalListField, + getOptionalArrayField, + getUnsupportedKeysError, + getMultipleUrlsOrHostsError, +} from './common_fields'; export const getNormalizeHTTPFields = ({ locations = [], @@ -21,18 +31,30 @@ export const getNormalizeHTTPFields = ({ monitor, projectId, namespace, -}: NormalizedProjectProps): { normalizedFields: HTTPFields; unsupportedKeys: string[] } => { + version, +}: NormalizedProjectProps): NormalizerResult => { const defaultFields = DEFAULT_FIELDS[DataStream.HTTP]; + const errors = []; const { yamlConfig, unsupportedKeys } = normalizeYamlConfig(monitor); - const commonFields = getNormalizeCommonFields({ locations, privateLocations, monitor, projectId, namespace, + version, }); + /* Check if monitor has multiple urls */ + const urls = getOptionalListField(monitor.urls); + if (urls.length > 1) { + errors.push(getMultipleUrlsOrHostsError(monitor, 'urls', version)); + } + + if (unsupportedKeys.length) { + errors.push(getUnsupportedKeysError(monitor, unsupportedKeys, version)); + } + const normalizedFields = { ...yamlConfig, ...commonFields, @@ -41,9 +63,13 @@ export const getNormalizeHTTPFields = ({ [ConfigKey.URLS]: getOptionalArrayField(monitor.urls) || defaultFields[ConfigKey.URLS], [ConfigKey.MAX_REDIRECTS]: monitor[ConfigKey.MAX_REDIRECTS] || defaultFields[ConfigKey.MAX_REDIRECTS], - [ConfigKey.TIMEOUT]: monitor.timeout - ? getValueInSeconds(monitor.timeout) - : defaultFields[ConfigKey.TIMEOUT], + [ConfigKey.REQUEST_BODY_CHECK]: getRequestBodyField( + (yamlConfig as Record)[ConfigKey.REQUEST_BODY_CHECK] as string, + defaultFields[ConfigKey.REQUEST_BODY_CHECK] + ), + [ConfigKey.TLS_VERSION]: get(monitor, ConfigKey.TLS_VERSION) + ? (getOptionalListField(get(monitor, ConfigKey.TLS_VERSION)) as TLSVersion[]) + : defaultFields[ConfigKey.TLS_VERSION], }; return { normalizedFields: { @@ -51,5 +77,26 @@ export const getNormalizeHTTPFields = ({ ...normalizedFields, }, unsupportedKeys, + errors, + }; +}; + +export const getRequestBodyField = ( + value: string, + defaultValue: HTTPFields[ConfigKey.REQUEST_BODY_CHECK] +): HTTPFields[ConfigKey.REQUEST_BODY_CHECK] => { + let parsedValue: string; + let type: Mode; + + if (typeof value === 'object') { + parsedValue = JSON.stringify(value); + type = Mode.JSON; + } else { + parsedValue = value; + type = Mode.PLAINTEXT; + } + return { + type, + value: parsedValue || defaultValue.value, }; }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/icmp_monitor.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/icmp_monitor.test.ts new file mode 100644 index 000000000000..e32ddf4f328a --- /dev/null +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/icmp_monitor.test.ts @@ -0,0 +1,227 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Locations, LocationStatus, PrivateLocation } from '../../../../common/runtime_types'; +import { normalizeProjectMonitors } from '.'; + +describe('http normalizers', () => { + describe('normalize push monitors', () => { + const projectId = 'test-project-id'; + const locations: Locations = [ + { + id: 'us_central', + label: 'Test Location', + geo: { lat: 33.333, lon: 73.333 }, + url: 'test-url', + isServiceManaged: true, + status: LocationStatus.GA, + }, + { + id: 'us_east', + label: 'Test Location', + geo: { lat: 33.333, lon: 73.333 }, + url: 'test-url', + isServiceManaged: true, + status: LocationStatus.GA, + }, + ]; + const privateLocations: PrivateLocation[] = [ + { + id: 'germany', + label: 'Germany', + isServiceManaged: false, + concurrentMonitors: 1, + agentPolicyId: 'germany', + }, + ]; + const monitors = [ + { + locations: ['us_central'], + type: 'icmp', + id: 'Cloudflare-DNS', + name: 'Cloudflare DNS', + hosts: ['1.1.1.1'], + schedule: 1, + tags: ['service:smtp', 'org:google'], + privateLocations: ['Test private location 0'], + timeout: '1m', + wait: '30s', + 'service.name': 'test service', + }, + { + locations: ['us_central'], + type: 'icmp', + id: 'Cloudflare-DNS-2', + name: 'Cloudflare DNS 2', + hosts: '1.1.1.1', + schedule: 1, + tags: 'tag1,tag2', + privateLocations: ['Test private location 0'], + wait: '1m', + service: { + name: 'test service', + }, + }, + { + locations: ['us_central'], + type: 'icmp', + id: 'Cloudflare-DNS-3', + name: 'Cloudflare DNS 3', + hosts: '1.1.1.1,2.2.2.2', + schedule: 1, + tags: 'tag1,tag2', + privateLocations: ['Test private location 0'], + unsupportedKey: { + nestedUnsupportedKey: 'unnsuportedValue', + }, + }, + ]; + + it('properly normalizes http monitors', () => { + const actual = normalizeProjectMonitors({ + locations, + privateLocations, + monitors, + projectId, + namespace: 'test-space', + version: '8.5.0', + }); + expect(actual).toEqual([ + { + errors: [], + normalizedFields: { + config_id: '', + custom_heartbeat_id: 'Cloudflare-DNS-test-project-id-test-space', + enabled: true, + form_monitor_type: 'icmp', + hosts: '1.1.1.1', + journey_id: 'Cloudflare-DNS', + locations: [ + { + geo: { + lat: 33.333, + lon: 73.333, + }, + id: 'us_central', + isServiceManaged: true, + label: 'Test Location', + status: 'ga', + url: 'test-url', + }, + ], + name: 'Cloudflare DNS', + namespace: 'test_space', + origin: 'project', + original_space: 'test-space', + project_id: 'test-project-id', + schedule: { + number: '1', + unit: 'm', + }, + 'service.name': 'test service', + tags: ['service:smtp', 'org:google'], + timeout: '60', + type: 'icmp', + wait: '30', + }, + unsupportedKeys: [], + }, + { + errors: [], + normalizedFields: { + config_id: '', + custom_heartbeat_id: 'Cloudflare-DNS-2-test-project-id-test-space', + enabled: true, + form_monitor_type: 'icmp', + hosts: '1.1.1.1', + journey_id: 'Cloudflare-DNS-2', + locations: [ + { + geo: { + lat: 33.333, + lon: 73.333, + }, + id: 'us_central', + isServiceManaged: true, + label: 'Test Location', + status: 'ga', + url: 'test-url', + }, + ], + name: 'Cloudflare DNS 2', + namespace: 'test_space', + origin: 'project', + original_space: 'test-space', + project_id: 'test-project-id', + schedule: { + number: '1', + unit: 'm', + }, + 'service.name': 'test service', + tags: ['tag1', 'tag2'], + timeout: '16', + type: 'icmp', + wait: '60', + }, + unsupportedKeys: [], + }, + { + errors: [ + { + details: + 'Multiple hosts are not supported for icmp project monitors in 8.5.0. Please set only 1 host per monitor. You monitor was not created or updated.', + id: 'Cloudflare-DNS-3', + reason: 'Unsupported Heartbeat option', + }, + { + details: + 'The following Heartbeat options are not supported for icmp project monitors in 8.5.0: unsupportedKey.nestedUnsupportedKey. You monitor was not created or updated.', + id: 'Cloudflare-DNS-3', + reason: 'Unsupported Heartbeat option', + }, + ], + normalizedFields: { + config_id: '', + custom_heartbeat_id: 'Cloudflare-DNS-3-test-project-id-test-space', + enabled: true, + form_monitor_type: 'icmp', + hosts: '1.1.1.1', + journey_id: 'Cloudflare-DNS-3', + locations: [ + { + geo: { + lat: 33.333, + lon: 73.333, + }, + id: 'us_central', + isServiceManaged: true, + label: 'Test Location', + status: 'ga', + url: 'test-url', + }, + ], + name: 'Cloudflare DNS 3', + namespace: 'test_space', + origin: 'project', + original_space: 'test-space', + project_id: 'test-project-id', + schedule: { + number: '1', + unit: 'm', + }, + 'service.name': '', + tags: ['tag1', 'tag2'], + timeout: '16', + type: 'icmp', + wait: '1', + }, + unsupportedKeys: ['unsupportedKey.nestedUnsupportedKey'], + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/icmp_monitor.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/icmp_monitor.ts index 282475f94d7c..ea4eb7dbdba8 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/icmp_monitor.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/icmp_monitor.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { getNormalizeCommonFields } from './common_fields'; -import { NormalizedProjectProps } from './browser_monitor'; import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; import { ConfigKey, @@ -14,7 +12,17 @@ import { FormMonitorType, ICMPFields, } from '../../../../common/runtime_types/monitor_management'; -import { normalizeYamlConfig, getValueInSeconds, getOptionalArrayField } from './common_fields'; +import { + NormalizerResult, + NormalizedProjectProps, + normalizeYamlConfig, + getNormalizeCommonFields, + getValueInSeconds, + getOptionalArrayField, + getOptionalListField, + getMultipleUrlsOrHostsError, + getUnsupportedKeysError, +} from './common_fields'; export const getNormalizeICMPFields = ({ locations = [], @@ -22,8 +30,10 @@ export const getNormalizeICMPFields = ({ monitor, projectId, namespace, -}: NormalizedProjectProps): { normalizedFields: ICMPFields; unsupportedKeys: string[] } => { + version, +}: NormalizedProjectProps): NormalizerResult => { const defaultFields = DEFAULT_FIELDS[DataStream.ICMP]; + const errors = []; const { yamlConfig, unsupportedKeys } = normalizeYamlConfig(monitor); const commonFields = getNormalizeCommonFields({ @@ -32,8 +42,19 @@ export const getNormalizeICMPFields = ({ monitor, projectId, namespace, + version, }); + /* Check if monitor has multiple hosts */ + const hosts = getOptionalListField(monitor.hosts); + if (hosts.length > 1) { + errors.push(getMultipleUrlsOrHostsError(monitor, 'hosts', version)); + } + + if (unsupportedKeys.length) { + errors.push(getUnsupportedKeysError(monitor, unsupportedKeys, version)); + } + const normalizedFields = { ...yamlConfig, ...commonFields, @@ -41,9 +62,6 @@ export const getNormalizeICMPFields = ({ [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.ICMP, [ConfigKey.HOSTS]: getOptionalArrayField(monitor[ConfigKey.HOSTS]) || defaultFields[ConfigKey.HOSTS], - [ConfigKey.TIMEOUT]: monitor.timeout - ? getValueInSeconds(monitor.timeout) - : defaultFields[ConfigKey.TIMEOUT], [ConfigKey.WAIT]: monitor.wait ? getValueInSeconds(monitor.wait) || defaultFields[ConfigKey.WAIT] : defaultFields[ConfigKey.WAIT], @@ -54,5 +72,6 @@ export const getNormalizeICMPFields = ({ ...normalizedFields, }, unsupportedKeys, + errors, }; }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/index.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/index.ts index 82b2acfacbf5..6bac17c0fb54 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/index.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/index.ts @@ -14,14 +14,7 @@ import { getNormalizeBrowserFields } from './browser_monitor'; import { getNormalizeICMPFields } from './icmp_monitor'; import { getNormalizeTCPFields } from './tcp_monitor'; import { getNormalizeHTTPFields } from './http_monitor'; - -export interface NormalizedProjectProps { - locations: Locations; - privateLocations: PrivateLocation[]; - monitor: ProjectMonitor; - projectId: string; - namespace: string; -} +import { NormalizedProjectProps } from './common_fields'; export const normalizeProjectMonitor = (props: NormalizedProjectProps) => { const { monitor } = props; @@ -50,14 +43,23 @@ export const normalizeProjectMonitors = ({ monitors = [], projectId, namespace, + version, }: { locations: Locations; privateLocations: PrivateLocation[]; monitors: ProjectMonitor[]; projectId: string; namespace: string; + version: string; }) => { return monitors.map((monitor) => { - return normalizeProjectMonitor({ monitor, locations, privateLocations, projectId, namespace }); + return normalizeProjectMonitor({ + monitor, + locations, + privateLocations, + projectId, + namespace, + version, + }); }); }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/tcp_monitor.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/tcp_monitor.test.ts new file mode 100644 index 000000000000..094bf018ba12 --- /dev/null +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/tcp_monitor.test.ts @@ -0,0 +1,265 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Locations, LocationStatus, PrivateLocation } from '../../../../common/runtime_types'; +import { normalizeProjectMonitors } from '.'; + +describe('http normalizers', () => { + describe('normalize push monitors', () => { + const projectId = 'test-project-id'; + const locations: Locations = [ + { + id: 'us_central', + label: 'Test Location', + geo: { lat: 33.333, lon: 73.333 }, + url: 'test-url', + isServiceManaged: true, + status: LocationStatus.GA, + }, + { + id: 'us_east', + label: 'Test Location', + geo: { lat: 33.333, lon: 73.333 }, + url: 'test-url', + isServiceManaged: true, + status: LocationStatus.GA, + }, + ]; + const privateLocations: PrivateLocation[] = [ + { + id: 'germany', + label: 'Germany', + isServiceManaged: false, + concurrentMonitors: 1, + agentPolicyId: 'germany', + }, + ]; + const monitors = [ + { + locations: ['us_central'], + type: 'tcp', + id: 'gmail-smtp', + name: 'GMail SMTP', + hosts: ['smtp.gmail.com:587'], + schedule: 1, + tags: ['service:smtp', 'org:google'], + privateLocations: ['BEEP'], + 'service.name': 'test service', + 'ssl.supported_protocols': ['TLSv1.2', 'TLSv1.3'], + }, + { + locations: ['us_central'], + type: 'tcp', + id: 'always-down', + name: 'Always Down', + hosts: 'localhost:18278', + schedule: 1, + tags: 'tag1,tag2', + privateLocations: ['BEEP'], + service: { + name: 'test service', + }, + ssl: { + supported_protocols: 'TLSv1.2,TLSv1.3', + }, + }, + { + locations: ['us_central'], + type: 'tcp', + id: 'always-down', + name: 'Always Down', + hosts: ['localhost', 'anotherhost'], + ports: ['5698'], + schedule: 1, + tags: 'tag1,tag2', + privateLocations: ['BEEP'], + unsupportedKey: { + nestedUnsupportedKey: 'unnsuportedValue', + }, + }, + ]; + + it('properly normalizes http monitors', () => { + const actual = normalizeProjectMonitors({ + locations, + privateLocations, + monitors, + projectId, + namespace: 'test-space', + version: '8.5.0', + }); + expect(actual).toEqual([ + { + errors: [], + normalizedFields: { + __ui: { + is_tls_enabled: false, + }, + 'check.receive': '', + 'check.send': '', + config_id: '', + custom_heartbeat_id: 'gmail-smtp-test-project-id-test-space', + enabled: true, + form_monitor_type: 'tcp', + hosts: 'smtp.gmail.com:587', + journey_id: 'gmail-smtp', + locations: [ + { + geo: { + lat: 33.333, + lon: 73.333, + }, + id: 'us_central', + isServiceManaged: true, + label: 'Test Location', + status: 'ga', + url: 'test-url', + }, + ], + name: 'GMail SMTP', + namespace: 'test_space', + origin: 'project', + original_space: 'test-space', + project_id: 'test-project-id', + proxy_url: '', + proxy_use_local_resolver: false, + schedule: { + number: '1', + unit: 'm', + }, + 'service.name': 'test service', + 'ssl.certificate': '', + 'ssl.certificate_authorities': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.supported_protocols': ['TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + tags: ['service:smtp', 'org:google'], + timeout: '16', + type: 'tcp', + }, + unsupportedKeys: [], + }, + { + errors: [], + normalizedFields: { + __ui: { + is_tls_enabled: false, + }, + 'check.receive': '', + 'check.send': '', + config_id: '', + custom_heartbeat_id: 'always-down-test-project-id-test-space', + enabled: true, + form_monitor_type: 'tcp', + hosts: 'localhost:18278', + journey_id: 'always-down', + locations: [ + { + geo: { + lat: 33.333, + lon: 73.333, + }, + id: 'us_central', + isServiceManaged: true, + label: 'Test Location', + status: 'ga', + url: 'test-url', + }, + ], + name: 'Always Down', + namespace: 'test_space', + origin: 'project', + original_space: 'test-space', + project_id: 'test-project-id', + proxy_url: '', + proxy_use_local_resolver: false, + schedule: { + number: '1', + unit: 'm', + }, + 'service.name': 'test service', + 'ssl.certificate': '', + 'ssl.certificate_authorities': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.supported_protocols': ['TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + tags: ['tag1', 'tag2'], + timeout: '16', + type: 'tcp', + }, + unsupportedKeys: [], + }, + { + errors: [ + { + details: + 'Multiple hosts are not supported for tcp project monitors in 8.5.0. Please set only 1 host per monitor. You monitor was not created or updated.', + id: 'always-down', + reason: 'Unsupported Heartbeat option', + }, + { + details: + 'The following Heartbeat options are not supported for tcp project monitors in 8.5.0: ports|unsupportedKey.nestedUnsupportedKey. You monitor was not created or updated.', + id: 'always-down', + reason: 'Unsupported Heartbeat option', + }, + ], + normalizedFields: { + __ui: { + is_tls_enabled: false, + }, + 'check.receive': '', + 'check.send': '', + config_id: '', + custom_heartbeat_id: 'always-down-test-project-id-test-space', + enabled: true, + form_monitor_type: 'tcp', + hosts: 'localhost', + journey_id: 'always-down', + locations: [ + { + geo: { + lat: 33.333, + lon: 73.333, + }, + id: 'us_central', + isServiceManaged: true, + label: 'Test Location', + status: 'ga', + url: 'test-url', + }, + ], + name: 'Always Down', + namespace: 'test_space', + origin: 'project', + original_space: 'test-space', + project_id: 'test-project-id', + proxy_url: '', + proxy_use_local_resolver: false, + schedule: { + number: '1', + unit: 'm', + }, + 'service.name': '', + 'ssl.certificate': '', + 'ssl.certificate_authorities': '', + 'ssl.key': '', + 'ssl.key_passphrase': '', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + tags: ['tag1', 'tag2'], + timeout: '16', + type: 'tcp', + }, + unsupportedKeys: ['ports', 'unsupportedKey.nestedUnsupportedKey'], + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/tcp_monitor.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/tcp_monitor.ts index 8a85b2959d80..1045bc5ebff7 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/tcp_monitor.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/tcp_monitor.ts @@ -4,18 +4,25 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { NormalizedProjectProps } from './browser_monitor'; +import { get } from 'lodash'; import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults'; -import { normalizeYamlConfig, getValueInSeconds } from './common_fields'; - import { ConfigKey, DataStream, FormMonitorType, TCPFields, + TLSVersion, } from '../../../../common/runtime_types/monitor_management'; -import { getNormalizeCommonFields, getOptionalArrayField } from './common_fields'; +import { + NormalizedProjectProps, + NormalizerResult, + normalizeYamlConfig, + getNormalizeCommonFields, + getOptionalArrayField, + getOptionalListField, + getMultipleUrlsOrHostsError, + getUnsupportedKeysError, +} from './common_fields'; export const getNormalizeTCPFields = ({ locations = [], @@ -23,8 +30,10 @@ export const getNormalizeTCPFields = ({ monitor, projectId, namespace, -}: NormalizedProjectProps): { normalizedFields: TCPFields; unsupportedKeys: string[] } => { + version, +}: NormalizedProjectProps): NormalizerResult => { const defaultFields = DEFAULT_FIELDS[DataStream.TCP]; + const errors = []; const { yamlConfig, unsupportedKeys } = normalizeYamlConfig(monitor); const commonFields = getNormalizeCommonFields({ @@ -33,8 +42,19 @@ export const getNormalizeTCPFields = ({ monitor, projectId, namespace, + version, }); + /* Check if monitor has multiple hosts */ + const hosts = getOptionalListField(monitor.hosts); + if (hosts.length > 1) { + errors.push(getMultipleUrlsOrHostsError(monitor, 'hosts', version)); + } + + if (unsupportedKeys.length) { + errors.push(getUnsupportedKeysError(monitor, unsupportedKeys, version)); + } + const normalizedFields = { ...yamlConfig, ...commonFields, @@ -42,9 +62,9 @@ export const getNormalizeTCPFields = ({ [ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP, [ConfigKey.HOSTS]: getOptionalArrayField(monitor[ConfigKey.HOSTS]) || defaultFields[ConfigKey.HOSTS], - [ConfigKey.TIMEOUT]: monitor.timeout - ? getValueInSeconds(monitor.timeout) - : defaultFields[ConfigKey.TIMEOUT], + [ConfigKey.TLS_VERSION]: get(monitor, ConfigKey.TLS_VERSION) + ? (getOptionalListField(get(monitor, ConfigKey.TLS_VERSION)) as TLSVersion[]) + : defaultFields[ConfigKey.TLS_VERSION], }; return { normalizedFields: { @@ -52,5 +72,6 @@ export const getNormalizeTCPFields = ({ ...normalizedFields, }, unsupportedKeys, + errors, }; }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts index aa0be87f0e81..1ca27869aa2c 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts @@ -197,22 +197,17 @@ export class ProjectMonitorFormatter { try { await this.validatePermissions({ monitor }); - const { normalizedFields: normalizedMonitor, unsupportedKeys } = normalizeProjectMonitor({ + const { normalizedFields: normalizedMonitor, errors } = normalizeProjectMonitor({ monitor, locations: this.locations, privateLocations: this.privateLocations, projectId: this.projectId, namespace: this.spaceId, + version: this.server.kibanaVersion, }); - if (unsupportedKeys.length) { - this.failedMonitors.push({ - id: monitor.id, - reason: 'Unsupported Heartbeat option', - details: `The following Heartbeat options are not supported for ${ - monitor.type - } project monitors in ${this.server.kibanaVersion}: ${unsupportedKeys.join('|')}`, - }); + if (errors.length) { + this.failedMonitors.push(...errors); this.handleStreamingMessage({ message: `${monitor.id}: failed to create or update monitor`, }); diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 0c389000f6c8..9a47ad9de093 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4644,6 +4644,18 @@ "properties": { "all": { "properties": { + "total": { + "type": "long" + }, + "monthly": { + "type": "long" + }, + "weekly": { + "type": "long" + }, + "daily": { + "type": "long" + }, "assignees": { "properties": { "total": { @@ -4657,18 +4669,6 @@ } } }, - "total": { - "type": "long" - }, - "monthly": { - "type": "long" - }, - "weekly": { - "type": "long" - }, - "daily": { - "type": "long" - }, "status": { "properties": { "open": { @@ -4720,6 +4720,18 @@ }, "sec": { "properties": { + "total": { + "type": "long" + }, + "monthly": { + "type": "long" + }, + "weekly": { + "type": "long" + }, + "daily": { + "type": "long" + }, "assignees": { "properties": { "total": { @@ -4732,7 +4744,11 @@ "type": "long" } } - }, + } + } + }, + "obs": { + "properties": { "total": { "type": "long" }, @@ -4744,11 +4760,7 @@ }, "daily": { "type": "long" - } - } - }, - "obs": { - "properties": { + }, "assignees": { "properties": { "total": { @@ -4761,7 +4773,11 @@ "type": "long" } } - }, + } + } + }, + "main": { + "properties": { "total": { "type": "long" }, @@ -4773,11 +4789,7 @@ }, "daily": { "type": "long" - } - } - }, - "main": { - "properties": { + }, "assignees": { "properties": { "total": { @@ -4790,18 +4802,6 @@ "type": "long" } } - }, - "total": { - "type": "long" - }, - "monthly": { - "type": "long" - }, - "weekly": { - "type": "long" - }, - "daily": { - "type": "long" } } } @@ -10029,6 +10029,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10161,6 +10183,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10293,6 +10337,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10425,6 +10491,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10557,6 +10645,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10689,6 +10799,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10847,6 +10979,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10979,6 +11133,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11111,6 +11287,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11243,6 +11441,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11375,6 +11595,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11507,6 +11749,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11665,6 +11929,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11797,6 +12083,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11929,6 +12237,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -12061,6 +12391,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -12193,6 +12545,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -12325,6 +12699,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { diff --git a/x-pack/plugins/threat_intelligence/FAQ.md b/x-pack/plugins/threat_intelligence/FAQ.md deleted file mode 100644 index d3f128771384..000000000000 --- a/x-pack/plugins/threat_intelligence/FAQ.md +++ /dev/null @@ -1,15 +0,0 @@ -# FAQ - -### Where can I find the UI for the Threat Intelligence plugin? - -Kibana recommends working on a fork of the [elastic/kibana repository](https://github.com/elastic/kibana) (see [here](https://docs.github.com/en/get-started/quickstart/fork-a-repo) to learn about forks). - -### How is the Threat Intelligence code loaded in Kibana? - -The Threat Intelligence plugin is loaded within the [security_solution](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution) plugin. - -### I'm not seeing any data in the Indicators' table - -See this [documentation here](https://github.com/elastic/security-team/blob/main/docs/protections-team/threat-intelligence-services/protections-experience/development-setup.mdx) to get Threat Intelligence feed in Kibana. - -Once you have the feed running, go to `Management > Advanced Settings > Threat indices` and add `filebeat-*` to the list (comma separated). \ No newline at end of file diff --git a/x-pack/plugins/threat_intelligence/README.md b/x-pack/plugins/threat_intelligence/README.md index 945ab9b85a4f..7395ca0df8a7 100755 --- a/x-pack/plugins/threat_intelligence/README.md +++ b/x-pack/plugins/threat_intelligence/README.md @@ -6,28 +6,55 @@ Elastic Threat Intelligence makes it easy to analyze and investigate potential s The Threat Intelligence UI is displayed in Kibana Security, under the Explore section. -## Quick Start +## Development setup -See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. +### Kibana development in general -Verify your node version [here](https://github.com/elastic/kibana/blob/main/.node-version). +Best source - [internal Kibana docs](https://docs.elastic.dev/kibana-dev-docs/getting-started/welcome). If you have any issues with setting up your Kibana dev environment [#kibana](https://elastic.slack.com/archives/C0D8P2XK5) Slack channel is a good way to get help. -**Run ES:** +### Essential `kibana.yml` settings -`yarn es snapshot --license trial` +You can make a copy of `kibana.yml` file into `kibana.dev.yml` and make adjustments to the settings. External documentation on the flags available is [here](https://www.elastic.co/guide/en/kibana/current/settings.html) -**Run Kibana:** +It is recommended to set `server.basePath: "/kbn"` to make you local instance persist the base Kibana path. If you don't do it, the base path will be a random string every time you start Kibana. Any other value than `/kbn` will also work. -> **Important:** -> -> See here to get your `kibana.yaml` to enable the Threat Intelligence plugin. +### Getting Threat Intelligence feeds data into Kibana -``` -yarn kbn reset && yarn kbn bootstrap -yarn start --no-base-path -``` +There are many ways to get data for you local development. We first focus on getting Threat Intelligence data specifically. + +### Setting up filebeat threatintel integrations locally + +1. install [mage](https://github.com/magefile/mage). It is a Go build tool used to build `beats`. Installation from the sources requires Go lang set up. A simpler option might be to install it from a package manager available in your system (eg. `brew` on MacOs) or use their [binary distribution](https://github.com/magefile/mage/releases) +1. start Elasticsearch and Kibana +1. clone [beats](https://github.com/elastic/beats) repository +1. inside beats repository, update `x-pack/filebeat/filebeat.yml` with your local Elasticsearch and Kibana connection configs + + ``` + output.elasticsearch: + hosts: ["localhost:9200"] + username: "elastic" + password: "changeme" + + setup.kibana: + host: "localhost:5601" // make sure to run Kibana with --no-base-path option or specify server.basePath in Kibana config and use it here as a path, eg. localhost:5601/kbn + ``` + +1. go into `x-pack/filebeat` (that's where security related modules live) +1. build filebeat `mage build` +1. enable `threatintel` module by running `./filebeat modules enable threatintel` +1. enable specific Threat Intelligence integrations by updating `modules.d/threatintel.yml`. Update `enable` to `true` in every integration you want to enable and configs specific for these integrations. The bare minimum is to enable Abuse.CH feeds `abuseurl`, `abusemalware` and `malwarebazaar`. +1. run `./filebeat setup -E setup.dashboards.directory=build/kibana` to set up predefined dashboards +1. run `./filebeat -e` to start filebeat +1. to validate that the set up works, wait for some Threat Intel data to be ingested and then go in Analytics > Discover in your local Kibana to search `event.category : threat and event.type : indicator`. You should see some documents returned by this search. Abuse.CH feeds are up to date so you should see the results from the last 7 days. + +### More ways to get data -### Performance +There are many more tools available for getting the data for testing or local development, depending on the data type and usecase. + +- Kibana development docs > [Add data](https://docs.elastic.dev/kibana-dev-docs/getting-started/sample-data) +- [Dev/Design/Testing Environments and Frameworks](https://docs.google.com/document/d/1DGCcLMnVKQ_STlkbS4E0m4kbPivNtR8iMlg_IoCuCEw/edit#) gathered by Security Engineering Productivity team + +### Generate fixtures for local testing You can generate large volumes of threat indicators on demand with the following script: @@ -37,17 +64,29 @@ node scripts/generate_indicators.js see the file in order to adjust the amount of indicators generated. The default is one million. -### Useful hints +## Data for E2E tests -Export local instance data to es_archives (will be loaded in cypress tests). +Use es_archives to export data for e2e testing purposes, like so: ``` TEST_ES_PORT=9200 node scripts/es_archiver save x-pack/test/threat_intelligence_cypress/es_archives/threat_intelligence "logs-ti*" ``` +These can be loaded at will with `x-pack/plugins/threat_intelligence/cypress/tasks/es_archiver.ts` task. + +You can use this approach to load separate data dumps for every test case, to cover all critical scenarios. + ## FAQ -See [FAQ.md](https://github.com/elastic/kibana/blob/main/x-pack/plugins/threat_intelligence/FAQ.md) for questions you may have. +### How is the Threat Intelligence code loaded in Kibana? + +The Threat Intelligence plugin is loaded lazily within the [security_solution](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution) plugin, +from `x-pack/plugins/security_solution/public/threat_intelligence` owned by the Protections Experience Team. + +## QA and demo for implemented features + +One way to QA and demo the feature merged into `main` branch is to run the latest `main` locally. +Another option is to deploy a Staging instance. For Staging environment snapshots are being build every night with the latest state of the `main` branch. More documentation can be found [here](https://cloud.elastic.dev/environments/Staging/#automatic-termination-of-staging-deployments) ## Contributing diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_indicators_filters_context.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_indicators_filters_context.tsx index 6e9c0b373a0f..4b7832631fd1 100644 --- a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_indicators_filters_context.tsx +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_indicators_filters_context.tsx @@ -6,11 +6,19 @@ */ import { FilterManager } from '@kbn/data-plugin/public'; -import { IndicatorsFiltersContextValue } from '../../modules/indicators/context'; +import { IndicatorsFiltersContextValue } from '../../modules/indicators/containers/indicators_filters/context'; export const mockIndicatorsFiltersContext: IndicatorsFiltersContextValue = { filterManager: { getFilters: () => [], setFilters: () => {}, } as unknown as FilterManager, + filters: [], + filterQuery: { + language: 'kuery', + query: '', + }, + handleSavedQuery: () => {}, + handleSubmitQuery: () => {}, + handleSubmitTimeRange: () => {}, }; diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx index eea2596327fb..7cea653c45e5 100644 --- a/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx @@ -16,7 +16,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { mockIndicatorsFiltersContext } from './mock_indicators_filters_context'; import { SecuritySolutionContext } from '../../containers/security_solution_context'; import { getSecuritySolutionContextMock } from './mock_security_context'; -import { IndicatorsFiltersContext } from '../../modules/indicators/context'; +import { IndicatorsFiltersContext } from '../../modules/indicators/containers/indicators_filters/context'; import { FieldTypesContext } from '../../containers/field_types_provider'; import { generateFieldTypeMap } from './mock_field_type_map'; import { mockUiSettingsService } from './mock_kibana_ui_settings_service'; diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx index a93b6bfe3046..c41f8972fd60 100644 --- a/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx @@ -18,12 +18,13 @@ import { createTGridMocks } from '@kbn/timelines-plugin/public/mock'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { MemoryRouter } from 'react-router-dom'; import { KibanaContext } from '../../hooks/use_kibana'; import { SecuritySolutionPluginContext } from '../../types'; import { getSecuritySolutionContextMock } from './mock_security_context'; import { mockUiSetting } from './mock_kibana_ui_settings_service'; import { SecuritySolutionContext } from '../../containers/security_solution_context'; -import { IndicatorsFiltersContext } from '../../modules/indicators/context'; +import { IndicatorsFiltersContext } from '../../modules/indicators/containers/indicators_filters/context'; import { mockIndicatorsFiltersContext } from './mock_indicators_filters_context'; import { FieldTypesContext } from '../../containers/field_types_provider'; import { generateFieldTypeMap } from './mock_field_type_map'; @@ -128,23 +129,25 @@ export const mockedServices = { }; export const TestProvidersComponent: FC = ({ children }) => ( - - - - - - - - - {children} - - - - - - - - + + + + + + + + + + {children} + + + + + + + + + ); export type MockedSearch = jest.Mocked; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.stories.tsx index 8d3f13ae01af..eb0ed8fb045e 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/fields_table/fields_table.stories.tsx @@ -10,7 +10,7 @@ import { mockIndicatorsFiltersContext } from '../../../../../common/mocks/mock_i import { IndicatorFieldsTable } from '.'; import { generateMockIndicator } from '../../../../../../common/types/indicator'; import { StoryProvidersComponent } from '../../../../../common/mocks/story_providers'; -import { IndicatorsFiltersContext } from '../../../context'; +import { IndicatorsFiltersContext } from '../../../containers/indicators_filters'; export default { component: IndicatorFieldsTable, diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/flyout.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/flyout.stories.tsx index 12345056c7a9..69236e778178 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/flyout.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/flyout.stories.tsx @@ -14,7 +14,7 @@ import { mockUiSettingsService } from '../../../../common/mocks/mock_kibana_ui_s import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service'; import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; import { IndicatorsFlyout } from '.'; -import { IndicatorsFiltersContext } from '../../context'; +import { IndicatorsFiltersContext } from '../../containers/indicators_filters'; export default { component: IndicatorsFlyout, diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/block/block.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/block/block.stories.tsx index 1049518f4620..e30d352c2644 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/block/block.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/block/block.stories.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { IndicatorsFiltersContext } from '../../../../context'; +import { IndicatorsFiltersContext } from '../../../../containers/indicators_filters'; import { StoryProvidersComponent } from '../../../../../../common/mocks/story_providers'; import { generateMockIndicator } from '../../../../../../../common/types/indicator'; import { IndicatorBlock } from '.'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.stories.tsx index d543b6b6d112..005edd9c4201 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/overview_tab/overview_tab.stories.tsx @@ -10,7 +10,7 @@ import { Story } from '@storybook/react'; import { StoryProvidersComponent } from '../../../../../common/mocks/story_providers'; import { generateMockIndicator, Indicator } from '../../../../../../common/types/indicator'; import { IndicatorsFlyoutOverview } from '.'; -import { IndicatorsFiltersContext } from '../../../context'; +import { IndicatorsFiltersContext } from '../../../containers/indicators_filters'; export default { component: IndicatorsFlyoutOverview, diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.stories.tsx index 014d57b8ec11..60808a46356a 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/flyout/table_tab/table_tab.stories.tsx @@ -14,7 +14,7 @@ import { mockUiSettingsService } from '../../../../../common/mocks/mock_kibana_u import { mockKibanaTimelinesService } from '../../../../../common/mocks/mock_kibana_timelines_service'; import { generateMockIndicator, Indicator } from '../../../../../../common/types/indicator'; import { IndicatorsFlyoutTable } from '.'; -import { IndicatorsFiltersContext } from '../../../context'; +import { IndicatorsFiltersContext } from '../../../containers/indicators_filters'; export default { component: IndicatorsFlyoutTable, diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx index 213c750c5d1e..f08e8f3b2f0e 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx @@ -120,7 +120,16 @@ export const Default: Story = () => { - + ); }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx index 997cfc7922b9..48d084b0e832 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx @@ -13,6 +13,7 @@ import { TestProvidersComponent } from '../../../../common/mocks/test_providers' import { IndicatorsBarChartWrapper } from './indicators_barchart_wrapper'; import { DEFAULT_TIME_RANGE } from '../../../query_bar/hooks/use_filters/utils'; import { useFilters } from '../../../query_bar/hooks/use_filters'; +import moment from 'moment'; jest.mock('../../../query_bar/hooks/use_filters'); @@ -47,7 +48,14 @@ describe('', () => { it('should render barchart and field selector dropdown', () => { const component = render( - + ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.tsx index fab1cfc5473d..31148685e370 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.tsx @@ -9,11 +9,12 @@ import React, { memo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { TimeRange } from '@kbn/es-query'; +import { TimeRangeBounds } from '@kbn/data-plugin/common'; import { SecuritySolutionDataViewBase } from '../../../../types'; import { RawIndicatorFieldId } from '../../../../../common/types/indicator'; -import { useAggregatedIndicators } from '../../hooks/use_aggregated_indicators'; import { IndicatorsFieldSelector } from '../indicators_field_selector/indicators_field_selector'; import { IndicatorsBarChart } from '../indicators_barchart/indicators_barchart'; +import { ChartSeries } from '../../services/fetch_aggregated_indicators'; const DEFAULT_FIELD = RawIndicatorFieldId.Feed; @@ -26,6 +27,14 @@ export interface IndicatorsBarChartWrapperProps { * List of fields coming from the Security Solution sourcerer data view, passed down to the {@link IndicatorFieldSelector} to populate the dropdown. */ indexPattern: SecuritySolutionDataViewBase; + + series: ChartSeries[]; + + dateRange: TimeRangeBounds; + + field: string; + + onFieldChange: (value: string) => void; } /** @@ -33,11 +42,7 @@ export interface IndicatorsBarChartWrapperProps { * and handles retrieving aggregated indicator data. */ export const IndicatorsBarChartWrapper = memo( - ({ timeRange, indexPattern }) => { - const { dateRange, indicators, selectedField, onFieldChange } = useAggregatedIndicators({ - timeRange, - }); - + ({ timeRange, indexPattern, series, dateRange, field, onFieldChange }) => { return ( <> @@ -60,7 +65,7 @@ export const IndicatorsBarChartWrapper = memo( {timeRange ? ( - + ) : ( <> )} diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx index 034fc1363043..6505996a26a7 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx @@ -11,7 +11,7 @@ import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indi import { StoryProvidersComponent } from '../../../../common/mocks/story_providers'; import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; import { IndicatorsTable } from './indicators_table'; -import { IndicatorsFiltersContext } from '../../context'; +import { IndicatorsFiltersContext } from '../../containers/indicators_filters/context'; import { DEFAULT_COLUMNS } from './hooks/use_column_settings'; export default { diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/context.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/containers/indicators_filters/context.ts similarity index 55% rename from x-pack/plugins/threat_intelligence/public/modules/indicators/context.ts rename to x-pack/plugins/threat_intelligence/public/modules/indicators/containers/indicators_filters/context.ts index b6a4d17754f1..b075668c5015 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/context.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/containers/indicators_filters/context.ts @@ -6,13 +6,18 @@ */ import { createContext } from 'react'; -import { FilterManager } from '@kbn/data-plugin/public'; +import { FilterManager, SavedQuery } from '@kbn/data-plugin/public'; +import { Filter, Query, TimeRange } from '@kbn/es-query'; export interface IndicatorsFiltersContextValue { - /** - * FilterManager is used to interact with KQL bar. - */ + timeRange?: TimeRange; + filters: Filter[]; + filterQuery: Query; + handleSavedQuery: (savedQuery: SavedQuery | undefined) => void; + handleSubmitTimeRange: (timeRange?: TimeRange) => void; + handleSubmitQuery: (filterQuery: Query) => void; filterManager: FilterManager; + savedQuery?: SavedQuery; } export const IndicatorsFiltersContext = createContext( diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/containers/indicators_filters/index.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/containers/indicators_filters/index.ts new file mode 100644 index 000000000000..4ef23e3e9500 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/containers/indicators_filters/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './indicators_filters'; + +export * from './context'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/containers/indicators_filters/indicators_filters.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/containers/indicators_filters/indicators_filters.tsx index 0fdd5c60ce5c..2e8a2b55e438 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/containers/indicators_filters/indicators_filters.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/containers/indicators_filters/indicators_filters.tsx @@ -5,26 +5,105 @@ * 2.0. */ -import React, { ReactNode, VFC } from 'react'; -import { FilterManager } from '@kbn/data-plugin/public'; -import { IndicatorsFiltersContext, IndicatorsFiltersContextValue } from '../../context'; - -export interface IndicatorsFiltersProps { - /** - * Get {@link FilterManager} from the useFilters hook and save it in context to use within the indicators table. - */ - filterManager: FilterManager; - /** - * Component(s) to be displayed inside - */ - children: ReactNode; -} +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { SavedQuery } from '@kbn/data-plugin/common'; +import { Filter, Query, TimeRange } from '@kbn/es-query'; +import deepEqual from 'fast-deep-equal'; +import { IndicatorsFiltersContext, IndicatorsFiltersContextValue } from './context'; +import { useKibana } from '../../../../hooks/use_kibana'; + +import { + DEFAULT_QUERY, + DEFAULT_TIME_RANGE, + encodeState, + FILTERS_QUERYSTRING_NAMESPACE, + stateFromQueryParams, +} from '../../../query_bar/hooks/use_filters/utils'; /** * Container used to wrap components and share the {@link FilterManager} through React context. */ -export const IndicatorsFilters: VFC = ({ filterManager, children }) => { - const contextValue: IndicatorsFiltersContextValue = { filterManager }; +export const IndicatorsFilters: FC = ({ children }) => { + const { pathname: browserPathName, search } = useLocation(); + const history = useHistory(); + const [savedQuery, setSavedQuery] = useState(undefined); + + const { + services: { + data: { + query: { filterManager }, + }, + }, + } = useKibana(); + + // Filters are picked using the UI widgets + const [filters, setFilters] = useState([]); + + // Time range is self explanatory + const [timeRange, setTimeRange] = useState(DEFAULT_TIME_RANGE); + + // filterQuery is raw kql query that user can type in to filter results + const [filterQuery, setFilterQuery] = useState(DEFAULT_QUERY); + + // Serialize filters into query string + useEffect(() => { + const filterStateAsString = encodeState({ filters, filterQuery, timeRange }); + if (!deepEqual(filterManager.getFilters(), filters)) { + filterManager.setFilters(filters); + } + + history.replace({ + pathname: browserPathName, + search: `${FILTERS_QUERYSTRING_NAMESPACE}=${filterStateAsString}`, + }); + }, [browserPathName, filterManager, filterQuery, filters, history, timeRange]); + + // Sync filterManager to local state (after they are changed from the ui) + useEffect(() => { + const subscription = filterManager.getUpdates$().subscribe(() => { + setFilters(filterManager.getFilters()); + }); + + return () => subscription.unsubscribe(); + }, [filterManager]); + + // Update local state with filter values from the url (on initial mount) + useEffect(() => { + const { + filters: filtersFromQuery, + timeRange: timeRangeFromQuery, + filterQuery: filterQueryFromQuery, + } = stateFromQueryParams(search); + + setTimeRange(timeRangeFromQuery); + setFilterQuery(filterQueryFromQuery); + setFilters(filtersFromQuery); + + // We only want to have it done on initial render with initial 'search' value; + // that is why 'search' is ommited from the deps array + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filterManager]); + + const onSavedQuery = useCallback( + (newSavedQuery: SavedQuery | undefined) => setSavedQuery(newSavedQuery), + [] + ); + + const contextValue: IndicatorsFiltersContextValue = useMemo( + () => ({ + timeRange, + filters, + filterQuery, + handleSavedQuery: onSavedQuery, + handleSubmitTimeRange: setTimeRange, + handleSubmitQuery: setFilterQuery, + filterManager, + savedQuery, + }), + [filterManager, filterQuery, filters, onSavedQuery, savedQuery, timeRange] + ); return ( diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx index 6b3feb740690..85c703cf5dca 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx @@ -12,33 +12,22 @@ import { mockedTimefilterService, TestProvidersComponent, } from '../../../common/mocks/test_providers'; -import { useFilters } from '../../query_bar/hooks/use_filters'; import { createFetchAggregatedIndicators } from '../services/fetch_aggregated_indicators'; jest.mock('../services/fetch_aggregated_indicators'); -jest.mock('../../query_bar/hooks/use_filters'); const useAggregatedIndicatorsParams: UseAggregatedIndicatorsParam = { timeRange: DEFAULT_TIME_RANGE, + filters: [], + filterQuery: { language: 'kuery', query: '' }, }; -const stub = () => {}; - const renderUseAggregatedIndicators = () => - renderHook((props) => useAggregatedIndicators(props), { + renderHook((props: UseAggregatedIndicatorsParam) => useAggregatedIndicators(props), { initialProps: useAggregatedIndicatorsParams, wrapper: TestProvidersComponent, }); -const initialFiltersValue = { - filters: [], - filterQuery: { language: 'kuery', query: '' }, - filterManager: {} as any, - handleSavedQuery: stub, - handleSubmitQuery: stub, - handleSubmitTimeRange: stub, -}; - describe('useAggregatedIndicators()', () => { beforeEach(jest.clearAllMocks); @@ -56,14 +45,12 @@ describe('useAggregatedIndicators()', () => { (createFetchAggregatedIndicators as MockedCreateFetchAggregatedIndicators).mockReturnValue( aggregatedIndicatorsQuery ); - - (useFilters as jest.MockedFunction).mockReturnValue(initialFiltersValue); }); it('should create and call the aggregatedIndicatorsQuery correctly', async () => { aggregatedIndicatorsQuery.mockResolvedValue([]); - const { rerender } = renderUseAggregatedIndicators(); + const { result, rerender } = renderUseAggregatedIndicators(); // indicators service and the query should be called just once expect( @@ -81,14 +68,12 @@ describe('useAggregatedIndicators()', () => { expect.any(AbortSignal) ); - // After filter values change, the hook will be re-rendered and should call the query function again, with - // updated values - (useFilters as jest.MockedFunction).mockReturnValue({ - ...initialFiltersValue, - filterQuery: { language: 'kuery', query: "threat.indicator.type: 'file'" }, - }); - - await act(async () => rerender()); + await act(async () => + rerender({ + filterQuery: { language: 'kuery', query: "threat.indicator.type: 'file'" }, + filters: [], + }) + ); expect(aggregatedIndicatorsQuery).toHaveBeenCalledTimes(2); expect(aggregatedIndicatorsQuery).toHaveBeenLastCalledWith( @@ -97,5 +82,16 @@ describe('useAggregatedIndicators()', () => { }), expect.any(AbortSignal) ); + expect(result.current).toMatchInlineSnapshot(` + Object { + "dateRange": Object { + "max": "2022-01-02T00:00:00.000Z", + "min": "2022-01-01T00:00:00.000Z", + }, + "onFieldChange": [Function], + "selectedField": "threat.feed.name", + "series": Array [], + } + `); }); }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts index 2609dbda5eb1..98e672ac3ad9 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { TimeRange } from '@kbn/es-query'; +import { useQuery } from '@tanstack/react-query'; +import { Filter, Query, TimeRange } from '@kbn/es-query'; import { useMemo, useState } from 'react'; import { TimeRangeBounds } from '@kbn/data-plugin/common'; -import { useQuery } from '@tanstack/react-query'; import { useInspector } from '../../../hooks/use_inspector'; -import { useFilters } from '../../query_bar/hooks/use_filters'; import { RawIndicatorFieldId } from '../../../../common/types/indicator'; import { useKibana } from '../../../hooks/use_kibana'; import { DEFAULT_TIME_RANGE } from '../../query_bar/hooks/use_filters/utils'; @@ -27,13 +26,15 @@ export interface UseAggregatedIndicatorsParam { * to query indicators for the Indicators barchart. */ timeRange?: TimeRange; + filters: Filter[]; + filterQuery: Query; } export interface UseAggregatedIndicatorsValue { /** * Array of {@link ChartSeries}, ready to be used in the Indicators barchart. */ - indicators: ChartSeries[]; + series: ChartSeries[]; /** * Callback used by the IndicatorsFieldSelector component to query a new set of * aggregated indicators. @@ -54,6 +55,8 @@ const DEFAULT_FIELD = RawIndicatorFieldId.Feed; export const useAggregatedIndicators = ({ timeRange = DEFAULT_TIME_RANGE, + filters, + filterQuery, }: UseAggregatedIndicatorsParam): UseAggregatedIndicatorsValue => { const { services: { @@ -66,7 +69,6 @@ export const useAggregatedIndicators = ({ const { inspectorAdapters } = useInspector(); const [field, setField] = useState(DEFAULT_FIELD); - const { filters, filterQuery } = useFilters(); const aggregatedIndicatorsQuery = useMemo( () => @@ -105,7 +107,7 @@ export const useAggregatedIndicators = ({ return { dateRange, - indicators: data || [], + series: data || [], onFieldChange: setField, selectedField: field, }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx index 0b0620813078..7292dbcd1b03 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx @@ -98,6 +98,27 @@ describe('useIndicators()', () => { }), expect.any(AbortSignal) ); + + expect(hookResult.result.current).toMatchInlineSnapshot(` + Object { + "handleRefresh": [Function], + "indicatorCount": 0, + "indicators": Array [], + "isFetching": true, + "isLoading": true, + "onChangeItemsPerPage": [Function], + "onChangePage": [Function], + "pagination": Object { + "pageIndex": 0, + "pageSize": 50, + "pageSizeOptions": Array [ + 10, + 25, + 50, + ], + }, + } + `); }); }); }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts index 5303b5dae06c..1855d411eb8c 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts @@ -8,6 +8,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { Filter, Query, TimeRange } from '@kbn/es-query'; import { useQuery } from '@tanstack/react-query'; +import { EuiDataGridSorting } from '@elastic/eui'; import { useInspector } from '../../../hooks/use_inspector'; import { Indicator } from '../../../../common/types/indicator'; import { useKibana } from '../../../hooks/use_kibana'; @@ -22,11 +23,15 @@ export interface UseIndicatorsParams { filterQuery: Query; filters: Filter[]; timeRange?: TimeRange; - sorting: any[]; + sorting: EuiDataGridSorting['columns']; } export interface UseIndicatorsValue { handleRefresh: () => void; + + /** + * Array of {@link Indicator} ready to render inside the IndicatorTable component + */ indicators: Indicator[]; indicatorCount: number; pagination: Pagination; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators_filters_context.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators_filters_context.ts index e8cc4b18474b..e4c7c48d03d1 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators_filters_context.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators_filters_context.ts @@ -6,7 +6,10 @@ */ import { useContext } from 'react'; -import { IndicatorsFiltersContext, IndicatorsFiltersContextValue } from '../context'; +import { + IndicatorsFiltersContext, + IndicatorsFiltersContextValue, +} from '../containers/indicators_filters/context'; /** * Hook to retrieve {@link IndicatorsFiltersContext} (contains FilterManager) diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx index 11bdb8fc8e6e..7740345f468d 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx @@ -27,7 +27,7 @@ describe('', () => { useAggregatedIndicators as jest.MockedFunction ).mockReturnValue({ dateRange: { min: moment(), max: moment() }, - indicators: [], + series: [], selectedField: '', onFieldChange: () => {}, }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx index f51e062e1c3c..fff2caad5715 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx @@ -7,7 +7,6 @@ import React, { FC, VFC } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { IndicatorsFilters } from './containers/indicators_filters/indicators_filters'; import { IndicatorsBarChartWrapper } from './components/indicators_barchart_wrapper/indicators_barchart_wrapper'; import { IndicatorsTable } from './components/indicators_table/indicators_table'; import { useIndicators } from './hooks/use_indicators'; @@ -19,14 +18,18 @@ import { useSourcererDataView } from './hooks/use_sourcerer_data_view'; import { FieldTypesProvider } from '../../containers/field_types_provider'; import { InspectorProvider } from '../../containers/inspector'; import { useColumnSettings } from './components/indicators_table/hooks/use_column_settings'; +import { useAggregatedIndicators } from './hooks/use_aggregated_indicators'; +import { IndicatorsFilters } from './containers/indicators_filters'; const queryClient = new QueryClient(); const IndicatorsPageProviders: FC = ({ children }) => ( - - {children} - + + + {children} + + ); @@ -46,43 +49,68 @@ const IndicatorsPageContent: VFC = () => { savedQuery, } = useFilters(); - const { handleRefresh, ...indicators } = useIndicators({ + const { + handleRefresh, + indicatorCount, + indicators, + isLoading, + onChangeItemsPerPage, + onChangePage, + pagination, + } = useIndicators({ filters, filterQuery, timeRange, sorting: columnSettings.sorting.columns, }); + const { dateRange, series, selectedField, onFieldChange } = useAggregatedIndicators({ + timeRange, + filters, + filterQuery, + }); + return ( - - - + + + + + - - - - - + + ); }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.stories.tsx index 1e6c7d0c2614..08297774e51f 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_in/filter_in.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Story } from '@storybook/react'; import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context'; import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; -import { IndicatorsFiltersContext } from '../../../indicators/context'; +import { IndicatorsFiltersContext } from '../../../indicators/containers/indicators_filters/context'; import { FilterIn } from '.'; export default { diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.stories.tsx index a2f0061d36a9..cf625c23754a 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/components/filter_out/filter_out.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Story } from '@storybook/react'; import { mockIndicatorsFiltersContext } from '../../../../common/mocks/mock_indicators_filters_context'; import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator'; -import { IndicatorsFiltersContext } from '../../../indicators/context'; +import { IndicatorsFiltersContext } from '../../../indicators/containers/indicators_filters/context'; import { FilterOut } from '.'; export default { diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filters/use_filters.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filters/use_filters.test.tsx index b9c16240df4b..4d34c9ee6ff0 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filters/use_filters.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filters/use_filters.test.tsx @@ -9,28 +9,37 @@ import { mockUseKibanaForFilters } from '../../../../common/mocks/mock_use_kiban import { renderHook, act, RenderHookResult, Renderer } from '@testing-library/react-hooks'; import { useFilters, UseFiltersValue } from './use_filters'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useHistory } from 'react-router-dom'; import { Filter } from '@kbn/es-query'; import { TestProvidersComponent } from '../../../../common/mocks/test_providers'; +import React, { FC } from 'react'; +import { IndicatorsFilters } from '../../../indicators/containers/indicators_filters'; jest.mock('react-router-dom', () => ({ - useLocation: jest.fn().mockReturnValue({ - search: '', - }), + MemoryRouter: ({ children }: any) => <>{children}, + useLocation: jest.fn().mockReturnValue({ pathname: '', search: '' }), useHistory: jest.fn().mockReturnValue({ replace: jest.fn() }), })); +const FiltersWrapper: FC = ({ children }) => ( + + {children}{' '} + +); + +const renderUseFilters = () => renderHook(() => useFilters(), { wrapper: FiltersWrapper }); + describe('useFilters()', () => { let hookResult: RenderHookResult<{}, UseFiltersValue, Renderer>; let mockRef: ReturnType; + afterAll(() => jest.unmock('react-router-dom')); + describe('when mounted', () => { beforeEach(async () => { mockRef = mockUseKibanaForFilters(); - hookResult = renderHook(() => useFilters(), { - wrapper: TestProvidersComponent, - }); + hookResult = renderUseFilters(); }); it('should have valid initial filterQuery value', () => { @@ -44,9 +53,7 @@ describe('useFilters()', () => { '?indicators=(filterQuery:(language:kuery,query:%27threat.indicator.type%20:%20"file"%20%27),filters:!(),timeRange:(from:now/d,to:now/d))', }); - hookResult = renderHook(() => useFilters(), { - wrapper: TestProvidersComponent, - }); + hookResult = renderUseFilters(); expect(hookResult.result.current.filterQuery).toMatchObject({ language: 'kuery', @@ -61,9 +68,7 @@ describe('useFilters()', () => { beforeEach(async () => { mockRef = mockUseKibanaForFilters(); - hookResult = renderHook(() => useFilters(), { - wrapper: TestProvidersComponent, - }); + hookResult = renderUseFilters(); (useHistory as jest.Mock).mockReturnValue({ replace: historyReplace }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filters/use_filters.ts b/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filters/use_filters.ts index 73ce49691d5a..c50a10c17b70 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filters/use_filters.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filters/use_filters.ts @@ -5,110 +5,24 @@ * 2.0. */ -import { Filter, Query, TimeRange } from '@kbn/es-query'; -import { useCallback, useEffect, useState } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; -import deepEqual from 'fast-deep-equal'; -import type { FilterManager, SavedQuery } from '@kbn/data-plugin/public'; -import { useKibana } from '../../../../hooks/use_kibana'; +import { useContext } from 'react'; import { - DEFAULT_QUERY, - DEFAULT_TIME_RANGE, - encodeState, - FILTERS_QUERYSTRING_NAMESPACE, - stateFromQueryParams, -} from './utils'; + IndicatorsFiltersContext, + IndicatorsFiltersContextValue, +} from '../../../indicators/containers/indicators_filters'; -export interface UseFiltersValue { - timeRange?: TimeRange; - filters: Filter[]; - filterQuery: Query; - handleSavedQuery: (savedQuery: SavedQuery | undefined) => void; - handleSubmitTimeRange: (timeRange?: TimeRange) => void; - handleSubmitQuery: (filterQuery: Query) => void; - filterManager: FilterManager; - savedQuery?: SavedQuery; -} +export type UseFiltersValue = IndicatorsFiltersContextValue; /** * Custom react hook housing logic for KQL bar * @returns Filters and TimeRange for use with KQL bar */ -export const useFilters = (): UseFiltersValue => { - const { pathname: browserPathName, search } = useLocation(); - const history = useHistory(); - const [savedQuery, setSavedQuery] = useState(undefined); +export const useFilters = () => { + const contextValue = useContext(IndicatorsFiltersContext); - const { - services: { - data: { - query: { filterManager }, - }, - }, - } = useKibana(); + if (!contextValue) { + throw new Error('Filters can only be used inside IndicatorFiltersContext'); + } - // Filters are picked using the UI widgets - const [filters, setFilters] = useState([]); - - // Time range is self explanatory - const [timeRange, setTimeRange] = useState(DEFAULT_TIME_RANGE); - - // filterQuery is raw kql query that user can type in to filter results - const [filterQuery, setFilterQuery] = useState(DEFAULT_QUERY); - - // Serialize filters into query string - useEffect(() => { - const filterStateAsString = encodeState({ filters, filterQuery, timeRange }); - if (!deepEqual(filterManager.getFilters(), filters)) { - filterManager.setFilters(filters); - } - - history.replace({ - pathname: browserPathName, - search: `${FILTERS_QUERYSTRING_NAMESPACE}=${filterStateAsString}`, - }); - }, [browserPathName, filterManager, filterQuery, filters, history, timeRange]); - - // Sync filterManager to local state (after they are changed from the ui) - useEffect(() => { - const subscription = filterManager.getUpdates$().subscribe(() => { - setFilters(filterManager.getFilters()); - }); - - return () => subscription.unsubscribe(); - }, [filterManager]); - - // Update local state with filter values from the url (on initial mount) - useEffect(() => { - const { - filters: filtersFromQuery, - timeRange: timeRangeFromQuery, - filterQuery: filterQueryFromQuery, - } = stateFromQueryParams(search); - - setTimeRange(timeRangeFromQuery); - setFilterQuery(filterQueryFromQuery); - setFilters(filtersFromQuery); - - // We only want to have it done on initial render with initial 'search' value; - // that is why 'search' is ommited from the deps array - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filterManager]); - - const onSavedQuery = useCallback( - (newSavedQuery: SavedQuery | undefined) => setSavedQuery(newSavedQuery), - [] - ); - - return { - timeRange, - filters, - filterQuery, - handleSavedQuery: onSavedQuery, - handleSubmitTimeRange: setTimeRange, - handleSubmitQuery: setFilterQuery, - filterManager, - savedQuery, - }; + return contextValue; }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b2984037b030..60b692e720ec 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -988,7 +988,6 @@ "dashboard.panel.copyToDashboard.goToDashboard": "Copier et accéder au tableau de bord", "dashboard.panel.copyToDashboard.newDashboardOptionLabel": "Nouveau tableau de bord", "dashboard.panel.copyToDashboard.title": "Copier dans le tableau de bord", - "dashboard.panel.invalidData": "Données non valides dans l'url", "dashboard.panel.LibraryNotification": "Notification de la bibliothèque Visualize", "dashboard.panel.libraryNotification.ariaLabel": "Afficher les informations de la bibliothèque et dissocier ce panneau", "dashboard.panel.libraryNotification.toolTip": "La modification de ce panneau pourrait affecter d’autres tableaux de bord. Pour modifier ce panneau uniquement, dissociez-le de la bibliothèque.", @@ -7543,7 +7542,6 @@ "xpack.apm.serviceGroups.selectServicesForm.preview": "Aperçu", "xpack.apm.serviceGroups.selectServicesForm.refresh": "Actualiser", "xpack.apm.serviceGroups.selectServicesForm.saveGroup": "Enregistrer le groupe", - "xpack.apm.serviceGroups.selectServicesForm.subtitle": "Utilisez une requête pour sélectionner les services pour ce groupe. Les services qui correspondent à cette requête dans les dernières 24 heures seront affectés au groupe.", "xpack.apm.serviceGroups.selectServicesForm.title": "Sélectionner des services", "xpack.apm.serviceGroups.selectServicesList.environmentColumnLabel": "Environnements", "xpack.apm.serviceGroups.selectServicesList.nameColumnLabel": "Nom", @@ -9286,7 +9284,6 @@ "xpack.cases.confirmDeleteCase.confirmQuestion": "Si vous supprimez {quantity, plural, =1 {ce cas} other {ces cas}}, toutes les données des cas connexes seront définitivement retirées et vous ne pourrez plus transmettre de données à un système de gestion des incidents externes. Voulez-vous vraiment continuer ?", "xpack.cases.confirmDeleteCase.deleteCase": "Supprimer {quantity, plural, =1 {le cas} other {les cas}}", "xpack.cases.confirmDeleteCase.deleteTitle": "Supprimer \"{caseTitle}\"", - "xpack.cases.confirmDeleteCase.selectedCases": "Supprimer \"{quantity, plural, =1 {{title}} other {{quantity} cas sélectionnés}}\"", "xpack.cases.connecors.get.missingCaseConnectorErrorMessage": "Le type d'objet \"{id}\" n'est pas enregistré.", "xpack.cases.connecors.register.duplicateCaseConnectorErrorMessage": "Le type d'objet \"{id}\" est déjà enregistré.", "xpack.cases.connectors.card.createCommentWarningDesc": "Configurez les champs Create Comment URL et Create Comment Objects pour que le connecteur {connectorName} puisse partager les commentaires.", @@ -9296,7 +9293,6 @@ "xpack.cases.connectors.cases.externalIncidentUpdated": "(mis à jour le {date} par {user})", "xpack.cases.connectors.jira.unableToGetIssueMessage": "Impossible d'obtenir le problème ayant l'ID {id}", "xpack.cases.containers.closedCases": "Fermeture de {totalCases, plural, =1 {\"{caseTitle}\"} other {{totalCases} cas}} effectuée", - "xpack.cases.containers.deletedCases": "Suppression de {totalCases, plural, =1 {\"{caseTitle}\"} other {{totalCases} cas}} effectuée", "xpack.cases.containers.markInProgressCases": "Marquage effectué de {totalCases, plural, =1 {\"{caseTitle}\"} other {{totalCases} cas}} comme étant en cours", "xpack.cases.containers.pushToExternalService": "Envoyé avec succès à { serviceName }", "xpack.cases.containers.reopenedCases": "Ouverture de {totalCases, plural, =1 {\"{caseTitle}\"} other {{totalCases} cas}} effectuée", @@ -9328,7 +9324,6 @@ "xpack.cases.caseTable.changeStatus": "Modifier le statut", "xpack.cases.caseTable.closed": "Fermé", "xpack.cases.caseTable.closedCases": "Cas fermés", - "xpack.cases.caseTable.delete": "Supprimer", "xpack.cases.caseTable.incidentSystem": "Système de gestion des incidents", "xpack.cases.caseTable.inProgressCases": "Cas en cours", "xpack.cases.caseTable.noCases.body": "Créer un cas ou modifiez vos filtres.", @@ -13922,9 +13917,6 @@ "xpack.graph.icon.tachometer": "Tachymètre", "xpack.graph.icon.user": "Utilisateur", "xpack.graph.icon.users": "Utilisateurs", - "xpack.graph.inspect.requestTabTitle": "Requête", - "xpack.graph.inspect.responseTabTitle": "Réponse", - "xpack.graph.inspect.title": "Inspecter", "xpack.graph.leaveWorkspace.confirmButtonLabel": "Quitter", "xpack.graph.leaveWorkspace.confirmText": "Si vous quittez maintenant, vous perdez les modifications non enregistrées.", "xpack.graph.leaveWorkspace.modalTitle": "Modifications non enregistrées", @@ -15485,8 +15477,6 @@ "xpack.infra.deprecations.tiebreakerAdjustIndexing": "Ajustez votre indexation pour utiliser \"{field}\" comme moyen de départager.", "xpack.infra.deprecations.timestampAdjustIndexing": "Ajustez votre indexation pour utiliser \"{field}\" comme horodatage.", "xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "Dernières {duration} de données pour l'heure sélectionnée", - "xpack.infra.infrastructureMetricsExplorerPage.documentTitle": "{previousTitle} | Metrics Explorer", - "xpack.infra.infrastructureSnapshotPage.documentTitle": "{previousTitle} | Inventory", "xpack.infra.inventoryTimeline.header": "{metricLabel} moyen", "xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage": "Le modèle de {metricId} nécessite un cloudId, mais aucun n'a été attribué à {nodeId}.", "xpack.infra.kibanaMetrics.invalidInfraMetricErrorMessage": "{id} n'est pas une valeur inframétrique valide", @@ -15526,7 +15516,6 @@ "xpack.infra.logs.searchResultTooltip": "{bucketCount, plural, one {# entrée mise en surbrillance} other {# entrées mises en surbrillance}}", "xpack.infra.logs.showingEntriesFromTimestamp": "Affichage des entrées à partir de {timestamp}", "xpack.infra.logs.showingEntriesUntilTimestamp": "Affichage des entrées jusqu'à {timestamp}", - "xpack.infra.logs.streamPage.documentTitle": "{previousTitle} | Flux", "xpack.infra.logs.viewInContext.logsFromContainerTitle": "Les logs affichés proviennent du conteneur {container}", "xpack.infra.logs.viewInContext.logsFromFileTitle": "Les logs affichés proviennent du fichier {file} et de l'hôte {host}", "xpack.infra.logSourceConfiguration.invalidMessageFieldTypeErrorMessage": "Le champ {messageField} doit être un champ textuel.", @@ -15535,8 +15524,7 @@ "xpack.infra.logSourceConfiguration.missingDataViewsLabel": "Vue de données {indexPatternId} manquante", "xpack.infra.logSourceConfiguration.missingMessageFieldErrorMessage": "La vue de données doit contenir un champ {messageField}.", "xpack.infra.logSourceErrorPage.savedObjectNotFoundErrorMessage": "Impossible de localiser ce {savedObjectType} : {savedObjectId}", - "xpack.infra.metricDetailPage.documentTitle": "Infrastructure | Indicateurs | {name}", - "xpack.infra.metricDetailPage.documentTitleError": "{previousTitle} | Oups", + "xpack.infra.metricDetailPage.documentTitleError": "Oups", "xpack.infra.metrics.alertFlyout.alertPerRedundantFilterError": "Il est possible que cette règle signale {matchedGroups} moins que prévu, car la requête de filtre comporte une correspondance pour {groupCount, plural, one {ce champ} other {ces champs}}. Pour en savoir plus, veuillez consulter {filteringAndGroupingLink}.", "xpack.infra.metrics.alertFlyout.ofExpression.helpTextDetail": "Vous ne trouvez pas d'indicateur ? {documentationLink}.", "xpack.infra.metrics.alerting.anomaly.summaryHigher": "{differential} x plus élevé", @@ -15690,7 +15678,6 @@ "xpack.infra.header.logsTitle": "Logs", "xpack.infra.header.observabilityTitle": "Observability", "xpack.infra.hideHistory": "Masquer l'historique", - "xpack.infra.homePage.documentTitle": "Indicateurs", "xpack.infra.homePage.inventoryTabTitle": "Inventory", "xpack.infra.homePage.metricsExplorerTabTitle": "Metrics Explorer", "xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "Voir les instructions de configuration", @@ -25702,9 +25689,6 @@ "xpack.securitySolution.endpoint.resolver.relatedEventLimitTitle": "Cette liste inclut {numberOfEntries} événements de processus.", "xpack.securitySolution.endpointPolicyStatus.revisionNumber": "rév. {revNumber}", "xpack.securitySolution.endpointResponseActions.actionError.errorMessage": "{ errorCount, plural, =1 {Erreur rencontrée} other {Erreurs rencontrées}} :", - "xpack.securitySolution.endpointResponseActions.getProcesses.performApiErrorMessage": "L'erreur suivante a été rencontrée : {error}", - "xpack.securitySolution.endpointResponseActions.killProcess.performApiErrorMessage": "L'erreur suivante a été rencontrée : {error}", - "xpack.securitySolution.endpointResponseActions.suspendProcess.performApiErrorMessage": "L'erreur suivante a été rencontrée : {error}", "xpack.securitySolution.event.reason.reasonRendererTitle": "Outil de rendu d'événement : {eventRendererName} ", "xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel": "Le champ {field} est un objet, et il est composé de champs imbriqués qui peuvent être ajoutés en tant que colonne", "xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "Afficher la colonne {field}", @@ -28124,8 +28108,6 @@ "xpack.securitySolution.endpointManagement.noPermissionsSubText": "Vous devez disposer du rôle de superutilisateur pour utiliser cette fonctionnalité. Si vous ne disposez pas de ce rôle, ni d'autorisations pour modifier les rôles d'utilisateur, contactez votre administrateur Kibana.", "xpack.securitySolution.endpointManagemnet.noPermissionsText": "Vous ne disposez pas des autorisations Kibana requises pour utiliser Elastic Security Administration", "xpack.securitySolution.endpointPolicyStatus.tooltipTitleLabel": "Politique appliquée", - "xpack.securitySolution.endpointResponseActions.getProcesses.errorMessageTitle": "Échec de l’obtention des processus", - "xpack.securitySolution.endpointResponseActions.getProcesses.performApiErrorMessageTitle": "Échec de l’exécution de l’action d’obtention des processus", "xpack.securitySolution.endpointResponseActions.getProcesses.table.header.command": "COMMANDE", "xpack.securitySolution.endpointResponseActions.getProcesses.table.header.enityId": "ID D’ENTITÉ", "xpack.securitySolution.endpointResponseActions.getProcesses.table.header.pid": "PID", @@ -32547,7 +32529,6 @@ "xpack.triggersActionsUI.sections.ruleDetails.alertsList.ruleTypeExcessDurationMessage": "La durée dépasse le temps d'exécution attendu de la règle.", "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.activeLabel": "Actif", "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.allAlertsLabel": "Toutes les alertes", - "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.recentAlertHistoryTitle": "Historique récent des alertes", "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.title": "Alertes", "xpack.triggersActionsUI.sections.ruleDetails.deleteRuleButtonLabel": "Supprimer la règle", "xpack.triggersActionsUI.sections.ruleDetails.disableRuleButtonLabel": "Désactiver", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a09bdfe578cb..0aa5fa39b93e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -986,7 +986,6 @@ "dashboard.panel.copyToDashboard.goToDashboard": "コピーしてダッシュボードを開く", "dashboard.panel.copyToDashboard.newDashboardOptionLabel": "新規ダッシュボード", "dashboard.panel.copyToDashboard.title": "ダッシュボードにコピー", - "dashboard.panel.invalidData": "URL の無効なデータ", "dashboard.panel.LibraryNotification": "Visualize ライブラリ通知", "dashboard.panel.libraryNotification.ariaLabel": "ライブラリ情報を表示し、このパネルのリンクを解除します", "dashboard.panel.libraryNotification.toolTip": "このパネルを編集すると、他のダッシュボードに影響する場合があります。このパネルのみを変更するには、ライブラリからリンクを解除します。", @@ -7530,7 +7529,6 @@ "xpack.apm.serviceGroups.selectServicesForm.preview": "プレビュー", "xpack.apm.serviceGroups.selectServicesForm.refresh": "更新", "xpack.apm.serviceGroups.selectServicesForm.saveGroup": "グループを保存", - "xpack.apm.serviceGroups.selectServicesForm.subtitle": "クエリを使用してこのグループのサービスを選択します。過去24時間以内にこのクエリと一致したサービスはグループに割り当てられます。", "xpack.apm.serviceGroups.selectServicesForm.title": "サービスを選択", "xpack.apm.serviceGroups.selectServicesList.environmentColumnLabel": "環境", "xpack.apm.serviceGroups.selectServicesList.nameColumnLabel": "名前", @@ -9273,7 +9271,6 @@ "xpack.cases.confirmDeleteCase.confirmQuestion": "{quantity, plural, =1 {このケース} other {これらのケース}}を削除すると、関連するすべてのケースデータが完全に削除され、外部インシデント管理システムにデータをプッシュできなくなります。続行していいですか?", "xpack.cases.confirmDeleteCase.deleteCase": "{quantity, plural, other {ケース}}を削除", "xpack.cases.confirmDeleteCase.deleteTitle": "「{caseTitle}」を削除", - "xpack.cases.confirmDeleteCase.selectedCases": "\"{quantity, plural, =1 {{title}} other {選択した{quantity}個のケース}}\"を削除", "xpack.cases.connecors.get.missingCaseConnectorErrorMessage": "オブジェクトタイプ「{id}」は登録されていません。", "xpack.cases.connecors.register.duplicateCaseConnectorErrorMessage": "オブジェクトタイプ「{id}」はすでに登録されています。", "xpack.cases.connectors.card.createCommentWarningDesc": "コメントを外部で共有するには、{connectorName}コネクターの[コメントURLを作成]および[コメントオブジェクトを作成]フィールドを構成します。", @@ -9283,7 +9280,6 @@ "xpack.cases.connectors.cases.externalIncidentUpdated": "({date}に{user}が更新)", "xpack.cases.connectors.jira.unableToGetIssueMessage": "ID {id}の問題を取得できません", "xpack.cases.containers.closedCases": "{totalCases, plural, =1 {\"{caseTitle}\"} other {{totalCases}件のケース}}をクローズしました", - "xpack.cases.containers.deletedCases": "{totalCases, plural, =1 {\"{caseTitle}\"} other {{totalCases}件のケース}}を削除しました", "xpack.cases.containers.markInProgressCases": "{totalCases, plural, =1 {\"{caseTitle}\"} other {{totalCases}件のケース}}を進行中に設定しました", "xpack.cases.containers.pushToExternalService": "{ serviceName }への送信が正常に完了しました", "xpack.cases.containers.reopenedCases": "{totalCases, plural, =1 {\"{caseTitle}\"} other {{totalCases}件のケース}}をオープンしました", @@ -9315,7 +9311,6 @@ "xpack.cases.caseTable.changeStatus": "ステータスの変更", "xpack.cases.caseTable.closed": "終了", "xpack.cases.caseTable.closedCases": "終了したケース", - "xpack.cases.caseTable.delete": "削除", "xpack.cases.caseTable.incidentSystem": "インシデント管理システム", "xpack.cases.caseTable.inProgressCases": "進行中のケース", "xpack.cases.caseTable.noCases.body": "ケースを作成するか、フィルターを編集します。", @@ -13908,9 +13903,6 @@ "xpack.graph.icon.tachometer": "タコメーター", "xpack.graph.icon.user": "ユーザー", "xpack.graph.icon.users": "ユーザー", - "xpack.graph.inspect.requestTabTitle": "リクエスト", - "xpack.graph.inspect.responseTabTitle": "応答", - "xpack.graph.inspect.title": "検査", "xpack.graph.leaveWorkspace.confirmButtonLabel": "それでも移動", "xpack.graph.leaveWorkspace.confirmText": "今移動すると、保存されていない変更が失われます。", "xpack.graph.leaveWorkspace.modalTitle": "保存されていない変更", @@ -15471,8 +15463,6 @@ "xpack.infra.deprecations.tiebreakerAdjustIndexing": "インデックスを調整し、\"{field}\"をタイブレーカーとして使用します。", "xpack.infra.deprecations.timestampAdjustIndexing": "インデックスを調整し、\"{field}\"をタイムスタンプとして使用します。", "xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "指定期間のデータの最後の{duration}", - "xpack.infra.infrastructureMetricsExplorerPage.documentTitle": "{previousTitle} | メトリックエクスプローラー", - "xpack.infra.infrastructureSnapshotPage.documentTitle": "{previousTitle} | インベントリ", "xpack.infra.inventoryTimeline.header": "平均{metricLabel}", "xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage": "{metricId} のモデルには cloudId が必要ですが、{nodeId} に cloudId が指定されていません。", "xpack.infra.kibanaMetrics.invalidInfraMetricErrorMessage": "{id} は有効な InfraMetric ではありません", @@ -15512,7 +15502,6 @@ "xpack.infra.logs.searchResultTooltip": "{bucketCount, plural, other {# 件のハイライトされたエントリー}}", "xpack.infra.logs.showingEntriesFromTimestamp": "{timestamp} 以降のエントリーを表示中", "xpack.infra.logs.showingEntriesUntilTimestamp": "{timestamp} までのエントリーを表示中", - "xpack.infra.logs.streamPage.documentTitle": "{previousTitle} | Stream", "xpack.infra.logs.viewInContext.logsFromContainerTitle": "表示されたログはコンテナー{container}から取得されました", "xpack.infra.logs.viewInContext.logsFromFileTitle": "表示されたログは、ファイル{file}およびホスト{host}から取得されました", "xpack.infra.logSourceConfiguration.invalidMessageFieldTypeErrorMessage": "{messageField}フィールドはテキストフィールドでなければなりません。", @@ -15520,8 +15509,7 @@ "xpack.infra.logSourceConfiguration.missingDataViewsLabel": "データビュー{indexPatternId}が見つかりません", "xpack.infra.logSourceConfiguration.missingMessageFieldErrorMessage": "データビューには{messageField}フィールドが必要です。", "xpack.infra.logSourceErrorPage.savedObjectNotFoundErrorMessage": "{savedObjectType}:{savedObjectId}が見つかりませんでした", - "xpack.infra.metricDetailPage.documentTitle": "インフラストラクチャ | メトリック | {name}", - "xpack.infra.metricDetailPage.documentTitleError": "{previousTitle} | おっと", + "xpack.infra.metricDetailPage.documentTitleError": "おっと", "xpack.infra.metrics.alertFlyout.alertPerRedundantFilterError": "このルールは想定未満の{matchedGroups}に対してアラートを通知できます。フィルタークエリには{groupCount, plural, one {このフィールド} other {これらのフィールド}}の完全一致が含まれるためです。詳細については、{filteringAndGroupingLink}を参照してください。", "xpack.infra.metrics.alertFlyout.ofExpression.helpTextDetail": "メトリックが見つからない場合は、{documentationLink}。", "xpack.infra.metrics.alerting.anomaly.summaryHigher": "{differential}x高い", @@ -15675,7 +15663,6 @@ "xpack.infra.header.logsTitle": "ログ", "xpack.infra.header.observabilityTitle": "Observability", "xpack.infra.hideHistory": "履歴を表示しない", - "xpack.infra.homePage.documentTitle": "メトリック", "xpack.infra.homePage.inventoryTabTitle": "インベントリ", "xpack.infra.homePage.metricsExplorerTabTitle": "メトリックエクスプローラー", "xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "セットアップの手順を表示", @@ -25677,9 +25664,6 @@ "xpack.securitySolution.endpoint.resolver.relatedEventLimitTitle": "このリストには、{numberOfEntries} 件のプロセスイベントが含まれています。", "xpack.securitySolution.endpointPolicyStatus.revisionNumber": "rev. {revNumber}", "xpack.securitySolution.endpointResponseActions.actionError.errorMessage": "次の{ errorCount, plural, other {件のエラー}}が発生しました:", - "xpack.securitySolution.endpointResponseActions.getProcesses.performApiErrorMessage": "次のエラーが発生しました:{error}", - "xpack.securitySolution.endpointResponseActions.killProcess.performApiErrorMessage": "次のエラーが発生しました:{error}", - "xpack.securitySolution.endpointResponseActions.suspendProcess.performApiErrorMessage": "次のエラーが発生しました:{error}", "xpack.securitySolution.event.reason.reasonRendererTitle": "イベントレンダラー:{eventRendererName} ", "xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel": "{field}フィールドはオブジェクトであり、列として追加できるネストされたフィールドに分解されます", "xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "{field} 列を表示", @@ -28099,8 +28083,6 @@ "xpack.securitySolution.endpointManagement.noPermissionsSubText": "この機能を使用するには、スーパーユーザーロールが必要です。スーパーユーザーロールがなく、ユーザーロールを編集する権限もない場合は、Kibana管理者に問い合わせてください。", "xpack.securitySolution.endpointManagemnet.noPermissionsText": "Elastic Security Administrationを使用するために必要なKibana権限がありません。", "xpack.securitySolution.endpointPolicyStatus.tooltipTitleLabel": "ポリシーが適用されました", - "xpack.securitySolution.endpointResponseActions.getProcesses.errorMessageTitle": "プロセスの取得アクションが失敗しました", - "xpack.securitySolution.endpointResponseActions.getProcesses.performApiErrorMessageTitle": "プロセスの取得アクションの実行が失敗しました", "xpack.securitySolution.endpointResponseActions.getProcesses.table.header.command": "コマンド", "xpack.securitySolution.endpointResponseActions.getProcesses.table.header.enityId": "エンティティID", "xpack.securitySolution.endpointResponseActions.getProcesses.table.header.pid": "PID", @@ -32521,7 +32503,6 @@ "xpack.triggersActionsUI.sections.ruleDetails.alertsList.ruleTypeExcessDurationMessage": "期間がルールの想定実行時間を超えています。", "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.activeLabel": "アクティブ", "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.allAlertsLabel": "すべてのアラート", - "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.recentAlertHistoryTitle": "最近のアラート履歴", "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.title": "アラート", "xpack.triggersActionsUI.sections.ruleDetails.deleteRuleButtonLabel": "ルールの削除", "xpack.triggersActionsUI.sections.ruleDetails.disableRuleButtonLabel": "無効にする", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5ade45168ae7..baf98e70b8dd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -988,7 +988,6 @@ "dashboard.panel.copyToDashboard.goToDashboard": "复制并前往仪表板", "dashboard.panel.copyToDashboard.newDashboardOptionLabel": "新仪表板", "dashboard.panel.copyToDashboard.title": "复制到仪表板", - "dashboard.panel.invalidData": "url 中的数据无效", "dashboard.panel.LibraryNotification": "可视化库通知", "dashboard.panel.libraryNotification.ariaLabel": "查看库信息并取消链接此面板", "dashboard.panel.libraryNotification.toolTip": "编辑此面板可能会影响其他仪表板。要仅更改此面板,请取消其与库的链接。", @@ -7547,7 +7546,6 @@ "xpack.apm.serviceGroups.selectServicesForm.preview": "预览", "xpack.apm.serviceGroups.selectServicesForm.refresh": "刷新", "xpack.apm.serviceGroups.selectServicesForm.saveGroup": "保存组", - "xpack.apm.serviceGroups.selectServicesForm.subtitle": "使用查询为该组选择服务。过去 24 小时内与此查询匹配的服务将分配给该组。", "xpack.apm.serviceGroups.selectServicesForm.title": "选择服务", "xpack.apm.serviceGroups.selectServicesList.environmentColumnLabel": "环境", "xpack.apm.serviceGroups.selectServicesList.nameColumnLabel": "名称", @@ -9291,7 +9289,6 @@ "xpack.cases.confirmDeleteCase.confirmQuestion": "删除{quantity, plural, =1 {此案例} other {这些案例}}即会永久移除所有相关案例数据,而且您将无法再将数据推送到外部事件管理系统。是否确定要继续?", "xpack.cases.confirmDeleteCase.deleteCase": "删除{quantity, plural, other {案例}}", "xpack.cases.confirmDeleteCase.deleteTitle": "删除“{caseTitle}”", - "xpack.cases.confirmDeleteCase.selectedCases": "删除“{quantity, plural, =1 {{title}} other {选定的 {quantity} 个案例}}”", "xpack.cases.connecors.get.missingCaseConnectorErrorMessage": "对象类型“{id}”未注册。", "xpack.cases.connecors.register.duplicateCaseConnectorErrorMessage": "已注册对象类型“{id}”。", "xpack.cases.connectors.card.createCommentWarningDesc": "为 {connectorName} 连接器配置“创建注释 URL”和“创建注释对象”字段以在外部共享注释。", @@ -9301,7 +9298,6 @@ "xpack.cases.connectors.cases.externalIncidentUpdated": "(由 {user} 于 {date}更新)", "xpack.cases.connectors.jira.unableToGetIssueMessage": "无法获取 ID 为 {id} 的问题", "xpack.cases.containers.closedCases": "已关闭{totalCases, plural, =1 {“{caseTitle}”} other { {totalCases} 个案例}}", - "xpack.cases.containers.deletedCases": "已删除{totalCases, plural, =1 {“{caseTitle}”} other { {totalCases} 个案例}}", "xpack.cases.containers.markInProgressCases": "已将{totalCases, plural, =1 {“{caseTitle}”} other { {totalCases} 个案例}}标记为进行中", "xpack.cases.containers.pushToExternalService": "已成功发送到 { serviceName }", "xpack.cases.containers.reopenedCases": "已打开{totalCases, plural, =1 {“{caseTitle}”} other { {totalCases} 个案例}}", @@ -9333,7 +9329,6 @@ "xpack.cases.caseTable.changeStatus": "更改状态", "xpack.cases.caseTable.closed": "已关闭", "xpack.cases.caseTable.closedCases": "已关闭案例", - "xpack.cases.caseTable.delete": "删除", "xpack.cases.caseTable.incidentSystem": "事件管理系统", "xpack.cases.caseTable.inProgressCases": "进行中的案例", "xpack.cases.caseTable.noCases.body": "创建案例或编辑筛选。", @@ -13928,9 +13923,6 @@ "xpack.graph.icon.tachometer": "转速表", "xpack.graph.icon.user": "用户", "xpack.graph.icon.users": "用户", - "xpack.graph.inspect.requestTabTitle": "请求", - "xpack.graph.inspect.responseTabTitle": "响应", - "xpack.graph.inspect.title": "检查", "xpack.graph.leaveWorkspace.confirmButtonLabel": "离开", "xpack.graph.leaveWorkspace.confirmText": "如果现在离开,将丢失未保存的更改。", "xpack.graph.leaveWorkspace.modalTitle": "未保存的更改", @@ -15491,8 +15483,6 @@ "xpack.infra.deprecations.tiebreakerAdjustIndexing": "调整索引以将“{field}”用作决胜属性。", "xpack.infra.deprecations.timestampAdjustIndexing": "调整索引以将“{field}”用作时间戳。", "xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "选定时间过去 {duration}的数据", - "xpack.infra.infrastructureMetricsExplorerPage.documentTitle": "{previousTitle} | 指标浏览器", - "xpack.infra.infrastructureSnapshotPage.documentTitle": "{previousTitle} | 库存", "xpack.infra.inventoryTimeline.header": "平均值 {metricLabel}", "xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage": "{metricId} 的模型需要云 ID,但没有为 {nodeId} 提供。", "xpack.infra.kibanaMetrics.invalidInfraMetricErrorMessage": "{id} 不是有效的 InfraMetric", @@ -15532,7 +15522,6 @@ "xpack.infra.logs.searchResultTooltip": "{bucketCount, plural, other {# 个高亮条目}}", "xpack.infra.logs.showingEntriesFromTimestamp": "正在显示自 {timestamp} 起的条目", "xpack.infra.logs.showingEntriesUntilTimestamp": "正在显示截止于 {timestamp} 的条目", - "xpack.infra.logs.streamPage.documentTitle": "{previousTitle} | 流式传输", "xpack.infra.logs.viewInContext.logsFromContainerTitle": "显示的日志来自容器 {container}", "xpack.infra.logs.viewInContext.logsFromFileTitle": "显示的日志来自文件 {file} 和主机 {host}", "xpack.infra.logSourceConfiguration.invalidMessageFieldTypeErrorMessage": "{messageField} 字段必须是文本字段。", @@ -15541,8 +15530,7 @@ "xpack.infra.logSourceConfiguration.missingDataViewsLabel": "缺少数据视图 {indexPatternId}", "xpack.infra.logSourceConfiguration.missingMessageFieldErrorMessage": "数据视图必须包含 {messageField} 字段。", "xpack.infra.logSourceErrorPage.savedObjectNotFoundErrorMessage": "无法找到该{savedObjectType}:{savedObjectId}", - "xpack.infra.metricDetailPage.documentTitle": "Infrastructure | 指标 | {name}", - "xpack.infra.metricDetailPage.documentTitleError": "{previousTitle} | 啊哦", + "xpack.infra.metricDetailPage.documentTitleError": "啊哦", "xpack.infra.metrics.alertFlyout.alertPerRedundantFilterError": "此规则可能针对低于预期的 {matchedGroups} 告警,因为筛选查询包含{groupCount, plural, one {此字段} other {这些字段}}的匹配项。有关更多信息,请参阅 {filteringAndGroupingLink}。", "xpack.infra.metrics.alertFlyout.ofExpression.helpTextDetail": "找不到指标?{documentationLink}。", "xpack.infra.metrics.alerting.anomaly.summaryHigher": "高 {differential} 倍", @@ -15696,7 +15684,6 @@ "xpack.infra.header.logsTitle": "日志", "xpack.infra.header.observabilityTitle": "Observability", "xpack.infra.hideHistory": "隐藏历史记录", - "xpack.infra.homePage.documentTitle": "指标", "xpack.infra.homePage.inventoryTabTitle": "库存", "xpack.infra.homePage.metricsExplorerTabTitle": "指标浏览器", "xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "查看设置说明", @@ -25711,9 +25698,6 @@ "xpack.securitySolution.endpoint.resolver.relatedEventLimitTitle": "此列表包括 {numberOfEntries} 个进程事件。", "xpack.securitySolution.endpointPolicyStatus.revisionNumber": "修订版 {revNumber}", "xpack.securitySolution.endpointResponseActions.actionError.errorMessage": "遇到以下{ errorCount, plural, other {错误}}:", - "xpack.securitySolution.endpointResponseActions.getProcesses.performApiErrorMessage": "遇到以下错误:{error}", - "xpack.securitySolution.endpointResponseActions.killProcess.performApiErrorMessage": "遇到以下错误:{error}", - "xpack.securitySolution.endpointResponseActions.suspendProcess.performApiErrorMessage": "遇到以下错误:{error}", "xpack.securitySolution.event.reason.reasonRendererTitle": "事件渲染器:{eventRendererName} ", "xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel": "{field} 字段是对象,并分解为可以添加为列的嵌套字段", "xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel": "查看 {field} 列", @@ -28133,8 +28117,6 @@ "xpack.securitySolution.endpointManagement.noPermissionsSubText": "您必须具有超级用户角色才能使用此功能。如果您不具有超级用户角色,且无权编辑用户角色,请与 Kibana 管理员联系。", "xpack.securitySolution.endpointManagemnet.noPermissionsText": "您没有所需的 Kibana 权限,无法使用 Elastic Security 管理", "xpack.securitySolution.endpointPolicyStatus.tooltipTitleLabel": "已应用策略", - "xpack.securitySolution.endpointResponseActions.getProcesses.errorMessageTitle": "获取进程操作失败", - "xpack.securitySolution.endpointResponseActions.getProcesses.performApiErrorMessageTitle": "执行获取进程操作失败", "xpack.securitySolution.endpointResponseActions.getProcesses.table.header.command": "命令", "xpack.securitySolution.endpointResponseActions.getProcesses.table.header.enityId": "实体 ID", "xpack.securitySolution.endpointResponseActions.getProcesses.table.header.pid": "PID", @@ -32558,7 +32540,6 @@ "xpack.triggersActionsUI.sections.ruleDetails.alertsList.ruleTypeExcessDurationMessage": "持续时间超出了规则的预期运行时间。", "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.activeLabel": "活动", "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.allAlertsLabel": "所有告警", - "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.recentAlertHistoryTitle": "最近告警历史记录", "xpack.triggersActionsUI.sections.ruleDetails.alertsSummary.title": "告警", "xpack.triggersActionsUI.sections.ruleDetails.deleteRuleButtonLabel": "删除规则", "xpack.triggersActionsUI.sections.ruleDetails.disableRuleButtonLabel": "禁用", diff --git a/x-pack/plugins/triggers_actions_ui/.storybook/main.js b/x-pack/plugins/triggers_actions_ui/.storybook/main.js new file mode 100644 index 000000000000..86b48c32f103 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/.storybook/main.js @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/x-pack/plugins/triggers_actions_ui/.storybook/preview.js b/x-pack/plugins/triggers_actions_ui/.storybook/preview.js new file mode 100644 index 000000000000..3200746243d4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/.storybook/preview.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common'; + +export const decorators = [EuiThemeProviderDecorator]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx index 0c6f3edaf3b8..c3072c123ff8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx @@ -6,6 +6,7 @@ */ import { EuiConfirmModal } from '@elastic/eui'; +import { KueryNode } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState, useMemo } from 'react'; import { HttpSetup } from '@kbn/core/public'; @@ -25,7 +26,7 @@ export const UpdateApiKeyModalConfirmation = ({ }: { onCancel: () => void; idsToUpdate: string[]; - idsToUpdateFilter?: string; + idsToUpdateFilter?: KueryNode | null | undefined; numberOfSelectedRules?: number; apiUpdateApiKeyCall: ({ ids, @@ -33,7 +34,7 @@ export const UpdateApiKeyModalConfirmation = ({ filter, }: { ids?: string[]; - filter?: string; + filter?: KueryNode | null | undefined; http: HttpSetup; }) => Promise; setIsLoadingState: (isLoading: boolean) => void; @@ -50,7 +51,7 @@ export const UpdateApiKeyModalConfirmation = ({ const { showToast } = useBulkEditResponse({ onSearchPopulate }); useEffect(() => { - if (idsToUpdateFilter) { + if (typeof idsToUpdateFilter !== 'undefined') { setUpdateModalVisibility(true); } else { setUpdateModalVisibility(idsToUpdate.length > 0); @@ -58,7 +59,7 @@ export const UpdateApiKeyModalConfirmation = ({ }, [idsToUpdate, idsToUpdateFilter]); const numberOfIdsToUpdate = useMemo(() => { - if (idsToUpdateFilter) { + if (typeof idsToUpdateFilter !== 'undefined') { return numberOfSelectedRules; } return idsToUpdate.length; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.test.tsx new file mode 100644 index 000000000000..07bf3516fbde --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.test.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useBulkEditSelect } from './use_bulk_edit_select'; +import { RuleTableItem } from '../../types'; + +const items = [ + { + id: '1', + isEditable: true, + }, + { + id: '2', + isEditable: true, + }, + { + id: '3', + isEditable: true, + }, + { + id: '4', + isEditable: true, + }, +] as RuleTableItem[]; + +describe('useBulkEditSelectTest', () => { + it('getFilter should return null when nothing is selected', async () => { + const { result } = renderHook(() => + useBulkEditSelect({ + items, + totalItemCount: 4, + }) + ); + + expect(result.current.getFilter()).toEqual(null); + }); + + it('getFilter should return rule list filter when nothing is selected', async () => { + const { result } = renderHook(() => + useBulkEditSelect({ + items, + totalItemCount: 4, + tagsFilter: ['test: 123'], + searchText: 'rules*', + }) + ); + + expect(result.current.getFilter()?.arguments.length).toEqual(2); + }); + + it('getFilter should return rule list filter when something is selected', async () => { + const { result } = renderHook(() => + useBulkEditSelect({ + items, + totalItemCount: 4, + tagsFilter: ['test: 123'], + searchText: 'rules*', + }) + ); + + act(() => { + result.current.onSelectRow(items[0]); + }); + + expect(result.current.getFilter()?.arguments.length).toEqual(2); + expect([...result.current.selectedIds]).toEqual([items[0].id]); + }); + + it('getFilter should return null when selecting all', async () => { + const { result } = renderHook(() => + useBulkEditSelect({ + items, + totalItemCount: 4, + }) + ); + + act(() => { + result.current.onSelectAll(); + }); + + expect(result.current.getFilter()).toEqual(null); + }); + + it('getFilter should return rule list filter when selecting all with excluded ids', async () => { + const { result } = renderHook(() => + useBulkEditSelect({ + items, + totalItemCount: 4, + }) + ); + + act(() => { + result.current.onSelectAll(); + result.current.onSelectRow(items[0]); + }); + + expect(result.current.getFilter()?.arguments.length).toEqual(1); + }); + + it('getFilter should return rule list filter when selecting all', async () => { + const { result } = renderHook(() => + useBulkEditSelect({ + items, + totalItemCount: 4, + tagsFilter: ['test: 123'], + searchText: 'rules*', + }) + ); + + act(() => { + result.current.onSelectAll(); + }); + + expect(result.current.getFilter()?.arguments.length).toEqual(2); + }); + + it('getFilter should return rule list filter and exclude ids when selecting all with excluded ids', async () => { + const { result } = renderHook(() => + useBulkEditSelect({ + items, + totalItemCount: 4, + tagsFilter: ['test: 123'], + searchText: 'rules*', + }) + ); + + act(() => { + result.current.onSelectAll(); + result.current.onSelectRow(items[0]); + }); + + expect(result.current.getFilter()?.arguments.length).toEqual(2); + expect(result.current.getFilter()?.arguments[1].arguments[0].arguments).toEqual([ + expect.objectContaining({ + value: 'alert.id', + }), + expect.objectContaining({ + value: 'alert:1', + }), + ]); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx index 20950d35b026..31c248ae1254 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_bulk_edit_select.tsx @@ -5,7 +5,9 @@ * 2.0. */ import { useReducer, useMemo, useCallback } from 'react'; -import { RuleTableItem } from '../../types'; +import { fromKueryExpression, nodeBuilder } from '@kbn/es-query'; +import { mapFiltersToKueryNode } from '../lib/rule_api/map_filters_to_kuery_node'; +import { RuleTableItem, RuleStatus } from '../../types'; interface BulkEditSelectionState { selectedIds: Set; @@ -71,9 +73,26 @@ const reducer = (state: BulkEditSelectionState, action: Action) => { interface UseBulkEditSelectProps { totalItemCount: number; items: RuleTableItem[]; + typesFilter?: string[]; + actionTypesFilter?: string[]; + tagsFilter?: string[]; + ruleExecutionStatusesFilter?: string[]; + ruleStatusesFilter?: RuleStatus[]; + searchText?: string; } -export function useBulkEditSelect({ totalItemCount = 0, items = [] }: UseBulkEditSelectProps) { +export function useBulkEditSelect(props: UseBulkEditSelectProps) { + const { + totalItemCount = 0, + items = [], + typesFilter, + actionTypesFilter, + tagsFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + searchText, + } = props; + const [state, dispatch] = useReducer(reducer, initialState); const itemIds = useMemo(() => { @@ -161,18 +180,54 @@ export function useBulkEditSelect({ totalItemCount = 0, items = [] }: UseBulkEdi dispatch({ type: ActionTypes.CLEAR_SELECTION }); }, []); + const getFilterKueryNode = useCallback( + (idsToExclude?: string[]) => { + const ruleFilterKueryNode = mapFiltersToKueryNode({ + typesFilter, + actionTypesFilter, + tagsFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + searchText, + }); + + if (idsToExclude && idsToExclude.length) { + const excludeFilter = fromKueryExpression( + `NOT (${idsToExclude.map((id) => `alert.id: "alert:${id}"`).join(' or ')})` + ); + if (ruleFilterKueryNode) { + return nodeBuilder.and([ruleFilterKueryNode, excludeFilter]); + } + return excludeFilter; + } + + return ruleFilterKueryNode; + }, + [ + typesFilter, + actionTypesFilter, + tagsFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + searchText, + ] + ); + const getFilter = useCallback(() => { const { selectedIds, isAllSelected } = state; const idsArray = [...selectedIds]; if (isAllSelected) { + // Select all but nothing is selected to exclude if (idsArray.length === 0) { - return 'alert.id: *'; + return getFilterKueryNode(); } - return `NOT (${idsArray.map((id) => `alert.id: "alert:${id}"`).join(' or ')})`; + // Select all, exclude certain alerts + return getFilterKueryNode(idsArray); } - return ''; - }, [state]); + + return getFilterKueryNode(); + }, [state, getFilterKueryNode]); return useMemo(() => { return { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.test.ts index 293c1e992ac1..03be9f6b9bd5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.test.ts @@ -8,7 +8,7 @@ import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; import { renderHook } from '@testing-library/react-hooks'; import { useKibana } from '../../common/lib/kibana'; -import { mockAggsResponse, mockChartData } from '../mock/rule_details/alert_summary'; +import { mockAggsResponse } from '../mock/rule_details/alert_summary'; import { useLoadRuleAlertsAggs } from './use_load_rule_alerts_aggregations'; jest.mock('../../common/lib/kibana'); @@ -21,7 +21,7 @@ describe('useLoadRuleAlertsAggs', () => { useKibanaMock().services.http.get = jest.fn().mockResolvedValue({ index_name: ['mock_index'] }); }); - it('should return the expected chart data from the Elasticsearch Aggs. query', async () => { + it('should return the expected data from the Elasticsearch Aggs. query', async () => { const { result, waitForNextUpdate } = renderHook(() => useLoadRuleAlertsAggs({ features: ALERTS_FEATURE_ID, @@ -31,18 +31,15 @@ describe('useLoadRuleAlertsAggs', () => { expect(result.current).toEqual({ isLoadingRuleAlertsAggs: true, ruleAlertsAggs: { active: 0, recovered: 0 }, - alertsChartData: [], }); await waitForNextUpdate(); - const { ruleAlertsAggs, errorRuleAlertsAggs, alertsChartData } = result.current; + const { ruleAlertsAggs, errorRuleAlertsAggs } = result.current; expect(ruleAlertsAggs).toEqual({ active: 1, recovered: 7, }); - expect(alertsChartData).toEqual(mockChartData()); expect(errorRuleAlertsAggs).toBeFalsy(); - expect(alertsChartData.length).toEqual(33); }); it('should have the correct query body sent to Elasticsearch', async () => { @@ -55,7 +52,7 @@ describe('useLoadRuleAlertsAggs', () => { ); await waitForNextUpdate(); - const body = `{"index":"mock_index","size":0,"query":{"bool":{"must":[{"term":{"kibana.alert.rule.uuid":"${ruleId}"}},{"range":{"@timestamp":{"gte":"now-30d","lt":"now"}}},{"bool":{"should":[{"term":{"kibana.alert.status":"active"}},{"term":{"kibana.alert.status":"recovered"}}]}}]}},"aggs":{"total":{"filters":{"filters":{"totalActiveAlerts":{"term":{"kibana.alert.status":"active"}},"totalRecoveredAlerts":{"term":{"kibana.alert.status":"recovered"}}}}},"statusPerDay":{"date_histogram":{"field":"@timestamp","fixed_interval":"1d","extended_bounds":{"min":"now-30d","max":"now"}},"aggs":{"alertStatus":{"terms":{"field":"kibana.alert.status"}}}}}}`; + const body = `{"index":"mock_index","size":0,"query":{"bool":{"must":[{"term":{"kibana.alert.rule.uuid":"${ruleId}"}},{"range":{"@timestamp":{"gte":"now-30d","lt":"now"}}},{"bool":{"should":[{"term":{"kibana.alert.status":"active"}},{"term":{"kibana.alert.status":"recovered"}}]}}]}},"aggs":{"total":{"filters":{"filters":{"totalActiveAlerts":{"term":{"kibana.alert.status":"active"}},"totalRecoveredAlerts":{"term":{"kibana.alert.status":"recovered"}}}}}}}`; expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( '/internal/rac/alerts/find', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts index c938b0b2cc13..3df1722abc00 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_load_rule_alerts_aggregations.ts @@ -10,7 +10,6 @@ import { AsApiContract } from '@kbn/actions-plugin/common'; import { HttpSetup } from '@kbn/core/public'; import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common/constants'; import { useKibana } from '../../common/lib/kibana'; -import { AlertChartData } from '../sections/rule_details/components/alert_summary'; interface UseLoadRuleAlertsAggs { features: string; @@ -29,7 +28,6 @@ interface LoadRuleAlertsAggs { recovered: number; }; errorRuleAlertsAggs?: string; - alertsChartData: AlertChartData[]; } interface IndexName { index: string; @@ -40,7 +38,6 @@ export function useLoadRuleAlertsAggs({ features, ruleId }: UseLoadRuleAlertsAgg const [ruleAlertsAggs, setRuleAlertsAggs] = useState({ isLoadingRuleAlertsAggs: true, ruleAlertsAggs: { active: 0, recovered: 0 }, - alertsChartData: [], }); const isCancelledRef = useRef(false); const abortCtrlRef = useRef(new AbortController()); @@ -54,7 +51,7 @@ export function useLoadRuleAlertsAggs({ features, ruleId }: UseLoadRuleAlertsAgg http, features, }); - const { active, recovered, error, alertsChartData } = await fetchRuleAlertsAggByTimeRange({ + const { active, recovered, error } = await fetchRuleAlertsAggByTimeRange({ http, index, ruleId, @@ -68,7 +65,6 @@ export function useLoadRuleAlertsAggs({ features, ruleId }: UseLoadRuleAlertsAgg active, recovered, }, - alertsChartData, isLoadingRuleAlertsAggs: false, })); } @@ -79,7 +75,6 @@ export function useLoadRuleAlertsAggs({ features, ruleId }: UseLoadRuleAlertsAgg ...oldState, isLoadingRuleAlertsAggs: false, errorRuleAlertsAggs: error, - alertsChartData: [], })); } } @@ -92,7 +87,7 @@ export function useLoadRuleAlertsAggs({ features, ruleId }: UseLoadRuleAlertsAgg return ruleAlertsAggs; } -export async function fetchIndexNameAPI({ +async function fetchIndexNameAPI({ http, features, }: { @@ -107,14 +102,7 @@ export async function fetchIndexNameAPI({ }; } -interface RuleAlertsAggs { - active: number; - recovered: number; - error?: string; - alertsChartData: AlertChartData[]; -} - -export async function fetchRuleAlertsAggByTimeRange({ +async function fetchRuleAlertsAggByTimeRange({ http, index, ruleId, @@ -183,109 +171,22 @@ export async function fetchRuleAlertsAggByTimeRange({ }, }, }, - statusPerDay: { - date_histogram: { - field: '@timestamp', - fixed_interval: '1d', - extended_bounds: { - min: 'now-30d', - max: 'now', - }, - }, - aggs: { - alertStatus: { - terms: { - field: 'kibana.alert.status', - }, - }, - }, - }, }, }), }); const active = res?.aggregations?.total.buckets.totalActiveAlerts?.doc_count ?? 0; const recovered = res?.aggregations?.total.buckets.totalRecoveredAlerts?.doc_count ?? 0; - let maxTotalAlertPerDay = 0; - res?.aggregations?.statusPerDay.buckets.forEach( - (dayAlerts: { - key: number; - doc_count: number; - alertStatus: { - buckets: Array<{ - key: 'active' | 'recovered'; - doc_count: number; - }>; - }; - }) => { - if (dayAlerts.doc_count > maxTotalAlertPerDay) { - maxTotalAlertPerDay = dayAlerts.doc_count; - } - } - ); - const alertsChartData = [ - ...res?.aggregations?.statusPerDay.buckets.reduce( - ( - acc: AlertChartData[], - dayAlerts: { - key: number; - doc_count: number; - alertStatus: { - buckets: Array<{ - key: 'active' | 'recovered'; - doc_count: number; - }>; - }; - } - ) => { - // We are adding this to each day to construct the 30 days bars (background bar) when there is no data for a given day or to show the delta today alerts/total alerts. - const totalDayAlerts = { - date: dayAlerts.key, - count: maxTotalAlertPerDay === 0 ? 1 : maxTotalAlertPerDay, - status: 'total', - }; - - if (dayAlerts.doc_count > 0) { - const localAlertChartData = acc; - // If there are alerts in this day, we construct the chart data - dayAlerts.alertStatus.buckets.forEach((alert) => { - localAlertChartData.push({ - date: dayAlerts.key, - count: alert.doc_count, - status: alert.key, - }); - }); - const deltaAlertsCount = maxTotalAlertPerDay - dayAlerts.doc_count; - if (deltaAlertsCount > 0) { - localAlertChartData.push({ - date: dayAlerts.key, - count: deltaAlertsCount, - status: 'total', - }); - } - return localAlertChartData; - } - return [...acc, totalDayAlerts]; - }, - [] - ), - ]; return { active, recovered, - alertsChartData: [ - ...alertsChartData.filter((acd) => acd.status === 'recovered'), - ...alertsChartData.filter((acd) => acd.status === 'active'), - ...alertsChartData.filter((acd) => acd.status === 'total'), - ], }; } catch (error) { return { error, active: 0, recovered: 0, - alertsChartData: [], } as RuleAlertsAggs; } } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts index 26a69093ae48..cbe226ce68aa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.test.ts @@ -5,40 +5,47 @@ * 2.0. */ -import { getAlertingSectionBreadcrumb, getRuleDetailsBreadcrumb } from './breadcrumb'; +import { getAlertingSectionBreadcrumb } from './breadcrumb'; import { i18n } from '@kbn/i18n'; import { routeToConnectors, routeToRules, routeToHome } from '../constants'; describe('getAlertingSectionBreadcrumb', () => { test('if change calls return proper breadcrumb title ', async () => { - expect(getAlertingSectionBreadcrumb('connectors')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('connectors', true)).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.connectors.breadcrumbTitle', { defaultMessage: 'Connectors', }), href: `${routeToConnectors}`, }); - expect(getAlertingSectionBreadcrumb('rules')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('rules', true)).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.rules.breadcrumbTitle', { defaultMessage: 'Rules', }), href: `${routeToRules}`, }); - expect(getAlertingSectionBreadcrumb('home')).toMatchObject({ + expect(getAlertingSectionBreadcrumb('home', true)).toMatchObject({ text: i18n.translate('xpack.triggersActionsUI.home.breadcrumbTitle', { defaultMessage: 'Rules and Connectors', }), href: `${routeToHome}`, }); }); -}); - -describe('getRuleDetailsBreadcrumb', () => { - test('if select an alert should return proper breadcrumb title with alert name ', async () => { - expect(getRuleDetailsBreadcrumb('testId', 'testName')).toMatchObject({ - text: i18n.translate('xpack.triggersActionsUI.alertDetails.breadcrumbTitle', { - defaultMessage: 'testName', + test('if boolean is passed in returns proper breadcrumb href ', async () => { + expect(getAlertingSectionBreadcrumb('connectors', true)).toMatchObject({ + text: i18n.translate('xpack.triggersActionsUI.connectors.breadcrumbTitle', { + defaultMessage: 'Connectors', + }), + href: `${routeToConnectors}`, + }); + expect(getAlertingSectionBreadcrumb('rules', false)).toMatchObject({ + text: i18n.translate('xpack.triggersActionsUI.rules.breadcrumbTitle', { + defaultMessage: 'Rules', + }), + }); + expect(getAlertingSectionBreadcrumb('home', false)).toMatchObject({ + text: i18n.translate('xpack.triggersActionsUI.home.breadcrumbTitle', { + defaultMessage: 'Rules and Connectors', }), - href: '/rule/testId', }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts index d26cdbcb3d17..46a15b12bb73 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/breadcrumb.ts @@ -6,9 +6,12 @@ */ import { i18n } from '@kbn/i18n'; -import { routeToHome, routeToConnectors, routeToRules, routeToRuleDetails } from '../constants'; +import { routeToHome, routeToConnectors, routeToRules } from '../constants'; -export const getAlertingSectionBreadcrumb = (type: string): { text: string; href: string } => { +export const getAlertingSectionBreadcrumb = ( + type: string, + returnHref: boolean = false +): { text: string; href?: string } => { // Home and sections switch (type) { case 'connectors': @@ -16,31 +19,33 @@ export const getAlertingSectionBreadcrumb = (type: string): { text: string; href text: i18n.translate('xpack.triggersActionsUI.connectors.breadcrumbTitle', { defaultMessage: 'Connectors', }), - href: `${routeToConnectors}`, + ...(returnHref + ? { + href: `${routeToConnectors}`, + } + : {}), }; case 'rules': return { text: i18n.translate('xpack.triggersActionsUI.rules.breadcrumbTitle', { defaultMessage: 'Rules', }), - href: `${routeToRules}`, + ...(returnHref + ? { + href: `${routeToRules}`, + } + : {}), }; default: return { text: i18n.translate('xpack.triggersActionsUI.home.breadcrumbTitle', { defaultMessage: 'Rules and Connectors', }), - href: `${routeToHome}`, + ...(returnHref + ? { + href: `${routeToHome}`, + } + : {}), }; } }; - -export const getRuleDetailsBreadcrumb = ( - id: string, - name: string -): { text: string; href: string } => { - return { - text: name, - href: `${routeToRuleDetails.replace(':ruleId', id)}`, - }; -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_filter.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_filter.ts new file mode 100644 index 000000000000..65ace4fa72c7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_filter.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. + */ + +// TODO (Jiawei): Use node builder instead of strings +export const getFilter = ({ + message, + outcomeFilter, + runId, +}: { + message?: string; + outcomeFilter?: string[]; + runId?: string; +}) => { + const filter: string[] = []; + + if (message) { + const escapedMessage = message.replace(/([\)\(\<\>\}\{\"\:\\])/gm, '\\$&'); + filter.push(`message: "${escapedMessage}" OR error.message: "${escapedMessage}"`); + } + + if (outcomeFilter && outcomeFilter.length) { + filter.push(`event.outcome: ${outcomeFilter.join(' or ')}`); + } + + if (runId) { + filter.push(`kibana.alert.rule.execution.uuid: ${runId}`); + } + + return filter; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts index e841506595c0..e23fe787e87c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts @@ -28,6 +28,10 @@ export { loadExecutionLogAggregations, loadGlobalExecutionLogAggregations, } from './load_execution_log_aggregations'; +export type { LoadExecutionKPIAggregationsProps } from './load_execution_kpi_aggregations'; +export { loadExecutionKPIAggregations } from './load_execution_kpi_aggregations'; +export type { LoadGlobalExecutionKPIAggregationsProps } from './load_global_execution_kpi_aggregations'; +export { loadGlobalExecutionKPIAggregations } from './load_global_execution_kpi_aggregations'; export type { LoadActionErrorLogProps } from './load_action_error_log'; export { loadActionErrorLog } from './load_action_error_log'; export { unmuteAlertInstance } from './unmute_alert'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts index b4384713336f..d06447be31fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts @@ -117,7 +117,7 @@ describe('loadActionErrorLog', () => { "query": Object { "date_end": "2022-03-23T16:17:53.482Z", "date_start": "2022-03-23T16:17:53.482Z", - "filter": "kibana.alert.rule.execution.uuid: 123 and message: \\"test\\" OR error.message: \\"test\\"", + "filter": "message: \\"test\\" OR error.message: \\"test\\" and kibana.alert.rule.execution.uuid: 123", "page": 1, "per_page": 10, "sort": "[{\\"@timestamp\\":{\\"order\\":\\"asc\\"}}]", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts index 4ec1a6949bfe..10f2879085cd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts @@ -9,6 +9,7 @@ import { HttpSetup } from '@kbn/core/public'; import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IExecutionErrorsResult, ActionErrorLogSortFields } from '@kbn/alerting-plugin/common'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { getFilter } from './get_filter'; export type SortField = Record< ActionErrorLogSortFields, @@ -49,22 +50,6 @@ const getRenamedSort = (sort?: SortField[]) => { }); }; -// TODO (Jiawei): Use node builder instead of strings -const getFilter = ({ runId, message }: { runId?: string; message?: string }) => { - const filter: string[] = []; - - if (runId) { - filter.push(`kibana.alert.rule.execution.uuid: ${runId}`); - } - - if (message) { - const escapedMessage = message.replace(/([\)\(\<\>\}\{\"\:\\])/gm, '\\$&'); - filter.push(`message: "${escapedMessage}" OR error.message: "${escapedMessage}"`); - } - - return filter; -}; - export const loadActionErrorLog = ({ id, http, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_kpi_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_kpi_aggregations.ts new file mode 100644 index 000000000000..076e1167f444 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_kpi_aggregations.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpSetup } from '@kbn/core/public'; +import { IExecutionKPIResult } from '@kbn/alerting-plugin/common'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { getFilter } from './get_filter'; + +export interface LoadExecutionKPIAggregationsProps { + id: string; + outcomeFilter?: string[]; + message?: string; + dateStart: string; + dateEnd?: string; +} + +export const loadExecutionKPIAggregations = ({ + id, + http, + outcomeFilter, + message, + dateStart, + dateEnd, +}: LoadExecutionKPIAggregationsProps & { http: HttpSetup }) => { + const filter = getFilter({ outcomeFilter, message }); + + return http.get( + `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${id}/_execution_kpi`, + { + query: { + filter: filter.length ? filter.join(' and ') : undefined, + date_start: dateStart, + date_end: dateEnd, + }, + } + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts index c1f8487d842c..bf5e529499b4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts @@ -16,6 +16,7 @@ import { } from '@kbn/alerting-plugin/common'; import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { getFilter } from './get_filter'; const getRenamedLog = (data: IExecutionLog) => { const { @@ -40,22 +41,6 @@ const rewriteBodyRes: RewriteRequestCase = ({ data, ...rest ...rest, }); -// TODO (Jiawei): Use node builder instead of strings -const getFilter = ({ outcomeFilter, message }: { outcomeFilter?: string[]; message?: string }) => { - const filter: string[] = []; - - if (outcomeFilter && outcomeFilter.length) { - filter.push(`event.outcome: ${outcomeFilter.join(' or ')}`); - } - - if (message) { - const escapedMessage = message.replace(/([\)\(\<\>\}\{\"\:\\])/gm, '\\$&'); - filter.push(`message: "${escapedMessage}" OR error.message: "${escapedMessage}"`); - } - - return filter; -}; - export type SortField = Record< ExecutionLogSortFields, { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_global_execution_kpi_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_global_execution_kpi_aggregations.ts new file mode 100644 index 000000000000..332e14ad4383 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_global_execution_kpi_aggregations.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpSetup } from '@kbn/core/public'; +import { IExecutionKPIResult } from '@kbn/alerting-plugin/common'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { getFilter } from './get_filter'; + +export interface LoadGlobalExecutionKPIAggregationsProps { + id: string; + outcomeFilter?: string[]; + message?: string; + dateStart: string; + dateEnd?: string; +} + +export const loadGlobalExecutionKPIAggregations = ({ + id, + http, + outcomeFilter, + message, + dateStart, + dateEnd, +}: LoadGlobalExecutionKPIAggregationsProps & { http: HttpSetup }) => { + const filter = getFilter({ outcomeFilter, message }); + + return http.get(`${INTERNAL_BASE_ALERTING_API_PATH}/_global_execution_kpi`, { + query: { + filter: filter.length ? filter.join(' and ') : undefined, + date_start: dateStart, + date_end: dateEnd, + }, + }); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/snooze.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/snooze.ts index 1f020aff106f..b82efdb5e09c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/snooze.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/snooze.ts @@ -5,6 +5,7 @@ * 2.0. */ import { HttpSetup } from '@kbn/core/public'; +import { KueryNode } from '@kbn/es-query'; import { SnoozeSchedule, BulkEditResponse } from '../../../types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; @@ -37,7 +38,7 @@ export async function snoozeRule({ export interface BulkSnoozeRulesProps { ids?: string[]; - filter?: string; + filter?: KueryNode | null | undefined; snoozeSchedule: SnoozeSchedule; } @@ -51,7 +52,7 @@ export function bulkSnoozeRules({ try { body = JSON.stringify({ ids: ids?.length ? ids : undefined, - filter, + ...(filter ? { filter: JSON.stringify(filter) } : {}), operations: [ { operation: 'set', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unsnooze.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unsnooze.ts index 7bfadf1e1b82..d055149fdfbd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unsnooze.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/unsnooze.ts @@ -5,6 +5,7 @@ * 2.0. */ import { HttpSetup } from '@kbn/core/public'; +import { KueryNode } from '@kbn/es-query'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; import { BulkEditResponse } from '../../../types'; @@ -26,7 +27,7 @@ export async function unsnoozeRule({ export interface BulkUnsnoozeRulesProps { ids?: string[]; - filter?: string; + filter?: KueryNode | null | undefined; scheduleIds?: string[]; } @@ -40,7 +41,7 @@ export function bulkUnsnoozeRules({ try { body = JSON.stringify({ ids: ids?.length ? ids : undefined, - filter, + ...(filter ? { filter: JSON.stringify(filter) } : {}), operations: [ { operation: 'delete', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.ts index 53327bbdb1e1..f9a6912c1d43 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.ts @@ -5,6 +5,7 @@ * 2.0. */ import { HttpSetup } from '@kbn/core/public'; +import { KueryNode } from '@kbn/es-query'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; import { BulkEditResponse } from '../../../types'; @@ -16,7 +17,7 @@ export async function updateAPIKey({ id, http }: { id: string; http: HttpSetup } export interface BulkUpdateAPIKeyProps { ids?: string[]; - filter?: string; + filter?: KueryNode | null | undefined; } export function bulkUpdateAPIKey({ @@ -28,7 +29,7 @@ export function bulkUpdateAPIKey({ try { body = JSON.stringify({ ids: ids?.length ? ids : undefined, - filter, + ...(filter ? { filter: JSON.stringify(filter) } : {}), operations: [ { operation: 'set', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/mock/rule_details/alert_summary/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/mock/rule_details/alert_summary/index.ts index b0e5416bbf63..2d5af068ae55 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/mock/rule_details/alert_summary/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/mock/rule_details/alert_summary/index.ts @@ -6,7 +6,6 @@ */ import { Rule } from '../../../../types'; -import { AlertChartData } from '../../../sections/rule_details/components/alert_summary'; export const mockRule = (): Rule => { return { @@ -63,383 +62,12 @@ export const mockRule = (): Rule => { }; }; -export function mockChartData(): AlertChartData[] { - return [ - { - date: 1660608000000, - count: 6, - status: 'recovered', - }, - { - date: 1660694400000, - count: 1, - status: 'recovered', - }, - { - date: 1660694400000, - count: 1, - status: 'active', - }, - { - date: 1658102400000, - count: 6, - status: 'total', - }, - { - date: 1658188800000, - count: 6, - status: 'total', - }, - { - date: 1658275200000, - count: 6, - status: 'total', - }, - { - date: 1658361600000, - count: 6, - status: 'total', - }, - { - date: 1658448000000, - count: 6, - status: 'total', - }, - { - date: 1658534400000, - count: 6, - status: 'total', - }, - { - date: 1658620800000, - count: 6, - status: 'total', - }, - { - date: 1658707200000, - count: 6, - status: 'total', - }, - { - date: 1658793600000, - count: 6, - status: 'total', - }, - { - date: 1658880000000, - count: 6, - status: 'total', - }, - { - date: 1658966400000, - count: 6, - status: 'total', - }, - { - date: 1659052800000, - count: 6, - status: 'total', - }, - { - date: 1659139200000, - count: 6, - status: 'total', - }, - { - date: 1659225600000, - count: 6, - status: 'total', - }, - { - date: 1659312000000, - count: 6, - status: 'total', - }, - { - date: 1659398400000, - count: 6, - status: 'total', - }, - { - date: 1659484800000, - count: 6, - status: 'total', - }, - { - date: 1659571200000, - count: 6, - status: 'total', - }, - { - date: 1659657600000, - count: 6, - status: 'total', - }, - { - date: 1659744000000, - count: 6, - status: 'total', - }, - { - date: 1659830400000, - count: 6, - status: 'total', - }, - { - date: 1659916800000, - count: 6, - status: 'total', - }, - { - date: 1660003200000, - count: 6, - status: 'total', - }, - { - date: 1660089600000, - count: 6, - status: 'total', - }, - { - date: 1660176000000, - count: 6, - status: 'total', - }, - { - date: 1660262400000, - count: 6, - status: 'total', - }, - { - date: 1660348800000, - count: 6, - status: 'total', - }, - { - date: 1660435200000, - count: 6, - status: 'total', - }, - { - date: 1660521600000, - count: 6, - status: 'total', - }, - { - date: 1660694400000, - count: 4, - status: 'total', - }, - ]; -} - export const mockAggsResponse = () => { return { aggregations: { total: { buckets: { totalActiveAlerts: { doc_count: 1 }, totalRecoveredAlerts: { doc_count: 7 } }, }, - statusPerDay: { - buckets: [ - { - key_as_string: '2022-07-18T00:00:00.000Z', - key: 1658102400000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-19T00:00:00.000Z', - key: 1658188800000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-20T00:00:00.000Z', - key: 1658275200000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-21T00:00:00.000Z', - key: 1658361600000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-22T00:00:00.000Z', - key: 1658448000000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-23T00:00:00.000Z', - key: 1658534400000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-24T00:00:00.000Z', - key: 1658620800000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-25T00:00:00.000Z', - key: 1658707200000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-26T00:00:00.000Z', - key: 1658793600000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-27T00:00:00.000Z', - key: 1658880000000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-28T00:00:00.000Z', - key: 1658966400000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-29T00:00:00.000Z', - key: 1659052800000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-30T00:00:00.000Z', - key: 1659139200000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-07-31T00:00:00.000Z', - key: 1659225600000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-01T00:00:00.000Z', - key: 1659312000000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-02T00:00:00.000Z', - key: 1659398400000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-03T00:00:00.000Z', - key: 1659484800000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-04T00:00:00.000Z', - key: 1659571200000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-05T00:00:00.000Z', - key: 1659657600000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-06T00:00:00.000Z', - key: 1659744000000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-07T00:00:00.000Z', - key: 1659830400000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-08T00:00:00.000Z', - key: 1659916800000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-09T00:00:00.000Z', - key: 1660003200000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-10T00:00:00.000Z', - key: 1660089600000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-11T00:00:00.000Z', - key: 1660176000000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-12T00:00:00.000Z', - key: 1660262400000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-13T00:00:00.000Z', - key: 1660348800000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-14T00:00:00.000Z', - key: 1660435200000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-15T00:00:00.000Z', - key: 1660521600000, - doc_count: 0, - alertStatus: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - { - key_as_string: '2022-08-16T00:00:00.000Z', - key: 1660608000000, - doc_count: 6, - alertStatus: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'recovered', doc_count: 6 }], - }, - }, - { - key_as_string: '2022-08-17T00:00:00.000Z', - key: 1660694400000, - doc_count: 2, - alertStatus: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'active', doc_count: 1 }, - { key: 'recovered', doc_count: 1 }, - ], - }, - }, - ], - }, }, }; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx index e57e8c990206..3ddefe1c4cf6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx @@ -99,7 +99,7 @@ export const AlertsFlyout: React.FunctionComponent = ({ ); return ( - + {isLoading && } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.test.tsx index 4e6ae5c7cdb9..fcf1ba99b7af 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.test.tsx @@ -20,9 +20,13 @@ jest.mock('../../../../common/lib/kibana', () => ({ const setRulesToUpdateAPIKey = jest.fn(); const setRulesToSnooze = jest.fn(); +const setRulesToUnsnooze = jest.fn(); const setRulesToSchedule = jest.fn(); +const setRulesToUnschedule = jest.fn(); const setRulesToSnoozeFilter = jest.fn(); +const setRulesToUnsnoozeFilter = jest.fn(); const setRulesToScheduleFilter = jest.fn(); +const setRulesToUnscheduleFilter = jest.fn(); const setRulesToUpdateAPIKeyFilter = jest.fn(); describe('rule_quick_edit_buttons', () => { @@ -39,16 +43,20 @@ describe('rule_quick_edit_buttons', () => { const wrapper = mountWithIntl( ''} + getFilter={() => null} selectedItems={[mockRule]} onPerformingAction={() => {}} onActionPerformed={() => {}} setRulesToDelete={() => {}} setRulesToUpdateAPIKey={() => {}} setRulesToSnooze={() => {}} + setRulesToUnsnooze={() => {}} setRulesToSchedule={() => {}} + setRulesToUnschedule={() => {}} setRulesToSnoozeFilter={() => {}} + setRulesToUnsnoozeFilter={() => {}} setRulesToScheduleFilter={() => {}} + setRulesToUnscheduleFilter={() => {}} setRulesToUpdateAPIKeyFilter={() => {}} /> ); @@ -58,7 +66,9 @@ describe('rule_quick_edit_buttons', () => { expect(wrapper.find('[data-test-subj="updateAPIKeys"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="deleteAll"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="bulkSnooze"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="bulkUnsnooze"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="bulkSnoozeSchedule"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="bulkRemoveSnoozeSchedule"]').exists()).toBeTruthy(); }); it('renders enableAll if rules are all disabled', async () => { @@ -70,16 +80,20 @@ describe('rule_quick_edit_buttons', () => { const wrapper = mountWithIntl( ''} + getFilter={() => null} selectedItems={[mockRule]} onPerformingAction={() => {}} onActionPerformed={() => {}} setRulesToDelete={() => {}} setRulesToUpdateAPIKey={() => {}} setRulesToSnooze={() => {}} + setRulesToUnsnooze={() => {}} setRulesToSchedule={() => {}} + setRulesToUnschedule={() => {}} setRulesToSnoozeFilter={() => {}} + setRulesToUnsnoozeFilter={() => {}} setRulesToScheduleFilter={() => {}} + setRulesToUnscheduleFilter={() => {}} setRulesToUpdateAPIKeyFilter={() => {}} /> ); @@ -97,27 +111,34 @@ describe('rule_quick_edit_buttons', () => { const wrapper = mountWithIntl( ''} + getFilter={() => null} selectedItems={[mockRule]} onPerformingAction={() => {}} onActionPerformed={() => {}} setRulesToDelete={() => {}} setRulesToUpdateAPIKey={() => {}} setRulesToSnooze={() => {}} + setRulesToUnsnooze={() => {}} setRulesToSchedule={() => {}} + setRulesToUnschedule={() => {}} setRulesToSnoozeFilter={() => {}} + setRulesToUnsnoozeFilter={() => {}} setRulesToScheduleFilter={() => {}} + setRulesToUnscheduleFilter={() => {}} setRulesToUpdateAPIKeyFilter={() => {}} /> ); expect(wrapper.find('[data-test-subj="disableAll"]').first().prop('isDisabled')).toBeTruthy(); expect(wrapper.find('[data-test-subj="deleteAll"]').first().prop('isDisabled')).toBeTruthy(); - - expect(wrapper.find('[data-test-subj="updateAPIKeys"]').first().prop('isDiabled')).toBeFalsy(); - expect(wrapper.find('[data-test-subj="bulkSnooze"]').first().prop('isDiabled')).toBeFalsy(); + expect(wrapper.find('[data-test-subj="updateAPIKeys"]').first().prop('isDisabled')).toBeFalsy(); + expect(wrapper.find('[data-test-subj="bulkSnooze"]').first().prop('isDisabled')).toBeFalsy(); + expect(wrapper.find('[data-test-subj="bulkUnsnooze"]').first().prop('isDisabled')).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="bulkSnoozeSchedule"]').first().prop('isDisabled') + ).toBeFalsy(); expect( - wrapper.find('[data-test-subj="bulkSnoozeSchedule"]').first().prop('isDiabled') + wrapper.find('[data-test-subj="bulkRemoveSnoozeSchedule"]').first().prop('isDisabled') ).toBeFalsy(); }); @@ -131,16 +152,20 @@ describe('rule_quick_edit_buttons', () => { const wrapper = mountWithIntl( ''} + getFilter={() => null} selectedItems={[mockRule]} onPerformingAction={() => {}} onActionPerformed={() => {}} setRulesToDelete={() => {}} setRulesToSnooze={setRulesToSnooze} + setRulesToUnsnooze={setRulesToUnsnooze} setRulesToSchedule={setRulesToSchedule} + setRulesToUnschedule={setRulesToUnschedule} setRulesToUpdateAPIKey={setRulesToUpdateAPIKey} setRulesToSnoozeFilter={setRulesToSnoozeFilter} + setRulesToUnsnoozeFilter={setRulesToUnsnoozeFilter} setRulesToScheduleFilter={setRulesToScheduleFilter} + setRulesToUnscheduleFilter={setRulesToUnscheduleFilter} setRulesToUpdateAPIKeyFilter={setRulesToUpdateAPIKeyFilter} /> ); @@ -148,14 +173,22 @@ describe('rule_quick_edit_buttons', () => { wrapper.find('[data-test-subj="bulkSnooze"]').first().simulate('click'); expect(setRulesToSnooze).toHaveBeenCalledTimes(1); + wrapper.find('[data-test-subj="bulkUnsnooze"]').first().simulate('click'); + expect(setRulesToUnsnooze).toHaveBeenCalledTimes(1); + wrapper.find('[data-test-subj="bulkSnoozeSchedule"]').first().simulate('click'); expect(setRulesToSchedule).toHaveBeenCalledTimes(1); + wrapper.find('[data-test-subj="bulkRemoveSnoozeSchedule"]').first().simulate('click'); + expect(setRulesToUnschedule).toHaveBeenCalledTimes(1); + wrapper.find('[data-test-subj="updateAPIKeys"]').first().simulate('click'); expect(setRulesToUpdateAPIKey).toHaveBeenCalledTimes(1); expect(setRulesToSnoozeFilter).not.toHaveBeenCalled(); + expect(setRulesToUnsnoozeFilter).not.toHaveBeenCalled(); expect(setRulesToScheduleFilter).not.toHaveBeenCalled(); + expect(setRulesToUnscheduleFilter).not.toHaveBeenCalled(); expect(setRulesToUpdateAPIKeyFilter).not.toHaveBeenCalled(); }); @@ -169,16 +202,20 @@ describe('rule_quick_edit_buttons', () => { const wrapper = mountWithIntl( ''} + getFilter={() => null} selectedItems={[mockRule]} onPerformingAction={() => {}} onActionPerformed={() => {}} setRulesToDelete={() => {}} setRulesToSnooze={setRulesToSnooze} + setRulesToUnsnooze={setRulesToUnsnooze} setRulesToSchedule={setRulesToSchedule} + setRulesToUnschedule={setRulesToUnschedule} setRulesToUpdateAPIKey={setRulesToUpdateAPIKey} setRulesToSnoozeFilter={setRulesToSnoozeFilter} + setRulesToUnsnoozeFilter={setRulesToUnsnoozeFilter} setRulesToScheduleFilter={setRulesToScheduleFilter} + setRulesToUnscheduleFilter={setRulesToUnscheduleFilter} setRulesToUpdateAPIKeyFilter={setRulesToUpdateAPIKeyFilter} /> ); @@ -186,14 +223,22 @@ describe('rule_quick_edit_buttons', () => { wrapper.find('[data-test-subj="bulkSnooze"]').first().simulate('click'); expect(setRulesToSnoozeFilter).toHaveBeenCalledTimes(1); + wrapper.find('[data-test-subj="bulkUnsnooze"]').first().simulate('click'); + expect(setRulesToUnsnoozeFilter).toHaveBeenCalledTimes(1); + wrapper.find('[data-test-subj="bulkSnoozeSchedule"]').first().simulate('click'); expect(setRulesToScheduleFilter).toHaveBeenCalledTimes(1); + wrapper.find('[data-test-subj="bulkRemoveSnoozeSchedule"]').first().simulate('click'); + expect(setRulesToUnscheduleFilter).toHaveBeenCalledTimes(1); + wrapper.find('[data-test-subj="updateAPIKeys"]').first().simulate('click'); expect(setRulesToUpdateAPIKeyFilter).toHaveBeenCalledTimes(1); - expect(setRulesToSchedule).not.toHaveBeenCalled(); expect(setRulesToSnooze).not.toHaveBeenCalled(); + expect(setRulesToUnsnooze).not.toHaveBeenCalled(); + expect(setRulesToSchedule).not.toHaveBeenCalled(); + expect(setRulesToUnschedule).not.toHaveBeenCalled(); expect(setRulesToUpdateAPIKey).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx index 0b7db1ebeceb..f3cbae535d77 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/rule_quick_edit_buttons.tsx @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { KueryNode } from '@kbn/es-query'; import React, { useState, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup, EuiIconTip } from '@elastic/eui'; @@ -21,19 +22,25 @@ import { useKibana } from '../../../../common/lib/kibana'; export type ComponentOpts = { selectedItems: RuleTableItem[]; isAllSelected?: boolean; - getFilter: () => string; + getFilter: () => KueryNode | null; onPerformingAction?: () => void; onActionPerformed?: () => void; isSnoozingRules?: boolean; + isUnsnoozingRules?: boolean; isSchedulingRules?: boolean; + isUnschedulingRules?: boolean; isUpdatingRuleAPIKeys?: boolean; setRulesToDelete: React.Dispatch>; setRulesToUpdateAPIKey: React.Dispatch>; setRulesToSnooze: React.Dispatch>; + setRulesToUnsnooze: React.Dispatch>; setRulesToSchedule: React.Dispatch>; - setRulesToSnoozeFilter: React.Dispatch>; - setRulesToScheduleFilter: React.Dispatch>; - setRulesToUpdateAPIKeyFilter: React.Dispatch>; + setRulesToUnschedule: React.Dispatch>; + setRulesToSnoozeFilter: React.Dispatch>; + setRulesToUnsnoozeFilter: React.Dispatch>; + setRulesToScheduleFilter: React.Dispatch>; + setRulesToUnscheduleFilter: React.Dispatch>; + setRulesToUpdateAPIKeyFilter: React.Dispatch>; } & BulkOperationsComponentOpts; const ButtonWithTooltip = ({ @@ -65,16 +72,22 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ onPerformingAction = noop, onActionPerformed = noop, isSnoozingRules = false, + isUnsnoozingRules = false, isSchedulingRules = false, + isUnschedulingRules = false, isUpdatingRuleAPIKeys = false, enableRules, disableRules, setRulesToDelete, setRulesToUpdateAPIKey, setRulesToSnooze, + setRulesToUnsnooze, setRulesToSchedule, + setRulesToUnschedule, setRulesToSnoozeFilter, + setRulesToUnsnoozeFilter, setRulesToScheduleFilter, + setRulesToUnscheduleFilter, setRulesToUpdateAPIKeyFilter, }: ComponentOpts) => { const { @@ -90,7 +103,9 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ isDisablingRules || isDeletingRules || isSnoozingRules || + isUnsnoozingRules || isSchedulingRules || + isUnschedulingRules || isUpdatingRuleAPIKeys; const allRulesDisabled = useMemo(() => { @@ -220,6 +235,28 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ } } + async function onUnsnoozeAllClick() { + onPerformingAction(); + try { + if (isAllSelected) { + setRulesToUnsnoozeFilter(getFilter()); + } else { + setRulesToUnsnooze(selectedItems); + } + } catch (e) { + toasts.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToSnoozeRules', + { + defaultMessage: 'Failed to snooze or unsnooze rules', + } + ), + }); + } finally { + onActionPerformed(); + } + } + async function onScheduleAllClick() { onPerformingAction(); try { @@ -242,6 +279,28 @@ export const RuleQuickEditButtons: React.FunctionComponent = ({ } } + async function onUnscheduleAllClick() { + onPerformingAction(); + try { + if (isAllSelected) { + setRulesToUnscheduleFilter(getFilter()); + } else { + setRulesToUnschedule(selectedItems); + } + } catch (e) { + toasts.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.bulkActionPopover.failedToSnoozeRules', + { + defaultMessage: 'Failed to snooze or unsnooze rules', + } + ), + }); + } finally { + onActionPerformed(); + } + } + return ( = ({ /> + + + + + = ({ /> + + + + + Promise; loadRuleSummary: (id: Rule['id'], numberOfExecutions?: number) => Promise; loadRuleTypes: () => Promise; + loadExecutionKPIAggregations: ( + props: LoadExecutionKPIAggregationsProps + ) => Promise; loadExecutionLogAggregations: ( props: LoadExecutionLogAggregationsProps ) => Promise; loadGlobalExecutionLogAggregations: ( props: LoadGlobalExecutionLogAggregationsProps ) => Promise; + loadGlobalExecutionKPIAggregations: ( + props: LoadGlobalExecutionKPIAggregationsProps + ) => Promise; loadActionErrorLog: (props: LoadActionErrorLogProps) => Promise; getHealth: () => Promise; resolveRule: (id: Rule['id']) => Promise; @@ -177,6 +191,22 @@ export function withBulkRuleOperations( http, }) } + loadExecutionKPIAggregations={async ( + loadExecutionKPIAggregationProps: LoadExecutionKPIAggregationsProps + ) => + loadExecutionKPIAggregations({ + ...loadExecutionKPIAggregationProps, + http, + }) + } + loadGlobalExecutionKPIAggregations={async ( + loadGlobalExecutionKPIAggregationsProps: LoadGlobalExecutionKPIAggregationsProps + ) => + loadGlobalExecutionKPIAggregations({ + ...loadGlobalExecutionKPIAggregationsProps, + http, + }) + } resolveRule={async (ruleId: Rule['id']) => resolveRule({ http, ruleId })} getHealth={async () => alertingFrameworkHealth({ http })} snoozeRule={async (rule: Rule, snoozeSchedule: SnoozeSchedule) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_error.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_error.tsx new file mode 100644 index 000000000000..1ee04fe2f69d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_error.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export const AlertSummaryWidgetError = () => { + return ( + + + + } + body={ +

    + { + + } +

    + } + /> + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.stories.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.stories.tsx new file mode 100644 index 000000000000..ab064893ae6f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.stories.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertsSummaryWidgetUI as Component } from './alert_summary_widget_ui'; + +export default { + component: Component, + title: 'app/AlertsSummaryWidgetUI', +}; + +export const Overview = { + args: { + active: 15, + recovered: 53, + timeRange: 'Last 30 days', + }, +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.tsx new file mode 100644 index 000000000000..e4003e803032 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/alert_summary_widget_ui.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiLightVars } from '@kbn/ui-theme'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { AlertsSummaryWidgetUIProps } from './types'; + +export const AlertsSummaryWidgetUI = ({ + active, + recovered, + timeRange, +}: AlertsSummaryWidgetUIProps) => { + return ( + + + + + + +
    + +
    +
    + {!!timeRange && ( + <> + + + {timeRange} + + + )} +
    + + + + + +

    {active + recovered}

    +
    + + + +
    + + + +

    {recovered}

    +
    +
    + + + +
    + + +

    {active}

    +
    + + + +
    +
    +
    +
    +
    +
    +
    + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/index.ts new file mode 100644 index 000000000000..e1f4ef4d231e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AlertsSummaryWidgetUI } from './alert_summary_widget_ui'; +export { AlertSummaryWidgetError } from './alert_summary_widget_error'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/types.ts new file mode 100644 index 000000000000..81437071fcea --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/components/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface AlertsSummaryWidgetUIProps { + active: number; + recovered: number; + timeRange: JSX.Element | string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/helpers.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/helpers.tsx deleted file mode 100644 index 167c98a678f5..000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/helpers.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LIGHT_THEME, XYChartSeriesIdentifier } from '@elastic/charts'; -import { AlertChartData } from './types'; - -export const formatChartAlertData = ( - data: AlertChartData[] -): Array<{ x: number; y: number; g: string }> => - data.map((alert) => ({ - x: alert.date, - y: alert.count, - g: alert.status, - })); - -export const getColorSeries = ({ seriesKeys }: XYChartSeriesIdentifier) => { - switch (seriesKeys[0]) { - case 'active': - return LIGHT_THEME.colors.vizColors[2]; - case 'recovered': - return LIGHT_THEME.colors.vizColors[1]; - case 'total': - return '#f5f7fa'; - default: - return null; - } -}; - -/** - * This function may be passed to `Array.find()` to locate the `P1DT` - * configuration (sub) setting, a string array that contains two entries - * like the following example: `['P1DT', 'YYYY-MM-DD']`. - */ -export const isP1DTFormatterSetting = (formatNameFormatterPair?: string[]) => - Array.isArray(formatNameFormatterPair) && - formatNameFormatterPair[0] === 'P1DT' && - formatNameFormatterPair.length === 2; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/index.tsx index 80f221f436c4..ede15d4bac29 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/index.tsx @@ -6,5 +6,4 @@ */ export { RuleAlertsSummary } from './rule_alerts_summary'; -export type { RuleAlertsSummaryProps, AlertChartData, AlertsChartProps } from './types'; -export { formatChartAlertData, getColorSeries } from './helpers'; +export type { RuleAlertsSummaryProps } from './types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.test.tsx index 39264020f30a..019d4f2ba549 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.test.tsx @@ -12,7 +12,6 @@ import { mount, ReactWrapper } from 'enzyme'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; import { mockRule } from '../../../../mock/rule_details/alert_summary'; -import { AlertChartData } from './types'; jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({ useUiSetting: jest.fn().mockImplementation(() => true), @@ -25,7 +24,6 @@ jest.mock('../../../../hooks/use_load_rule_types', () => ({ jest.mock('../../../../hooks/use_load_rule_alerts_aggregations', () => ({ useLoadRuleAlertsAggs: jest.fn().mockReturnValue({ ruleAlertsAggs: { active: 1, recovered: 7 }, - alertsChartData: mockChartData(), }), })); @@ -80,174 +78,3 @@ describe('Rule Alert Summary', () => { expect(wrapper.find('[data-test-subj="totalAlertsCount"]').text()).toBe('8'); }); }); - -// This function should stay in the same file as the test otherwise the test will fail. -function mockChartData(): AlertChartData[] { - return [ - { - date: 1660608000000, - count: 6, - status: 'recovered', - }, - { - date: 1660694400000, - count: 1, - status: 'recovered', - }, - { - date: 1660694400000, - count: 1, - status: 'active', - }, - { - date: 1658102400000, - count: 6, - status: 'total', - }, - { - date: 1658188800000, - count: 6, - status: 'total', - }, - { - date: 1658275200000, - count: 6, - status: 'total', - }, - { - date: 1658361600000, - count: 6, - status: 'total', - }, - { - date: 1658448000000, - count: 6, - status: 'total', - }, - { - date: 1658534400000, - count: 6, - status: 'total', - }, - { - date: 1658620800000, - count: 6, - status: 'total', - }, - { - date: 1658707200000, - count: 6, - status: 'total', - }, - { - date: 1658793600000, - count: 6, - status: 'total', - }, - { - date: 1658880000000, - count: 6, - status: 'total', - }, - { - date: 1658966400000, - count: 6, - status: 'total', - }, - { - date: 1659052800000, - count: 6, - status: 'total', - }, - { - date: 1659139200000, - count: 6, - status: 'total', - }, - { - date: 1659225600000, - count: 6, - status: 'total', - }, - { - date: 1659312000000, - count: 6, - status: 'total', - }, - { - date: 1659398400000, - count: 6, - status: 'total', - }, - { - date: 1659484800000, - count: 6, - status: 'total', - }, - { - date: 1659571200000, - count: 6, - status: 'total', - }, - { - date: 1659657600000, - count: 6, - status: 'total', - }, - { - date: 1659744000000, - count: 6, - status: 'total', - }, - { - date: 1659830400000, - count: 6, - status: 'total', - }, - { - date: 1659916800000, - count: 6, - status: 'total', - }, - { - date: 1660003200000, - count: 6, - status: 'total', - }, - { - date: 1660089600000, - count: 6, - status: 'total', - }, - { - date: 1660176000000, - count: 6, - status: 'total', - }, - { - date: 1660262400000, - count: 6, - status: 'total', - }, - { - date: 1660348800000, - count: 6, - status: 'total', - }, - { - date: 1660435200000, - count: 6, - status: 'total', - }, - { - date: 1660521600000, - count: 6, - status: 'total', - }, - { - date: 1660694400000, - count: 4, - status: 'total', - }, - ]; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx index 708b12ceb7b4..da4378356865 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/rule_alerts_summary.tsx @@ -5,65 +5,17 @@ * 2.0. */ -import { - BarSeries, - Chart, - FilterPredicate, - LIGHT_THEME, - ScaleType, - Settings, - TooltipType, -} from '@elastic/charts'; -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useEffect, useMemo, useState } from 'react'; -import { - EUI_CHARTS_THEME_DARK, - EUI_CHARTS_THEME_LIGHT, - EUI_SPARKLINE_THEME_PARTIAL, -} from '@elastic/eui/dist/eui_charts_theme'; -import { useUiSetting } from '@kbn/kibana-react-plugin/public'; -import moment from 'moment'; +import React, { useEffect, useState } from 'react'; import { useLoadRuleAlertsAggs } from '../../../../hooks/use_load_rule_alerts_aggregations'; import { useLoadRuleTypes } from '../../../../hooks/use_load_rule_types'; -import { formatChartAlertData, getColorSeries } from '.'; import { RuleAlertsSummaryProps } from '.'; -import { isP1DTFormatterSetting } from './helpers'; +import { AlertSummaryWidgetError, AlertsSummaryWidgetUI } from './components'; -const Y_ACCESSORS = ['y']; -const X_ACCESSORS = ['x']; -const G_ACCESSORS = ['g']; -const FALLBACK_DATE_FORMAT_SCALED_P1DT = 'YYYY-MM-DD'; export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummaryProps) => { const [features, setFeatures] = useState(''); - const isDarkMode = useUiSetting('theme:darkMode'); - - const scaledDateFormatPreference = useUiSetting('dateFormat:scaled'); - const maybeP1DTFormatter = Array.isArray(scaledDateFormatPreference) - ? scaledDateFormatPreference.find(isP1DTFormatterSetting) - : null; - const p1dtFormat = - Array.isArray(maybeP1DTFormatter) && maybeP1DTFormatter.length === 2 - ? maybeP1DTFormatter[1] - : FALLBACK_DATE_FORMAT_SCALED_P1DT; - - const theme = useMemo( - () => [ - EUI_SPARKLINE_THEME_PARTIAL, - isDarkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme, - ], - [isDarkMode] - ); const { ruleTypes } = useLoadRuleTypes({ filteredRuleTypes, }); @@ -71,21 +23,10 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary ruleAlertsAggs: { active, recovered }, isLoadingRuleAlertsAggs, errorRuleAlertsAggs, - alertsChartData, } = useLoadRuleAlertsAggs({ ruleId: rule.id, features, }); - const chartData = useMemo(() => formatChartAlertData(alertsChartData), [alertsChartData]); - const tooltipSettings = useMemo( - () => ({ - type: TooltipType.VerticalCursor, - headerFormatter: ({ value }: { value: number }) => { - return <>{moment(value).format(p1dtFormat)}; - }, - }), - [p1dtFormat] - ); useEffect(() => { const matchedRuleType = ruleTypes.find((type) => type.id === rule.ruleTypeId); @@ -95,133 +36,18 @@ export const RuleAlertsSummary = ({ rule, filteredRuleTypes }: RuleAlertsSummary }, [rule, ruleTypes]); if (isLoadingRuleAlertsAggs) return ; - if (errorRuleAlertsAggs) - return ( - - - - } - body={ -

    - { - - } -

    - } - /> - ); - const isVisibleFunction: FilterPredicate = (series) => series.splitAccessors.get('g') !== 'total'; + if (errorRuleAlertsAggs) return ; return ( - - - - - - -
    - -
    -
    - - - - -
    - - - - - - - - - -

    {active + recovered}

    -
    -
    -
    - - - - - - -

    {active}

    -
    -
    -
    - - - - - - - -

    {recovered}

    -
    -
    -
    -
    -
    -
    -
    -
    - - - -
    - -
    -
    -
    -
    - - - - - -
    + } + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/types.ts index ae4d8696572b..7ae05d1eaa10 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/alert_summary/types.ts @@ -11,12 +11,3 @@ export interface RuleAlertsSummaryProps { rule: Rule; filteredRuleTypes: string[]; } -export interface AlertChartData { - status: 'active' | 'recovered' | 'total'; - count: number; - date: number; -} - -export interface AlertsChartProps { - data: AlertChartData[]; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx index bfe337a1fb88..5eac73c4e87a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx @@ -419,8 +419,8 @@ describe('tabbed content', () => { tabbedContent.update(); }); - expect(tabbedContent.find('[aria-labelledby="rule_event_log_list"]').exists()).toBeTruthy(); - expect(tabbedContent.find('[aria-labelledby="rule_alert_list"]').exists()).toBeFalsy(); + expect(tabbedContent.find('[aria-labelledby="rule_event_log_list"]').exists()).toBeFalsy(); + expect(tabbedContent.find('[aria-labelledby="rule_alert_list"]').exists()).toBeTruthy(); tabbedContent.find('[data-test-subj="eventLogListTab"]').simulate('click'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx index 5c44b9161b2c..db82f36cbc90 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx @@ -97,6 +97,14 @@ export function RuleComponent({ }; const tabs = [ + { + id: ALERT_LIST_TAB, + name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText', { + defaultMessage: 'Alerts', + }), + 'data-test-subj': 'ruleAlertListTab', + content: renderRuleAlertList(), + }, { id: EVENT_LOG_LIST_TAB, name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText', { @@ -118,14 +126,6 @@ export function RuleComponent({ requestRefresh, }), }, - { - id: ALERT_LIST_TAB, - name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText', { - defaultMessage: 'Alerts', - }), - 'data-test-subj': 'ruleAlertListTab', - content: renderRuleAlertList(), - }, ]; const renderTabs = () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index c17bc4008666..38357d7c5605 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -36,7 +36,7 @@ import { hasExecuteActionsCapability, hasManageApiKeysCapability, } from '../../../lib/capabilities'; -import { getAlertingSectionBreadcrumb, getRuleDetailsBreadcrumb } from '../../../lib/breadcrumb'; +import { getAlertingSectionBreadcrumb } from '../../../lib/breadcrumb'; import { getCurrentDocTitle } from '../../../lib/doc_title'; import { Rule, @@ -117,10 +117,7 @@ export const RuleDetails: React.FunctionComponent = ({ // Set breadcrumb and page title useEffect(() => { - setBreadcrumbs([ - getAlertingSectionBreadcrumb('rules'), - getRuleDetailsBreadcrumb(rule.id, rule.name), - ]); + setBreadcrumbs([getAlertingSectionBreadcrumb('rules', true), { text: rule.name }]); chrome.docTitle.change(getCurrentDocTitle('rules')); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.test.tsx new file mode 100644 index 000000000000..f77494e4f3c1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.test.tsx @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import { loadExecutionKPIAggregations } from '../../../lib/rule_api/load_execution_kpi_aggregations'; +import { loadGlobalExecutionKPIAggregations } from '../../../lib/rule_api/load_global_execution_kpi_aggregations'; +import { RuleEventLogListKPI } from './rule_event_log_list_kpi'; + +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: jest.fn().mockReturnValue({ + services: { + notifications: { toast: { addDanger: jest.fn() } }, + }, + }), +})); + +jest.mock('../../../lib/rule_api/load_execution_kpi_aggregations', () => ({ + loadExecutionKPIAggregations: jest.fn(), +})); + +jest.mock('../../../lib/rule_api/load_global_execution_kpi_aggregations', () => ({ + loadGlobalExecutionKPIAggregations: jest.fn(), +})); + +const mockKpiResponse = { + success: 4, + unknown: 10, + failure: 60, + activeAlerts: 100, + newAlerts: 40, + recoveredAlerts: 30, + erroredActions: 60, + triggeredActions: 140, +}; + +const loadExecutionKPIAggregationsMock = + loadExecutionKPIAggregations as unknown as jest.MockedFunction; +const loadGlobalExecutionKPIAggregationsMock = + loadGlobalExecutionKPIAggregations as unknown as jest.MockedFunction; + +describe('rule_event_log_list_kpi', () => { + beforeEach(() => { + jest.clearAllMocks(); + loadExecutionKPIAggregationsMock.mockResolvedValue(mockKpiResponse); + loadGlobalExecutionKPIAggregationsMock.mockResolvedValue(mockKpiResponse); + }); + + it('renders correctly', async () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="centerJustifiedSpinner"]').exists()).toBeTruthy(); + + // Let the load resolve + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="centerJustifiedSpinner"]').exists()).toBeFalsy(); + + expect(loadExecutionKPIAggregationsMock).toHaveBeenCalledWith( + expect.objectContaining({ + id: '123', + message: undefined, + outcomeFilter: undefined, + }) + ); + + expect(loadGlobalExecutionKPIAggregations).not.toHaveBeenCalled(); + + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-successOutcome"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.success}`); + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-unknownOutcome"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.unknown}`); + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-failureOutcome"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.failure}`); + expect( + wrapper.find('[data-test-subj="ruleEventLogKpi-activeAlerts"] .euiStat__title').first().text() + ).toEqual(`${mockKpiResponse.activeAlerts}`); + expect( + wrapper.find('[data-test-subj="ruleEventLogKpi-newAlerts"] .euiStat__title').first().text() + ).toEqual(`${mockKpiResponse.newAlerts}`); + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-recoveredAlerts"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.recoveredAlerts}`); + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-erroredActions"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.erroredActions}`); + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-triggeredActions"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.triggeredActions}`); + }); + + it('calls global KPI API if provided global rule id', async () => { + const wrapper = mountWithIntl( + + ); + // Let the load resolve + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(loadGlobalExecutionKPIAggregations).toHaveBeenCalledWith( + expect.objectContaining({ + id: '*', + message: undefined, + outcomeFilter: undefined, + }) + ); + + expect(loadExecutionKPIAggregationsMock).not.toHaveBeenCalled(); + }); + + it('calls KPI API with filters', async () => { + const wrapper = mountWithIntl( + + ); + + // Let the load resolve + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(loadExecutionKPIAggregationsMock).toHaveBeenCalledWith( + expect.objectContaining({ + id: '123', + message: 'test', + outcomeFilter: ['status: 123', 'test:456'], + }) + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx new file mode 100644 index 000000000000..7fe7dd8fdb02 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState, useMemo, useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import datemath from '@kbn/datemath'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiIconTip, EuiStat, EuiSpacer } from '@elastic/eui'; +import { IExecutionKPIResult } from '@kbn/alerting-plugin/common'; +import { + ComponentOpts as RuleApis, + withBulkRuleOperations, +} from '../../common/components/with_bulk_rule_api_operations'; +import { useKibana } from '../../../../common/lib/kibana'; +import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; +import { RuleEventLogListStatus } from './rule_event_log_list_status'; + +const getParsedDate = (date: string) => { + if (date.includes('now')) { + return datemath.parse(date)?.format() || date; + } + return date; +}; + +const API_FAILED_MESSAGE = i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError', + { + defaultMessage: 'Failed to fetch event log KPI.', + } +); + +const RESPONSE_TOOLTIP = i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip', + { + defaultMessage: 'The responses for the latest rule runs.', + } +); + +const ALERTS_TOOLTIP = i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.alertsTooltip', + { + defaultMessage: 'The alert statuses for the latest rule runs.', + } +); + +const ACTIONS_TOOLTIP = i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.actionsTooltip', + { + defaultMessage: 'The action statuses for the latest rule runs.', + } +); + +const Stat = ({ + title, + tooltip, + children, +}: { + title: string; + tooltip: string; + children?: JSX.Element; +}) => { + return ( + + + + {title} + + + + + + + {children} + + ); +}; + +export type RuleEventLogListKPIProps = { + ruleId: string; + dateStart: string; + dateEnd: string; + outcomeFilter?: string[]; + message?: string; + refreshToken?: number; +} & Pick; + +export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => { + const { + ruleId, + dateStart, + dateEnd, + outcomeFilter, + message, + refreshToken, + loadExecutionKPIAggregations, + loadGlobalExecutionKPIAggregations, + } = props; + const { + notifications: { toasts }, + } = useKibana().services; + + const isInitialized = useRef(false); + + const [isLoading, setIsLoading] = useState(false); + const [kpi, setKpi] = useState(); + + const loadKPIFn = useMemo(() => { + if (ruleId === '*') { + return loadGlobalExecutionKPIAggregations; + } + return loadExecutionKPIAggregations; + }, [ruleId, loadExecutionKPIAggregations, loadGlobalExecutionKPIAggregations]); + + const loadKPIs = async () => { + setIsLoading(true); + try { + const newKpi = await loadKPIFn({ + id: ruleId, + dateStart: getParsedDate(dateStart), + dateEnd: getParsedDate(dateEnd), + outcomeFilter, + message, + }); + setKpi(newKpi); + } catch (e) { + toasts.addDanger({ + title: API_FAILED_MESSAGE, + text: e.body?.message ?? e, + }); + } + setIsLoading(false); + }; + + useEffect(() => { + loadKPIs(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ruleId, dateStart, dateEnd, outcomeFilter, message]); + + useEffect(() => { + if (isInitialized.current) { + loadKPIs(); + } + isInitialized.current = true; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [refreshToken]); + + if (isLoading || !kpi) { + return ; + } + + const getStatDescription = (element: React.ReactNode) => { + return ( + <> + {element} + + + ); + }; + + return ( + + + + + + )} + titleSize="s" + title={kpi.success} + /> + + + )} + titleSize="s" + title={kpi.unknown} + /> + + + )} + titleSize="s" + title={kpi.failure} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const RuleEventLogListKPIWithApi = withBulkRuleOperations(RuleEventLogListKPI); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx index b647442a8eaf..2c1d6df71fc3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx @@ -30,9 +30,9 @@ import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filte import { RuleEventLogDataGrid } from './rule_event_log_data_grid'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; import { RuleActionErrorLogFlyout } from './rule_action_error_log_flyout'; - import { RefineSearchPrompt } from '../refine_search_prompt'; import { LoadExecutionLogAggregationsProps } from '../../../lib/rule_api'; +import { RuleEventLogListKPIWithApi as RuleEventLogListKPI } from './rule_event_log_list_kpi'; import { ComponentOpts as RuleApis, withBulkRuleOperations, @@ -114,6 +114,9 @@ export const RuleEventLogListTable = ( const [search, setSearch] = useState(''); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); const [selectedRunLog, setSelectedRunLog] = useState(); + const [internalRefreshToken, setInternalRefreshToken] = useState( + refreshToken + ); // Data grid states const [logs, setLogs] = useState(); @@ -243,6 +246,7 @@ export const RuleEventLogListTable = ( ); const onRefresh = () => { + setInternalRefreshToken(Date.now()); loadEventLogs(); }; @@ -339,6 +343,10 @@ export const RuleEventLogListTable = ( localStorage.setItem(localStorageKey, JSON.stringify(visibleColumns)); }, [localStorageKey, visibleColumns]); + useEffect(() => { + setInternalRefreshToken(refreshToken); + }, [refreshToken]); + return ( <> @@ -371,6 +379,15 @@ export const RuleEventLogListTable = ( + + {renderList()} {isOnLastPage && ( void; onSave: () => void; - setIsLoading: (isLoading: boolean) => void; + setIsSnoozingRule: (isLoading: boolean) => void; + setIsUnsnoozingRule: (isLoading: boolean) => void; onSearchPopulate?: (filter: string) => void; } & BulkOperationsComponentOpts; @@ -43,13 +48,29 @@ const failureMessage = i18n.translate( } ); +const deleteConfirmPlural = (total: number) => + i18n.translate('xpack.triggersActionsUI.sections.rulesList.bulkUnsnoozeConfirmationPlural', { + defaultMessage: 'Unsnooze {total, plural, one {# rule} other {# rules}}? ', + values: { total }, + }); + +const deleteConfirmSingle = (ruleName: string) => + i18n.translate('xpack.triggersActionsUI.sections.rulesList.bulkUnsnoozeConfirmationSingle', { + defaultMessage: 'Unsnooze {ruleName}?', + values: { ruleName }, + }); + export const BulkSnoozeModal = (props: BulkSnoozeModalProps) => { const { rulesToSnooze, + rulesToUnsnooze, rulesToSnoozeFilter, + rulesToUnsnoozeFilter, + numberOfSelectedRules = 0, onClose, onSave, - setIsLoading, + setIsSnoozingRule, + setIsUnsnoozingRule, onSearchPopulate, bulkSnoozeRules, bulkUnsnoozeRules, @@ -62,18 +83,18 @@ export const BulkSnoozeModal = (props: BulkSnoozeModalProps) => { const { showToast } = useBulkEditResponse({ onSearchPopulate }); const isSnoozeModalOpen = useMemo(() => { - if (rulesToSnoozeFilter) { + if (typeof rulesToSnoozeFilter !== 'undefined') { return true; } return rulesToSnooze.length > 0; }, [rulesToSnooze, rulesToSnoozeFilter]); - const isSnoozed = useMemo(() => { - if (rulesToSnoozeFilter) { + const isUnsnoozeModalOpen = useMemo(() => { + if (typeof rulesToUnsnoozeFilter !== 'undefined') { return true; } - return rulesToSnooze.some((item) => isRuleSnoozed(item)); - }, [rulesToSnooze, rulesToSnoozeFilter]); + return rulesToUnsnooze.length > 0; + }, [rulesToUnsnooze, rulesToUnsnoozeFilter]); const interval = useMemo(() => { if (rulesToSnoozeFilter) { @@ -87,7 +108,7 @@ export const BulkSnoozeModal = (props: BulkSnoozeModalProps) => { const onSnoozeRule = async (schedule: SnoozeSchedule) => { onClose(); - setIsLoading(true); + setIsSnoozingRule(true); try { const response = await bulkSnoozeRules({ ids: rulesToSnooze.map((item) => item.id), @@ -100,18 +121,17 @@ export const BulkSnoozeModal = (props: BulkSnoozeModalProps) => { title: failureMessage, }); } - setIsLoading(false); + setIsSnoozingRule(false); onSave(); }; - const onUnsnoozeRule = async (scheduleIds?: string[]) => { + const onUnsnoozeRule = async () => { onClose(); - setIsLoading(true); + setIsUnsnoozingRule(true); try { const response = await bulkUnsnoozeRules({ - ids: rulesToSnooze.map((item) => item.id), - filter: rulesToSnoozeFilter, - scheduleIds, + ids: rulesToUnsnooze.map((item) => item.id), + filter: rulesToUnsnoozeFilter, }); showToast(response, 'snooze'); } catch (error) { @@ -119,10 +139,42 @@ export const BulkSnoozeModal = (props: BulkSnoozeModalProps) => { title: failureMessage, }); } - setIsLoading(false); + setIsUnsnoozingRule(false); onSave(); }; + const confirmationTitle = useMemo(() => { + if (!rulesToUnsnoozeFilter && numberOfSelectedRules === 1 && rulesToUnsnooze[0]) { + return deleteConfirmSingle(rulesToUnsnooze[0].name); + } + return deleteConfirmPlural(numberOfSelectedRules); + }, [rulesToUnsnooze, rulesToUnsnoozeFilter, numberOfSelectedRules]); + + if (isUnsnoozeModalOpen) { + return ( + + ); + } + if (isSnoozeModalOpen) { return ( @@ -138,11 +190,11 @@ export const BulkSnoozeModal = (props: BulkSnoozeModalProps) => { @@ -153,6 +205,7 @@ export const BulkSnoozeModal = (props: BulkSnoozeModalProps) => { ); } + return null; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/bulk_snooze_schedule_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/bulk_snooze_schedule_modal.tsx index 56293c01614d..d5a1fd3d62b7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/bulk_snooze_schedule_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/bulk_snooze_schedule_modal.tsx @@ -5,18 +5,19 @@ * 2.0. */ -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { KueryNode } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { + EuiConfirmModal, EuiModal, EuiModalHeader, + EuiModalHeaderTitle, EuiModalBody, EuiModalFooter, EuiSpacer, EuiButtonEmpty, - EuiModalHeaderTitle, - EuiConfirmModal, } from '@elastic/eui'; import { withBulkRuleOperations, @@ -49,24 +50,30 @@ const deleteConfirmSingle = (ruleName: string) => export type BulkSnoozeScheduleModalProps = { rulesToSchedule: RuleTableItem[]; - rulesToScheduleFilter?: string; + rulesToUnschedule: RuleTableItem[]; + rulesToScheduleFilter?: KueryNode | null | undefined; + rulesToUnscheduleFilter?: KueryNode | null | undefined; numberOfSelectedRules?: number; onClose: () => void; onSave: () => void; - setIsLoading: (isLoading: boolean) => void; + setIsSchedulingRule: (isLoading: boolean) => void; + setIsUnschedulingRule: (isLoading: boolean) => void; onSearchPopulate?: (filter: string) => void; } & BulkOperationsComponentOpts; export const BulkSnoozeScheduleModal = (props: BulkSnoozeScheduleModalProps) => { const { rulesToSchedule, + rulesToUnschedule, rulesToScheduleFilter, + rulesToUnscheduleFilter, numberOfSelectedRules = 0, onClose, onSave, bulkSnoozeRules, bulkUnsnoozeRules, - setIsLoading, + setIsSchedulingRule, + setIsUnschedulingRule, onSearchPopulate, } = props; @@ -76,18 +83,23 @@ export const BulkSnoozeScheduleModal = (props: BulkSnoozeScheduleModalProps) => const { showToast } = useBulkEditResponse({ onSearchPopulate }); - const [showConfirmation, setShowConfirmation] = useState(false); - const isScheduleModalOpen = useMemo(() => { - if (rulesToScheduleFilter) { + if (typeof rulesToScheduleFilter !== 'undefined') { return true; } return rulesToSchedule.length > 0; }, [rulesToSchedule, rulesToScheduleFilter]); + const isUnscheduleModalOpen = useMemo(() => { + if (typeof rulesToUnscheduleFilter !== 'undefined') { + return true; + } + return rulesToUnschedule.length > 0; + }, [rulesToUnschedule, rulesToUnscheduleFilter]); + const onAddSnoozeSchedule = async (schedule: SnoozeSchedule) => { onClose(); - setIsLoading(true); + setIsSchedulingRule(true); try { const response = await bulkSnoozeRules({ ids: rulesToSchedule.map((item) => item.id), @@ -100,18 +112,17 @@ export const BulkSnoozeScheduleModal = (props: BulkSnoozeScheduleModalProps) => title: failureMessage, }); } - setIsLoading(false); + setIsSchedulingRule(false); onSave(); }; const onRemoveSnoozeSchedule = async () => { - setShowConfirmation(false); onClose(); - setIsLoading(true); + setIsUnschedulingRule(true); try { const response = await bulkUnsnoozeRules({ - ids: rulesToSchedule.map((item) => item.id), - filter: rulesToScheduleFilter, + ids: rulesToUnschedule.map((item) => item.id), + filter: rulesToUnscheduleFilter, scheduleIds: [], }); showToast(response, 'snoozeSchedule'); @@ -120,7 +131,7 @@ export const BulkSnoozeScheduleModal = (props: BulkSnoozeScheduleModalProps) => title: failureMessage, }); } - setIsLoading(false); + setIsUnschedulingRule(false); onSave(); }; @@ -131,14 +142,11 @@ export const BulkSnoozeScheduleModal = (props: BulkSnoozeScheduleModalProps) => return deleteConfirmPlural(numberOfSelectedRules); }, [rulesToSchedule, rulesToScheduleFilter, numberOfSelectedRules]); - if (showConfirmation) { + if (isUnscheduleModalOpen) { return ( { - setShowConfirmation(false); - onClose(); - }} + onCancel={onClose} onConfirm={onRemoveSnoozeSchedule} confirmButtonText={i18n.translate( 'xpack.triggersActionsUI.sections.rulesList.bulkDeleteConfirmButton', @@ -154,6 +162,7 @@ export const BulkSnoozeScheduleModal = (props: BulkSnoozeScheduleModalProps) => )} buttonColor="danger" defaultFocusedButton="confirm" + data-test-subj="bulkRemoveScheduleConfirmationModal" /> ); } @@ -172,13 +181,12 @@ export const BulkSnoozeScheduleModal = (props: BulkSnoozeScheduleModalProps) =>
    setShowConfirmation(true)} + onCancelSchedules={onRemoveSnoozeSchedule} onClose={() => {}} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 3bde062935a8..6bfe953e2869 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -80,8 +80,14 @@ jest.mock('../../../../common/get_experimental_features', () => ({ const ruleTags = ['a', 'b', 'c', 'd']; -const { loadRuleTypes, updateAPIKey, loadRuleTags, bulkSnoozeRules, bulkUpdateAPIKey } = - jest.requireMock('../../../lib/rule_api'); +const { + loadRuleTypes, + updateAPIKey, + loadRuleTags, + bulkSnoozeRules, + bulkUnsnoozeRules, + bulkUpdateAPIKey, +} = jest.requireMock('../../../lib/rule_api'); const { loadRuleAggregationsWithKueryFilter } = jest.requireMock( '../../../lib/rule_api/aggregate_kuery_filter' ); @@ -1994,6 +2000,33 @@ describe.skip('Rules list bulk actions', () => { ); }); + it('can bulk unsnooze', async () => { + await setup(); + wrapper.find('[data-test-subj="checkboxSelectRow-1"]').at(1).simulate('change'); + wrapper.find('[data-test-subj="selectAllRulesButton"]').at(1).simulate('click'); + wrapper.find('[data-test-subj="showBulkActionButton"]').first().simulate('click'); + + // Unselect something to test filtering + wrapper.find('[data-test-subj="checkboxSelectRow-2"]').at(1).simulate('change'); + + wrapper.find('[data-test-subj="bulkUnsnooze"]').first().simulate('click'); + + expect(wrapper.find('[data-test-subj="bulkUnsnoozeConfirmationModal"]').exists()).toBeTruthy(); + wrapper.find('[data-test-subj="confirmModalConfirmButton"]').first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(bulkUnsnoozeRules).toHaveBeenCalledWith( + expect.objectContaining({ + ids: [], + filter: 'NOT (alert.id: "alert:2")', + }) + ); + }); + it('can bulk add snooze schedule', async () => { await setup(); wrapper.find('[data-test-subj="checkboxSelectRow-1"]').at(1).simulate('change'); @@ -2020,6 +2053,36 @@ describe.skip('Rules list bulk actions', () => { ); }); + it('can bulk remove snooze schedule', async () => { + await setup(); + wrapper.find('[data-test-subj="checkboxSelectRow-1"]').at(1).simulate('change'); + wrapper.find('[data-test-subj="selectAllRulesButton"]').at(1).simulate('click'); + wrapper.find('[data-test-subj="showBulkActionButton"]').first().simulate('click'); + + // Unselect something to test filtering + wrapper.find('[data-test-subj="checkboxSelectRow-2"]').at(1).simulate('change'); + + wrapper.find('[data-test-subj="bulkRemoveSnoozeSchedule"]').first().simulate('click'); + + expect( + wrapper.find('[data-test-subj="bulkRemoveScheduleConfirmationModal"]').exists() + ).toBeTruthy(); + wrapper.find('[data-test-subj="confirmModalConfirmButton"]').first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(bulkUnsnoozeRules).toHaveBeenCalledWith( + expect.objectContaining({ + ids: [], + filter: 'NOT (alert.id: "alert:2")', + scheduleIds: [], + }) + ); + }); + it('can bulk update API key', async () => { await setup(); wrapper.find('[data-test-subj="checkboxSelectRow-1"]').at(1).simulate('change'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index a725a0e916c1..a06958fb3072 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { capitalize, isEmpty, sortBy } from 'lodash'; +import { KueryNode } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useEffect, useState, ReactNode, useCallback, useMemo } from 'react'; import { @@ -206,17 +207,36 @@ export const RulesList = ({ const [rulesToDelete, setRulesToDelete] = useState([]); + // TODO - tech debt: Right now we're using null and undefined to determine if we should + // render the bulk edit modal. Refactor this to only keep track of 1 set of rules and types + // to determine which modal to show const [rulesToSnooze, setRulesToSnooze] = useState([]); - const [rulesToSnoozeFilter, setRulesToSnoozeFilter] = useState(''); + const [rulesToSnoozeFilter, setRulesToSnoozeFilter] = useState(); + + const [rulesToUnsnooze, setRulesToUnsnooze] = useState([]); + const [rulesToUnsnoozeFilter, setRulesToUnsnoozeFilter] = useState< + KueryNode | null | undefined + >(); const [rulesToSchedule, setRulesToSchedule] = useState([]); - const [rulesToScheduleFilter, setRulesToScheduleFilter] = useState(''); + const [rulesToScheduleFilter, setRulesToScheduleFilter] = useState< + KueryNode | null | undefined + >(); + + const [rulesToUnschedule, setRulesToUnschedule] = useState([]); + const [rulesToUnscheduleFilter, setRulesToUnscheduleFilter] = useState< + KueryNode | null | undefined + >(); const [rulesToUpdateAPIKey, setRulesToUpdateAPIKey] = useState([]); - const [rulesToUpdateAPIKeyFilter, setRulesToUpdateAPIKeyFilter] = useState(''); + const [rulesToUpdateAPIKeyFilter, setRulesToUpdateAPIKeyFilter] = useState< + KueryNode | null | undefined + >(); const [isSnoozingRules, setIsSnoozingRules] = useState(false); const [isSchedulingRules, setIsSchedulingRules] = useState(false); + const [isUnsnoozingRules, setIsUnsnoozingRules] = useState(false); + const [isUnschedulingRules, setIsUnschedulingRules] = useState(false); const [isUpdatingRuleAPIKeys, setIsUpdatingRuleAPIKeys] = useState(false); const hasAnyAuthorizedRuleType = useMemo(() => { @@ -578,6 +598,12 @@ export const RulesList = ({ } = useBulkEditSelect({ totalItemCount: rulesState.totalItemCount, items: tableItems, + searchText, + typesFilter: rulesTypesFilter, + actionTypesFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + tagsFilter, }); const authorizedToModifySelectedRules = useMemo(() => { @@ -594,17 +620,27 @@ export const RulesList = ({ const clearRulesToSnooze = () => { setRulesToSnooze([]); - setRulesToSnoozeFilter(''); + setRulesToSnoozeFilter(undefined); + }; + + const clearRulesToUnsnooze = () => { + setRulesToUnsnooze([]); + setRulesToUnsnoozeFilter(undefined); }; const clearRulesToSchedule = () => { setRulesToSchedule([]); - setRulesToScheduleFilter(''); + setRulesToScheduleFilter(undefined); + }; + + const clearRulesToUnschedule = () => { + setRulesToUnschedule([]); + setRulesToUnscheduleFilter(undefined); }; const clearRulesToUpdateAPIKey = () => { setRulesToUpdateAPIKey([]); - setRulesToUpdateAPIKeyFilter(''); + setRulesToUpdateAPIKeyFilter(undefined); }; const isRulesTableLoading = useMemo(() => { @@ -613,7 +649,9 @@ export const RulesList = ({ ruleTypesState.isLoading || isPerformingAction || isSnoozingRules || + isUnsnoozingRules || isSchedulingRules || + isUnschedulingRules || isUpdatingRuleAPIKeys ); }, [ @@ -621,7 +659,9 @@ export const RulesList = ({ ruleTypesState, isPerformingAction, isSnoozingRules, + isUnsnoozingRules, isSchedulingRules, + isUnschedulingRules, isUpdatingRuleAPIKeys, ]); @@ -903,14 +943,20 @@ export const RulesList = ({ setIsPerformingAction(false); }} isSnoozingRules={isSnoozingRules} + isUnsnoozingRules={isUnsnoozingRules} isSchedulingRules={isSchedulingRules} + isUnschedulingRules={isUnschedulingRules} isUpdatingRuleAPIKeys={isUpdatingRuleAPIKeys} setRulesToDelete={setRulesToDelete} setRulesToUpdateAPIKey={setRulesToUpdateAPIKey} setRulesToSnooze={setRulesToSnooze} + setRulesToUnsnooze={setRulesToUnsnooze} setRulesToSchedule={setRulesToSchedule} + setRulesToUnschedule={setRulesToUnschedule} setRulesToSnoozeFilter={setRulesToSnoozeFilter} + setRulesToUnsnoozeFilter={setRulesToUnsnoozeFilter} setRulesToScheduleFilter={setRulesToScheduleFilter} + setRulesToUnscheduleFilter={setRulesToUnscheduleFilter} setRulesToUpdateAPIKeyFilter={setRulesToUpdateAPIKeyFilter} /> @@ -983,13 +1029,19 @@ export const RulesList = ({ /> { clearRulesToSnooze(); + clearRulesToUnsnooze(); }} onSave={async () => { clearRulesToSnooze(); + clearRulesToUnsnooze(); onClearSelection(); await loadData(); }} @@ -997,14 +1049,19 @@ export const RulesList = ({ /> { clearRulesToSchedule(); + clearRulesToUnschedule(); }} onSave={async () => { clearRulesToSchedule(); + clearRulesToUnschedule(); onClearSelection(); await loadData(); }} diff --git a/x-pack/test/accessibility/apps/graph.ts b/x-pack/test/accessibility/apps/graph.ts index d13ed5a58f87..03ca3b2afbfe 100644 --- a/x-pack/test/accessibility/apps/graph.ts +++ b/x-pack/test/accessibility/apps/graph.ts @@ -63,12 +63,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('saveCancelButton'); }); - it('Graph inspect panel', async function () { - await testSubjects.click('graphInspectButton'); - await a11y.testAppSnapshot(); - await testSubjects.click('graphInspectButton'); - }); - it('Graph settings - advanced settings tab', async function () { await testSubjects.click('graphSettingsButton'); await a11y.testAppSnapshot(); @@ -85,12 +79,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.pressKeys(browser.keys.ESCAPE); }); - // https://github.com/elastic/kibana/issues/134693 - it.skip('Graph settings drilldown tab - add new drilldown', async function () { + it('Graph settings drilldown tab - add new drilldown', async function () { + await testSubjects.click('graphSettingsButton'); + await testSubjects.click('drillDowns'); await testSubjects.click('graphAddNewTemplate'); await a11y.testAppSnapshot(); - await testSubjects.click('graphRemoveUrlTemplate'); - await testSubjects.click('euiFlyoutCloseButton'); await browser.pressKeys(browser.keys.ESCAPE); }); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index 3d68c420e444..068c51661861 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -109,31 +109,21 @@ async function alwaysFiringExecutor(alertExecutorOptions: any) { rule, } = alertExecutorOptions; let group: string | null = 'default'; - let subgroup: string | null = null; const alertInfo = { alertId, spaceId, namespace, name, tags, createdBy, updatedBy, ...rule }; if (params.groupsToScheduleActionsInSeries) { const index = state.groupInSeriesIndex || 0; - const [scheduledGroup, scheduledSubgroup] = ( - params.groupsToScheduleActionsInSeries[index] ?? '' - ).split(':'); + const [scheduledGroup] = (params.groupsToScheduleActionsInSeries[index] ?? '').split(':'); group = scheduledGroup; - subgroup = scheduledSubgroup; } if (group) { const instance = services.alertFactory.create('1').replaceState({ instanceStateValue: true }); - if (subgroup) { - instance.scheduleActionsWithSubGroup(group, subgroup, { - instanceContextValue: true, - }); - } else { - instance.scheduleActions(group, { - instanceContextValue: true, - }); - } + instance.scheduleActions(group, { + instanceContextValue: true, + }); } await services.scopedClusterClient.asCurrentUser.index({ @@ -506,9 +496,7 @@ function getPatternFiringAlertType() { deep: DeepContextVariables, }); } else if (typeof scheduleByPattern === 'string') { - services.alertFactory - .create(instanceId) - .scheduleActionsWithSubGroup('default', scheduleByPattern); + services.alertFactory.create(instanceId).scheduleActions('default', scheduleByPattern); } } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts index 63c70e93ea19..8f1b8047e433 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts @@ -953,82 +953,6 @@ instanceStateValue: true } }); - it('should not throttle when changing subgroups', async () => { - const testStart = new Date(); - const reference = alertUtils.generateReference(); - const response = await alertUtils.createAlwaysFiringAction({ - reference, - overwrites: { - schedule: { interval: '1s' }, - params: { - index: ES_TEST_INDEX_NAME, - reference, - groupsToScheduleActionsInSeries: ['default:prev', 'default:next'], - }, - actions: [ - { - group: 'default', - id: indexRecordActionId, - params: { - index: ES_TEST_INDEX_NAME, - reference, - message: 'from:{{alertActionGroup}}:{{alertActionSubgroup}}', - }, - }, - ], - }, - }); - - switch (scenario.id) { - case 'no_kibana_privileges at space1': - case 'space_1_all at space2': - case 'global_read at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( - 'create', - 'test.always-firing', - 'alertsFixture' - ), - statusCode: 403, - }); - break; - case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: `Unauthorized to get actions`, - statusCode: 403, - }); - break; - case 'space_1_all at space1': - case 'space_1_all_with_restricted_fixture at space1': - case 'superuser at space1': - expect(response.statusCode).to.eql(200); - // Wait for actions to execute twice before disabling the alert and waiting for tasks to finish - await esTestIndexTool.waitForDocs('action:test.index-record', reference, 2); - await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForDisabled(response.body.id, testStart); - - // Ensure only 2 actions with proper params exists - const searchResult = await esTestIndexTool.search( - 'action:test.index-record', - reference - ); - // @ts-expect-error doesnt handle total: number - expect(searchResult.body.hits.total.value).to.eql(2); - const messages: string[] = searchResult.body.hits.hits.map( - // @ts-expect-error _source: unknown - (hit: { _source: { params: { message: string } } }) => hit._source.params.message - ); - expect(messages.sort()).to.eql(['from:default:next', 'from:default:prev']); - break; - default: - throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); - } - }); - it('should reset throttle window when not firing', async () => { const testStart = new Date(); const reference = alertUtils.generateReference(); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/get_global_execution_kpi.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/get_global_execution_kpi.ts new file mode 100644 index 000000000000..133cca59f947 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/get_global_execution_kpi.ts @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function getGlobalExecutionKpiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + const retry = getService('retry'); + + describe('getGlobalExecutionKpi', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + it('should return KPI only from the current space', async () => { + const startTime = new Date().toISOString(); + + const spaceId = UserAtSpaceScenarios[1].space.id; + const user = UserAtSpaceScenarios[1].user; + const response = await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response.status).to.eql(200); + const ruleId = response.body.id; + objectRemover.add(spaceId, ruleId, 'rule', 'alerting'); + + const response2 = await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response2.status).to.eql(200); + const ruleId2 = response2.body.id; + objectRemover.add(spaceId, ruleId2, 'rule', 'alerting'); + + await retry.try(async () => { + // break AAD + await supertest + .put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${ruleId2}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + name: 'bar', + }, + }) + .expect(200); + }); + + await retry.try(async () => { + // there can be a successful execute before the error one + const someEvents = await getEventLog({ + getService, + spaceId, + type: 'alert', + id: ruleId2, + provider: 'alerting', + actions: new Map([['execute', { gte: 1 }]]), + }); + const errorEvents = someEvents.filter( + (event) => event?.kibana?.alerting?.status === 'error' + ); + expect(errorEvents.length).to.be.above(0); + }); + + await retry.try(async () => { + // there can be a successful execute before the error one + const logResponse = await supertestWithoutAuth + .get( + `${getUrlPrefix( + spaceId + )}/internal/alerting/_global_execution_logs?date_start=${startTime}&date_end=9999-12-31T23:59:59Z&per_page=50&page=1` + ) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + expect(logResponse.statusCode).to.be(200); + expect(logResponse.body.data.length).to.be.above(1); + }); + + await retry.try(async () => { + // break AAD + await supertest + .put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${ruleId}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + name: 'bar', + }, + }) + .expect(200); + }); + + await retry.try(async () => { + // there can be a successful execute before the error one + const someEvents = await getEventLog({ + getService, + spaceId, + type: 'alert', + id: ruleId, + provider: 'alerting', + actions: new Map([['execute', { gte: 1 }]]), + }); + const errorEvents = someEvents.filter( + (event) => event?.kibana?.alerting?.status === 'error' + ); + expect(errorEvents.length).to.be.above(0); + }); + + const kpiLogs = await retry.try(async () => { + // there can be a successful execute before the error one + const logResponse = await supertestWithoutAuth + .get( + `${getUrlPrefix( + spaceId + )}/internal/alerting/_global_execution_kpi?date_start=${startTime}&date_end=9999-12-31T23:59:59Z` + ) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + expect(logResponse.statusCode).to.be(200); + + return logResponse.body; + }); + + expect(Object.keys(kpiLogs)).to.eql([ + 'success', + 'unknown', + 'failure', + 'activeAlerts', + 'newAlerts', + 'recoveredAlerts', + 'erroredActions', + 'triggeredActions', + ]); + // it should be above 1 since we have two rule running + expect(kpiLogs.success).to.be.above(1); + expect(kpiLogs.failure).to.be.above(0); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/get_rule_execution_kpi.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/get_rule_execution_kpi.ts new file mode 100644 index 000000000000..2303fc616d8d --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/get_rule_execution_kpi.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function getRuleExecutionKpiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + const retry = getService('retry'); + + describe('getRuleExecutionKpi', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + it('should return KPI only from the current space', async () => { + const startTime = new Date().toISOString(); + + const spaceId = UserAtSpaceScenarios[1].space.id; + const user = UserAtSpaceScenarios[1].user; + const response = await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response.status).to.eql(200); + const ruleId = response.body.id; + objectRemover.add(spaceId, ruleId, 'rule', 'alerting'); + + const spaceId2 = UserAtSpaceScenarios[4].space.id; + const response2 = await supertest + .post(`${getUrlPrefix(spaceId2)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response2.status).to.eql(200); + const ruleId2 = response2.body.id; + objectRemover.add(spaceId2, ruleId2, 'rule', 'alerting'); + + await retry.try(async () => { + // there can be a successful execute before the error one + const someEvents = await getEventLog({ + getService, + spaceId, + type: 'alert', + id: ruleId, + provider: 'alerting', + actions: new Map([['execute', { gte: 1 }]]), + }); + + expect(someEvents.length).to.be.above(0); + }); + + await retry.try(async () => { + // break AAD + await supertest + .put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${ruleId}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + name: 'bar', + }, + }) + .expect(200); + }); + + await retry.try(async () => { + // there can be a successful execute before the error one + const someEvents = await getEventLog({ + getService, + spaceId, + type: 'alert', + id: ruleId, + provider: 'alerting', + actions: new Map([['execute', { gte: 1 }]]), + }); + const errorEvents = someEvents.filter( + (event) => event?.kibana?.alerting?.status === 'error' + ); + expect(errorEvents.length).to.be.above(0); + }); + + const kpiLogs = await retry.try(async () => { + // there can be a successful execute before the error one + const logResponse = await supertestWithoutAuth + .get( + `${getUrlPrefix( + spaceId + )}/internal/alerting/rule/${ruleId}/_execution_kpi?date_start=${startTime}&date_end=9999-12-31T23:59:59Z` + ) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + expect(logResponse.statusCode).to.be(200); + + return logResponse.body; + }); + + expect(Object.keys(kpiLogs)).to.eql([ + 'success', + 'unknown', + 'failure', + 'activeAlerts', + 'newAlerts', + 'recoveredAlerts', + 'erroredActions', + 'triggeredActions', + ]); + expect(kpiLogs.success).to.be.above(0); + expect(kpiLogs.failure).to.be.above(0); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index 206036ef7fca..2e63bed19786 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import uuid from 'uuid'; import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server'; import { Spaces } from '../../scenarios'; import { @@ -447,237 +446,6 @@ export default function eventLogTests({ getService }: FtrProviderContext) { } }); - it('should generate expected events for normal operation with subgroups', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - // pattern of when the alert should fire - const [firstSubgroup, secondSubgroup] = [uuid.v4(), uuid.v4()]; - const pattern = { - instance: [false, firstSubgroup, secondSubgroup], - }; - - const response = await supertest - .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - rule_type_id: 'test.patternFiring', - schedule: { interval: '1s' }, - throttle: null, - params: { - pattern, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: {}, - }, - ], - }) - ); - - expect(response.status).to.eql(200); - const alertId = response.body.id; - objectRemover.add(space.id, alertId, 'rule', 'alerting'); - - // get the events we're expecting - const events = await retry.try(async () => { - return await getEventLog({ - getService, - spaceId: space.id, - type: 'alert', - id: alertId, - provider: 'alerting', - actions: new Map([ - // make sure the counts of the # of events per type are as expected - ['execute-start', { gte: 4 }], - ['execute', { gte: 4 }], - ['execute-action', { equal: 2 }], - ['new-instance', { equal: 1 }], - ['active-instance', { gte: 2 }], - ['recovered-instance', { equal: 1 }], - ]), - }); - }); - - const executeEvents = getEventsByAction(events, 'execute'); - const executeStartEvents = getEventsByAction(events, 'execute-start'); - const newInstanceEvents = getEventsByAction(events, 'new-instance'); - const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); - - // make sure the events are in the right temporal order - const executeTimes = getTimestamps(executeEvents); - const executeStartTimes = getTimestamps(executeStartEvents); - const newInstanceTimes = getTimestamps(newInstanceEvents); - const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); - - expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); - expect(executeTimes[1] >= newInstanceTimes[0]).to.be(true); - expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); - expect(executeStartTimes.length === executeTimes.length).to.be(true); - expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); - - // validate each event - let executeCount = 0; - let numActiveAlerts = 0; - let numNewAlerts = 0; - let numRecoveredAlerts = 0; - let currentExecutionId; - const executeStatuses = ['ok', 'active', 'active']; - for (const event of events) { - switch (event?.event?.action) { - case 'execute-start': - currentExecutionId = event?.kibana?.alert?.rule?.execution?.uuid; - validateEvent(event, { - spaceId: space.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `rule execution start: "${alertId}"`, - shouldHaveTask: true, - executionId: currentExecutionId, - ruleTypeId: response.body.rule_type_id, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - }, - consumer: 'alertsFixture', - }); - break; - case 'execute-action': - expect( - [firstSubgroup, secondSubgroup].includes( - event?.kibana?.alerting?.action_subgroup! - ) - ).to.be(true); - validateEvent(event, { - spaceId: space.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - { type: 'action', id: createdAction.id, type_id: 'test.noop' }, - ], - message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})' action: test.noop:${createdAction.id}`, - instanceId: 'instance', - actionGroupId: 'default', - executionId: currentExecutionId, - ruleTypeId: response.body.rule_type_id, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - name: response.body.name, - }, - consumer: 'alertsFixture', - }); - break; - case 'new-instance': - numNewAlerts++; - validateInstanceEvent( - event, - `created new alert: 'instance'`, - false, - currentExecutionId - ); - break; - case 'recovered-instance': - numRecoveredAlerts++; - validateInstanceEvent( - event, - `alert 'instance' has recovered`, - true, - currentExecutionId - ); - break; - case 'active-instance': - numActiveAlerts++; - expect( - [firstSubgroup, secondSubgroup].includes( - event?.kibana?.alerting?.action_subgroup! - ) - ).to.be(true); - validateInstanceEvent( - event, - `active alert: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'`, - false, - currentExecutionId - ); - break; - case 'execute': - validateEvent(event, { - spaceId: space.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - outcome: 'success', - message: `rule executed: test.patternFiring:${alertId}: 'abc'`, - status: executeStatuses[executeCount++], - shouldHaveTask: true, - executionId: currentExecutionId, - ruleTypeId: response.body.rule_type_id, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - name: response.body.name, - }, - consumer: 'alertsFixture', - numActiveAlerts, - numNewAlerts, - numRecoveredAlerts, - }); - numActiveAlerts = 0; - numNewAlerts = 0; - numRecoveredAlerts = 0; - break; - // this will get triggered as we add new event actions - default: - throw new Error(`unexpected event action "${event?.event?.action}"`); - } - } - - function validateInstanceEvent( - event: IValidatedEvent, - subMessage: string, - shouldHaveEventEnd: boolean, - executionId?: string - ) { - validateEvent(event, { - spaceId: space.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, - instanceId: 'instance', - actionGroupId: 'default', - shouldHaveEventEnd, - executionId, - ruleTypeId: response.body.rule_type_id, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - name: response.body.name, - }, - consumer: 'alertsFixture', - }); - } - }); - it('should generate events for execution errors', async () => { const response = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index 2d30465401ca..23dcc1abaea4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { SuperTest, Test } from 'supertest'; +import { fromKueryExpression } from '@kbn/es-query'; import { Spaces } from '../../scenarios'; import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -198,6 +199,20 @@ const findTestUtils = ( expect(response.body.data[0].params.strValue).to.eql('my b'); }); + it('should filter on kueryNode parameters', async () => { + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/${ + describeType === 'public' ? 'api' : 'internal' + }/alerting/rules/_find?filter=${JSON.stringify( + fromKueryExpression('alert.attributes.params.strValue:"my b"') + )}` + ); + + expect(response.status).to.eql(200); + expect(response.body.total).to.equal(1); + expect(response.body.data[0].params.strValue).to.eql('my b'); + }); + it('should sort by parameters', async () => { const response = await supertest.get( `${getUrlPrefix(Spaces.space1.id)}/${ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts index 5024a1489f4c..a7996f19554e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/notify_when.ts @@ -174,97 +174,6 @@ export default function createNotifyWhenTests({ getService }: FtrProviderContext ); expect(executeActionEventsActionGroup).to.eql(expectedActionGroupBasedOnPattern); }); - - it(`alert with notifyWhen=onActionGroupChange should only execute actions when action subgroup changes`, async () => { - const { body: defaultAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My Default Action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - const { body: recoveredAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'My Recovered Action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - const pattern = { - instance: [ - 'subgroup1', - 'subgroup1', - false, - false, - 'subgroup1', - 'subgroup2', - 'subgroup2', - false, - ], - }; - const expectedActionGroupBasedOnPattern = [ - 'default', - 'recovered', - 'default', - 'default', - 'recovered', - ]; - - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - rule_type_id: 'test.patternFiring', - params: { pattern }, - schedule: { interval: '1s' }, - throttle: null, - notify_when: 'onActionGroupChange', - actions: [ - { - id: defaultAction.id, - group: 'default', - params: {}, - }, - { - id: recoveredAction.id, - group: 'recovered', - params: {}, - }, - ], - }) - ) - .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const events = await retry.try(async () => { - return await getEventLog({ - getService, - spaceId: Spaces.space1.id, - type: 'alert', - id: createdAlert.id, - provider: 'alerting', - actions: new Map([ - ['execute-action', { gte: 5 }], - ['new-instance', { equal: 2 }], - ]), - }); - }); - - const executeActionEvents = getEventsByAction(events, 'execute-action'); - const executeActionEventsActionGroup = executeActionEvents.map( - (event) => event?.kibana?.alerting?.action_group_id - ); - expect(executeActionEventsActionGroup).to.eql(expectedActionGroupBasedOnPattern); - }); }); } diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 915d755ca97c..e76eef8cb82b 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -67,5 +67,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./saved_objects')); loadTestFile(require.resolve('./system')); loadTestFile(require.resolve('./trained_models')); + loadTestFile(require.resolve('./notifications')); }); } diff --git a/x-pack/test/api_integration/apis/ml/notifications/count_notifications.ts b/x-pack/test/api_integration/apis/ml/notifications/count_notifications.ts new file mode 100644 index 000000000000..58932ea199b5 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/notifications/count_notifications.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import moment from 'moment'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('GET notifications count', () => { + before(async () => { + await ml.api.initSavedObjects(); + await ml.testResources.setKibanaTimeZoneToUTC(); + + const adJobConfig = ml.commonConfig.getADFqSingleMetricJobConfig('fq_job'); + await ml.api.createAnomalyDetectionJob(adJobConfig); + + await ml.api.waitForJobNotificationsToIndex('fq_job'); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + it('return notifications count by level', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications/count`) + .query({ lastCheckedAt: moment().subtract(7, 'd').valueOf() }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(200, status, body); + + expect(body.info).to.eql(1); + expect(body.warning).to.eql(0); + expect(body.error).to.eql(0); + }); + + it('returns an error for unauthorized user', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications/count`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(403, status, body); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/notifications/get_notifications.ts b/x-pack/test/api_integration/apis/ml/notifications/get_notifications.ts new file mode 100644 index 000000000000..992065cdae67 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/notifications/get_notifications.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { + NotificationItem, + NotificationsSearchResponse, +} from '@kbn/ml-plugin/common/types/notifications'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('GET notifications', () => { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); + await ml.api.initSavedObjects(); + await ml.testResources.setKibanaTimeZoneToUTC(); + + const adJobConfig = ml.commonConfig.getADFqSingleMetricJobConfig('fq_job'); + await ml.api.createAnomalyDetectionJob(adJobConfig); + + const dfaJobConfig = ml.commonConfig.getDFABmClassificationJobConfig('df_job'); + await ml.api.createDataFrameAnalyticsJob(dfaJobConfig); + + // wait for notification to index + + await ml.api.waitForJobNotificationsToIndex('fq_job'); + await ml.api.waitForJobNotificationsToIndex('df_job'); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + it('return all notifications ', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications`) + .query({ earliest: 'now-1d', latest: 'now' }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(200, status, body); + + expect((body as NotificationsSearchResponse).total).to.eql(2); + }); + + it('return notifications based on the query string', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications`) + .query({ earliest: 'now-1d', latest: 'now', queryString: 'job_type:anomaly_detector' }) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(200, status, body); + + expect((body as NotificationsSearchResponse).total).to.eql(1); + expect( + (body as NotificationsSearchResponse).results.filter( + (result: NotificationItem) => result.job_type === 'anomaly_detector' + ) + ).to.length(body.total); + }); + + it('supports sorting asc sorting by field', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications`) + .query({ earliest: 'now-1d', latest: 'now', sortField: 'job_id', sortDirection: 'asc' }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(200, status, body); + + expect(body.results[0].job_id).to.eql('df_job'); + }); + + it('supports sorting desc sorting by field', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications`) + .query({ earliest: 'now-1h', latest: 'now', sortField: 'job_id', sortDirection: 'desc' }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(200, status, body); + + expect(body.results[0].job_id).to.eql('fq_job'); + }); + + it('returns an error for unauthorized user', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(403, status, body); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/notifications/index.ts b/x-pack/test/api_integration/apis/ml/notifications/index.ts new file mode 100644 index 000000000000..4a09fce5ee51 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/notifications/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Notifications', function () { + loadTestFile(require.resolve('./get_notifications')); + loadTestFile(require.resolve('./count_notifications')); + }); +} diff --git a/x-pack/test/api_integration/apis/osquery/packs.ts b/x-pack/test/api_integration/apis/osquery/packs.ts index 9d00249a4e1b..de490a489cec 100644 --- a/x-pack/test/api_integration/apis/osquery/packs.ts +++ b/x-pack/test/api_integration/apis/osquery/packs.ts @@ -45,7 +45,8 @@ limit 1000;`; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('Packs', () => { + // FLAKY: https://github.com/elastic/kibana/issues/133259 + describe.skip('Packs', () => { let packId: string = ''; let hostedPolicy: Record; let packagePolicyId: string; diff --git a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts index 1110bbb875c7..105e6521c1da 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts @@ -112,7 +112,12 @@ export default function ({ getService }: FtrProviderContext) { expect(messages[2].failedMonitors).eql([ { id: httpProjectMonitors.monitors[0].id, - details: `The following Heartbeat options are not supported for ${httpProjectMonitors.monitors[0].type} project monitors in ${kibanaVersion}: check.response.body|unsupportedKey.nestedUnsupportedKey`, + details: `Multiple urls are not supported for http project monitors in ${kibanaVersion}. Please set only 1 url per monitor. You monitor was not created or updated.`, + reason: 'Unsupported Heartbeat option', + }, + { + id: httpProjectMonitors.monitors[0].id, + details: `The following Heartbeat options are not supported for ${httpProjectMonitors.monitors[0].type} project monitors in ${kibanaVersion}: check.response.body|unsupportedKey.nestedUnsupportedKey. You monitor was not created or updated.`, reason: 'Unsupported Heartbeat option', }, ]); @@ -200,7 +205,12 @@ export default function ({ getService }: FtrProviderContext) { expect(messages[2].failedMonitors).eql([ { id: tcpProjectMonitors.monitors[2].id, - details: `The following Heartbeat options are not supported for ${tcpProjectMonitors.monitors[0].type} project monitors in ${kibanaVersion}: ports|unsupportedKey.nestedUnsupportedKey`, + details: `Multiple hosts are not supported for tcp project monitors in ${kibanaVersion}. Please set only 1 host per monitor. You monitor was not created or updated.`, + reason: 'Unsupported Heartbeat option', + }, + { + id: tcpProjectMonitors.monitors[2].id, + details: `The following Heartbeat options are not supported for ${tcpProjectMonitors.monitors[0].type} project monitors in ${kibanaVersion}: ports|unsupportedKey.nestedUnsupportedKey. You monitor was not created or updated.`, reason: 'Unsupported Heartbeat option', }, ]); @@ -284,7 +294,12 @@ export default function ({ getService }: FtrProviderContext) { expect(messages[2].failedMonitors).eql([ { id: icmpProjectMonitors.monitors[2].id, - details: `The following Heartbeat options are not supported for ${icmpProjectMonitors.monitors[0].type} project monitors in ${kibanaVersion}: unsupportedKey.nestedUnsupportedKey`, + details: `Multiple hosts are not supported for icmp project monitors in ${kibanaVersion}. Please set only 1 host per monitor. You monitor was not created or updated.`, + reason: 'Unsupported Heartbeat option', + }, + { + id: icmpProjectMonitors.monitors[2].id, + details: `The following Heartbeat options are not supported for ${icmpProjectMonitors.monitors[0].type} project monitors in ${kibanaVersion}: unsupportedKey.nestedUnsupportedKey. You monitor was not created or updated.`, reason: 'Unsupported Heartbeat option', }, ]); diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_http_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_http_monitor.json index fc754cbdcd03..42044c8ba9cf 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_http_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_http_monitor.json @@ -9,7 +9,8 @@ "id": "my-monitor-2", "name": "My Monitor 2", "urls": [ - "http://localhost:9200" + "http://localhost:9200", + "http://anotherurl:9200" ], "schedule": 60, "timeout": "80s", @@ -32,7 +33,6 @@ "saved" ] }, - "content": "", "unsupportedKey": { "nestedUnsupportedKey": "unsupportedValue" } @@ -69,8 +69,7 @@ "saved" ] } - }, - "content": "" + } } ] } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_icmp_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_icmp_monitor.json index 8dec1b28d50c..b19e91088258 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_icmp_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_icmp_monitor.json @@ -14,7 +14,6 @@ "schedule": 1, "tags": [ "service:smtp", "org:google" ], "privateLocations": [ "Test private location 0" ], - "content": "", "wait": "30s" }, { @@ -26,7 +25,6 @@ "schedule": 1, "tags": "tag1,tag2", "privateLocations": [ "Test private location 0" ], - "content": "", "wait": "1m" }, { @@ -34,11 +32,10 @@ "type": "icmp", "id": "Cloudflare-DNS-3", "name": "Cloudflare DNS 3", - "hosts": "1.1.1.1", + "hosts": "1.1.1.1,2.2.2.2", "schedule": 1, "tags": "tag1,tag2", "privateLocations": [ "Test private location 0" ], - "content": "", "unsupportedKey": { "nestedUnsupportedKey": "unnsuportedValue" } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_tcp_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_tcp_monitor.json index 30061673079d..82d6c8c557e7 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_tcp_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/project_tcp_monitor.json @@ -10,8 +10,7 @@ "hosts": [ "smtp.gmail.com:587" ], "schedule": 1, "tags": [ "service:smtp", "org:google" ], - "privateLocations": [ "BEEP" ], - "content": "" + "privateLocations": [ "BEEP" ] }, { "locations": [ "localhost" ], @@ -21,20 +20,18 @@ "hosts": "localhost:18278", "schedule": 1, "tags": "tag1,tag2", - "privateLocations": [ "BEEP" ], - "content": "" + "privateLocations": [ "BEEP" ] }, { "locations": [ "localhost" ], "type": "tcp", "id": "always-down", "name": "Always Down", - "hosts": "localhost", + "hosts": ["localhost", "anotherhost"], "ports": ["5698"], "schedule": 1, "tags": "tag1,tag2", "privateLocations": [ "BEEP" ], - "content": "", "unsupportedKey": { "nestedUnsupportedKey": "unnsuportedValue" } diff --git a/x-pack/test/apm_api_integration/common/bootstrap_apm_synthtrace.ts b/x-pack/test/apm_api_integration/common/bootstrap_apm_synthtrace.ts index 96d0cbf3f8e7..0a790d261ef4 100644 --- a/x-pack/test/apm_api_integration/common/bootstrap_apm_synthtrace.ts +++ b/x-pack/test/apm_api_integration/common/bootstrap_apm_synthtrace.ts @@ -7,7 +7,7 @@ import { apm, createLogger, LogLevel } from '@kbn/apm-synthtrace'; import { esTestConfig } from '@kbn/test'; -import { APM_TEST_PASSWORD } from './authentication'; +import { APM_TEST_PASSWORD } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/authentication'; import { InheritedFtrProviderContext } from './ftr_provider_context'; export async function bootstrapApmSynthtrace( diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index e01c5fbf4054..431b445d3a1a 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -8,11 +8,12 @@ import { FtrConfigProviderContext } from '@kbn/test'; import supertest from 'supertest'; import { format, UrlObject } from 'url'; -import { Client } from '@elastic/elasticsearch'; -import { ToolingLog } from '@kbn/tooling-log'; -import { SecurityServiceProvider } from '../../../../test/common/services/security'; +import { + ApmUsername, + APM_TEST_PASSWORD, +} from '@kbn/apm-plugin/server/test_helpers/create_apm_users/authentication'; +import { createApmUsers } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/create_apm_users'; import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context'; -import { createApmUser, APM_TEST_PASSWORD, ApmUsername } from './authentication'; import { APMFtrConfigName } from '../configs'; import { createApmApiClient } from './apm_api_supertest'; import { RegistryProvider } from './registry'; @@ -25,17 +26,8 @@ export interface ApmFtrConfig { kibanaConfig?: Record; } -type SecurityService = Awaited>; - function getLegacySupertestClient(kibanaServer: UrlObject, username: ApmUsername) { return async (context: InheritedFtrProviderContext) => { - const security = context.getService('security'); - const es = context.getService('es'); - const logger = context.getService('log'); - await security.init(); - - await createApmUser({ security, username, es, logger }); - const url = format({ ...kibanaServer, auth: `${username}:${APM_TEST_PASSWORD}`, @@ -47,19 +39,11 @@ function getLegacySupertestClient(kibanaServer: UrlObject, username: ApmUsername async function getApmApiClient({ kibanaServer, - security, username, - es, - logger, }: { kibanaServer: UrlObject; - security: SecurityService; username: ApmUsername; - es: Client; - logger: ToolingLog; }) { - await createApmUser({ security, username, es, logger }); - const url = format({ ...kibanaServer, auth: `${username}:${APM_TEST_PASSWORD}`, @@ -81,6 +65,8 @@ export function createTestConfig(config: ApmFtrConfig) { const services = xPackAPITestsConfig.get('services') as InheritedServices; const servers = xPackAPITestsConfig.get('servers'); const kibanaServer = servers.kibana as UrlObject; + const kibanaServerUrl = format(kibanaServer); + const esServer = servers.elasticsearch as UrlObject; return { testFiles: [require.resolve('../tests')], @@ -91,72 +77,50 @@ export function createTestConfig(config: ApmFtrConfig) { apmFtrConfig: () => config, registry: RegistryProvider, synthtraceEsClient: (context: InheritedFtrProviderContext) => { - const kibanaServerUrl = format(kibanaServer); return bootstrapApmSynthtrace(context, kibanaServerUrl); }, apmApiClient: async (context: InheritedFtrProviderContext) => { - const security = context.getService('security'); - const es = context.getService('es'); - const logger = context.getService('log'); + const { username, password } = servers.kibana; + const esUrl = format(esServer); - await security.init(); + // Creates APM users + await createApmUsers({ + elasticsearch: { node: esUrl, username, password }, + kibana: { hostname: kibanaServerUrl }, + }); return { noAccessUser: await getApmApiClient({ kibanaServer, - security, username: ApmUsername.noAccessUser, - es, - logger, }), readUser: await getApmApiClient({ kibanaServer, - security, username: ApmUsername.viewerUser, - es, - logger, }), writeUser: await getApmApiClient({ kibanaServer, - security, username: ApmUsername.editorUser, - es, - logger, }), annotationWriterUser: await getApmApiClient({ kibanaServer, - security, username: ApmUsername.apmAnnotationsWriteUser, - es, - logger, }), noMlAccessUser: await getApmApiClient({ kibanaServer, - security, username: ApmUsername.apmReadUserWithoutMlAccess, - es, - logger, }), manageOwnAgentKeysUser: await getApmApiClient({ kibanaServer, - security, username: ApmUsername.apmManageOwnAgentKeys, - es, - logger, }), createAndAllAgentKeysUser: await getApmApiClient({ kibanaServer, - security, username: ApmUsername.apmManageOwnAndCreateAgentKeys, - es, - logger, }), monitorIndicesUser: await getApmApiClient({ kibanaServer, - security, username: ApmUsername.apmMonitorIndices, - es, - logger, }), }; }, diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_keys/agent_keys.spec.ts b/x-pack/test/apm_api_integration/tests/settings/agent_keys/agent_keys.spec.ts index c0d6500fd298..19459d2a4ec1 100644 --- a/x-pack/test/apm_api_integration/tests/settings/agent_keys/agent_keys.spec.ts +++ b/x-pack/test/apm_api_integration/tests/settings/agent_keys/agent_keys.spec.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; import { first } from 'lodash'; import { PrivilegeType } from '@kbn/apm-plugin/common/privilege_type'; +import { ApmUsername } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/authentication'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { ApmApiError, ApmApiSupertest } from '../../../common/apm_api_supertest'; -import { ApmUsername } from '../../../common/authentication'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts index b60ce575c4cd..8f06bc93820a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts @@ -1513,10 +1513,9 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('throttle', () => { + // For bulk editing of rule actions, NOTIFICATION_THROTTLE_NO_ACTIONS + // is not available as payload, because "Perform No Actions" is not a valid option const casesForEmptyActions = [ - { - payloadThrottle: NOTIFICATION_THROTTLE_NO_ACTIONS, - }, { payloadThrottle: NOTIFICATION_THROTTLE_RULE, }, @@ -1561,10 +1560,6 @@ export default ({ getService }: FtrProviderContext): void => { }); const casesForNonEmptyActions = [ - { - payloadThrottle: NOTIFICATION_THROTTLE_NO_ACTIONS, - expectedThrottle: NOTIFICATION_THROTTLE_NO_ACTIONS, - }, { payloadThrottle: NOTIFICATION_THROTTLE_RULE, expectedThrottle: NOTIFICATION_THROTTLE_RULE, @@ -1616,12 +1611,9 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('notifyWhen', () => { + // For bulk editing of rule actions, NOTIFICATION_THROTTLE_NO_ACTIONS + // is not available as payload, because "Perform No Actions" is not a valid option const cases = [ - { - payload: { throttle: NOTIFICATION_THROTTLE_NO_ACTIONS }, - // keeps existing default value which is onActiveAlert - expected: { notifyWhen: 'onActiveAlert' }, - }, { payload: { throttle: '1d' }, expected: { notifyWhen: 'onThrottleInterval' }, @@ -1837,6 +1829,42 @@ export default ({ getService }: FtrProviderContext): void => { expect(setIndexRule.data_view_id).to.eql(undefined); }); + it('should return error when set an empty index pattern to a rule and overwrite the data view when overwrite_data_views is true', async () => { + const dataViewId = 'index1-*'; + const simpleRule = { + ...getSimpleRule(), + index: undefined, + data_view_id: dataViewId, + }; + const rule = await createRule(supertest, log, simpleRule); + + const { body } = await postBulkAction() + .send({ + query: '', + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.set_index_patterns, + value: [], + overwrite_data_views: true, + }, + ], + }) + .expect(500); + + expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.errors[0]).to.eql({ + message: "Mutated params invalid: Index patterns can't be empty", + status_code: 500, + rules: [ + { + id: rule.id, + name: rule.name, + }, + ], + }); + }); + it('should NOT set an index pattern to a rule and overwrite the data view when overwrite_data_views is false', async () => { const ruleId = 'ruleId'; const dataViewId = 'index1-*'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts index a8dc735376f6..705714ec3ba5 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts @@ -220,7 +220,7 @@ export default ({ getService }: FtrProviderContext) => { updatedRule.rule_id = createRuleBody.rule_id; updatedRule.name = 'some other name'; updatedRule.actions = [action1]; - updatedRule.throttle = '1m'; + updatedRule.throttle = '1d'; delete updatedRule.id; const { body } = await supertest @@ -243,7 +243,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, ]; - outputRule.throttle = '1m'; + outputRule.throttle = '1d'; const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); expect(bodyToCompare).to.eql(outputRule); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts index dc7209b9f1c9..2f5fb6338255 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts @@ -150,12 +150,12 @@ export default ({ getService }: FtrProviderContext) => { const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.name = 'some other name'; updatedRule1.actions = [action1]; - updatedRule1.throttle = '1m'; + updatedRule1.throttle = '1d'; const updatedRule2 = getSimpleRuleUpdate('rule-2'); updatedRule2.name = 'some other name'; updatedRule2.actions = [action1]; - updatedRule2.throttle = '1m'; + updatedRule2.throttle = '1d'; // update both rule names const { body }: { body: FullResponseSchema[] } = await supertest @@ -179,7 +179,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, ]; - outputRule.throttle = '1m'; + outputRule.throttle = '1d'; const bodyToCompare = removeServerGeneratedProperties(response); expect(bodyToCompare).to.eql(outputRule); }); diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 1283c9433ebb..2dd546511dd9 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -121,9 +121,18 @@ export default function (providerContext: FtrProviderContext) { ]); expect(agent2data.body.item.policy_id).to.eql('policy2'); expect(agent3data.body.item.policy_id).to.eql('policy2'); + + const { body } = await supertest + .get(`/api/fleet/agents/action_status`) + .set('kbn-xsrf', 'xxx'); + const actionStatus = body.items[0]; + + expect(actionStatus.status).to.eql('FAILED'); + expect(actionStatus.nbAgentsActionCreated).to.eql(2); + expect(actionStatus.nbAgentsFailed).to.eql(3); }); - it('should allow to reassign multiple agents by id -- mixed invalid, hosted, etc', async () => { + it('should return error when none of the agents can be reassigned -- mixed invalid, hosted, etc', async () => { // agent1 is enrolled in policy1. set policy1 to hosted await supertest .put(`/api/fleet/agent_policies/policy1`) @@ -131,13 +140,15 @@ export default function (providerContext: FtrProviderContext) { .send({ name: 'Test policy', namespace: 'default', is_managed: true }) .expect(200); - await supertest + const { body } = await supertest .post(`/api/fleet/agents/bulk_reassign`) .set('kbn-xsrf', 'xxx') .send({ agents: ['agent2', 'INVALID_ID', 'agent3'], policy_id: 'policy2', - }); + }) + .expect(400); + expect(body.message).to.eql('No agents to reassign, already assigned or hosted agents'); const [agent2data, agent3data] = await Promise.all([ supertest.get(`/api/fleet/agents/agent2`), diff --git a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts index fe31806038a5..7f778d77f1a5 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts @@ -140,6 +140,13 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent3data.body.item.unenrollment_started_at).to.be('undefined'); expect(typeof agent3data.body.item.unenrolled_at).to.be('undefined'); expect(agent2data.body.item.active).to.eql(true); + + const { body } = await supertest + .get(`/api/fleet/agents/action_status`) + .set('kbn-xsrf', 'xxx'); + const actionStatus = body.items[0]; + expect(actionStatus.status).to.eql('FAILED'); + expect(actionStatus.nbAgentsFailed).to.eql(2); }); it('/agents/bulk_unenroll should allow to unenroll multiple agents by id from an regular agent policy', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts index 88079ae5e1af..ff11076addce 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts @@ -88,27 +88,46 @@ export default function (providerContext: FtrProviderContext) { }); it('should bulk update tags of multiple agents by kuery in batches', async () => { - await supertest + const { body: actionBody } = await supertest .post(`/api/fleet/agents/bulk_update_agent_tags`) .set('kbn-xsrf', 'xxx') .send({ agents: 'active: true', tagsToAdd: ['newTag'], tagsToRemove: ['existingTag'], - batchSize: 2, + batchSize: 3, }) .expect(200); + const actionId = actionBody.actionId; + + const verifyActionResult = async () => { + const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); + expect(body.total).to.eql(4); + body.items.forEach((agent: any) => { + expect(agent.tags.includes('newTag')).to.be(true); + expect(agent.tags.includes('existingTag')).to.be(false); + }); + }; + await new Promise((resolve, reject) => { - setTimeout(async () => { - const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); - expect(body.total).to.eql(4); - body.items.forEach((agent: any) => { - expect(agent.tags.includes('newTag')).to.be(true); - expect(agent.tags.includes('existingTag')).to.be(false); - }); - resolve({}); - }, 2000); + let attempts = 0; + const intervalId = setInterval(async () => { + if (attempts > 4) { + clearInterval(intervalId); + reject('action timed out'); + } + ++attempts; + const { + body: { items: actionStatuses }, + } = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx'); + const action = actionStatuses.find((a: any) => a.actionId === actionId); + if (action && action.nbAgentsAck === 4) { + clearInterval(intervalId); + await verifyActionResult(); + resolve({}); + } + }, 1000); }).catch((e) => { throw e; }); @@ -156,6 +175,13 @@ export default function (providerContext: FtrProviderContext) { expect(agent1data.body.item.tags.includes('newTag')).to.be(false); expect(agent2data.body.item.tags.includes('newTag')).to.be(true); + + const { body } = await supertest + .get(`/api/fleet/agents/action_status`) + .set('kbn-xsrf', 'xxx'); + const actionStatus = body.items[0]; + expect(actionStatus.status).to.eql('FAILED'); + expect(actionStatus.nbAgentsFailed).to.eql(1); }); }); }); diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index e2762e21f33a..e8dad8624021 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -608,7 +608,7 @@ export default function (providerContext: FtrProviderContext) { .send({ agents: 'active:true', version: fleetServerVersion, - batchSize: 2, + batchSize: 3, }) .expect(200); @@ -626,7 +626,7 @@ export default function (providerContext: FtrProviderContext) { await new Promise((resolve, reject) => { let attempts = 0; const intervalId = setInterval(async () => { - if (attempts > 2) { + if (attempts > 4) { clearInterval(intervalId); reject('action timed out'); } @@ -636,7 +636,7 @@ export default function (providerContext: FtrProviderContext) { } = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx'); const action = actionStatuses.find((a: any) => a.actionId === actionId); // 2 upgradeable - if (action && action.nbAgentsActionCreated === 2) { + if (action && action.nbAgentsActionCreated === 2 && action.nbAgentsFailed === 3) { clearInterval(intervalId); await verifyActionResult(); resolve({}); @@ -1032,6 +1032,13 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); + + const { body } = await supertest + .get(`/api/fleet/agents/action_status`) + .set('kbn-xsrf', 'xxx'); + const actionStatus = body.items[0]; + expect(actionStatus.status).to.eql('FAILED'); + expect(actionStatus.nbAgentsFailed).to.eql(1); }); it('enrolled in a hosted agent policy bulk upgrade with force flag should respond with 200 and update the agent SOs', async () => { diff --git a/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts index 8f7fc4fe7306..3a33f14d682a 100644 --- a/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts @@ -6,10 +6,6 @@ */ import expect from '@kbn/expect'; -import { - createDashboardEditUrl, - DashboardConstants, -} from '@kbn/dashboard-plugin/public/dashboard_constants'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { @@ -32,6 +28,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const kbnServer = getService('kibanaServer'); + const navigationArgs = { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }; + describe('dashboard feature controls security', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); @@ -97,14 +98,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`landing page shows "Create new Dashboard" button`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.LANDING_PAGE_PATH, - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardListingURL({ + args: navigationArgs, + }); await testSubjects.existOrFail('dashboardLandingPage', { timeout: config.get('timeouts.waitFor'), }); @@ -116,28 +112,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`create new dashboard shows addNew button`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.CREATE_NEW_DASHBOARD_URL, - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardURL({ args: navigationArgs }); await testSubjects.existOrFail('emptyDashboardWidget', { timeout: config.get('timeouts.waitFor'), }); }); it(`can view existing Dashboard`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - createDashboardEditUrl('i-exist'), - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardURL({ + id: 'i-exist', + args: navigationArgs, + }); await testSubjects.existOrFail('embeddablePanelHeading-APie', { timeout: config.get('timeouts.waitFor'), }); @@ -307,14 +292,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.LANDING_PAGE_PATH, - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardListingURL({ + args: navigationArgs, + }); await testSubjects.existOrFail('dashboardLandingPage', { timeout: config.get('timeouts.waitFor'), }); @@ -322,38 +302,24 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`shows read-only badge`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.LANDING_PAGE_PATH, - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardListingURL({ + args: navigationArgs, + }); await globalNav.badgeExistsOrFail('Read only'); }); it(`create new dashboard shows the read only warning`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.CREATE_NEW_DASHBOARD_URL, - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardURL({ + args: navigationArgs, + }); await testSubjects.existOrFail('dashboardEmptyReadOnly', { timeout: 20000 }); }); it(`can view existing Dashboard`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - createDashboardEditUrl('i-exist'), - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardURL({ + id: 'i-exist', + args: navigationArgs, + }); await testSubjects.existOrFail('embeddablePanelHeading-APie', { timeout: config.get('timeouts.waitFor'), }); @@ -438,14 +404,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.LANDING_PAGE_PATH, - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardListingURL({ + args: navigationArgs, + }); await testSubjects.existOrFail('dashboardLandingPage', { timeout: 10000 }); await testSubjects.missingOrFail('newItemButton'); }); @@ -455,26 +416,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`create new dashboard shows the read only warning`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.CREATE_NEW_DASHBOARD_URL, - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardURL({ + args: navigationArgs, + }); await testSubjects.existOrFail('dashboardEmptyReadOnly', { timeout: 20000 }); }); it(`can view existing Dashboard`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - createDashboardEditUrl('i-exist'), - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardURL({ id: 'i-exist', args: navigationArgs }); await testSubjects.existOrFail('embeddablePanelHeading-APie', { timeout: 10000 }); }); @@ -552,50 +501,24 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`landing page shows 403`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.LANDING_PAGE_PATH, - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardListingURL({ + args: navigationArgs, + }); await PageObjects.error.expectForbidden(); }); it(`create new dashboard shows 403`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.CREATE_NEW_DASHBOARD_URL, - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardURL({ args: navigationArgs }); await PageObjects.error.expectForbidden(); }); it(`edit dashboard for object which doesn't exist shows 403`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - createDashboardEditUrl('i-dont-exist'), - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardURL({ id: 'i-dont-exist', args: navigationArgs }); await PageObjects.error.expectForbidden(); }); it(`edit dashboard for object which exists shows 403`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - createDashboardEditUrl('i-exist'), - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - } - ); + await PageObjects.dashboard.gotoDashboardURL({ id: 'i-exist', args: navigationArgs }); await PageObjects.error.expectForbidden(); }); }); diff --git a/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_spaces.ts index a702b7a716a1..ef8c83ec667b 100644 --- a/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_spaces.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_spaces.ts @@ -6,10 +6,6 @@ */ import expect from '@kbn/expect'; -import { - createDashboardEditUrl, - DashboardConstants, -} from '@kbn/dashboard-plugin/public/dashboard_constants'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { @@ -53,15 +49,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`landing page shows "Create new Dashboard" button`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.LANDING_PAGE_PATH, - { + await PageObjects.dashboard.gotoDashboardListingURL({ + args: { basePath: '/s/custom_space', ensureCurrentUrl: false, shouldLoginIfPrompted: false, - } - ); + }, + }); await testSubjects.existOrFail('dashboardLandingPage', { timeout: config.get('timeouts.waitFor'), }); @@ -69,30 +63,27 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`create new dashboard shows addNew button`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.CREATE_NEW_DASHBOARD_URL, - { + await PageObjects.dashboard.gotoDashboardURL({ + args: { basePath: '/s/custom_space', ensureCurrentUrl: false, shouldLoginIfPrompted: false, - } - ); + }, + }); await testSubjects.existOrFail('emptyDashboardWidget', { timeout: config.get('timeouts.waitFor'), }); }); it(`can view existing Dashboard`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - createDashboardEditUrl('8fba09d8-df3f-5aa1-83cc-65f7fbcbc0d9'), - { + await PageObjects.dashboard.gotoDashboardURL({ + id: '8fba09d8-df3f-5aa1-83cc-65f7fbcbc0d9', + args: { basePath: '/s/custom_space', ensureCurrentUrl: false, shouldLoginIfPrompted: false, - } - ); + }, + }); await testSubjects.existOrFail('embeddablePanelHeading-APie', { timeout: config.get('timeouts.waitFor'), }); @@ -125,41 +116,37 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`create new dashboard shows 404`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - DashboardConstants.CREATE_NEW_DASHBOARD_URL, - { + await PageObjects.dashboard.gotoDashboardURL({ + args: { basePath: '/s/custom_space', ensureCurrentUrl: false, shouldLoginIfPrompted: false, - } - ); + }, + }); await PageObjects.error.expectNotFound(); }); it(`edit dashboard for object which doesn't exist shows 404`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - createDashboardEditUrl('i-dont-exist'), - { + await PageObjects.dashboard.gotoDashboardURL({ + id: 'i-dont-exist', + args: { basePath: '/s/custom_space', ensureCurrentUrl: false, shouldLoginIfPrompted: false, - } - ); + }, + }); await PageObjects.error.expectNotFound(); }); it(`edit dashboard for object which exists shows 404`, async () => { - await PageObjects.common.navigateToActualUrl( - 'dashboard', - createDashboardEditUrl('i-exist'), - { + await PageObjects.dashboard.gotoDashboardURL({ + id: 'i-exist', + args: { basePath: '/s/custom_space', ensureCurrentUrl: false, shouldLoginIfPrompted: false, - } - ); + }, + }); await PageObjects.error.expectNotFound(); }); }); diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 6acaabcd5d20..1c0323808270 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -14,8 +14,9 @@ const DATE_WITHOUT_DATA = DATES.metricsAndLogs.hosts.withoutData; export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); + const browser = getService('browser'); const retry = getService('retry'); - const pageObjects = getPageObjects(['common', 'infraHome', 'infraSavedViews']); + const pageObjects = getPageObjects(['common', 'header', 'infraHome', 'infraSavedViews']); const kibanaServer = getService('kibanaServer'); describe('Home page', function () { @@ -34,6 +35,22 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.common.navigateToApp('infraOps'); await pageObjects.infraHome.getNoMetricsIndicesPrompt(); }); + + it('renders the correct error page title', async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'infraOps', + '/detail/host/test', + '', + { + ensureCurrentUrl: false, + } + ); + await pageObjects.infraHome.waitForLoading(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + const documentTitle = await browser.getTitle(); + expect(documentTitle).to.contain('Uh oh - Observability - Elastic'); + }); }); describe('with metrics present', () => { @@ -47,6 +64,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs') ); + it('renders the correct page title', async () => { + await pageObjects.header.waitUntilLoadingHasFinished(); + + const documentTitle = await browser.getTitle(); + expect(documentTitle).to.contain('Inventory - Infrastructure - Observability - Elastic'); + }); + it('renders an empty data prompt for dates with no data', async () => { await pageObjects.infraHome.goToTime(DATE_WITHOUT_DATA); await pageObjects.infraHome.getNoMetricsDataPrompt(); diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts index ebfcb740961b..05eccc8e57eb 100644 --- a/x-pack/test/functional/apps/infra/link_to.ts +++ b/x-pack/test/functional/apps/infra/link_to.ts @@ -42,6 +42,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.tryForTime(5000, async () => { const currentUrl = await browser.getCurrentUrl(); const parsedUrl = new URL(currentUrl); + const documentTitle = await browser.getTitle(); expect(parsedUrl.pathname).to.be('/app/logs/stream'); expect(parsedUrl.searchParams.get('logFilter')).to.be( @@ -51,6 +52,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { `(end:'${endDate}',position:(tiebreaker:0,time:${timestamp}),start:'${startDate}',streamLive:!f)` ); expect(parsedUrl.searchParams.get('sourceId')).to.be('default'); + expect(documentTitle).to.contain('Stream - Logs - Observability - Elastic'); }); }); }); diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index 56eed5ec4b63..38cc795034a2 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -16,6 +16,7 @@ const COMMON_REQUEST_HEADERS = { export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); + const browser = getService('browser'); const logsUi = getService('logsUi'); const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); const pageObjects = getPageObjects(['common', 'header', 'infraLogs']); @@ -49,6 +50,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); }); + it('renders the correct page title', async () => { + await pageObjects.infraLogs.navigateToTab('settings'); + + await pageObjects.header.waitUntilLoadingHasFinished(); + const documentTitle = await browser.getTitle(); + + expect(documentTitle).to.contain('Settings - Logs - Observability - Elastic'); + }); + it('can change the log indices to a pattern that matches nothing', async () => { await pageObjects.infraLogs.navigateToTab('settings'); diff --git a/x-pack/test/functional/apps/infra/metrics_explorer.ts b/x-pack/test/functional/apps/infra/metrics_explorer.ts index fc620d9ba566..4d6859a4e99e 100644 --- a/x-pack/test/functional/apps/infra/metrics_explorer.ts +++ b/x-pack/test/functional/apps/infra/metrics_explorer.ts @@ -16,6 +16,7 @@ const timepickerFormat = 'MMM D, YYYY @ HH:mm:ss.SSS'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const browser = getService('browser'); const pageObjects = getPageObjects([ 'common', 'infraHome', @@ -42,6 +43,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); + it('should render the correct page title', async () => { + const documentTitle = await browser.getTitle(); + expect(documentTitle).to.contain( + 'Metrics Explorer - Infrastructure - Observability - Elastic' + ); + }); + it('should have three metrics by default', async () => { const metrics = await pageObjects.infraMetricsExplorer.getMetrics(); expect(metrics.length).to.equal(3); diff --git a/x-pack/test/functional/apps/lens/group1/table.ts b/x-pack/test/functional/apps/lens/group1/table.ts index 7bf9b49c53d8..724efbe6c6c8 100644 --- a/x-pack/test/functional/apps/lens/group1/table.ts +++ b/x-pack/test/functional/apps/lens/group1/table.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + const PageObjects = getPageObjects(['visualize', 'lens', 'common']); const listingTable = getService('listingTable'); const find = getService('find'); const retry = getService('retry'); @@ -91,14 +91,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should allow to sort by transposed columns', async () => { await PageObjects.lens.changeTableSortingBy(2, 'ascending'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); expect(await PageObjects.lens.getDatatableCellText(0, 2)).to.eql('17,246'); }); it('should show dynamic coloring feature for numeric columns', async () => { await PageObjects.lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger'); await PageObjects.lens.setTableDynamicColoring('text'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); expect(styleObj['background-color']).to.be(undefined); expect(styleObj.color).to.be('rgb(133, 189, 177)'); @@ -106,7 +106,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should allow to color cell background rather than text', async () => { await PageObjects.lens.setTableDynamicColoring('cell'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); expect(styleObj['background-color']).to.be('rgb(133, 189, 177)'); // should also set text color when in cell mode @@ -115,9 +115,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should open the palette panel to customize the palette look', async () => { await PageObjects.lens.openPalettePanel('lnsDatatable'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); await PageObjects.lens.changePaletteTo('temperature'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); }); @@ -125,7 +125,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should keep the coloring consistent when changing mode', async () => { // Change mode from percent to number await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_number'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); // check that all remained the same const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); @@ -133,7 +133,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should keep the coloring consistent when moving to custom palette from default', async () => { await PageObjects.lens.changePaletteTo('custom'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); // check that all remained the same const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); @@ -149,7 +149,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); // when clicking on another row will trigger a sorting + update await testSubjects.click('lnsPalettePanel_dynamicColoring_range_value_1'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); // pick a cell without color as is below the range const styleObj = await PageObjects.lens.getDatatableCellStyle(3, 3); expect(styleObj['background-color']).to.be(undefined); @@ -159,7 +159,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should allow the user to reverse the palette', async () => { await testSubjects.click('lnsPalettePanel_dynamicColoring_reverseColors'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); const styleObj = await PageObjects.lens.getDatatableCellStyle(1, 1); expect(styleObj['background-color']).to.be('rgb(168, 191, 218)'); // should also set text color when in cell mode @@ -169,7 +169,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should allow to show a summary table for metric columns', async () => { await PageObjects.lens.setTableSummaryRowFunction('sum'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); await PageObjects.lens.assertExactText( '[data-test-subj="lnsDataTable-footer-169.228.188.120-›-Average-of-bytes"]', 'Sum: 18,994' diff --git a/x-pack/test/functional/apps/lens/group2/tsdb.ts b/x-pack/test/functional/apps/lens/group2/tsdb.ts index 7a43fc47471a..d19ab9d19db7 100644 --- a/x-pack/test/functional/apps/lens/group2/tsdb.ts +++ b/x-pack/test/functional/apps/lens/group2/tsdb.ts @@ -119,7 +119,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('lns-indexPatternDimension-median'); await PageObjects.lens.waitForVisualization('xyVisChart'); await PageObjects.lens.assertEditorWarning( - '"Median of kubernetes.container.memory.available.bytes" does not work for all indices in the selected data view because it\'s using a function which is not supported on rolled up data. Please edit the visualization to use another function or change the time range.' + 'Median of kubernetes.container.memory.available.bytes uses a function that is unsupported by rolled up data. Select a different function or change the time range.' ); }); it('shows warnings in dashboards as well', async () => { @@ -127,7 +127,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); await PageObjects.lens.assertInlineWarning( - '"Median of kubernetes.container.memory.available.bytes" does not work for all indices in the selected data view because it\'s using a function which is not supported on rolled up data. Please edit the visualization to use another function or change the time range.' + 'Median of kubernetes.container.memory.available.bytes uses a function that is unsupported by rolled up data. Select a different function or change the time range.' ); }); it('still shows other warnings as toast', async () => { diff --git a/x-pack/test/functional/apps/lens/group3/index.ts b/x-pack/test/functional/apps/lens/group3/index.ts index 44365d5333d1..627e9d560ca2 100644 --- a/x-pack/test/functional/apps/lens/group3/index.ts +++ b/x-pack/test/functional/apps/lens/group3/index.ts @@ -86,7 +86,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext loadTestFile(require.resolve('./error_handling')); loadTestFile(require.resolve('./lens_tagging')); loadTestFile(require.resolve('./lens_reporting')); - loadTestFile(require.resolve('./tsvb_open_in_lens')); + loadTestFile(require.resolve('./open_in_lens')); // keep these two last in the group in this order because they are messing with the default saved objects loadTestFile(require.resolve('./rollup')); loadTestFile(require.resolve('./no_data')); diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/index.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/index.ts new file mode 100644 index 000000000000..b279f0d8a93c --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Agg based Vis to Lens', function () { + loadTestFile(require.resolve('./pie')); + }); +} diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/pie.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/pie.ts new file mode 100644 index 000000000000..1640a6a14631 --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/pie.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const { visualize, visEditor, lens, timePicker, header } = getPageObjects([ + 'visualize', + 'lens', + 'visEditor', + 'timePicker', + 'header', + ]); + + const testSubjects = getService('testSubjects'); + const pieChart = getService('pieChart'); + + describe('Pie', function describeIndexTests() { + const isNewChartsLibraryEnabled = true; + + before(async () => { + await visualize.initTests(isNewChartsLibraryEnabled); + }); + + beforeEach(async () => { + await visualize.navigateToNewAggBasedVisualization(); + await visualize.clickPieChart(); + await visualize.clickNewSearch(); + await timePicker.setDefaultAbsoluteRange(); + }); + + it('should hide the "Edit Visualization in Lens" menu item if no split slices were defined', async () => { + const button = await testSubjects.exists('visualizeEditInLensButton'); + expect(button).to.eql(false); + }); + + it('should show the "Edit Visualization in Lens" menu item', async () => { + await visEditor.clickBucket('Split slices'); + await visEditor.selectAggregation('Terms'); + await visEditor.selectField('machine.os.raw'); + await header.waitUntilLoadingHasFinished(); + await visEditor.clickGo(isNewChartsLibraryEnabled); + + const button = await testSubjects.exists('visualizeEditInLensButton'); + expect(button).to.eql(true); + }); + + it('should convert to Lens', async () => { + const expectedTableData = ['ios', 'osx', 'win 7', 'win 8', 'win xp']; + await visEditor.clickBucket('Split slices'); + await visEditor.selectAggregation('Terms'); + await visEditor.selectField('machine.os.raw'); + await header.waitUntilLoadingHasFinished(); + await visEditor.clickGo(isNewChartsLibraryEnabled); + + const button = await testSubjects.find('visualizeEditInLensButton'); + await button.click(); + await lens.waitForVisualization('partitionVisChart'); + + await pieChart.expectPieChartLabels(expectedTableData, isNewChartsLibraryEnabled); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/index.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/index.ts new file mode 100644 index 000000000000..b1d5a1cbb3c5 --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Open in Lens', function () { + loadTestFile(require.resolve('./tsvb')); + loadTestFile(require.resolve('./agg_based')); + }); +} diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/dashboard.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/dashboard.ts new file mode 100644 index 000000000000..bd74e109326b --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/dashboard.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const { visualize, visualBuilder, lens, timeToVisualize, dashboard, canvas } = getPageObjects([ + 'visualBuilder', + 'visualize', + 'lens', + 'timeToVisualize', + 'dashboard', + 'canvas', + ]); + + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const panelActions = getService('dashboardPanelActions'); + const dashboardAddPanel = getService('dashboardAddPanel'); + + describe('Dashboard to TSVB to Lens', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); + + it('should convert a by value TSVB viz to a Lens viz', async () => { + await visualize.navigateToNewVisualization(); + await visualize.clickVisualBuilder(); + await visualBuilder.checkVisualBuilderIsPresent(); + await visualBuilder.resetPage(); + await testSubjects.click('visualizeSaveButton'); + + await timeToVisualize.saveFromModal('My TSVB to Lens viz 1', { + addToDashboard: 'new', + saveToLibrary: false, + }); + + await dashboard.waitForRenderComplete(); + const originalEmbeddableCount = await canvas.getEmbeddableCount(); + await panelActions.openContextMenu(); + await panelActions.clickEdit(); + + const button = await testSubjects.find('visualizeEditInLensButton'); + await button.click(); + await lens.waitForVisualization('xyVisChart'); + await retry.try(async () => { + const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); + expect(await dimensions[1].getVisibleText()).to.be('Count of records'); + }); + + await lens.saveAndReturn(); + await retry.try(async () => { + const embeddableCount = await canvas.getEmbeddableCount(); + expect(embeddableCount).to.eql(originalEmbeddableCount); + }); + await panelActions.removePanel(); + }); + + it('should convert a by reference TSVB viz to a Lens viz', async () => { + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickVisType('metrics'); + await testSubjects.click('visualizesaveAndReturnButton'); + // save it to library + const originalPanel = await testSubjects.find('embeddablePanelHeading-'); + await panelActions.saveToLibrary('My TSVB to Lens viz 2', originalPanel); + + await dashboard.waitForRenderComplete(); + const originalEmbeddableCount = await canvas.getEmbeddableCount(); + await panelActions.openContextMenu(); + await panelActions.clickEdit(); + + const button = await testSubjects.find('visualizeEditInLensButton'); + await button.click(); + await lens.waitForVisualization('legacyMtrVis'); + await retry.try(async () => { + const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); + expect(await dimensions[1].getVisibleText()).to.be('Count of records'); + }); + + await lens.saveAndReturn(); + await retry.try(async () => { + const embeddableCount = await canvas.getEmbeddableCount(); + expect(embeddableCount).to.eql(originalEmbeddableCount); + }); + + const panel = await testSubjects.find(`embeddablePanelHeading-`); + const descendants = await testSubjects.findAllDescendant( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + panel + ); + expect(descendants.length).to.equal(0); + + await panelActions.removePanel(); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/index.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/index.ts new file mode 100644 index 000000000000..c786ed22c6a1 --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('TSVB to Lens', function () { + loadTestFile(require.resolve('./metric')); + loadTestFile(require.resolve('./timeseries')); + loadTestFile(require.resolve('./dashboard')); + }); +} diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/metric.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/metric.ts new file mode 100644 index 000000000000..273473b67a31 --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/metric.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const { visualize, visualBuilder, lens } = getPageObjects(['visualBuilder', 'visualize', 'lens']); + + const testSubjects = getService('testSubjects'); + + describe('Metric', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); + + beforeEach(async () => { + await visualize.navigateToNewVisualization(); + await visualize.clickVisualBuilder(); + await visualBuilder.checkVisualBuilderIsPresent(); + await visualBuilder.resetPage(); + await visualBuilder.clickMetric(); + await visualBuilder.clickDataTab('metric'); + }); + + it('should show the "Edit Visualization in Lens" menu item', async () => { + const button = await testSubjects.exists('visualizeEditInLensButton'); + expect(button).to.eql(true); + }); + + it('should convert to Lens', async () => { + const button = await testSubjects.find('visualizeEditInLensButton'); + await button.click(); + await lens.waitForVisualization('mtrVis'); + + const metricData = await lens.getMetricVisualizationData(); + expect(metricData[0].title).to.eql('Count of records'); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/timeseries.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/timeseries.ts new file mode 100644 index 000000000000..17e3a6b1bee8 --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/timeseries.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const { visualize, visualBuilder, lens, header } = getPageObjects([ + 'visualBuilder', + 'visualize', + 'header', + 'lens', + ]); + + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const find = getService('find'); + const filterBar = getService('filterBar'); + const queryBar = getService('queryBar'); + + describe('Time Series', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); + + it('should show the "Edit Visualization in Lens" menu item for a count aggregation', async () => { + await visualize.navigateToNewVisualization(); + await visualize.clickVisualBuilder(); + await visualBuilder.checkVisualBuilderIsPresent(); + await visualBuilder.resetPage(); + const isMenuItemVisible = await find.existsByCssSelector( + '[data-test-subj="visualizeEditInLensButton"]' + ); + expect(isMenuItemVisible).to.be(true); + }); + + it('visualizes field to Lens and loads fields to the dimesion editor', async () => { + const button = await testSubjects.find('visualizeEditInLensButton'); + await button.click(); + await lens.waitForVisualization('xyVisChart'); + await retry.try(async () => { + const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); + expect(dimensions).to.have.length(2); + expect(await dimensions[0].getVisibleText()).to.be('@timestamp'); + expect(await dimensions[1].getVisibleText()).to.be('Count of records'); + }); + }); + + it('navigates back to TSVB when the Back button is clicked', async () => { + const goBackBtn = await testSubjects.find('lnsApp_goBackToAppButton'); + goBackBtn.click(); + await visualBuilder.checkVisualBuilderIsPresent(); + await retry.try(async () => { + const actualCount = await visualBuilder.getRhythmChartLegendValue(); + expect(actualCount).to.be('56'); + }); + }); + + it('should preserve app filters in lens', async () => { + await filterBar.addFilter('extension', 'is', 'css'); + await header.waitUntilLoadingHasFinished(); + const button = await testSubjects.find('visualizeEditInLensButton'); + await button.click(); + await lens.waitForVisualization('xyVisChart'); + + expect(await filterBar.hasFilter('extension', 'css')).to.be(true); + }); + + it('should preserve query in lens', async () => { + const goBackBtn = await testSubjects.find('lnsApp_goBackToAppButton'); + goBackBtn.click(); + await visualBuilder.checkVisualBuilderIsPresent(); + await queryBar.setQuery('machine.os : ios'); + await queryBar.submitQuery(); + await header.waitUntilLoadingHasFinished(); + const button = await testSubjects.find('visualizeEditInLensButton'); + await button.click(); + await lens.waitForVisualization('xyVisChart'); + + expect(await queryBar.getQueryString()).to.equal('machine.os : ios'); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts b/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts deleted file mode 100644 index a7acd8bf5ba1..000000000000 --- a/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts +++ /dev/null @@ -1,183 +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 expect from '@kbn/expect'; - -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { visualize, visualBuilder, header, lens, timeToVisualize, dashboard, canvas } = - getPageObjects([ - 'visualBuilder', - 'visualize', - 'header', - 'lens', - 'timeToVisualize', - 'dashboard', - 'canvas', - ]); - const testSubjects = getService('testSubjects'); - const find = getService('find'); - const dashboardAddPanel = getService('dashboardAddPanel'); - const panelActions = getService('dashboardPanelActions'); - const retry = getService('retry'); - const filterBar = getService('filterBar'); - const queryBar = getService('queryBar'); - - describe('TSVB to Lens', function describeIndexTests() { - before(async () => { - await visualize.initTests(); - }); - - describe('Time Series', () => { - it('should show the "Edit Visualization in Lens" menu item for a count aggregation', async () => { - await visualize.navigateToNewVisualization(); - await visualize.clickVisualBuilder(); - await visualBuilder.checkVisualBuilderIsPresent(); - await visualBuilder.resetPage(); - const isMenuItemVisible = await find.existsByCssSelector( - '[data-test-subj="visualizeEditInLensButton"]' - ); - expect(isMenuItemVisible).to.be(true); - }); - - it('visualizes field to Lens and loads fields to the dimesion editor', async () => { - const button = await testSubjects.find('visualizeEditInLensButton'); - await button.click(); - await lens.waitForVisualization('xyVisChart'); - await retry.try(async () => { - const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); - expect(dimensions).to.have.length(2); - expect(await dimensions[0].getVisibleText()).to.be('@timestamp'); - expect(await dimensions[1].getVisibleText()).to.be('Count of records'); - }); - }); - - it('navigates back to TSVB when the Back button is clicked', async () => { - const goBackBtn = await testSubjects.find('lnsApp_goBackToAppButton'); - goBackBtn.click(); - await visualBuilder.checkVisualBuilderIsPresent(); - await retry.try(async () => { - const actualCount = await visualBuilder.getRhythmChartLegendValue(); - expect(actualCount).to.be('56'); - }); - }); - - it('should preserve app filters in lens', async () => { - await filterBar.addFilter('extension', 'is', 'css'); - await header.waitUntilLoadingHasFinished(); - const button = await testSubjects.find('visualizeEditInLensButton'); - await button.click(); - await lens.waitForVisualization('xyVisChart'); - - expect(await filterBar.hasFilter('extension', 'css')).to.be(true); - }); - - it('should preserve query in lens', async () => { - const goBackBtn = await testSubjects.find('lnsApp_goBackToAppButton'); - goBackBtn.click(); - await visualBuilder.checkVisualBuilderIsPresent(); - await queryBar.setQuery('machine.os : ios'); - await queryBar.submitQuery(); - await header.waitUntilLoadingHasFinished(); - const button = await testSubjects.find('visualizeEditInLensButton'); - await button.click(); - await lens.waitForVisualization('xyVisChart'); - - expect(await queryBar.getQueryString()).to.equal('machine.os : ios'); - }); - }); - - describe('Metric', () => { - beforeEach(async () => { - await visualize.navigateToNewVisualization(); - await visualize.clickVisualBuilder(); - await visualBuilder.checkVisualBuilderIsPresent(); - await visualBuilder.resetPage(); - await visualBuilder.clickMetric(); - await visualBuilder.clickDataTab('metric'); - }); - - it('should hide the "Edit Visualization in Lens" menu item', async () => { - const button = await testSubjects.exists('visualizeEditInLensButton'); - expect(button).to.eql(false); - }); - }); - - describe('Dashboard to TSVB to Lens', () => { - it('should convert a by value TSVB viz to a Lens viz', async () => { - await visualize.navigateToNewVisualization(); - await visualize.clickVisualBuilder(); - await visualBuilder.checkVisualBuilderIsPresent(); - await visualBuilder.resetPage(); - await testSubjects.click('visualizeSaveButton'); - - await timeToVisualize.saveFromModal('My TSVB to Lens viz 1', { - addToDashboard: 'new', - saveToLibrary: false, - }); - - await dashboard.waitForRenderComplete(); - const originalEmbeddableCount = await canvas.getEmbeddableCount(); - await panelActions.openContextMenu(); - await panelActions.clickEdit(); - - const button = await testSubjects.find('visualizeEditInLensButton'); - await button.click(); - await lens.waitForVisualization('xyVisChart'); - await retry.try(async () => { - const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); - expect(await dimensions[1].getVisibleText()).to.be('Count of records'); - }); - - await lens.saveAndReturn(); - await retry.try(async () => { - const embeddableCount = await canvas.getEmbeddableCount(); - expect(embeddableCount).to.eql(originalEmbeddableCount); - }); - await panelActions.removePanel(); - }); - - it('should convert a by reference TSVB viz to a Lens viz', async () => { - await dashboardAddPanel.clickEditorMenuButton(); - await dashboardAddPanel.clickVisType('metrics'); - await testSubjects.click('visualizesaveAndReturnButton'); - // save it to library - const originalPanel = await testSubjects.find('embeddablePanelHeading-'); - await panelActions.saveToLibrary('My TSVB to Lens viz 2', originalPanel); - - await dashboard.waitForRenderComplete(); - const originalEmbeddableCount = await canvas.getEmbeddableCount(); - await panelActions.openContextMenu(); - await panelActions.clickEdit(); - - const button = await testSubjects.find('visualizeEditInLensButton'); - await button.click(); - await lens.waitForVisualization('legacyMtrVis'); - await retry.try(async () => { - const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); - expect(await dimensions[1].getVisibleText()).to.be('Count of records'); - }); - - await lens.saveAndReturn(); - await retry.try(async () => { - const embeddableCount = await canvas.getEmbeddableCount(); - expect(embeddableCount).to.eql(originalEmbeddableCount); - }); - - const panel = await testSubjects.find(`embeddablePanelHeading-`); - const descendants = await testSubjects.findAllDescendant( - 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', - panel - ); - expect(descendants.length).to.equal(0); - - await panelActions.removePanel(); - }); - }); - }); -} diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 2ba4ac6f0835..ea9552506270 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - describe('classification creation', function () { + // FLAKY: https://github.com/elastic/kibana/issues/142102 + describe.skip('classification creation', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); @@ -92,7 +93,8 @@ export default function ({ getService }: FtrProviderContext) { }, ]; for (const testData of testDataList) { - describe(`${testData.suiteTitle}`, function () { + // FLAKY: https://github.com/elastic/kibana/issues/142102 + describe.skip(`${testData.suiteTitle}`, function () { after(async () => { await ml.api.deleteIndices(testData.destinationIndex); await ml.testResources.deleteIndexPatternByTitle(testData.destinationIndex); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index 3a33c95edba4..7d6cc5ea9881 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -15,7 +15,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('jobs cloning supported by UI form', function () { + // FLAKY: https://github.com/elastic/kibana/issues/142118 + describe.skip('jobs cloning supported by UI form', function () { const testDataList: Array<{ suiteTitle: string; archive: string; @@ -135,7 +136,8 @@ export default function ({ getService }: FtrProviderContext) { }); for (const testData of testDataList) { - describe(`${testData.suiteTitle}`, function () { + // FLAKY: https://github.com/elastic/kibana/issues/142118 + describe.skip(`${testData.suiteTitle}`, function () { const cloneJobId = `${testData.job.id}_clone`; const cloneDestIndex = `${testData.job!.dest!.index}_clone`; diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index 947cd82cdd34..fb04cd793d9d 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - describe('outlier detection creation', function () { + // FLAKY: https://github.com/elastic/kibana/issues/142083 + describe.skip('outlier detection creation', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); @@ -108,7 +109,8 @@ export default function ({ getService }: FtrProviderContext) { ]; for (const testData of testDataList) { - describe(`${testData.suiteTitle}`, function () { + // FLAKY: https://github.com/elastic/kibana/issues/142083 + describe.skip(`${testData.suiteTitle}`, function () { after(async () => { await ml.api.deleteIndices(testData.destinationIndex); await ml.testResources.deleteIndexPatternByTitle(testData.destinationIndex); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 7a84c41aa4a6..744b61a2a06c 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - describe('regression creation', function () { + // FLAKY: https://github.com/elastic/kibana/issues/142095 + describe.skip('regression creation', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); @@ -86,7 +87,8 @@ export default function ({ getService }: FtrProviderContext) { ]; for (const testData of testDataList) { - describe(`${testData.suiteTitle}`, function () { + // FLAKY: https://github.com/elastic/kibana/issues/142095 + describe.skip(`${testData.suiteTitle}`, function () { after(async () => { await ml.api.deleteIndices(testData.destinationIndex); await ml.testResources.deleteIndexPatternByTitle(testData.destinationIndex); diff --git a/x-pack/test/functional/apps/ml/short_tests/index.ts b/x-pack/test/functional/apps/ml/short_tests/index.ts index f96d2b91ee0e..d446a3593347 100644 --- a/x-pack/test/functional/apps/ml/short_tests/index.ts +++ b/x-pack/test/functional/apps/ml/short_tests/index.ts @@ -33,5 +33,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./model_management')); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./settings')); + loadTestFile(require.resolve('./notifications')); }); } diff --git a/x-pack/test/functional/apps/ml/short_tests/notifications/index.ts b/x-pack/test/functional/apps/ml/short_tests/notifications/index.ts new file mode 100644 index 000000000000..e026d44a67af --- /dev/null +++ b/x-pack/test/functional/apps/ml/short_tests/notifications/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Notifcations', function () { + this.tags(['ml', 'skipFirefox']); + + loadTestFile(require.resolve('./notification_list')); + }); +} diff --git a/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts b/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts new file mode 100644 index 000000000000..c9ad8d2423ef --- /dev/null +++ b/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common']); + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const browser = getService('browser'); + + describe('Notifications list', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // Prepare jobs to generate notifications + await Promise.all( + [ + { jobId: 'fq_001', spaceId: undefined }, + { jobId: 'fq_002', spaceId: 'space1' }, + ].map(async (v) => { + const datafeedConfig = ml.commonConfig.getADFqDatafeedConfig(v.jobId); + + await ml.api.createAnomalyDetectionJob( + ml.commonConfig.getADFqSingleMetricJobConfig(v.jobId), + v.spaceId + ); + await ml.api.openAnomalyDetectionJob(v.jobId); + await ml.api.createDatafeed(datafeedConfig, v.spaceId); + await ml.api.startDatafeed(datafeedConfig.datafeed_id); + }) + ); + + await ml.securityUI.loginAsMlPowerUser(); + await PageObjects.common.navigateToApp('ml', { + basePath: '', + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + await ml.testResources.deleteIndexPatternByTitle('ft_farequote'); + }); + + it('displays a generic notification indicator', async () => { + await ml.notifications.assertNotificationIndicatorExist(); + }); + + it('opens the Notifications page', async () => { + await ml.navigation.navigateToNotifications(); + + await ml.notifications.table.waitForTableToLoad(); + await ml.notifications.table.assertRowsNumberPerPage(25); + }); + + it('does not show notifications from another space', async () => { + await ml.notifications.table.filterWithSearchString('Job created', 1); + }); + + it('display a number of errors in the notification indicator', async () => { + await ml.navigation.navigateToOverview(); + + const jobConfig = ml.commonConfig.getADFqSingleMetricJobConfig('fq_fail'); + jobConfig.analysis_config = { + bucket_span: '15m', + influencers: ['airline'], + detectors: [ + { function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }, + { function: 'min', field_name: 'responsetime', partition_field_name: 'airline' }, + { function: 'max', field_name: 'responsetime', partition_field_name: 'airline' }, + ], + }; + // Set extremely low memory limit to trigger an error + jobConfig.analysis_limits!.model_memory_limit = '1024kb'; + + const datafeedConfig = ml.commonConfig.getADFqDatafeedConfig(jobConfig.job_id); + + await ml.api.createAnomalyDetectionJob(jobConfig); + await ml.api.openAnomalyDetectionJob(jobConfig.job_id); + await ml.api.createDatafeed(datafeedConfig); + await ml.api.startDatafeed(datafeedConfig.datafeed_id); + await ml.api.waitForJobMemoryStatus(jobConfig.job_id, 'hard_limit'); + + // refresh the page to avoid 1m wait + await browser.refresh(); + await ml.notifications.assertNotificationErrorsCount(0); + }); + }); +} diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 07bd1f346cf2..62d46e644f17 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -273,6 +273,16 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return state; }, + async getJobMemoryStatus(jobId: string): Promise<'hard_limit' | 'soft_limit' | 'ok'> { + const jobStats = await this.getADJobStats(jobId); + + expect(jobStats.jobs).to.have.length( + 1, + `Expected job stats to have exactly one job (got '${jobStats.length}')` + ); + return jobStats.jobs[0].model_size_stats.memory_status; + }, + async getADJobStats(jobId: string): Promise { log.debug(`Fetching anomaly detection job stats for job ${jobId}...`); const { body: jobStats, status } = await esSupertest.get( @@ -299,6 +309,27 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, + async waitForJobMemoryStatus( + jobId: string, + expectedMemoryStatus: 'hard_limit' | 'soft_limit' | 'ok', + timeout: number = 2 * 60 * 1000 + ) { + await retry.waitForWithTimeout( + `job memory status to be ${expectedMemoryStatus}`, + timeout, + async () => { + const memoryStatus = await this.getJobMemoryStatus(jobId); + if (memoryStatus === expectedMemoryStatus) { + return true; + } else { + throw new Error( + `expected job memory status to be ${expectedMemoryStatus} but got ${memoryStatus}` + ); + } + } + ); + }, + async getDatafeedState(datafeedId: string): Promise { log.debug(`Fetching datafeed state for datafeed ${datafeedId}`); const { body: datafeedStats, status } = await esSupertest.get( @@ -576,6 +607,18 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return response; }, + async hasNotifications(query: object) { + const body = await es.search({ + index: '.ml-notifications*', + body: { + size: 10000, + query, + }, + }); + + return body.hits.hits.length > 0; + }, + async adJobExist(jobId: string) { this.validateJobId(jobId); try { @@ -608,6 +651,24 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, + async waitForJobNotificationsToIndex(jobId: string, timeout: number = 60 * 1000) { + await retry.waitForWithTimeout(`Notifications for '${jobId}' to exist`, timeout, async () => { + if ( + await this.hasNotifications({ + term: { + job_id: { + value: jobId, + }, + }, + }) + ) { + return true; + } else { + throw new Error(`expected '${jobId}' notifications to exist`); + } + }); + }, + async createAnomalyDetectionJob(jobConfig: Job, space?: string) { const jobId = jobConfig.job_id; log.debug( diff --git a/x-pack/test/functional/services/ml/common_table_service.ts b/x-pack/test/functional/services/ml/common_table_service.ts new file mode 100644 index 000000000000..50a40ab43f35 --- /dev/null +++ b/x-pack/test/functional/services/ml/common_table_service.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export type MlTableService = ReturnType; + +export function MlTableServiceProvider({ getPageObject, getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const commonPage = getPageObject('common'); + + const TableService = class { + constructor( + public readonly tableTestSubj: string, + public readonly tableRowSubj: string, + public readonly columns: Array<{ id: string; testSubj: string }>, + public readonly searchInputSubj: string + ) {} + + public async assertTableLoaded() { + await testSubjects.existOrFail(`~${this.tableTestSubj} loaded`); + } + + public async assertTableLoading() { + await testSubjects.existOrFail(`~${this.tableTestSubj} loading`); + } + + public async parseTable() { + const table = await testSubjects.find(`~${this.tableTestSubj}`); + const $ = await table.parseDomContent(); + const rows = []; + + for (const tr of $.findTestSubjects(`~${this.tableRowSubj}`).toArray()) { + const $tr = $(tr); + + const rowObject = this.columns.reduce((acc, curr) => { + acc[curr.id] = $tr + .findTestSubject(curr.testSubj) + .find('.euiTableCellContent') + .text() + .trim(); + return acc; + }, {} as Record); + + rows.push(rowObject); + } + + return rows; + } + + public async assertRowsNumberPerPage(rowsNumber: 10 | 25 | 50 | 100) { + const textContent = await testSubjects.getVisibleText( + `~${this.tableTestSubj} > tablePaginationPopoverButton` + ); + expect(textContent).to.be(`Rows per page: ${rowsNumber}`); + } + + public async waitForTableToStartLoading() { + await testSubjects.existOrFail(`~${this.tableTestSubj}`, { timeout: 60 * 1000 }); + await testSubjects.existOrFail(`${this.tableTestSubj} loading`, { timeout: 30 * 1000 }); + } + + public async waitForTableToLoad() { + await testSubjects.existOrFail(`~${this.tableTestSubj}`, { timeout: 60 * 1000 }); + await testSubjects.existOrFail(`${this.tableTestSubj} loaded`, { timeout: 30 * 1000 }); + } + + async getSearchInput(): Promise { + return await testSubjects.find(this.searchInputSubj); + } + + public async assertSearchInputValue(expectedSearchValue: string) { + const searchBarInput = await this.getSearchInput(); + const actualSearchValue = await searchBarInput.getAttribute('value'); + expect(actualSearchValue).to.eql( + expectedSearchValue, + `Search input value should be '${expectedSearchValue}' (got '${actualSearchValue}')` + ); + } + + public async filterWithSearchString(queryString: string, expectedRowCount: number = 1) { + await this.waitForTableToLoad(); + const searchBarInput = await this.getSearchInput(); + await searchBarInput.clearValueWithKeyboard(); + await searchBarInput.type(queryString); + await commonPage.pressEnterKey(); + await this.assertSearchInputValue(queryString); + await this.waitForTableToStartLoading(); + await this.waitForTableToLoad(); + + const rows = await this.parseTable(); + + expect(rows).to.have.length( + expectedRowCount, + `Filtered table should have ${expectedRowCount} row(s) for filter '${queryString}' (got ${rows.length} matching items)` + ); + } + }; + + return { + getServiceInstance( + name: string, + tableTestSubj: string, + tableRowSubj: string, + columns: Array<{ id: string; testSubj: string }>, + searchInputSubj: string + ) { + Object.defineProperty(TableService, 'name', { value: name }); + return new TableService(tableTestSubj, tableRowSubj, columns, searchInputSubj); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index 0d8ee7d11a8b..ef69f909437c 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -164,22 +164,27 @@ export function MachineLearningCommonUIProvider({ }, async setMultiSelectFilter(testDataSubj: string, fieldTypes: string[]) { - await testSubjects.clickWhenNotDisabledWithoutRetry(`${testDataSubj}-button`); - await testSubjects.existOrFail(`${testDataSubj}-popover`); - await testSubjects.existOrFail(`${testDataSubj}-searchInput`); - const searchBarInput = await testSubjects.find(`${testDataSubj}-searchInput`); + await retry.tryForTime(60 * 1000, async () => { + // escape popover + await browser.pressKeys(browser.keys.ESCAPE); - for (const fieldType of fieldTypes) { - await retry.tryForTime(5000, async () => { - await searchBarInput.clearValueWithKeyboard(); - await searchBarInput.type(fieldType); - if (!(await testSubjects.exists(`${testDataSubj}-option-${fieldType}-checked`))) { - await testSubjects.existOrFail(`${testDataSubj}-option-${fieldType}`); - await testSubjects.click(`${testDataSubj}-option-${fieldType}`); - await testSubjects.existOrFail(`${testDataSubj}-option-${fieldType}-checked`); - } - }); - } + await testSubjects.clickWhenNotDisabledWithoutRetry(`${testDataSubj}-button`); + await testSubjects.existOrFail(`${testDataSubj}-popover`); + await testSubjects.existOrFail(`${testDataSubj}-searchInput`); + const searchBarInput = await testSubjects.find(`${testDataSubj}-searchInput`); + + for (const fieldType of fieldTypes) { + await retry.tryForTime(5000, async () => { + await searchBarInput.clearValueWithKeyboard(); + await searchBarInput.type(fieldType); + if (!(await testSubjects.exists(`${testDataSubj}-option-${fieldType}-checked`))) { + await testSubjects.existOrFail(`${testDataSubj}-option-${fieldType}`); + await testSubjects.click(`${testDataSubj}-option-${fieldType}`); + await testSubjects.existOrFail(`${testDataSubj}-option-${fieldType}-checked`); + } + }); + } + }); // escape popover await browser.pressKeys(browser.keys.ESCAPE); diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index d8c6924ec4cf..9452baa32489 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -59,6 +59,8 @@ import { MachineLearningJobAnnotationsProvider } from './job_annotations_table'; import { MlNodesPanelProvider } from './ml_nodes_list'; import { MachineLearningCasesProvider } from './cases'; import { AnomalyChartsProvider } from './anomaly_charts'; +import { NotificationsProvider } from './notifications'; +import { MlTableServiceProvider } from './common_table_service'; export function MachineLearningProvider(context: FtrProviderContext) { const commonAPI = MachineLearningCommonAPIProvider(context); @@ -123,6 +125,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const settingsFilterList = MachineLearningSettingsFilterListProvider(context, commonUI); const singleMetricViewer = MachineLearningSingleMetricViewerProvider(context, commonUI); const stackManagementJobs = MachineLearningStackManagementJobsProvider(context); + const tableService = MlTableServiceProvider(context); const testExecution = MachineLearningTestExecutionProvider(context); const testResources = MachineLearningTestResourcesProvider(context, api); const alerting = MachineLearningAlertingProvider(context, api, commonUI); @@ -130,6 +133,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const trainedModels = TrainedModelsProvider(context, commonUI); const trainedModelsTable = TrainedModelsTableProvider(context, commonUI); const mlNodesPanel = MlNodesPanelProvider(context); + const notifications = NotificationsProvider(context, commonUI, tableService); const cases = MachineLearningCasesProvider(context, swimLane, anomalyCharts); @@ -171,7 +175,9 @@ export function MachineLearningProvider(context: FtrProviderContext) { jobWizardMultiMetric, jobWizardPopulation, lensVisualizations, + mlNodesPanel, navigation, + notifications, overviewPage, securityCommon, securityUI, @@ -181,10 +187,10 @@ export function MachineLearningProvider(context: FtrProviderContext) { singleMetricViewer, stackManagementJobs, swimLane, + tableService, testExecution, testResources, trainedModels, trainedModelsTable, - mlNodesPanel, }; } diff --git a/x-pack/test/functional/services/ml/notifications.ts b/x-pack/test/functional/services/ml/notifications.ts new file mode 100644 index 000000000000..2365717d7226 --- /dev/null +++ b/x-pack/test/functional/services/ml/notifications.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommonUI } from './common_ui'; +import { MlTableService } from './common_table_service'; + +export function NotificationsProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI, + tableService: MlTableService +) { + const testSubjects = getService('testSubjects'); + + return { + async assertNotificationIndicatorExist(expectExist = true) { + if (expectExist) { + await testSubjects.existOrFail('mlNotificationsIndicator'); + } else { + await testSubjects.missingOrFail('mlNotificationsIndicator'); + } + }, + + async assertNotificationErrorsCount(expectedCount: number) { + const actualCount = await testSubjects.getVisibleText('mlNotificationErrorsIndicator'); + expect(actualCount).to.greaterThan(expectedCount); + }, + + table: tableService.getServiceInstance( + 'NotificationsTable', + 'mlNotificationsTable', + 'mlNotificationsTableRow', + [ + { id: 'timestamp', testSubj: 'mlNotificationTime' }, + { id: 'level', testSubj: 'mlNotificationLevel' }, + { id: 'job_type', testSubj: 'mlNotificationType' }, + { id: 'job_id', testSubj: 'mlNotificationEntity' }, + { id: 'message', testSubj: 'mlNotificationMessage' }, + ], + 'mlNotificationsSearchBarInput' + ), + }; +} diff --git a/x-pack/test/functional/services/ml/test_resources.ts b/x-pack/test/functional/services/ml/test_resources.ts index 4fccde6712e6..d1a7557caf2b 100644 --- a/x-pack/test/functional/services/ml/test_resources.ts +++ b/x-pack/test/functional/services/ml/test_resources.ts @@ -46,6 +46,14 @@ export function MachineLearningTestResourcesProvider( await kibanaServer.uiSettings.unset('dateFormat:tz'); }, + async disableKibanaAnnouncements() { + await kibanaServer.uiSettings.update({ hideAnnouncements: true }); + }, + + async resetKibanaAnnouncements() { + await kibanaServer.uiSettings.unset('hideAnnouncements'); + }, + async savedObjectExistsById(id: string, objectType: SavedObjectType): Promise { const response = await supertest.get(`/api/saved_objects/${objectType}/${id}`); return response.status === 200; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/upgrade.ts b/x-pack/test/functional_with_es_ssl/apps/cases/upgrade.ts index cbbbb90c05ab..c1e2979d313b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/upgrade.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/upgrade.ts @@ -18,6 +18,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const supertest = getService('supertest'); const testSubjects = getService('testSubjects'); const find = getService('find'); + const toasts = getService('toasts'); const updateConnector = async (id: string, req: Record) => { const { body: connector } = await supertest @@ -74,6 +75,10 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); describe('Case view page', function () { + it('does not show any error toasters', async () => { + expect(await toasts.getToastCount()).to.be(0); + }); + it('shows the title correctly', async () => { const title = await testSubjects.find('header-page-title'); expect(await title.getVisibleText()).equal('Upgrade test in Kibana'); @@ -275,6 +280,10 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { it('shows the add comment button', async () => { await testSubjects.exists('submit-comment'); }); + + it('shows the assignees section', async () => { + await testSubjects.exists('case-view-assignees'); + }); }); }); }; diff --git a/x-pack/test/osquery_cypress/artifact_manager.ts b/x-pack/test/osquery_cypress/artifact_manager.ts index bef18d017722..7ee2680e21f8 100644 --- a/x-pack/test/osquery_cypress/artifact_manager.ts +++ b/x-pack/test/osquery_cypress/artifact_manager.ts @@ -6,5 +6,5 @@ */ export async function getLatestVersion(): Promise { - return '8.5.0-SNAPSHOT'; + return '8.6.0-SNAPSHOT'; } diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/index.ts b/x-pack/test/screenshot_creation/apps/ml_docs/index.ts index 1c12efc89caf..806414939cd8 100644 --- a/x-pack/test/screenshot_creation/apps/ml_docs/index.ts +++ b/x-pack/test/screenshot_creation/apps/ml_docs/index.ts @@ -22,6 +22,7 @@ export default function ({ getPageObject, getService, loadTestFile }: FtrProvide before(async () => { await ml.testResources.installAllKibanaSampleData(); await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.testResources.disableKibanaAnnouncements(); await browser.setWindowSize(1920, 1080); }); @@ -29,6 +30,7 @@ export default function ({ getPageObject, getService, loadTestFile }: FtrProvide await securityPage.forceLogout(); await ml.testResources.removeAllKibanaSampleData(); await ml.testResources.resetKibanaTimeZone(); + await ml.testResources.resetKibanaAnnouncements(); }); loadTestFile(require.resolve('./anomaly_detection')); diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/index.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/index.ts index 64ed2599d65b..e836e3e63c9b 100644 --- a/x-pack/test/screenshot_creation/apps/response_ops_docs/index.ts +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/index.ts @@ -23,6 +23,7 @@ export default function ({ getPageObject, getService, loadTestFile }: FtrProvide before(async () => { await ml.testResources.installAllKibanaSampleData(); await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.testResources.disableKibanaAnnouncements(); await browser.setWindowSize(1920, 1080); await securityPage.login( esTestConfig.getUrlParts().username, @@ -34,6 +35,7 @@ export default function ({ getPageObject, getService, loadTestFile }: FtrProvide await securityPage.forceLogout(); await ml.testResources.removeAllKibanaSampleData(); await ml.testResources.resetKibanaTimeZone(); + await ml.testResources.resetKibanaAnnouncements(); }); loadTestFile(require.resolve('./stack_cases')); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts index 9caf23108986..ddcbbc6251fd 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts @@ -116,7 +116,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('from timeline', () => { + // FLAKY: https://github.com/elastic/kibana/issues/140546 + describe.skip('from timeline', () => { let timeline: TimelineResponse; before(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.fixtures.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.fixtures.ts index 281ba95d8e0c..3a81ad1c71dc 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.fixtures.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.fixtures.ts @@ -247,8 +247,8 @@ export function generateMetadataDocs(timestamp: number) { dataset: 'endpoint.metadata', }, host: { - hostname: 'rezzani-7.example.com', - name: 'rezzani-7.example.com', + hostname: 'Example-host-name-XYZ', + name: 'Example-host-name-XYZ', id: 'fc0ff548-feba-41b6-8367-65e8790d0eaf', ip: ['10.101.149.26', '2606:a000:ffc0:39:11ef:37b9:3371:578c'], mac: ['e2-6d-f9-0-46-2e'], @@ -399,8 +399,8 @@ export function generateMetadataDocs(timestamp: number) { }, host: { architecture: 'x86', - hostname: 'rezzani-7.example.com', - name: 'rezzani-7.example.com', + hostname: 'Example-host-name-XYZ', + name: 'Example-host-name-XYZ', id: 'fc0ff548-feba-41b6-8367-65e8790d0eaf', ip: ['10.101.149.26', '2606:a000:ffc0:39:11ef:37b9:3371:578c'], mac: ['e2-6d-f9-0-46-2e'], @@ -550,8 +550,8 @@ export function generateMetadataDocs(timestamp: number) { }, host: { architecture: 'x86', - hostname: 'rezzani-7.example.com', - name: 'rezzani-7.example.com', + hostname: 'Example-host-name-XYZ', + name: 'Example-host-name-XYZ', id: 'fc0ff548-feba-41b6-8367-65e8790d0eaf', ip: ['10.101.149.26', '2606:a000:ffc0:39:11ef:37b9:3371:578c'], mac: ['e2-6d-f9-0-46-2e'], diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 508aa8884fcf..0ee6b4495257 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -259,6 +259,26 @@ export default function ({ getService }: FtrProviderContext) { expect(body.pageSize).to.eql(10); }); + it('metadata api should return the endpoint based on the agent hostname', async () => { + const targetEndpointId = 'fc0ff548-feba-41b6-8367-65e8790d0eaf'; + const targetAgentHostname = 'Example-host-name-XYZ'; + const { body } = await supertest + .get(HOST_METADATA_LIST_ROUTE) + .set('kbn-xsrf', 'xxx') + .query({ + kuery: `united.endpoint.host.hostname:${targetAgentHostname}`, + }) + .expect(200); + expect(body.total).to.eql(1); + const resultHostId: string = body.data[0].metadata.host.id; + const resultElasticAgentName: string = body.data[0].metadata.host.hostname; + expect(resultHostId).to.eql(targetEndpointId); + expect(resultElasticAgentName).to.eql(targetAgentHostname); + expect(body.data.length).to.eql(1); + expect(body.page).to.eql(0); + expect(body.pageSize).to.eql(10); + }); + it('metadata api should return all hosts when filter is empty string', async () => { const { body } = await supertest .get(HOST_METADATA_LIST_ROUTE) diff --git a/yarn.lock b/yarn.lock index f809bc0cac46..804502ebeb4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2954,6 +2954,14 @@ version "0.0.0" uid "" +"@kbn/core-http-request-handler-context-server-internal@link:bazel-bin/packages/core/http/core-http-request-handler-context-server-internal": + version "0.0.0" + uid "" + +"@kbn/core-http-request-handler-context-server@link:bazel-bin/packages/core/http/core-http-request-handler-context-server": + version "0.0.0" + uid "" + "@kbn/core-http-router-server-internal@link:bazel-bin/packages/core/http/core-http-router-server-internal": version "0.0.0" uid "" @@ -3558,6 +3566,10 @@ version "0.0.0" uid "" +"@kbn/securitysolution-exception-list-components@link:bazel-bin/packages/kbn-securitysolution-exception-list-components": + version "0.0.0" + uid "" + "@kbn/securitysolution-hook-utils@link:bazel-bin/packages/kbn-securitysolution-hook-utils": version "0.0.0" uid "" @@ -7075,6 +7087,14 @@ version "0.0.0" uid "" +"@types/kbn__core-http-request-handler-context-server-internal@link:bazel-bin/packages/core/http/core-http-request-handler-context-server-internal/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__core-http-request-handler-context-server@link:bazel-bin/packages/core/http/core-http-request-handler-context-server/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__core-http-router-server-internal@link:bazel-bin/packages/core/http/core-http-router-server-internal/npm_module_types": version "0.0.0" uid "" @@ -7667,6 +7687,10 @@ version "0.0.0" uid "" +"@types/kbn__securitysolution-exception-list-components@link:bazel-bin/packages/kbn-securitysolution-exception-list-components/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__securitysolution-hook-utils@link:bazel-bin/packages/kbn-securitysolution-hook-utils/npm_module_types": version "0.0.0" uid "" @@ -27926,10 +27950,10 @@ vega-label@~1.2.0: vega-scenegraph "^4.9.2" vega-util "^1.15.2" -vega-lite@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-5.3.0.tgz#b9b9ecd80e869e823e6848c67d0a8ad94954bdee" - integrity sha512-6giodZ/bJnWyLq6Gj4OyiDt7EndoGyC9f5xDQjo82yPpUiO4MuG9iiPMqR1SPKmG9/qPBf+klWQR0v/7Mgju0Q== +vega-lite@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-5.5.0.tgz#07345713d538cd63278748ec119c261722be66ff" + integrity sha512-MQBJt/iaUegvhRTS/hZVWfMOSF5ai4awlR2qtwTgHd84bErf9v7GtaZ9ArhJqXCb+FizvZ2jatmoYCzovgAhkg== dependencies: "@types/clone" "~2.1.1" array-flat-polyfill "^1.0.1"